| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- <template>
- <div class="manage-page">
- <header class="manage-header">
- <div>
- <span class="header-kicker">CONFIGURATION CENTER</span>
- <h1>预警事项管理</h1>
- <p>维护预警编码、类型、等级、时长与处置环节,形成标准化预警清单。</p>
- </div>
- <el-button
- v-hasPermi="['warning:item:add']"
- type="primary"
- size="large"
- :icon="Plus"
- @click="openCreate"
- >
- 新增预警事项
- </el-button>
- </header>
- <div class="workspace">
- <aside class="side-panel">
- <div class="side-title">快速筛选</div>
- <el-input v-model="query.itemName" :prefix-icon="Search" placeholder="事项名称 / 编码" clearable @keyup.enter="handleQuery" />
- <div class="filter-label">预警类型</div>
- <el-select v-model="query.warningType" placeholder="全部类型" clearable @change="handleQuery">
- <el-option v-for="item in warningTypes" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- <div class="filter-label">预警级别</div>
- <div class="level-filter">
- <button
- v-for="item in warningLevels"
- :key="item.value"
- :class="{ active: query.warningLevel === item.value }"
- :style="{ '--level-color': item.color }"
- @click="toggleLevel(item.value)"
- >
- <span></span>{{ item.shortLabel }}
- </button>
- </div>
- <div class="filter-label">事项状态</div>
- <div class="status-filter">
- <button
- v-for="item in itemStatuses"
- :key="item.value"
- :class="{ active: query.status === item.value }"
- @click="toggleStatus(item.value)"
- >
- {{ item.label }}
- </button>
- </div>
- <el-button class="reset-button" :icon="Refresh" @click="resetQuery">清空筛选</el-button>
- </aside>
- <main class="main-panel">
- <el-card class="table-card" shadow="never">
- <template #header>
- <div class="table-heading">
- <div>
- <strong>事项配置清单</strong>
- <span>{{ total }} 条配置记录</span>
- </div>
- <el-button :icon="RefreshRight" circle @click="loadData" />
- </div>
- </template>
- <el-table v-loading="loading" :data="tableData" row-key="itemId">
- <el-table-column label="事项信息" min-width="270">
- <template #default="{ row }">
- <div class="item-cell">
- <div class="item-mark" :style="{ background: levelMeta(row.warningLevel).color }"></div>
- <div>
- <strong>{{ row.itemName }}</strong>
- <span>{{ row.itemCode }}</span>
- </div>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="类型 / 专项" min-width="200">
- <template #default="{ row }">
- <div class="stack-cell">
- <span>{{ typeLabel(row.warningType) }}</span>
- <small>{{ row.warningSpecial || '通用专项' }}</small>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="级别" width="110">
- <template #default="{ row }">
- <el-tag :type="levelMeta(row.warningLevel).tagType">{{ levelMeta(row.warningLevel).shortLabel }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="响应时长" width="110">
- <template #default="{ row }"><strong>{{ row.duration || '-' }}</strong> 小时</template>
- </el-table-column>
- <el-table-column label="环节数" width="90">
- <template #default="{ row }">{{ parseLinkConfig(row.linkConfig).length }} 个</template>
- </el-table-column>
- <el-table-column label="状态" width="100">
- <template #default="{ row }">
- <el-tag :type="statusMeta(row.status).type" effect="light">{{ statusMeta(row.status).label }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="250" fixed="right">
- <template #default="{ row }">
- <el-button v-hasPermi="['warning:item:query']" link type="primary" :icon="View" @click="openPreview(row)">详情</el-button>
- <el-button v-hasPermi="['warning:item:edit']" link type="primary" :icon="Edit" @click="openEdit(row)">修改</el-button>
- <el-dropdown v-hasPermi="['warning:item:edit']" trigger="click" @command="command => changeStatus(row, command)">
- <el-button link type="primary">状态<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="ENABLED" :disabled="row.status === 'ENABLED'">启用</el-dropdown-item>
- <el-dropdown-item command="DISABLED" :disabled="row.status === 'DISABLED'">禁用</el-dropdown-item>
- <el-dropdown-item command="INVALID" :disabled="row.status === 'INVALID'" divided>作废</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </template>
- </el-table-column>
- </el-table>
- <el-empty v-if="!loading && !tableData.length" description="暂无预警事项配置" />
- <pagination
- v-show="total > 0"
- v-model:page="query.pageNum"
- v-model:limit="query.pageSize"
- :total="total"
- @pagination="loadData"
- />
- </el-card>
- </main>
- </div>
- <el-drawer
- v-model="editorVisible"
- :title="form.itemId ? '修改预警事项' : '新增预警事项'"
- size="720px"
- destroy-on-close
- class="editor-drawer"
- >
- <el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="editor-form">
- <div class="form-section">
- <div class="form-section-title"><span>01</span> 基本信息</div>
- <el-row :gutter="16">
- <el-col :span="16">
- <el-form-item label="事项名称" prop="itemName">
- <el-input v-model="form.itemName" maxlength="100" show-word-limit placeholder="请输入清晰、可识别的事项名称" />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="所属行业" prop="industry">
- <el-select v-model="form.industry" placeholder="请选择">
- <el-option v-for="item in industryOptions" :key="item" :label="item" :value="item" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="16">
- <el-col :span="12">
- <el-form-item label="预警类型" prop="warningType">
- <el-select v-model="form.warningType" placeholder="请选择预警类型" @change="handleCodeDependencyChange">
- <el-option v-for="item in warningTypes" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="预警级别" prop="warningLevel">
- <el-select v-model="form.warningLevel" placeholder="请选择预警级别" @change="handleCodeDependencyChange">
- <el-option v-for="item in warningLevels" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="16">
- <el-col :span="14">
- <el-form-item label="事项编码" prop="itemCode">
- <el-input v-model="form.itemCode" placeholder="选择类型和等级后自动生成">
- <template #append>
- <el-button :icon="MagicStick" :loading="codeLoading" @click="generateCode">生成</el-button>
- </template>
- </el-input>
- </el-form-item>
- </el-col>
- <el-col :span="10">
- <el-form-item label="预警专项" prop="warningSpecial">
- <el-input v-model="form.warningSpecial" placeholder="如:城市内涝专项" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="16">
- <el-col :span="12">
- <el-form-item label="预警时长(小时)" prop="duration">
- <el-input-number v-model="form.duration" :min="1" :max="720" controls-position="right" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="初始状态" prop="status">
- <el-radio-group v-model="form.status">
- <el-radio-button label="ENABLED">启用</el-radio-button>
- <el-radio-button label="DISABLED">禁用</el-radio-button>
- </el-radio-group>
- </el-form-item>
- </el-col>
- </el-row>
- </div>
- <div class="form-section">
- <div class="form-section-title">
- <div><span>02</span> 预警环节配置</div>
- <el-button link type="primary" :icon="Plus" @click="addLink">增加环节</el-button>
- </div>
- <div class="link-builder">
- <div v-for="(link, index) in form.links" :key="index" class="link-row">
- <div class="step-number">{{ String(index + 1).padStart(2, '0') }}</div>
- <div class="link-fields">
- <el-row :gutter="12">
- <el-col :span="9"><el-input v-model="link.name" placeholder="环节名称" /></el-col>
- <el-col :span="8"><el-input v-model="link.handler" placeholder="责任角色/部门" /></el-col>
- <el-col :span="7">
- <el-input-number v-model="link.timeLimit" :min="1" :max="1440" controls-position="right" />
- <small>分钟</small>
- </el-col>
- </el-row>
- <el-input v-model="link.description" type="textarea" :rows="2" placeholder="描述该环节的触发条件和处置要求" />
- </div>
- <el-button type="danger" link :icon="Delete" :disabled="form.links.length === 1" @click="removeLink(index)" />
- </div>
- </div>
- </div>
- <div class="form-section">
- <div class="form-section-title"><span>03</span> 补充说明</div>
- <el-form-item label="备注">
- <el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="填写适用范围、口径或其他说明" />
- </el-form-item>
- </div>
- </el-form>
- <template #footer>
- <el-button @click="editorVisible = false">取消</el-button>
- <el-button type="primary" :loading="saving" @click="submitForm">保存事项配置</el-button>
- </template>
- </el-drawer>
- <el-dialog v-model="previewVisible" title="预警事项详情" width="760px">
- <div class="preview-title">
- <div>
- <span>{{ preview.itemCode }}</span>
- <h3>{{ preview.itemName }}</h3>
- </div>
- <el-tag :type="statusMeta(preview.status).type">{{ statusMeta(preview.status).label }}</el-tag>
- </div>
- <el-descriptions :column="3" border>
- <el-descriptions-item label="预警类型">{{ typeLabel(preview.warningType) }}</el-descriptions-item>
- <el-descriptions-item label="预警级别">{{ levelMeta(preview.warningLevel).label }}</el-descriptions-item>
- <el-descriptions-item label="时长">{{ preview.duration || '-' }} 小时</el-descriptions-item>
- <el-descriptions-item label="预警专项">{{ preview.warningSpecial || '-' }}</el-descriptions-item>
- <el-descriptions-item label="所属行业">{{ preview.industry || '-' }}</el-descriptions-item>
- <el-descriptions-item label="更新时间">{{ preview.updateTime || preview.createTime || '-' }}</el-descriptions-item>
- <el-descriptions-item label="备注" :span="3">{{ preview.remark || '-' }}</el-descriptions-item>
- </el-descriptions>
- <div class="preview-links">
- <div v-for="(link, index) in parseLinkConfig(preview.linkConfig)" :key="index" class="preview-link">
- <span>{{ index + 1 }}</span>
- <div><strong>{{ link.name }}</strong><small>{{ link.handler }} · {{ link.timeLimit || '-' }} 分钟</small></div>
- <p>{{ link.description }}</p>
- </div>
- </div>
- </el-dialog>
- </div>
- </template>
- <script setup name="WarningItemManage">
- import { onMounted, reactive, ref } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { ArrowDown, Delete, Edit, MagicStick, Plus, Refresh, RefreshRight, Search, View } from '@element-plus/icons-vue'
- import { getDicts } from '@/api/system/dict/data'
- import {
- addWarningItem,
- generateWarningItemCode,
- getWarningItemDetail,
- listWarningItems,
- updateWarningItem,
- updateWarningItemStatus
- } from '@/api/warning/list'
- import {
- cloneDefaultLinks,
- fallbackWarningTypes,
- industryOptions,
- itemStatuses,
- levelMeta,
- normalizeWarningTypeDict,
- optionLabel,
- parseLinkConfig,
- statusMeta,
- warningLevels
- } from '../shared'
- const loading = ref(false)
- const saving = ref(false)
- const codeLoading = ref(false)
- const editorVisible = ref(false)
- const previewVisible = ref(false)
- const formRef = ref()
- const tableData = ref([])
- const total = ref(0)
- const warningTypes = ref(fallbackWarningTypes)
- const preview = reactive({})
- const query = reactive({ pageNum: 1, pageSize: 10, itemName: '', warningType: '', warningLevel: '', status: '' })
- const form = reactive(createEmptyForm())
- const rules = {
- itemName: [{ required: true, message: '请输入事项名称', trigger: 'blur' }],
- warningType: [{ required: true, message: '请选择预警类型', trigger: 'change' }],
- warningLevel: [{ required: true, message: '请选择预警级别', trigger: 'change' }],
- itemCode: [{ required: true, message: '请生成或填写事项编码', trigger: 'blur' }],
- industry: [{ required: true, message: '请选择所属行业', trigger: 'change' }],
- duration: [{ required: true, message: '请设置预警时长', trigger: 'change' }]
- }
- function createEmptyForm() {
- return {
- itemId: '',
- itemCode: '',
- itemName: '',
- warningType: '',
- warningLevel: '',
- warningSpecial: '',
- industry: '',
- duration: 24,
- status: 'ENABLED',
- remark: '',
- links: cloneDefaultLinks()
- }
- }
- 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 loadData() {
- loading.value = true
- try {
- const params = { ...query }
- if (query.itemName && /^WI/i.test(query.itemName.trim())) {
- params.itemCode = query.itemName.trim()
- params.itemName = ''
- }
- const res = await listWarningItems(params)
- tableData.value = res.data?.records || []
- total.value = Number(res.data?.total || 0)
- } finally {
- loading.value = false
- }
- }
- function handleQuery() {
- query.pageNum = 1
- loadData()
- }
- function resetQuery() {
- Object.assign(query, { pageNum: 1, pageSize: 10, itemName: '', warningType: '', warningLevel: '', status: '' })
- loadData()
- }
- function toggleLevel(value) {
- query.warningLevel = query.warningLevel === value ? '' : value
- handleQuery()
- }
- function toggleStatus(value) {
- query.status = query.status === value ? '' : value
- handleQuery()
- }
- function resetForm(data = createEmptyForm()) {
- Object.keys(form).forEach(key => delete form[key])
- Object.assign(form, data)
- }
- function openCreate() {
- resetForm()
- editorVisible.value = true
- }
- async function openEdit(row) {
- const res = await getWarningItemDetail(row.itemId)
- const data = res.data || row
- resetForm({ ...data, links: parseLinkConfig(data.linkConfig).length ? parseLinkConfig(data.linkConfig) : cloneDefaultLinks() })
- editorVisible.value = true
- }
- async function openPreview(row) {
- const res = await getWarningItemDetail(row.itemId)
- Object.keys(preview).forEach(key => delete preview[key])
- Object.assign(preview, res.data || row)
- previewVisible.value = true
- }
- function handleCodeDependencyChange() {
- if (!form.itemId) form.itemCode = ''
- }
- async function generateCode() {
- if (!form.warningType || !form.warningLevel) {
- ElMessage.warning('请先选择预警类型和预警级别')
- return
- }
- codeLoading.value = true
- try {
- const res = await generateWarningItemCode(form.warningType, form.warningLevel)
- form.itemCode = res.data
- } finally {
- codeLoading.value = false
- }
- }
- function addLink() {
- form.links.push({ name: '', handler: '', timeLimit: 15, description: '' })
- }
- function removeLink(index) {
- form.links.splice(index, 1)
- }
- async function submitForm() {
- await formRef.value?.validate()
- if (form.links.some(item => !item.name || !item.handler || !item.timeLimit)) {
- ElMessage.warning('请完整填写每个预警环节的名称、责任方和时限')
- return
- }
- saving.value = true
- try {
- const { links, ...payload } = form
- payload.linkConfig = JSON.stringify(links)
- if (payload.itemId) {
- await updateWarningItem(payload)
- ElMessage.success('预警事项修改成功')
- } else {
- await addWarningItem(payload)
- ElMessage.success('预警事项新增成功')
- }
- editorVisible.value = false
- loadData()
- } finally {
- saving.value = false
- }
- }
- async function changeStatus(row, status) {
- const meta = statusMeta(status)
- await ElMessageBox.confirm(
- status === 'INVALID' ? '事项作废后将不再用于新预警,确认作废?' : `确认将该事项设置为“${meta.label}”?`,
- '状态变更确认',
- { type: status === 'INVALID' ? 'warning' : 'info' }
- )
- await updateWarningItemStatus(row.itemId, status)
- ElMessage.success('事项状态已更新')
- loadData()
- }
- onMounted(async () => {
- await loadTypes()
- loadData()
- })
- </script>
- <style scoped lang="scss">
- .manage-page {
- min-height: calc(100vh - 84px);
- padding: 20px;
- background: linear-gradient(180deg, #eef4fb 0, #f7f9fc 260px);
- }
- .manage-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 18px;
- padding: 26px 30px;
- color: #fff;
- border-radius: 18px;
- background: linear-gradient(120deg, #111d38, #173b72 58%, #2065a8);
- box-shadow: 0 14px 35px rgba(20, 50, 95, .2);
- h1 { margin: 5px 0 7px; font-size: 27px; }
- p { margin: 0; color: #b9cae1; }
- }
- .header-kicker { color: #65d4ff; font-size: 11px; letter-spacing: 2.6px; }
- .workspace { display: grid; grid-template-columns: 230px minmax(0, 1fr); gap: 16px; }
- .side-panel {
- align-self: start;
- padding: 20px;
- border: 1px solid #e1e8f1;
- border-radius: 16px;
- background: #fff;
- box-shadow: 0 8px 24px rgba(42, 67, 96, .06);
- :deep(.el-select) { width: 100%; }
- }
- .side-title { margin-bottom: 16px; color: #1e293b; font-size: 16px; font-weight: 700; }
- .filter-label { margin: 20px 0 9px; color: #64748b; font-size: 12px; font-weight: 600; }
- .level-filter {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
- button {
- padding: 8px;
- color: #64748b;
- border: 1px solid #e5eaf1;
- border-radius: 8px;
- background: #fff;
- cursor: pointer;
- span { display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: var(--level-color); }
- &.active { color: var(--level-color); border-color: var(--level-color); background: color-mix(in srgb, var(--level-color) 7%, #fff); }
- }
- }
- .status-filter {
- display: flex;
- flex-direction: column;
- gap: 7px;
- button {
- padding: 9px 12px;
- color: #64748b;
- text-align: left;
- border: 0;
- border-radius: 8px;
- background: #f6f8fb;
- cursor: pointer;
- &.active { color: #1d5fd1; font-weight: 600; background: #eaf2ff; }
- }
- }
- .reset-button { width: 100%; margin-top: 20px; }
- .table-card { border: 1px solid #e1e8f1; border-radius: 16px; }
- .table-heading {
- display: flex;
- align-items: center;
- justify-content: space-between;
- div { display: flex; flex-direction: column; gap: 3px; }
- strong { color: #1e293b; font-size: 16px; }
- span { color: #94a3b8; font-size: 12px; }
- }
- .table-card :deep(th.el-table__cell) { color: #52647a; background: #f7f9fc; }
- .item-cell {
- display: flex;
- align-items: center;
- gap: 12px;
- .item-mark { width: 5px; height: 38px; border-radius: 8px; }
- div:last-child { display: flex; flex-direction: column; gap: 4px; }
- strong { color: #1f2d3d; }
- span { color: #8a98aa; font-family: Consolas, monospace; font-size: 12px; }
- }
- .stack-cell { display: flex; flex-direction: column; gap: 4px; color: #334155; small { color: #94a3b8; } }
- .form-section {
- margin-bottom: 18px;
- padding: 20px;
- border: 1px solid #e5eaf1;
- border-radius: 14px;
- background: #fff;
- }
- .form-section-title {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 18px;
- color: #1e293b;
- font-weight: 700;
- > span, > div span {
- display: inline-grid;
- width: 28px;
- height: 28px;
- margin-right: 8px;
- color: #2563eb;
- font-size: 11px;
- border-radius: 8px;
- background: #eaf2ff;
- place-items: center;
- }
- }
- .editor-form :deep(.el-select), .editor-form :deep(.el-input-number) { width: 100%; }
- .link-builder { display: flex; flex-direction: column; gap: 12px; }
- .link-row {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 14px;
- border: 1px solid #e7ebf1;
- border-radius: 12px;
- background: #f8fafc;
- }
- .step-number {
- display: grid;
- flex: 0 0 34px;
- width: 34px;
- height: 34px;
- color: #fff;
- font-size: 12px;
- border-radius: 10px;
- background: #255fc1;
- place-items: center;
- }
- .link-fields {
- flex: 1;
- :deep(.el-textarea) { margin-top: 10px; }
- .el-col:last-child { display: flex; align-items: center; gap: 6px; small { color: #94a3b8; } }
- }
- .preview-title {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 16px;
- padding: 18px;
- border-radius: 12px;
- background: linear-gradient(110deg, #eaf2ff, #f7fbff);
- span { color: #5f78a0; font: 12px Consolas, monospace; }
- h3 { margin: 5px 0 0; color: #1d3557; }
- }
- .preview-links {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 10px;
- margin-top: 18px;
- }
- .preview-link {
- display: grid;
- grid-template-columns: 30px 1fr;
- gap: 8px;
- padding: 13px;
- border: 1px solid #e6ebf2;
- border-radius: 10px;
- > span { display: grid; width: 28px; height: 28px; color: #2563eb; border-radius: 8px; background: #edf4ff; place-items: center; }
- div { display: flex; flex-direction: column; gap: 3px; }
- small { color: #8a98aa; }
- p { grid-column: 2; margin: 3px 0 0; color: #64748b; line-height: 1.5; }
- }
- @media (max-width: 900px) {
- .workspace { grid-template-columns: 1fr; }
- .manage-header { align-items: flex-start; gap: 16px; }
- }
- </style>
|