index.vue 23 KB

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