| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- <template>
- <div class="code-page">
- <div class="code-hero">
- <div>
- <span>IDENTITY GENERATOR</span>
- <h1>事项编码生成</h1>
- <p>选择区域、预警类型及排水雨污分类,系统按统一编码规则生成唯一事项编码。</p>
- </div>
- <div class="code-orbit">
- <div class="orbit-line"></div>
- <el-icon><Connection /></el-icon>
- </div>
- </div>
- <div class="generator-grid">
- <el-card class="steps-card" shadow="never">
- <div class="step-heading">
- <span>编码配置</span>
- <small>STEP {{ currentStep }}/3</small>
- </div>
- <div class="step-track">
- <div class="track-item active"><span>01</span><div><strong>选择区域</strong><small>确定编码所属区域</small></div></div>
- <div class="track-line"></div>
- <div class="track-item" :class="{ active: form.warningType }"><span>02</span><div><strong>选择类型</strong><small>确定预警所属领域</small></div></div>
- <div class="track-line"></div>
- <div class="track-item" :class="{ active: generatedCode }"><span>03</span><div><strong>生成编码</strong><small>得到唯一事项标识</small></div></div>
- </div>
- <div class="field-block">
- <label>区域</label>
- <div class="type-grid region-grid">
- <button v-for="item in warningRegions" :key="item.value" :class="{ selected: form.region === item.value }" @click="selectRegion(item.value)">
- <span class="type-symbol"><el-icon><Location /></el-icon></span>
- <span>{{ item.label }}({{ item.code.toLowerCase() }})</span>
- <el-icon v-if="form.region === item.value" class="selected-icon"><Check /></el-icon>
- </button>
- </div>
- </div>
- <div class="field-block">
- <label>预警类型</label>
- <div class="type-grid">
- <button v-for="item in warningTypes" :key="item.value" :class="{ selected: form.warningType === item.value }" @click="selectType(item.value)">
- <span class="type-symbol"><el-icon><Warning /></el-icon></span>
- <span>{{ item.label.replace('预警', '') }}</span>
- <el-icon v-if="form.warningType === item.value" class="selected-icon"><Check /></el-icon>
- </button>
- </div>
- </div>
- <div v-if="isDrainage" class="field-block">
- <label>排水类型</label>
- <div class="type-grid drainage-grid">
- <button v-for="item in drainageSubtypes" :key="item.value" :class="{ selected: form.drainageType === item.value }" @click="selectDrainageType(item.value)">
- <span class="type-symbol"><el-icon><Connection /></el-icon></span>
- <span>{{ item.label }}({{ item.code.toLowerCase() }})</span>
- <el-icon v-if="form.drainageType === item.value" class="selected-icon"><Check /></el-icon>
- </button>
- </div>
- </div>
- <div class="generate-actions">
- <el-button @click="reset">重新选择</el-button>
- <el-button type="primary" :disabled="!canGenerate" :loading="loading" :icon="MagicStick" @click="generate">生成事项编码</el-button>
- </div>
- </el-card>
- <el-card class="result-card" shadow="never">
- <div class="result-heading"><span>生成结果</span><el-tag v-if="generatedCode" type="success">已生成</el-tag></div>
- <div v-if="generatedCode" class="code-result">
- <div class="result-icon"><el-icon><Key /></el-icon></div>
- <span class="result-label">预警事项编码</span>
- <strong>{{ generatedCode }}</strong>
- <button class="copy-button" @click="copyCode"><el-icon><CopyDocument /></el-icon> 复制编码</button>
- <div class="code-parts">
- <div><b>{{ regionPrefix }}</b><span>区域缩写</span></div>
- <div><b>{{ typePrefix }}</b><span>类型缩写</span></div>
- <div v-if="isDrainage"><b>{{ drainagePrefix }}</b><span>雨污分类</span></div>
- <div><b>{{ datePart }}</b><span>生成日期</span></div>
- <div><b>{{ sequencePart }}</b><span>流水号</span></div>
- </div>
- </div>
- <div v-else class="empty-result">
- <div class="empty-illustration"><el-icon><MagicStick /></el-icon></div>
- <strong>等待生成编码</strong>
- <p>完成左侧区域和类型选择后<br />点击“生成事项编码”获取唯一标识</p>
- </div>
- <div class="rule-box">
- <div class="rule-title"><el-icon><InfoFilled /></el-icon> 编码规则</div>
- <p><code>区域 + 类型 + [雨污分类] + YYYYMMDD + 4位流水号</code></p>
- <span>排水事项必须选择雨水或污水;同一组合和日期下流水号自动递增。</span>
- </div>
- </el-card>
- </div>
- </div>
- </template>
- <script setup name="WarningCodeGenerate">
- import { computed, onMounted, reactive, ref } from 'vue'
- import { ElMessage } from 'element-plus'
- import { Check, Connection, CopyDocument, InfoFilled, Key, Location, MagicStick, Warning } from '@element-plus/icons-vue'
- import { getDicts } from '@/api/system/dict/data'
- import { generateWarningItemCode } from '@/api/warning/list'
- import {
- drainageSubtypes,
- fallbackWarningTypes,
- normalizeWarningTypeDict,
- warningRegions,
- warningTypeCodes
- } from '../shared'
- const loading = ref(false)
- const generatedCode = ref('')
- const warningTypes = ref(fallbackWarningTypes)
- const form = reactive({ region: '', warningType: '', drainageType: '' })
- const isDrainage = computed(() => warningTypeCodes[form.warningType] === 'PS' || form.warningType === '排水')
- const canGenerate = computed(() => !!form.region && !!form.warningType && (!isDrainage.value || !!form.drainageType))
- const currentStep = computed(() => generatedCode.value ? 3 : form.warningType ? 2 : 1)
- const regionPrefix = computed(() => warningRegions.find(item => item.value === form.region)?.code || '--')
- const typePrefix = computed(() => warningTypeCodes[form.warningType] || '--')
- const drainagePrefix = computed(() => drainageSubtypes.find(item => item.value === form.drainageType)?.code || '--')
- const datePart = computed(() => generatedCode.value ? generatedCode.value.slice(-12, -4) : 'YYYYMMDD')
- const sequencePart = computed(() => generatedCode.value ? generatedCode.value.slice(-4) : '0001')
- async function loadTypes() {
- try {
- const res = await getDicts('warning_type')
- warningTypes.value = normalizeWarningTypeDict(res.data)
- } catch {
- warningTypes.value = fallbackWarningTypes
- }
- }
- function selectType(value) {
- form.warningType = value
- if (!isDrainage.value) form.drainageType = ''
- generatedCode.value = ''
- }
- function selectRegion(value) {
- form.region = value
- generatedCode.value = ''
- }
- function selectDrainageType(value) {
- form.drainageType = value
- generatedCode.value = ''
- }
- function reset() {
- form.region = ''
- form.warningType = ''
- form.drainageType = ''
- generatedCode.value = ''
- }
- async function generate() {
- if (!canGenerate.value) return
- loading.value = true
- try {
- const res = await generateWarningItemCode({
- region: form.region,
- warningType: form.warningType,
- drainageType: isDrainage.value ? form.drainageType : undefined
- })
- generatedCode.value = res.data || ''
- if (generatedCode.value) ElMessage.success('事项编码生成成功')
- } finally {
- loading.value = false
- }
- }
- async function copyCode() {
- if (!generatedCode.value) return
- await navigator.clipboard.writeText(generatedCode.value)
- ElMessage.success('编码已复制')
- }
- onMounted(loadTypes)
- </script>
- <style scoped lang="scss">
- .code-page { min-height: calc(100vh - 84px); padding: 20px; background: #f4f7fb; }
- .code-hero {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 28px 34px;
- color: #fff;
- border-radius: 18px;
- background: linear-gradient(120deg, #312e81, #4f46a5 52%, #7c3aed);
- box-shadow: 0 15px 40px rgba(73, 54, 163, .25);
- span { color: #d9d4ff; font-size: 11px; letter-spacing: 3px; }
- h1 { margin: 6px 0 8px; font-size: 28px; }
- p { margin: 0; color: #ddd9ff; }
- }
- .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; }
- .orbit-line { position: absolute; width: 112px; height: 38px; border: 1px solid rgba(255, 255, 255, .35); border-radius: 50%; transform: rotate(-32deg); }
- .generator-grid { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(340px, .7fr); gap: 16px; margin-top: 18px; }
- .steps-card, .result-card { border: 1px solid #e4e9f1; border-radius: 16px; }
- .step-heading, .result-heading { display: flex; align-items: center; justify-content: space-between; color: #1e293b; font-weight: 700; }
- .step-heading small { color: #8fa0b3; font: 11px Consolas, monospace; }
- .step-track { display: flex; align-items: center; margin: 26px 0 32px; }
- .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; } } }
- .track-line { flex: 1; height: 1px; margin: 0 12px; background: #e3e8ef; }
- .field-block { margin-bottom: 27px; label { display: block; margin-bottom: 12px; color: #334155; font-size: 14px; font-weight: 700; } }
- .type-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
- .type-grid button {
- position: relative;
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 13px;
- color: #526477;
- text-align: left;
- border: 1px solid #e5eaf1;
- border-radius: 10px;
- background: #fff;
- cursor: pointer;
- &:hover, &.selected { color: #4f46a5; border-color: #9b94eb; background: #f5f3ff; }
- }
- .type-symbol { display: grid; width: 29px; height: 29px; color: #746ce1; border-radius: 8px; background: #ebe9ff; place-items: center; }
- .selected-icon { position: absolute; top: 8px; right: 8px; color: #4f46a5; }
- .level-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
- .level-grid button {
- display: flex;
- align-items: center;
- gap: 9px;
- padding: 13px 10px;
- color: #64748b;
- text-align: left;
- border: 1px solid #e6eaf0;
- border-radius: 10px;
- background: #fff;
- cursor: pointer;
- .level-dot { flex: 0 0 10px; width: 10px; height: 10px; border-radius: 50%; background: var(--level-color); }
- div { display: flex; flex-direction: column; gap: 3px; }
- strong { color: #334155; font-size: 12px; }
- small { color: #94a3b8; font-size: 10px; }
- .el-icon { margin-left: auto; color: var(--level-color); }
- &:hover, &.selected { border-color: var(--level-color); background: color-mix(in srgb, var(--level-color) 7%, #fff); }
- }
- .generate-actions { display: flex; justify-content: flex-end; gap: 10px; padding-top: 10px; border-top: 1px solid #edf0f4; }
- .result-card { min-height: 500px; }
- .result-heading { padding-bottom: 16px; border-bottom: 1px solid #edf0f4; small { color: #94a3b8; } }
- .code-result { display: flex; align-items: center; flex-direction: column; padding: 34px 8px 24px; text-align: center; }
- .result-icon { display: grid; width: 58px; height: 58px; margin-bottom: 14px; color: #4f46a5; font-size: 26px; border-radius: 18px; background: #efedff; place-items: center; }
- .result-label { color: #94a3b8; font-size: 12px; }
- .code-result > strong { margin: 9px 0 15px; color: #27205d; font: 25px Consolas, monospace; letter-spacing: 1px; }
- .copy-button { padding: 7px 12px; color: #4f46a5; border: 1px solid #c9c5fa; border-radius: 7px; background: #f7f6ff; cursor: pointer; }
- .code-parts { display: flex; width: 100%; justify-content: center; gap: 8px; margin-top: 28px; }
- .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; }
- .code-parts b { color: #4f46a5; font: 12px Consolas, monospace; }
- .code-parts span { color: #94a3b8; font-size: 9px; }
- .empty-result { display: flex; align-items: center; flex-direction: column; padding: 70px 0 60px; text-align: center; }
- .empty-illustration { display: grid; width: 72px; height: 72px; margin-bottom: 14px; color: #a39df1; font-size: 34px; border-radius: 22px; background: #f2f1ff; place-items: center; }
- .empty-result strong { color: #48566b; }
- .empty-result p { color: #9aa7b6; font-size: 12px; line-height: 1.8; }
- .rule-box { padding: 14px; border-radius: 10px; background: #f7f7ff; }
- .rule-title { color: #5c55bc; font-size: 12px; font-weight: 700; }
- .rule-box p { margin: 8px 0; color: #5e6581; font-size: 11px; }
- .rule-box code { color: #4f46a5; font: 11px Consolas, monospace; }
- .rule-box > span { color: #9aa0b3; font-size: 10px; }
- @media (max-width: 1100px) { .generator-grid { grid-template-columns: 1fr; } }
- @media (max-width: 700px) { .type-grid, .level-grid { grid-template-columns: repeat(2, 1fr); } .track-item div { display: none; } }
- </style>
|