index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <template>
  2. <div class="code-page">
  3. <div class="code-hero">
  4. <div>
  5. <span>IDENTITY GENERATOR</span>
  6. <h1>事项编码生成</h1>
  7. <p>选择区域、预警类型及排水雨污分类,系统按统一编码规则生成唯一事项编码。</p>
  8. </div>
  9. <div class="code-orbit">
  10. <div class="orbit-line"></div>
  11. <el-icon><Connection /></el-icon>
  12. </div>
  13. </div>
  14. <div class="generator-grid">
  15. <el-card class="steps-card" shadow="never">
  16. <div class="step-heading">
  17. <span>编码配置</span>
  18. <small>STEP {{ currentStep }}/3</small>
  19. </div>
  20. <div class="step-track">
  21. <div class="track-item active"><span>01</span><div><strong>选择区域</strong><small>确定编码所属区域</small></div></div>
  22. <div class="track-line"></div>
  23. <div class="track-item" :class="{ active: form.warningType }"><span>02</span><div><strong>选择类型</strong><small>确定预警所属领域</small></div></div>
  24. <div class="track-line"></div>
  25. <div class="track-item" :class="{ active: generatedCode }"><span>03</span><div><strong>生成编码</strong><small>得到唯一事项标识</small></div></div>
  26. </div>
  27. <div class="field-block">
  28. <label>区域</label>
  29. <div class="type-grid region-grid">
  30. <button v-for="item in warningRegions" :key="item.value" :class="{ selected: form.region === item.value }" @click="selectRegion(item.value)">
  31. <span class="type-symbol"><el-icon><Location /></el-icon></span>
  32. <span>{{ item.label }}({{ item.code.toLowerCase() }})</span>
  33. <el-icon v-if="form.region === item.value" class="selected-icon"><Check /></el-icon>
  34. </button>
  35. </div>
  36. </div>
  37. <div class="field-block">
  38. <label>预警类型</label>
  39. <div class="type-grid">
  40. <button v-for="item in warningTypes" :key="item.value" :class="{ selected: form.warningType === item.value }" @click="selectType(item.value)">
  41. <span class="type-symbol"><el-icon><Warning /></el-icon></span>
  42. <span>{{ item.label.replace('预警', '') }}</span>
  43. <el-icon v-if="form.warningType === item.value" class="selected-icon"><Check /></el-icon>
  44. </button>
  45. </div>
  46. </div>
  47. <div v-if="isDrainage" class="field-block">
  48. <label>排水类型</label>
  49. <div class="type-grid drainage-grid">
  50. <button v-for="item in drainageSubtypes" :key="item.value" :class="{ selected: form.drainageType === item.value }" @click="selectDrainageType(item.value)">
  51. <span class="type-symbol"><el-icon><Connection /></el-icon></span>
  52. <span>{{ item.label }}({{ item.code.toLowerCase() }})</span>
  53. <el-icon v-if="form.drainageType === item.value" class="selected-icon"><Check /></el-icon>
  54. </button>
  55. </div>
  56. </div>
  57. <div class="generate-actions">
  58. <el-button @click="reset">重新选择</el-button>
  59. <el-button type="primary" :disabled="!canGenerate" :loading="loading" :icon="MagicStick" @click="generate">生成事项编码</el-button>
  60. </div>
  61. </el-card>
  62. <el-card class="result-card" shadow="never">
  63. <div class="result-heading"><span>生成结果</span><el-tag v-if="generatedCode" type="success">已生成</el-tag></div>
  64. <div v-if="generatedCode" class="code-result">
  65. <div class="result-icon"><el-icon><Key /></el-icon></div>
  66. <span class="result-label">预警事项编码</span>
  67. <strong>{{ generatedCode }}</strong>
  68. <button class="copy-button" @click="copyCode"><el-icon><CopyDocument /></el-icon> 复制编码</button>
  69. <div class="code-parts">
  70. <div><b>{{ regionPrefix }}</b><span>区域缩写</span></div>
  71. <div><b>{{ typePrefix }}</b><span>类型缩写</span></div>
  72. <div v-if="isDrainage"><b>{{ drainagePrefix }}</b><span>雨污分类</span></div>
  73. <div><b>{{ datePart }}</b><span>生成日期</span></div>
  74. <div><b>{{ sequencePart }}</b><span>流水号</span></div>
  75. </div>
  76. </div>
  77. <div v-else class="empty-result">
  78. <div class="empty-illustration"><el-icon><MagicStick /></el-icon></div>
  79. <strong>等待生成编码</strong>
  80. <p>完成左侧区域和类型选择后<br />点击“生成事项编码”获取唯一标识</p>
  81. </div>
  82. <div class="rule-box">
  83. <div class="rule-title"><el-icon><InfoFilled /></el-icon> 编码规则</div>
  84. <p><code>区域 + 类型 + [雨污分类] + YYYYMMDD + 4位流水号</code></p>
  85. <span>排水事项必须选择雨水或污水;同一组合和日期下流水号自动递增。</span>
  86. </div>
  87. </el-card>
  88. </div>
  89. </div>
  90. </template>
  91. <script setup name="WarningCodeGenerate">
  92. import { computed, onMounted, reactive, ref } from 'vue'
  93. import { ElMessage } from 'element-plus'
  94. import { Check, Connection, CopyDocument, InfoFilled, Key, Location, MagicStick, Warning } from '@element-plus/icons-vue'
  95. import { getDicts } from '@/api/system/dict/data'
  96. import { generateWarningItemCode } from '@/api/warning/list'
  97. import {
  98. drainageSubtypes,
  99. fallbackWarningTypes,
  100. normalizeWarningTypeDict,
  101. warningRegions,
  102. warningTypeCodes
  103. } from '../shared'
  104. const loading = ref(false)
  105. const generatedCode = ref('')
  106. const warningTypes = ref(fallbackWarningTypes)
  107. const form = reactive({ region: '', warningType: '', drainageType: '' })
  108. const isDrainage = computed(() => warningTypeCodes[form.warningType] === 'PS' || form.warningType === '排水')
  109. const canGenerate = computed(() => !!form.region && !!form.warningType && (!isDrainage.value || !!form.drainageType))
  110. const currentStep = computed(() => generatedCode.value ? 3 : form.warningType ? 2 : 1)
  111. const regionPrefix = computed(() => warningRegions.find(item => item.value === form.region)?.code || '--')
  112. const typePrefix = computed(() => warningTypeCodes[form.warningType] || '--')
  113. const drainagePrefix = computed(() => drainageSubtypes.find(item => item.value === form.drainageType)?.code || '--')
  114. const datePart = computed(() => generatedCode.value ? generatedCode.value.slice(-12, -4) : 'YYYYMMDD')
  115. const sequencePart = computed(() => generatedCode.value ? generatedCode.value.slice(-4) : '0001')
  116. async function loadTypes() {
  117. try {
  118. const res = await getDicts('warning_type')
  119. warningTypes.value = normalizeWarningTypeDict(res.data)
  120. } catch {
  121. warningTypes.value = fallbackWarningTypes
  122. }
  123. }
  124. function selectType(value) {
  125. form.warningType = value
  126. if (!isDrainage.value) form.drainageType = ''
  127. generatedCode.value = ''
  128. }
  129. function selectRegion(value) {
  130. form.region = value
  131. generatedCode.value = ''
  132. }
  133. function selectDrainageType(value) {
  134. form.drainageType = value
  135. generatedCode.value = ''
  136. }
  137. function reset() {
  138. form.region = ''
  139. form.warningType = ''
  140. form.drainageType = ''
  141. generatedCode.value = ''
  142. }
  143. async function generate() {
  144. if (!canGenerate.value) return
  145. loading.value = true
  146. try {
  147. const res = await generateWarningItemCode({
  148. region: form.region,
  149. warningType: form.warningType,
  150. drainageType: isDrainage.value ? form.drainageType : undefined
  151. })
  152. generatedCode.value = res.data || ''
  153. if (generatedCode.value) ElMessage.success('事项编码生成成功')
  154. } finally {
  155. loading.value = false
  156. }
  157. }
  158. async function copyCode() {
  159. if (!generatedCode.value) return
  160. await navigator.clipboard.writeText(generatedCode.value)
  161. ElMessage.success('编码已复制')
  162. }
  163. onMounted(loadTypes)
  164. </script>
  165. <style scoped lang="scss">
  166. .code-page { min-height: calc(100vh - 84px); padding: 20px; background: #f4f7fb; }
  167. .code-hero {
  168. display: flex;
  169. align-items: center;
  170. justify-content: space-between;
  171. padding: 28px 34px;
  172. color: #fff;
  173. border-radius: 18px;
  174. background: linear-gradient(120deg, #312e81, #4f46a5 52%, #7c3aed);
  175. box-shadow: 0 15px 40px rgba(73, 54, 163, .25);
  176. span { color: #d9d4ff; font-size: 11px; letter-spacing: 3px; }
  177. h1 { margin: 6px 0 8px; font-size: 28px; }
  178. p { margin: 0; color: #ddd9ff; }
  179. }
  180. .code-orbit { position: relative; display: grid; width: 90px; height: 90px; font-size: 28px; border: 1px solid rgba(255, 255, 255, .3); border-radius: 50%; background: rgba(255, 255, 255, .1); place-items: center; }
  181. .orbit-line { position: absolute; width: 112px; height: 38px; border: 1px solid rgba(255, 255, 255, .35); border-radius: 50%; transform: rotate(-32deg); }
  182. .generator-grid { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(340px, .7fr); gap: 16px; margin-top: 18px; }
  183. .steps-card, .result-card { border: 1px solid #e4e9f1; border-radius: 16px; }
  184. .step-heading, .result-heading { display: flex; align-items: center; justify-content: space-between; color: #1e293b; font-weight: 700; }
  185. .step-heading small { color: #8fa0b3; font: 11px Consolas, monospace; }
  186. .step-track { display: flex; align-items: center; margin: 26px 0 32px; }
  187. .track-item { display: flex; align-items: center; gap: 9px; color: #a0adbc; white-space: nowrap; span { display: grid; width: 30px; height: 30px; border: 1px solid #dce4ed; border-radius: 50%; place-items: center; } div { display: flex; flex-direction: column; gap: 3px; } strong { font-size: 12px; } small { font-size: 10px; } &.active { color: #4f46a5; span { color: #fff; border-color: #4f46a5; background: #4f46a5; } } }
  188. .track-line { flex: 1; height: 1px; margin: 0 12px; background: #e3e8ef; }
  189. .field-block { margin-bottom: 27px; label { display: block; margin-bottom: 12px; color: #334155; font-size: 14px; font-weight: 700; } }
  190. .type-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
  191. .type-grid button {
  192. position: relative;
  193. display: flex;
  194. align-items: center;
  195. gap: 8px;
  196. padding: 13px;
  197. color: #526477;
  198. text-align: left;
  199. border: 1px solid #e5eaf1;
  200. border-radius: 10px;
  201. background: #fff;
  202. cursor: pointer;
  203. &:hover, &.selected { color: #4f46a5; border-color: #9b94eb; background: #f5f3ff; }
  204. }
  205. .type-symbol { display: grid; width: 29px; height: 29px; color: #746ce1; border-radius: 8px; background: #ebe9ff; place-items: center; }
  206. .selected-icon { position: absolute; top: 8px; right: 8px; color: #4f46a5; }
  207. .level-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
  208. .level-grid button {
  209. display: flex;
  210. align-items: center;
  211. gap: 9px;
  212. padding: 13px 10px;
  213. color: #64748b;
  214. text-align: left;
  215. border: 1px solid #e6eaf0;
  216. border-radius: 10px;
  217. background: #fff;
  218. cursor: pointer;
  219. .level-dot { flex: 0 0 10px; width: 10px; height: 10px; border-radius: 50%; background: var(--level-color); }
  220. div { display: flex; flex-direction: column; gap: 3px; }
  221. strong { color: #334155; font-size: 12px; }
  222. small { color: #94a3b8; font-size: 10px; }
  223. .el-icon { margin-left: auto; color: var(--level-color); }
  224. &:hover, &.selected { border-color: var(--level-color); background: color-mix(in srgb, var(--level-color) 7%, #fff); }
  225. }
  226. .generate-actions { display: flex; justify-content: flex-end; gap: 10px; padding-top: 10px; border-top: 1px solid #edf0f4; }
  227. .result-card { min-height: 500px; }
  228. .result-heading { padding-bottom: 16px; border-bottom: 1px solid #edf0f4; small { color: #94a3b8; } }
  229. .code-result { display: flex; align-items: center; flex-direction: column; padding: 34px 8px 24px; text-align: center; }
  230. .result-icon { display: grid; width: 58px; height: 58px; margin-bottom: 14px; color: #4f46a5; font-size: 26px; border-radius: 18px; background: #efedff; place-items: center; }
  231. .result-label { color: #94a3b8; font-size: 12px; }
  232. .code-result > strong { margin: 9px 0 15px; color: #27205d; font: 25px Consolas, monospace; letter-spacing: 1px; }
  233. .copy-button { padding: 7px 12px; color: #4f46a5; border: 1px solid #c9c5fa; border-radius: 7px; background: #f7f6ff; cursor: pointer; }
  234. .code-parts { display: flex; width: 100%; justify-content: center; gap: 8px; margin-top: 28px; }
  235. .code-parts div { display: flex; min-width: 46px; flex-direction: column; gap: 4px; padding: 8px 5px; border: 1px solid #eceef4; border-radius: 7px; background: #fafbfe; }
  236. .code-parts b { color: #4f46a5; font: 12px Consolas, monospace; }
  237. .code-parts span { color: #94a3b8; font-size: 9px; }
  238. .empty-result { display: flex; align-items: center; flex-direction: column; padding: 70px 0 60px; text-align: center; }
  239. .empty-illustration { display: grid; width: 72px; height: 72px; margin-bottom: 14px; color: #a39df1; font-size: 34px; border-radius: 22px; background: #f2f1ff; place-items: center; }
  240. .empty-result strong { color: #48566b; }
  241. .empty-result p { color: #9aa7b6; font-size: 12px; line-height: 1.8; }
  242. .rule-box { padding: 14px; border-radius: 10px; background: #f7f7ff; }
  243. .rule-title { color: #5c55bc; font-size: 12px; font-weight: 700; }
  244. .rule-box p { margin: 8px 0; color: #5e6581; font-size: 11px; }
  245. .rule-box code { color: #4f46a5; font: 11px Consolas, monospace; }
  246. .rule-box > span { color: #9aa0b3; font-size: 10px; }
  247. @media (max-width: 1100px) { .generator-grid { grid-template-columns: 1fr; } }
  248. @media (max-width: 700px) { .type-grid, .level-grid { grid-template-columns: repeat(2, 1fr); } .track-item div { display: none; } }
  249. </style>