index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <template>
  2. <div class="manage-page">
  3. <header class="manage-header">
  4. <div>
  5. <span class="header-kicker">CONFIGURATION CENTER</span>
  6. <h1>预警事项管理</h1>
  7. <p>维护预警编码、类型、等级、时长与处置环节,形成标准化预警清单。</p>
  8. </div>
  9. <el-button
  10. v-hasPermi="['warning:item:add']"
  11. type="primary"
  12. size="large"
  13. :icon="Plus"
  14. @click="openCreate"
  15. >
  16. 新增预警事项
  17. </el-button>
  18. </header>
  19. <div class="workspace">
  20. <aside class="side-panel">
  21. <div class="side-title">快速筛选</div>
  22. <el-input v-model="query.itemName" :prefix-icon="Search" placeholder="事项名称" clearable @keyup.enter="handleQuery" />
  23. <div class="filter-label">事项编码</div>
  24. <el-input v-model="query.itemCode" placeholder="输入事项编码" clearable @keyup.enter="handleQuery" />
  25. <div class="filter-label">预警专项</div>
  26. <el-input v-model="query.warningSpecial" placeholder="输入预警专项" clearable @keyup.enter="handleQuery" />
  27. <div class="filter-label">预警类型</div>
  28. <el-select v-model="query.warningType" placeholder="全部类型" clearable @change="handleQuery">
  29. <el-option v-for="item in warningTypes" :key="item.value" :label="item.label" :value="item.value" />
  30. </el-select>
  31. <div class="filter-label">预警级别</div>
  32. <div class="level-filter">
  33. <button
  34. v-for="item in warningLevels"
  35. :key="item.value"
  36. :class="{ active: query.warningLevel === item.value }"
  37. :style="{ '--level-color': item.color }"
  38. @click="toggleLevel(item.value)"
  39. >
  40. <span></span>{{ item.shortLabel }}
  41. </button>
  42. </div>
  43. <div class="filter-label">事项状态</div>
  44. <div class="status-filter">
  45. <button
  46. v-for="item in itemStatuses"
  47. :key="item.value"
  48. :class="{ active: query.status === item.value }"
  49. @click="toggleStatus(item.value)"
  50. >
  51. {{ item.label }}
  52. </button>
  53. </div>
  54. <el-button class="reset-button" :icon="Refresh" @click="resetQuery">清空筛选</el-button>
  55. </aside>
  56. <main class="main-panel">
  57. <el-card class="table-card" shadow="never">
  58. <template #header>
  59. <div class="table-heading">
  60. <div>
  61. <strong>事项配置清单</strong>
  62. <span>{{ total }} 条配置记录</span>
  63. </div>
  64. <el-button :icon="RefreshRight" circle @click="loadData" />
  65. </div>
  66. </template>
  67. <el-table v-loading="loading" :data="tableData" row-key="itemId">
  68. <el-table-column label="事项信息" min-width="270">
  69. <template #default="{ row }">
  70. <div class="item-cell">
  71. <div class="item-mark" :style="{ background: levelMeta(row.warningLevel).color }"></div>
  72. <div>
  73. <strong>{{ row.itemName }}</strong>
  74. <span>{{ row.itemCode }}</span>
  75. </div>
  76. </div>
  77. </template>
  78. </el-table-column>
  79. <el-table-column label="类型 / 专项" min-width="200">
  80. <template #default="{ row }">
  81. <div class="stack-cell">
  82. <span>{{ typeLabel(row.warningType) }}</span>
  83. <small>{{ row.warningSpecial || '通用专项' }}</small>
  84. </div>
  85. </template>
  86. </el-table-column>
  87. <el-table-column label="级别" width="110">
  88. <template #default="{ row }">
  89. <el-tag :type="levelMeta(row.warningLevel).tagType">{{ levelMeta(row.warningLevel).shortLabel }}</el-tag>
  90. </template>
  91. </el-table-column>
  92. <el-table-column label="处置时限" width="110">
  93. <template #default="{ row }"><strong>{{ row.duration || '-' }}</strong> 小时</template>
  94. </el-table-column>
  95. <el-table-column label="环节数" width="90">
  96. <template #default="{ row }">{{ parseLinkConfig(row.linkConfig).length }} 个</template>
  97. </el-table-column>
  98. <el-table-column label="状态" width="100">
  99. <template #default="{ row }">
  100. <el-tag :type="statusMeta(row.status).type" effect="light">{{ statusMeta(row.status).label }}</el-tag>
  101. </template>
  102. </el-table-column>
  103. <el-table-column label="操作" width="250" fixed="right">
  104. <template #default="{ row }">
  105. <el-button v-hasPermi="['warning:item:query']" link type="primary" :icon="View" @click="openPreview(row)">详情</el-button>
  106. <el-button v-hasPermi="['warning:item:edit']" link type="primary" :icon="Edit" :disabled="row.status === 'INVALID'" @click="openEdit(row)">修改</el-button>
  107. <el-dropdown v-hasPermi="['warning:item:edit']" trigger="click" @command="command => changeStatus(row, command)">
  108. <el-button link type="primary">状态<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
  109. <template #dropdown>
  110. <el-dropdown-menu>
  111. <el-dropdown-item command="ENABLED" :disabled="row.status === 'ENABLED' || row.status === 'INVALID'">启用</el-dropdown-item>
  112. <el-dropdown-item command="DISABLED" :disabled="row.status === 'DISABLED' || row.status === 'INVALID'">禁用</el-dropdown-item>
  113. <el-dropdown-item command="INVALID" :disabled="row.status === 'INVALID'" divided>作废</el-dropdown-item>
  114. </el-dropdown-menu>
  115. </template>
  116. </el-dropdown>
  117. </template>
  118. </el-table-column>
  119. </el-table>
  120. <el-empty v-if="!loading && !tableData.length" description="暂无预警事项配置" />
  121. <pagination
  122. v-show="total > 0"
  123. v-model:page="query.pageNum"
  124. v-model:limit="query.pageSize"
  125. :total="total"
  126. @pagination="loadData"
  127. />
  128. </el-card>
  129. </main>
  130. </div>
  131. <el-drawer
  132. v-model="editorVisible"
  133. :title="form.itemId ? '修改预警事项' : '新增预警事项'"
  134. size="720px"
  135. destroy-on-close
  136. class="editor-drawer detail-drawer"
  137. direction="rtl">
  138. <el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="editor-form">
  139. <div class="form-section">
  140. <div class="form-section-title"><span>01</span> 基本信息</div>
  141. <el-row :gutter="16">
  142. <el-col :span="16">
  143. <el-form-item label="事项名称" prop="itemName">
  144. <el-input v-model="form.itemName" maxlength="100" show-word-limit placeholder="请输入清晰、可识别的事项名称" />
  145. </el-form-item>
  146. </el-col>
  147. <el-col :span="8">
  148. <el-form-item label="所属行业" prop="industry">
  149. <el-select v-model="form.industry" placeholder="请选择">
  150. <el-option v-for="item in industryOptions" :key="item" :label="item" :value="item" />
  151. </el-select>
  152. </el-form-item>
  153. </el-col>
  154. </el-row>
  155. <el-row :gutter="16">
  156. <el-col :span="8">
  157. <el-form-item label="预警类型" prop="warningType">
  158. <el-select v-model="form.warningType" :disabled="!!form.itemId" placeholder="请选择预警类型" @change="handleCodeDependencyChange">
  159. <el-option v-for="item in warningTypes" :key="item.value" :label="item.label" :value="item.value" />
  160. </el-select>
  161. </el-form-item>
  162. </el-col>
  163. <el-col :span="8">
  164. <el-form-item label="预警级别" prop="warningLevel">
  165. <el-select v-model="form.warningLevel" :disabled="!!form.itemId" placeholder="请选择预警级别" @change="handleCodeDependencyChange">
  166. <el-option v-for="item in warningLevels" :key="item.value" :label="item.label" :value="item.value" />
  167. </el-select>
  168. </el-form-item>
  169. </el-col>
  170. </el-row>
  171. <el-row :gutter="16">
  172. <el-col :span="14">
  173. <el-form-item label="事项编码" prop="itemCode">
  174. <el-input v-model="form.itemCode" readonly placeholder="选择类型和等级后自动生成,保存时确定最终编码">
  175. <template #append>
  176. <el-button :icon="MagicStick" :loading="codeLoading" :disabled="!!form.itemId" @click="generateCode">生成</el-button>
  177. </template>
  178. </el-input>
  179. </el-form-item>
  180. </el-col>
  181. <el-col :span="10">
  182. <el-form-item label="预警专项" prop="warningSpecial">
  183. <el-input v-model="form.warningSpecial" placeholder="如:城市内涝专项" />
  184. </el-form-item>
  185. </el-col>
  186. </el-row>
  187. <el-form-item label="采集报警编码(可选)">
  188. <el-input v-model="form.sourceWarningCode" maxlength="64" placeholder="如 WARN-PRESSURE;不填则仅供人工选择" />
  189. <div class="el-form-item__extra">系统预警按该编码与级别匹配;有多个匹配事项时保留草稿供人工选择。</div>
  190. </el-form-item>
  191. <el-row :gutter="16">
  192. <el-col :span="12">
  193. <el-form-item label="处置时限(小时)" prop="duration">
  194. <el-input-number v-model="form.duration" :min="1" :max="720" controls-position="right" />
  195. </el-form-item>
  196. </el-col>
  197. <el-col :span="12">
  198. <el-form-item label="初始状态" prop="status">
  199. <el-radio-group v-model="form.status" :disabled="!!form.itemId">
  200. <el-radio-button value="ENABLED">启用</el-radio-button>
  201. <el-radio-button value="DISABLED">禁用</el-radio-button>
  202. </el-radio-group>
  203. </el-form-item>
  204. </el-col>
  205. </el-row>
  206. </div>
  207. <div class="form-section">
  208. <div class="form-section-title">
  209. <div><span>02</span> 预警环节配置</div>
  210. <small>保留接收 → 处置 → 核查闭环</small>
  211. </div>
  212. <div class="link-builder">
  213. <div v-for="(link, index) in form.links" :key="index" class="link-row">
  214. <div class="step-number">{{ String(index + 1).padStart(2, '0') }}</div>
  215. <div class="link-fields">
  216. <el-row :gutter="12">
  217. <el-col :span="9"><el-input v-model="link.name" readonly /></el-col>
  218. <el-col :span="8"><el-input v-model="link.handler" readonly /></el-col>
  219. <el-col :span="7">
  220. <el-input-number v-model="link.timeLimit" :min="1" :max="43200" controls-position="right" />
  221. <small>分钟</small>
  222. </el-col>
  223. </el-row>
  224. <el-input v-model="link.description" type="textarea" :rows="2" placeholder="描述该环节的触发条件和处置要求" />
  225. </div>
  226. </div>
  227. </div>
  228. </div>
  229. <div class="form-section">
  230. <div class="form-section-title"><span>03</span> 补充说明</div>
  231. <el-form-item label="备注">
  232. <el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="填写适用范围、口径或其他说明" />
  233. </el-form-item>
  234. </div>
  235. </el-form>
  236. <template #footer>
  237. <el-button @click="editorVisible = false">取消</el-button>
  238. <el-button type="primary" :loading="saving" @click="submitForm">保存事项配置</el-button>
  239. </template>
  240. </el-drawer>
  241. <el-drawer v-model="previewVisible" title="预警事项详情" size="760px" class="detail-drawer" direction="rtl">
  242. <div class="preview-title">
  243. <div>
  244. <span>{{ preview.itemCode }}</span>
  245. <h3>{{ preview.itemName }}</h3>
  246. </div>
  247. <el-tag :type="statusMeta(preview.status).type">{{ statusMeta(preview.status).label }}</el-tag>
  248. </div>
  249. <el-descriptions :column="3" border>
  250. <el-descriptions-item label="预警类型">{{ typeLabel(preview.warningType) }}</el-descriptions-item>
  251. <el-descriptions-item label="预警级别">{{ levelMeta(preview.warningLevel).label }}</el-descriptions-item>
  252. <el-descriptions-item label="时长">{{ preview.duration || '-' }} 小时</el-descriptions-item>
  253. <el-descriptions-item label="预警专项">{{ preview.warningSpecial || '-' }}</el-descriptions-item>
  254. <el-descriptions-item label="采集报警编码">{{ preview.sourceWarningCode || '未配置' }}</el-descriptions-item>
  255. <el-descriptions-item label="所属行业">{{ preview.industry || '-' }}</el-descriptions-item>
  256. <el-descriptions-item label="更新时间">{{ preview.updateTime || preview.createTime || '-' }}</el-descriptions-item>
  257. <el-descriptions-item label="备注" :span="3">{{ preview.remark || '-' }}</el-descriptions-item>
  258. </el-descriptions>
  259. <el-button v-hasPermi="['warning:reason:list']" style="margin-top:16px" @click="manageReasons(preview)">维护此类型和等级的原因库</el-button>
  260. <div class="preview-links">
  261. <div v-for="(link, index) in parseLinkConfig(preview.linkConfig)" :key="index" class="preview-link">
  262. <span>{{ index + 1 }}</span>
  263. <div><strong>{{ link.name }}</strong><small>{{ link.handler }} · {{ link.timeLimit || '-' }} 分钟</small></div>
  264. <p>{{ link.description }}</p>
  265. </div>
  266. </div>
  267. </el-drawer>
  268. </div>
  269. </template>
  270. <script setup name="WarningItemManage">
  271. import { listWarningTypes } from '@/api/warning/list'
  272. import { onMounted, reactive, ref, watch } from 'vue'
  273. import { useRouter, useRoute } from 'vue-router'
  274. import { ElMessage, ElMessageBox } from 'element-plus'
  275. import { ArrowDown, Delete, Edit, MagicStick, Plus, Refresh, RefreshRight, Search, View } from '@element-plus/icons-vue'
  276. import { getDicts } from '@/api/system/dict/data'
  277. import {
  278. addWarningItem,
  279. generateWarningItemCode,
  280. getWarningItemDetail,
  281. listWarningItems,
  282. updateWarningItem,
  283. updateWarningItemStatus
  284. } from '@/api/warning/list'
  285. import {
  286. cloneDefaultLinks,
  287. fallbackWarningTypes,
  288. industryOptions,
  289. itemStatuses,
  290. levelMeta,
  291. normalizeWarningTypeDict,
  292. optionLabel,
  293. parseLinkConfig,
  294. statusMeta,
  295. normalizeWarningLevelDict,
  296. } from '../shared'
  297. const router = useRouter()
  298. const route = useRoute()
  299. function manageReasons(item) { router.push({ path: '/subSystem/lifeCompany/warningList/reasonManage', query: { warningType: item.warningType, warningLevel: item.warningLevel } }); previewVisible.value = false }
  300. const loading = ref(false)
  301. const saving = ref(false)
  302. const codeLoading = ref(false)
  303. const editorVisible = ref(false)
  304. const previewVisible = ref(false)
  305. const formRef = ref()
  306. const tableData = ref([])
  307. const total = ref(0)
  308. const warningTypes = ref(fallbackWarningTypes)
  309. const warningLevels = ref([])
  310. const preview = reactive({})
  311. const query = reactive({ pageNum: 1, pageSize: 10, itemName: '', itemCode: '', warningSpecial: '', warningType: '', warningLevel: '', status: '' })
  312. const form = reactive(createEmptyForm())
  313. const rules = {
  314. itemName: [{ required: true, message: '请输入事项名称', trigger: 'blur' }],
  315. warningType: [{ required: true, message: '请选择预警类型', trigger: 'change' }],
  316. warningLevel: [{ required: true, message: '请选择预警级别', trigger: 'change' }],
  317. industry: [{ required: true, message: '请选择所属行业', trigger: 'change' }],
  318. duration: [{ required: true, message: '请设置预警时长', trigger: 'change' }]
  319. }
  320. function createEmptyForm() {
  321. return {
  322. itemId: '',
  323. itemCode: '',
  324. itemName: '',
  325. warningType: '',
  326. warningLevel: '',
  327. warningSpecial: '',
  328. sourceWarningCode: '',
  329. industry: '',
  330. duration: 24,
  331. status: 'ENABLED',
  332. remark: '',
  333. links: cloneDefaultLinks()
  334. }
  335. }
  336. function typeLabel(value) {
  337. return optionLabel(warningTypes.value, value)
  338. }
  339. async function loadTypes() {
  340. try {
  341. const [res, levels] = await Promise.all([listWarningTypes(), getDicts('alert_level')])
  342. warningTypes.value = normalizeWarningTypeDict(res.data)
  343. warningLevels.value = normalizeWarningLevelDict(levels.data)
  344. } catch {
  345. warningTypes.value = fallbackWarningTypes
  346. }
  347. }
  348. async function loadData() {
  349. loading.value = true
  350. try {
  351. const params = { ...query }
  352. const res = await listWarningItems(params)
  353. tableData.value = res.data?.records || []
  354. total.value = Number(res.data?.total || 0)
  355. } finally {
  356. loading.value = false
  357. }
  358. }
  359. function handleQuery() {
  360. query.pageNum = 1
  361. loadData()
  362. }
  363. function resetQuery() {
  364. Object.assign(query, { pageNum: 1, pageSize: 10, itemName: '', itemCode: '', warningSpecial: '', warningType: '', warningLevel: '', status: '' })
  365. loadData()
  366. }
  367. function toggleLevel(value) {
  368. query.warningLevel = query.warningLevel === value ? '' : value
  369. handleQuery()
  370. }
  371. function toggleStatus(value) {
  372. query.status = query.status === value ? '' : value
  373. handleQuery()
  374. }
  375. function resetForm(data = createEmptyForm()) {
  376. Object.keys(form).forEach(key => delete form[key])
  377. Object.assign(form, data)
  378. }
  379. function openCreate() {
  380. resetForm()
  381. editorVisible.value = true
  382. }
  383. async function openEdit(row) {
  384. const res = await getWarningItemDetail(row.itemId)
  385. const data = res.data || row
  386. resetForm({ ...data, links: parseLinkConfig(data.linkConfig).every(link => link.code) && parseLinkConfig(data.linkConfig).length === 3 ? parseLinkConfig(data.linkConfig) : cloneDefaultLinks() })
  387. editorVisible.value = true
  388. }
  389. async function openPreview(row) {
  390. const res = await getWarningItemDetail(row.itemId)
  391. Object.keys(preview).forEach(key => delete preview[key])
  392. Object.assign(preview, res.data || row)
  393. previewVisible.value = true
  394. }
  395. let codeRequest = 0
  396. function handleCodeDependencyChange() {
  397. if (form.itemId) return
  398. form.itemCode = ''
  399. if (form.warningType && form.warningLevel) generateCode()
  400. }
  401. async function generateCode() {
  402. if (form.itemId || !form.warningType || !form.warningLevel) return
  403. const request = ++codeRequest
  404. codeLoading.value = true
  405. try {
  406. const res = await generateWarningItemCode({ warningType: form.warningType, warningLevel: form.warningLevel })
  407. if (request === codeRequest) form.itemCode = res.data
  408. } finally {
  409. if (request === codeRequest) codeLoading.value = false
  410. }
  411. }
  412. async function submitForm() {
  413. if (!await formRef.value?.validate().catch(() => false)) return
  414. if (form.links.some(item => !item.name || !item.handler || !item.timeLimit)) {
  415. ElMessage.warning('请完整填写每个预警环节的名称、责任方和时限')
  416. return
  417. }
  418. saving.value = true
  419. try {
  420. const { links, ...payload } = form
  421. payload.linkConfig = JSON.stringify(links)
  422. if (payload.itemId) {
  423. await updateWarningItem(payload)
  424. ElMessage.success('预警事项修改成功')
  425. } else {
  426. await addWarningItem(payload)
  427. ElMessage.success('预警事项新增成功')
  428. }
  429. editorVisible.value = false
  430. loadData()
  431. } catch {
  432. // 错误由统一请求层提示,保留表单方便修正后重试。
  433. } finally {
  434. saving.value = false
  435. }
  436. }
  437. async function changeStatus(row, status) {
  438. const meta = statusMeta(status)
  439. try {
  440. await ElMessageBox.confirm(
  441. status === 'INVALID' ? '事项作废后将不再用于新预警,确认作废?' : `确认将该事项设置为“${meta.label}”?`,
  442. '状态变更确认',
  443. { type: status === 'INVALID' ? 'warning' : 'info' }
  444. )
  445. await updateWarningItemStatus(row.itemId, status)
  446. ElMessage.success('事项状态已更新')
  447. loadData()
  448. } catch {
  449. // 取消和请求错误均保留当前状态,由统一请求层提示错误。
  450. }
  451. }
  452. watch(() => [route.query.create, route.query.warningType, route.query.warningLevel], () => {
  453. if (route.path.endsWith('/itemManage') && route.query.create === '1') {
  454. openCreate(); form.warningType = String(route.query.warningType || ''); form.warningLevel = String(route.query.warningLevel || ''); handleCodeDependencyChange()
  455. }
  456. })
  457. onMounted(async () => {
  458. await loadTypes()
  459. loadData()
  460. if (route.query.create === '1') {
  461. openCreate(); form.warningType = String(route.query.warningType || ''); form.warningLevel = String(route.query.warningLevel || ''); handleCodeDependencyChange()
  462. }
  463. })
  464. </script>
  465. <style scoped lang="scss">
  466. .manage-page {
  467. min-height: calc(100vh - 84px);
  468. padding: 20px;
  469. background: linear-gradient(180deg, #eef4fb 0, #f7f9fc 260px);
  470. }
  471. .manage-header {
  472. display: flex;
  473. align-items: center;
  474. justify-content: space-between;
  475. margin-bottom: 18px;
  476. padding: 26px 30px;
  477. color: #fff;
  478. border-radius: 18px;
  479. background: linear-gradient(120deg, #111d38, #173b72 58%, #2065a8);
  480. box-shadow: 0 14px 35px rgba(20, 50, 95, .2);
  481. h1 { margin: 5px 0 7px; font-size: 27px; }
  482. p { margin: 0; color: #b9cae1; }
  483. }
  484. .header-kicker { color: #65d4ff; font-size: 11px; letter-spacing: 2.6px; }
  485. .workspace { display: grid; grid-template-columns: 230px minmax(0, 1fr); gap: 16px; }
  486. .side-panel {
  487. align-self: start;
  488. padding: 20px;
  489. border: 1px solid #e1e8f1;
  490. border-radius: 16px;
  491. background: #fff;
  492. box-shadow: 0 8px 24px rgba(42, 67, 96, .06);
  493. :deep(.el-select) { width: 100%; }
  494. }
  495. .side-title { margin-bottom: 16px; color: #1e293b; font-size: 16px; font-weight: 700; }
  496. .filter-label { margin: 20px 0 9px; color: #64748b; font-size: 12px; font-weight: 600; }
  497. .level-filter {
  498. display: grid;
  499. grid-template-columns: 1fr 1fr;
  500. gap: 8px;
  501. button {
  502. padding: 8px;
  503. color: #64748b;
  504. border: 1px solid #e5eaf1;
  505. border-radius: 8px;
  506. background: #fff;
  507. cursor: pointer;
  508. span { display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: var(--level-color); }
  509. &.active { color: var(--level-color); border-color: var(--level-color); background: color-mix(in srgb, var(--level-color) 7%, #fff); }
  510. }
  511. }
  512. .status-filter {
  513. display: flex;
  514. flex-direction: column;
  515. gap: 7px;
  516. button {
  517. padding: 9px 12px;
  518. color: #64748b;
  519. text-align: left;
  520. border: 0;
  521. border-radius: 8px;
  522. background: #f6f8fb;
  523. cursor: pointer;
  524. &.active { color: #1d5fd1; font-weight: 600; background: #eaf2ff; }
  525. }
  526. }
  527. .reset-button { width: 100%; margin-top: 20px; }
  528. .table-card { border: 1px solid #e1e8f1; border-radius: 16px; }
  529. .table-heading {
  530. display: flex;
  531. align-items: center;
  532. justify-content: space-between;
  533. div { display: flex; flex-direction: column; gap: 3px; }
  534. strong { color: #1e293b; font-size: 16px; }
  535. span { color: #94a3b8; font-size: 12px; }
  536. }
  537. .table-card :deep(th.el-table__cell) { color: #52647a; background: #f7f9fc; }
  538. .item-cell {
  539. display: flex;
  540. align-items: center;
  541. gap: 12px;
  542. .item-mark { width: 5px; height: 38px; border-radius: 8px; }
  543. div:last-child { display: flex; flex-direction: column; gap: 4px; }
  544. strong { color: #1f2d3d; }
  545. span { color: #8a98aa; font-family: Consolas, monospace; font-size: 12px; }
  546. }
  547. .stack-cell { display: flex; flex-direction: column; gap: 4px; color: #334155; small { color: #94a3b8; } }
  548. .form-section {
  549. margin-bottom: 18px;
  550. padding: 20px;
  551. border: 1px solid #e5eaf1;
  552. border-radius: 14px;
  553. background: #fff;
  554. }
  555. .form-section-title {
  556. display: flex;
  557. align-items: center;
  558. justify-content: space-between;
  559. margin-bottom: 18px;
  560. color: #1e293b;
  561. font-weight: 700;
  562. > span, > div span {
  563. display: inline-grid;
  564. width: 28px;
  565. height: 28px;
  566. margin-right: 8px;
  567. color: #2563eb;
  568. font-size: 11px;
  569. border-radius: 8px;
  570. background: #eaf2ff;
  571. place-items: center;
  572. }
  573. }
  574. .editor-form :deep(.el-select), .editor-form :deep(.el-input-number) { width: 100%; }
  575. .link-builder { display: flex; flex-direction: column; gap: 12px; }
  576. .link-row {
  577. display: flex;
  578. align-items: flex-start;
  579. gap: 12px;
  580. padding: 14px;
  581. border: 1px solid #e7ebf1;
  582. border-radius: 12px;
  583. background: #f8fafc;
  584. }
  585. .step-number {
  586. display: grid;
  587. flex: 0 0 34px;
  588. width: 34px;
  589. height: 34px;
  590. color: #fff;
  591. font-size: 12px;
  592. border-radius: 10px;
  593. background: #255fc1;
  594. place-items: center;
  595. }
  596. .link-fields {
  597. flex: 1;
  598. :deep(.el-textarea) { margin-top: 10px; }
  599. .el-col:last-child { display: flex; align-items: center; gap: 6px; small { color: #94a3b8; } }
  600. }
  601. .preview-title {
  602. display: flex;
  603. align-items: center;
  604. justify-content: space-between;
  605. margin-bottom: 16px;
  606. padding: 18px;
  607. border-radius: 12px;
  608. background: linear-gradient(110deg, #eaf2ff, #f7fbff);
  609. span { color: #5f78a0; font: 12px Consolas, monospace; }
  610. h3 { margin: 5px 0 0; color: #1d3557; }
  611. }
  612. .preview-links {
  613. display: grid;
  614. grid-template-columns: repeat(2, 1fr);
  615. gap: 10px;
  616. margin-top: 18px;
  617. }
  618. .preview-link {
  619. display: grid;
  620. grid-template-columns: 30px 1fr;
  621. gap: 8px;
  622. padding: 13px;
  623. border: 1px solid #e6ebf2;
  624. border-radius: 10px;
  625. > span { display: grid; width: 28px; height: 28px; color: #2563eb; border-radius: 8px; background: #edf4ff; place-items: center; }
  626. div { display: flex; flex-direction: column; gap: 3px; }
  627. small { color: #8a98aa; }
  628. p { grid-column: 2; margin: 3px 0 0; color: #64748b; line-height: 1.5; }
  629. }
  630. @media (max-width: 900px) {
  631. .workspace { grid-template-columns: 1fr; }
  632. .manage-header { align-items: flex-start; gap: 16px; }
  633. }
  634. </style>