|
|
@@ -0,0 +1,628 @@
|
|
|
+<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>
|
|
|
+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>
|