| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- <template>
- <div class="detail-page">
- <section class="detail-hero">
- <div>
- <span>WARNING ITEM PROFILE</span>
- <h1>预警事项详情</h1>
- <p>通过事项编码查看完整配置、处置环节和可选预警原因。</p>
- </div>
- <div class="search-box">
- <el-input v-model="itemCode" placeholder="输入预警事项编码" clearable @keyup.enter="search">
- <template #prepend><el-icon><Search /></el-icon></template>
- <template #append><el-button type="primary" @click="search">查询详情</el-button></template>
- </el-input>
- </div>
- </section>
- <template v-if="hasDetail">
- <section class="identity-card">
- <div class="identity-main">
- <div class="identity-icon" :style="{ '--level-color': levelMeta(detail.warningLevel).color }"><el-icon><Warning /></el-icon></div>
- <div>
- <span class="identity-code">{{ detail.itemCode }}</span>
- <h2>{{ detail.itemName }}</h2>
- <div class="identity-tags">
- <el-tag :type="levelMeta(detail.warningLevel).tagType">{{ levelMeta(detail.warningLevel).label }}</el-tag>
- <el-tag effect="plain">{{ typeLabel(detail.warningType) }}</el-tag>
- <el-tag :type="statusMeta(detail.status).type">{{ statusMeta(detail.status).label }}</el-tag>
- </div>
- </div>
- </div>
- <div class="identity-meta">
- <div><span>所属行业</span><strong>{{ detail.industry || '-' }}</strong></div>
- <div><span>预警专项</span><strong>{{ detail.warningSpecial || '通用专项' }}</strong></div>
- <div><span>响应时长</span><strong>{{ detail.duration || '-' }}<small> 小时</small></strong></div>
- </div>
- </section>
- <div class="detail-grid">
- <main>
- <el-card class="info-card" shadow="never">
- <template #header><div class="card-heading"><span>环节配置</span><small>{{ detailLinks.length }} 个标准环节</small></div></template>
- <div v-if="detailLinks.length" class="process-flow">
- <div v-for="(link, index) in detailLinks" :key="index" class="process-item">
- <div class="process-node">{{ String(index + 1).padStart(2, '0') }}</div>
- <div class="process-line" v-if="index < detailLinks.length - 1"></div>
- <div class="process-content">
- <div class="process-title"><strong>{{ link.name }}</strong><span>{{ link.timeLimit || '-' }} 分钟</span></div>
- <small>责任方:{{ link.handler || '未配置' }}</small>
- <p>{{ link.description || '暂无环节说明' }}</p>
- </div>
- </div>
- </div>
- <el-empty v-else description="暂未配置环节" />
- </el-card>
- <el-card class="info-card" shadow="never">
- <template #header><div class="card-heading"><span>事项备注</span></div></template>
- <div class="remark-content">{{ detail.remark || '暂无补充说明' }}</div>
- </el-card>
- </main>
- <aside>
- <el-card class="info-card reason-card" shadow="never">
- <template #header><div class="card-heading"><span>可选预警原因</span><small>{{ reasons.length }} 条</small></div></template>
- <div v-loading="reasonLoading">
- <div v-for="reason in reasons" :key="reason.reasonId" class="reason-row">
- <span class="reason-index">{{ reason.sortOrder ?? '-' }}</span>
- <div><strong>{{ reason.reasonName }}</strong><p>{{ reason.description || '暂无描述' }}</p></div>
- </div>
- <el-empty v-if="!reasonLoading && !reasons.length" description="暂无启用原因" :image-size="60" />
- </div>
- </el-card>
- <el-card class="info-card meta-card" shadow="never">
- <template #header><div class="card-heading"><span>记录信息</span></div></template>
- <el-descriptions :column="1" size="small">
- <el-descriptions-item label="创建时间">{{ detail.createTime || '-' }}</el-descriptions-item>
- <el-descriptions-item label="创建人">{{ detail.createBy || '-' }}</el-descriptions-item>
- <el-descriptions-item label="更新时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
- <el-descriptions-item label="更新人">{{ detail.updateBy || '-' }}</el-descriptions-item>
- </el-descriptions>
- </el-card>
- </aside>
- </div>
- </template>
- <el-card v-else class="empty-detail" shadow="never">
- <div class="empty-detail-icon"><el-icon><Search /></el-icon></div>
- <h3>{{ searched ? '未找到对应事项' : '输入编码开始查询' }}</h3>
- <p>{{ searched ? '请检查事项编码是否正确,或前往事项信息查询浏览完整清单。' : '事项编码是预警清单的唯一标识,支持精确查询。' }}</p>
- </el-card>
- </div>
- </template>
- <script setup name="WarningItemDetail">
- import { computed, onMounted, reactive, ref } from 'vue'
- import { useRoute } from 'vue-router'
- import { ElMessage } from 'element-plus'
- import { Search, Warning } from '@element-plus/icons-vue'
- import { getDicts } from '@/api/system/dict/data'
- import { getWarningItemByCode, listReasonsByTypeAndLevel } from '@/api/warning/list'
- import {
- fallbackWarningTypes,
- levelMeta,
- normalizeWarningTypeDict,
- optionLabel,
- parseLinkConfig,
- statusMeta
- } from '../shared'
- const route = useRoute()
- const itemCode = ref(String(route.query.code || ''))
- const searched = ref(false)
- const reasonLoading = ref(false)
- const warningTypes = ref(fallbackWarningTypes)
- const reasons = ref([])
- const detail = reactive({})
- const hasDetail = computed(() => !!detail.itemId)
- const detailLinks = computed(() => parseLinkConfig(detail.linkConfig))
- function typeLabel(value) {
- return optionLabel(warningTypes.value, value)
- }
- async function loadTypes() {
- try {
- const res = await getDicts('warning_type')
- warningTypes.value = normalizeWarningTypeDict(res.data)
- } catch {
- warningTypes.value = fallbackWarningTypes
- }
- }
- async function search() {
- if (!itemCode.value.trim()) {
- ElMessage.warning('请输入预警事项编码')
- return
- }
- searched.value = true
- Object.keys(detail).forEach(key => delete detail[key])
- reasons.value = []
- try {
- const res = await getWarningItemByCode(itemCode.value.trim())
- Object.assign(detail, res.data || {})
- await loadReasons()
- } catch {
- // request 统一提示错误,页面保留空状态
- }
- }
- async function loadReasons() {
- reasonLoading.value = true
- try {
- const res = await listReasonsByTypeAndLevel(detail.warningType, detail.warningLevel)
- reasons.value = res.data || []
- } finally {
- reasonLoading.value = false
- }
- }
- onMounted(async () => {
- await loadTypes()
- if (itemCode.value) search()
- })
- </script>
- <style scoped lang="scss">
- .detail-page { min-height: calc(100vh - 84px); padding: 20px; background: #f4f7fb; }
- .detail-hero {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 25px;
- padding: 28px 32px;
- color: #fff;
- border-radius: 18px;
- background: linear-gradient(120deg, #172554, #1e40af 55%, #2563eb);
- box-shadow: 0 14px 38px rgba(29, 64, 160, .23);
- > div:first-child { flex: 1; }
- span { color: #9bc7ff; font-size: 11px; letter-spacing: 3px; }
- h1 { margin: 6px 0 8px; font-size: 28px; }
- p { margin: 0; color: #cad9f7; }
- }
- .search-box { width: 390px; :deep(.el-input-group__prepend) { padding: 0 13px; color: #4f6ea8; background: #fff; } :deep(.el-input__wrapper) { box-shadow: none; } }
- .identity-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20px;
- margin: 16px 0;
- padding: 22px 26px;
- border: 1px solid #dfe7f1;
- border-radius: 16px;
- background: #fff;
- box-shadow: 0 8px 25px rgba(39, 61, 91, .06);
- }
- .identity-main { display: flex; align-items: center; gap: 16px; }
- .identity-icon { display: grid; width: 58px; height: 58px; color: var(--level-color); font-size: 27px; border: 1px solid color-mix(in srgb, var(--level-color) 25%, transparent); border-radius: 17px; background: color-mix(in srgb, var(--level-color) 10%, #fff); place-items: center; }
- .identity-code { color: #8392a7; font: 12px Consolas, monospace; }
- .identity-main h2 { margin: 5px 0 10px; color: #20314b; font-size: 22px; }
- .identity-tags { display: flex; gap: 7px; }
- .identity-meta { display: flex; gap: 34px; }
- .identity-meta div { display: flex; flex-direction: column; gap: 6px; }
- .identity-meta span { color: #94a3b8; font-size: 11px; }
- .identity-meta strong { color: #334155; font-size: 15px; }
- .identity-meta small { color: #94a3b8; font-size: 11px; }
- .detail-grid { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(320px, .55fr); gap: 16px; }
- .info-card { margin-bottom: 16px; border: 1px solid #e2e8f0; border-radius: 14px; }
- .card-heading { display: flex; align-items: center; justify-content: space-between; color: #1f3149; font-weight: 700; small { color: #94a3b8; font-size: 11px; font-weight: 400; } }
- .process-flow { padding: 8px 8px 12px; }
- .process-item { position: relative; display: flex; gap: 16px; min-height: 115px; }
- .process-node { z-index: 1; display: grid; flex: 0 0 42px; width: 42px; height: 42px; color: #fff; font: 12px Consolas, monospace; border: 4px solid #e9f0ff; border-radius: 50%; background: #3168d8; place-items: center; }
- .process-line { position: absolute; top: 42px; bottom: 0; left: 20px; width: 2px; background: #dbe6f8; }
- .process-content { flex: 1; padding: 3px 0 22px; }
- .process-title { display: flex; align-items: center; justify-content: space-between; strong { color: #273a54; } span { color: #3168d8; font: 12px Consolas, monospace; } }
- .process-content > small { display: block; margin: 7px 0; color: #8795a8; }
- .process-content p { margin: 0; color: #66778d; line-height: 1.65; }
- .remark-content { min-height: 90px; color: #66778d; line-height: 1.8; }
- .reason-row { display: flex; gap: 10px; padding: 13px 0; border-bottom: 1px dashed #e8edf3; &:last-child { border-bottom: 0; } }
- .reason-index { display: grid; flex: 0 0 25px; width: 25px; height: 25px; color: #2563eb; font-size: 11px; border-radius: 7px; background: #edf4ff; place-items: center; }
- .reason-row div { min-width: 0; }
- .reason-row strong { color: #334155; font-size: 13px; }
- .reason-row p { overflow: hidden; margin: 5px 0 0; color: #94a3b8; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
- .empty-detail { display: grid; min-height: 420px; margin-top: 16px; place-items: center; text-align: center; }
- .empty-detail-icon { display: grid; width: 72px; height: 72px; color: #7191d1; font-size: 35px; border-radius: 23px; background: #edf3ff; place-items: center; }
- .empty-detail h3 { margin: 16px 0 6px; color: #334155; }
- .empty-detail p { margin: 0; color: #94a3b8; font-size: 12px; }
- @media (max-width: 1000px) { .detail-grid { grid-template-columns: 1fr; } .identity-meta { gap: 16px; } }
- @media (max-width: 720px) { .detail-hero, .identity-card { align-items: flex-start; flex-direction: column; } .search-box { width: 100%; } .identity-meta { width: 100%; justify-content: space-between; } }
- </style>
|