bjsh.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. <template>
  2. <div class="app-container bjsh-page">
  3. <!-- ==================== 上半部分:GIS地图 ==================== -->
  4. <div class="map-section">
  5. <div id="bjshMap" class="map-container"></div>
  6. </div>
  7. <!-- ==================== 下半部分:搜索 + 报警审核表格 ==================== -->
  8. <div class="table-section">
  9. <!-- 搜索区 -->
  10. <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
  11. <el-form-item label="设备名称">
  12. <el-input v-model="queryParams.equipmentName" placeholder="请输入设备名称" clearable style="width: 200px" />
  13. </el-form-item>
  14. <el-form-item label="报警等级">
  15. <el-select v-model="queryParams.warningLevel" placeholder="请选择" clearable style="width: 200px">
  16. <el-option label="严重" :value="1" />
  17. <el-option label="重要" :value="2" />
  18. <el-option label="一般" :value="3" />
  19. </el-select>
  20. </el-form-item>
  21. <el-form-item label="审核状态">
  22. <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 200px">
  23. <el-option label="待审核" :value="1" />
  24. <el-option label="已审核" :value="2" />
  25. </el-select>
  26. </el-form-item>
  27. <el-form-item>
  28. <el-button type="primary" @click="handleQuery">搜索</el-button>
  29. <el-button @click="resetQuery">重置</el-button>
  30. </el-form-item>
  31. </el-form>
  32. <!-- 报警审核表格 -->
  33. <el-table :data="tableData" v-loading="loading" border stripe style="width: 100%">
  34. <el-table-column prop="id" label="报警编号" width="80" align="center" />
  35. <el-table-column prop="equipmentName" label="设备名称" min-width="140" />
  36. <el-table-column label="报警等级" width="100">
  37. <template #default="{ row }">
  38. <el-tag :type="levelTagType(row.warningLevel)" size="small">{{ levelText(row.warningLevel) }}</el-tag>
  39. </template>
  40. </el-table-column>
  41. <el-table-column prop="warningTime" label="报警时间" width="160" />
  42. <el-table-column label="监测值" width="100">
  43. <template #default="{ row }">
  44. <span :style="{ color: parseFloat(row.currentValue) > parseFloat(row.thresholdValue) ? '#f56c6c' : '#303133' }">{{ row.currentValue }}</span>
  45. </template>
  46. </el-table-column>
  47. <el-table-column label="审核状态" width="100">
  48. <template #default="{ row }">
  49. <el-tag :type="auditStatusTagType(row.status)" size="small">{{ auditStatusText(row.status) }}</el-tag>
  50. </template>
  51. </el-table-column>
  52. <el-table-column label="操作" width="160" align="center">
  53. <template #default="{ row }">
  54. <el-button v-if="row.status === 1" link type="primary" @click="openAudit(row)">审核</el-button>
  55. <el-button link type="primary" @click="openView(row)">查看</el-button>
  56. </template>
  57. </el-table-column>
  58. </el-table>
  59. </div>
  60. <!-- ==================== 审核弹窗 ==================== -->
  61. <el-dialog v-model="auditVisible" title="报警审核" width="700px" :before-close="closeAudit">
  62. <!-- 报警信息展示 -->
  63. <el-descriptions :column="2" border size="small" style="margin-bottom: 16px">
  64. <el-descriptions-item label="设备名称">{{ currentAlarm.equipmentName }}</el-descriptions-item>
  65. <el-descriptions-item label="报警时间">{{ currentAlarm.warningTime }}</el-descriptions-item>
  66. <el-descriptions-item label="报警等级">
  67. <el-tag :type="levelTagType(currentAlarm.warningLevel)" size="small">{{ levelText(currentAlarm.warningLevel) }}</el-tag>
  68. </el-descriptions-item>
  69. <el-descriptions-item label="指标值">{{ currentAlarm.warningType || '-' }}:{{ currentAlarm.currentValue }}(阈值 {{ currentAlarm.thresholdValue }})</el-descriptions-item>
  70. </el-descriptions>
  71. <!-- 审核操作 -->
  72. <div class="audit-form-section">
  73. <div class="section-title">审核操作</div>
  74. <el-form :model="auditForm" label-width="90px" style="margin-top: 12px">
  75. <el-form-item label="报警类型">
  76. <el-tag type="info" size="small">{{ currentAlarm.warningType || '设备报警' }}</el-tag>
  77. </el-form-item>
  78. <el-form-item label="审核结果" required>
  79. <el-radio-group v-model="auditForm.result">
  80. <el-radio v-for="opt in auditResultOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-radio>
  81. </el-radio-group>
  82. </el-form-item>
  83. <el-form-item label="审核/解除意见" required>
  84. <el-input v-model="auditForm.opinion" type="textarea" :rows="3" :placeholder="auditForm.result === '解除报警' ? '请输入解除原因及说明' : '请输入审核意见'" />
  85. </el-form-item>
  86. <el-form-item label="附件上传">
  87. <el-upload
  88. action="#"
  89. :auto-upload="false"
  90. :file-list="auditFiles"
  91. :limit="5"
  92. :on-change="handleFileChange"
  93. :on-remove="handleFileRemove"
  94. accept=".jpg,.jpeg,.png,.pdf,.doc,.docx"
  95. >
  96. <el-button type="primary" size="small">选择文件</el-button>
  97. <template #tip>
  98. <div class="upload-tip">支持jpg/png/pdf/doc格式,最多5个文件,单文件不超过10MB</div>
  99. </template>
  100. </el-upload>
  101. </el-form-item>
  102. </el-form>
  103. </div>
  104. <!-- 处置流程时间线 -->
  105. <div class="audit-timeline-section">
  106. <div class="section-title">处置全流程跟踪</div>
  107. <el-timeline style="margin-top: 12px; padding-left: 2px">
  108. <el-timeline-item :timestamp="currentAlarm.warningTime || ''" placement="top" color="#f56c6c">
  109. <div class="timeline-node">报警触发</div>
  110. <div class="timeline-desc">{{ currentAlarm.equipmentName }} {{ currentAlarm.warningType || '' }}超标,当前值:{{ currentAlarm.currentValue || '-' }},阈值:{{ currentAlarm.thresholdValue || '-' }}</div>
  111. </el-timeline-item>
  112. <el-timeline-item placement="top" color="#e6a23c">
  113. <div class="timeline-node">系统通知</div>
  114. <div class="timeline-desc">已自动推送至值守人员及相关负责人</div>
  115. </el-timeline-item>
  116. <el-timeline-item v-if="currentAlarm.firstApproverName" placement="top" color="#409eff">
  117. <div class="timeline-node">下派处理</div>
  118. <div class="timeline-desc">已下派至:{{ currentAlarm.firstApproverName }}</div>
  119. </el-timeline-item>
  120. <el-timeline-item timestamp="当前" placement="top" color="#409eff">
  121. <div class="timeline-node">人工审核</div>
  122. <div class="timeline-desc">审核进行中,等待审核结果</div>
  123. </el-timeline-item>
  124. <el-timeline-item timestamp="待完成" placement="top" color="#909399">
  125. <div class="timeline-node">处置完成</div>
  126. <div class="timeline-desc">审核通过后完成闭环</div>
  127. </el-timeline-item>
  128. </el-timeline>
  129. </div>
  130. <template #footer>
  131. <el-button @click="closeAudit">取消</el-button>
  132. <el-button type="primary" :loading="submitting" @click="submitAudit">提交审核</el-button>
  133. </template>
  134. </el-dialog>
  135. <!-- ==================== 查看详情弹窗 ==================== -->
  136. <el-dialog v-model="viewVisible" title="报警处置全流程" width="650px">
  137. <el-descriptions :column="2" border size="small">
  138. <el-descriptions-item label="报警编号">{{ viewRow.id }}</el-descriptions-item>
  139. <el-descriptions-item label="设备名称">{{ viewRow.equipmentName }}</el-descriptions-item>
  140. <el-descriptions-item label="报警等级">
  141. <el-tag :type="levelTagType(viewRow.warningLevel)" size="small">{{ levelText(viewRow.warningLevel) }}</el-tag>
  142. </el-descriptions-item>
  143. <el-descriptions-item label="报警时间">{{ viewRow.warningTime }}</el-descriptions-item>
  144. <el-descriptions-item label="监测指标">{{ viewRow.warningType || '-' }}</el-descriptions-item>
  145. <el-descriptions-item label="实时值">{{ viewRow.currentValue }}</el-descriptions-item>
  146. <el-descriptions-item label="阈值">{{ viewRow.thresholdValue }}</el-descriptions-item>
  147. <el-descriptions-item label="审核状态">
  148. <el-tag :type="auditStatusTagType(viewRow.status)" size="small">{{ auditStatusText(viewRow.status) }}</el-tag>
  149. </el-descriptions-item>
  150. <el-descriptions-item v-if="viewRow.auditResult" label="审核结果">{{ viewRow.auditResult }}</el-descriptions-item>
  151. <el-descriptions-item v-if="viewRow.auditOpinion" label="审核意见">{{ viewRow.auditOpinion }}</el-descriptions-item>
  152. <el-descriptions-item label="位置" :span="2">{{ viewRow.location || '-' }}</el-descriptions-item>
  153. <el-descriptions-item label="描述" :span="2">{{ viewRow.content || '-' }}</el-descriptions-item>
  154. </el-descriptions>
  155. <!-- 处置全流程跟踪 -->
  156. <div style="margin-top: 16px">
  157. <div class="section-title">处置全流程跟踪</div>
  158. <el-timeline style="margin-top: 12px; padding-left: 2px">
  159. <el-timeline-item :timestamp="viewRow.warningTime || ''" placement="top" color="#f56c6c">
  160. <div class="timeline-node">报警触发</div>
  161. <div class="timeline-desc">{{ viewRow.equipmentName }} {{ viewRow.warningType || '' }}超标,当前值:{{ viewRow.currentValue || '-' }}</div>
  162. </el-timeline-item>
  163. <el-timeline-item placement="top" color="#e6a23c">
  164. <div class="timeline-node">系统通知</div>
  165. <div class="timeline-desc">已自动推送至值守人员及相关负责人</div>
  166. </el-timeline-item>
  167. <el-timeline-item v-if="viewRow.firstApproverName" placement="top" color="#409eff">
  168. <div class="timeline-node">下派处理</div>
  169. <div class="timeline-desc">已下派至:{{ viewRow.firstApproverName }}</div>
  170. </el-timeline-item>
  171. <el-timeline-item v-if="viewRow.status === 2" placement="top" color="#67c23a">
  172. <div class="timeline-node">审核完成</div>
  173. <div class="timeline-desc">审核结果:{{ viewRow.auditResult || '-' }};意见:{{ viewRow.auditOpinion || '-' }}</div>
  174. </el-timeline-item>
  175. <el-timeline-item v-if="viewRow.status === 2" placement="top" color="#67c23a">
  176. <div class="timeline-node">处置完成</div>
  177. <div class="timeline-desc">报警处置流程已闭环</div>
  178. </el-timeline-item>
  179. <el-timeline-item v-if="viewRow.status !== 2" timestamp="进行中" placement="top" color="#909399">
  180. <div class="timeline-node">待审核</div>
  181. <div class="timeline-desc">等待人工审核处理</div>
  182. </el-timeline-item>
  183. </el-timeline>
  184. </div>
  185. </el-dialog>
  186. </div>
  187. </template>
  188. <script setup name="Bjsh">
  189. import { ref, computed, onMounted, nextTick } from 'vue'
  190. import { ElMessage } from 'element-plus'
  191. import locationIcon from '@/assets/images/location.png'
  192. import { getAlarmDataPage, handleAlarm } from '@/api/drainage'
  193. import useUserStore from '@/store/modules/user'
  194. // ==================== 用户信息 ====================
  195. const userStore = useUserStore()
  196. const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admin'))
  197. // ==================== 地图变量 ====================
  198. let mapInstance = null
  199. let mapInitialized = false
  200. // ==================== 等级/状态映射 ====================
  201. const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
  202. const levelColorMap = { 1: '#f56c6c', 2: '#e6a23c', 3: '#409eff' }
  203. const auditStatusMap = { 1: '待审核', 2: '已审核' }
  204. const auditStatusColorMap = { 1: '#e6a23c', 2: '#67c23a' }
  205. function levelText(level) {
  206. return levelMap[level] || '-'
  207. }
  208. function levelTagType(level) {
  209. if (level === 1) return 'danger'
  210. if (level === 2) return 'warning'
  211. return ''
  212. }
  213. function auditStatusText(status) {
  214. return auditStatusMap[status] || '-'
  215. }
  216. function auditStatusTagType(status) {
  217. if (status === 2) return 'success'
  218. return 'warning'
  219. }
  220. // ==================== 搜索参数 ====================
  221. const queryParams = ref({
  222. equipmentName: '',
  223. warningLevel: '',
  224. status: ''
  225. })
  226. const queryRef = ref(null)
  227. function handleQuery() {
  228. loadData()
  229. }
  230. function resetQuery() {
  231. queryParams.value = { equipmentName: '', warningLevel: '', status: '' }
  232. loadData()
  233. }
  234. // ==================== 表格数据 ====================
  235. const tableData = ref([])
  236. const loading = ref(false)
  237. async function loadData() {
  238. loading.value = true
  239. try {
  240. // 调用 alarm_data 真实接口,只取未处理报警(alarmStatus=0)
  241. const res = await getAlarmDataPage(1, 200, { equipmentType: 'drainage', alarmStatus: 0 })
  242. const pageData = res.code !== undefined ? res.data : res
  243. const records = pageData.records || pageData.rows || []
  244. let tableData_local = records.map(row => ({
  245. id: row.id,
  246. equipmentId: row.deviceCode,
  247. equipmentName: row.deviceName || '-',
  248. warningTime: row.alarmTime,
  249. warningLevel: row.alarmLevel,
  250. content: row.warningType || '-',
  251. location: '-',
  252. longitude: row.longitude ? parseFloat(row.longitude) : null,
  253. latitude: row.latitude ? parseFloat(row.latitude) : null,
  254. currentValue: row.actualValue,
  255. thresholdValue: row.maxValue,
  256. warningType: row.warningType,
  257. status: 1, // 未处理(alarmStatus=0)在审核页面显示为"待审核"
  258. source: 'alarm_data'
  259. }))
  260. // 前端筛选(equipmentName/warningLevel/status)
  261. let filtered = tableData_local
  262. if (queryParams.value.equipmentName) {
  263. filtered = filtered.filter(item => item.equipmentName.includes(queryParams.value.equipmentName))
  264. }
  265. if (queryParams.value.warningLevel) {
  266. filtered = filtered.filter(item => item.warningLevel === queryParams.value.warningLevel)
  267. }
  268. if (queryParams.value.status !== '' && queryParams.value.status !== null) {
  269. filtered = filtered.filter(item => item.status === queryParams.value.status)
  270. }
  271. tableData.value = filtered
  272. // 如果地图已初始化,刷新标记
  273. if (mapInitialized) {
  274. refreshMapMarkers()
  275. }
  276. } catch (error) {
  277. console.error('加载报警数据失败', error)
  278. ElMessage.error('加载报警数据失败')
  279. } finally {
  280. loading.value = false
  281. }
  282. }
  283. // ==================== 审核弹窗 ====================
  284. const auditVisible = ref(false)
  285. const currentAlarm = ref({})
  286. const auditForm = ref({ result: '', opinion: '' })
  287. // 各类报警预定义的审核结果选项
  288. const auditResultOptionsMap = {
  289. '设备报警': [
  290. { label: '确认报警 - 设备异常需维修', value: '确认报警' },
  291. { label: '解除报警 - 设备恢复正常', value: '解除报警' },
  292. { label: '误报处理 - 传感器误触发', value: '误报处理' },
  293. { label: '降级处理 - 下调报警等级', value: '降级处理' }
  294. ],
  295. '水位报警': [
  296. { label: '确认报警 - 水位超标需处置', value: '确认报警' },
  297. { label: '解除报警 - 水位已回落', value: '解除报警' },
  298. { label: '误报处理 - 监测数据异常', value: '误报处理' }
  299. ],
  300. '压力报警': [
  301. { label: '确认报警 - 管网压力异常', value: '确认报警' },
  302. { label: '解除报警 - 压力恢复正常', value: '解除报警' },
  303. { label: '误报处理 - 压力波动属正常范围', value: '误报处理' }
  304. ],
  305. '默认': [
  306. { label: '确认报警', value: '确认报警' },
  307. { label: '解除报警', value: '解除报警' },
  308. { label: '误报处理', value: '误报处理' }
  309. ]
  310. }
  311. const auditResultOptions = computed(() => {
  312. const type = currentAlarm.value.warningType || '默认'
  313. return auditResultOptionsMap[type] || auditResultOptionsMap['默认']
  314. })
  315. const auditFiles = ref([])
  316. const submitting = ref(false)
  317. async function openAudit(row) {
  318. currentAlarm.value = { ...row }
  319. auditForm.value = { result: '', opinion: '' }
  320. auditFiles.value = []
  321. auditVisible.value = true
  322. }
  323. function closeAudit() {
  324. auditVisible.value = false
  325. }
  326. function handleFileChange(file) {
  327. auditFiles.value.push(file)
  328. }
  329. function handleFileRemove(file) {
  330. const idx = auditFiles.value.findIndex(f => f.uid === file.uid)
  331. if (idx !== -1) auditFiles.value.splice(idx, 1)
  332. }
  333. async function submitAudit() {
  334. if (!auditForm.value.result) {
  335. ElMessage.warning('请选择审核结果')
  336. return
  337. }
  338. if (!auditForm.value.opinion) {
  339. ElMessage.warning('请输入审核意见')
  340. return
  341. }
  342. submitting.value = true
  343. try {
  344. // 调用 handleAlarm 真实接口处理报警
  345. const res = await handleAlarm(currentAlarm.value.id, userStore.name || 'admin', auditForm.value.opinion || auditForm.value.result)
  346. if (res.code === 200 || res.code === 0) {
  347. ElMessage.success('审核提交成功')
  348. auditVisible.value = false
  349. loadData()
  350. } else {
  351. ElMessage.error(res.msg || '审核提交失败')
  352. }
  353. } catch (error) {
  354. console.error('审核提交失败', error)
  355. ElMessage.error('审核提交失败')
  356. } finally {
  357. submitting.value = false
  358. }
  359. }
  360. // ==================== 查看详情弹窗 ====================
  361. const viewVisible = ref(false)
  362. const viewRow = ref({})
  363. async function openView(row) {
  364. viewRow.value = { ...row }
  365. viewVisible.value = true
  366. }
  367. // ==================== 地图初始化 ====================
  368. function initMap() {
  369. const container = document.getElementById('bjshMap')
  370. if (!container || typeof BMapGL === 'undefined') {
  371. console.warn('BMapGL 未加载或容器不存在')
  372. return
  373. }
  374. try {
  375. mapInstance = new BMapGL.Map('bjshMap')
  376. const centerPoint = new BMapGL.Point(110.393, 28.452)
  377. mapInstance.centerAndZoom(centerPoint, 15)
  378. mapInstance.enableScrollWheelZoom(true)
  379. refreshMapMarkers()
  380. mapInitialized = true
  381. } catch (e) {
  382. console.error('地图初始化失败:', e)
  383. }
  384. }
  385. function refreshMapMarkers() {
  386. if (!mapInstance) return
  387. mapInstance.clearOverlays()
  388. let firstPoint = null
  389. tableData.value.forEach(point => {
  390. const lng = parseFloat(point.longitude)
  391. const lat = parseFloat(point.latitude)
  392. if (!lng || !lat) return
  393. if (!firstPoint) firstPoint = { lng, lat }
  394. addMapMarker(point, lng, lat)
  395. })
  396. // 默认定位到第一个报警点位
  397. if (firstPoint) {
  398. mapInstance.centerAndZoom(new BMapGL.Point(firstPoint.lng, firstPoint.lat), 15)
  399. }
  400. }
  401. function addMapMarker(point, lng, lat) {
  402. const color = levelColorMap[point.warningLevel] || '#409eff'
  403. const levelTextStr = levelMap[point.warningLevel] || '一般'
  404. const statusTextStr = auditStatusMap[point.status] || '待审核'
  405. const statusColor = auditStatusColorMap[point.status] || '#e6a23c'
  406. const bPoint = new BMapGL.Point(lng, lat)
  407. const html = `<div style="text-align:center;cursor:pointer;">
  408. <div style="color:#fff;background:${color};border-radius:4px;padding:2px 6px;font-size:11px;white-space:nowrap;display:inline-block;margin-bottom:2px;">${point.equipmentName || '-'}</div>
  409. <div style="color:#fff;font-size:10px;background:rgba(0,0,0,0.6);border-radius:3px;padding:1px 4px;margin-bottom:2px;">${levelTextStr} | ${point.warningType || '-'}:${point.currentValue || '-'}</div>
  410. <div><img src="${locationIcon}" style="width:24px;height:24px;display:block;margin:0 auto;"/></div>
  411. </div>`
  412. const label = new BMapGL.Label(html, {
  413. position: bPoint,
  414. offset: new BMapGL.Size(-30, -55)
  415. })
  416. label.setStyle({
  417. border: 'none',
  418. background: 'transparent',
  419. padding: '0'
  420. })
  421. mapInstance.addOverlay(label)
  422. // 点击标记弹出信息窗口
  423. label.addEventListener('click', function () {
  424. const infoContent = `<div style="font-size:13px;line-height:1.8;">
  425. <div><b>${point.equipmentName || '-'}</b></div>
  426. <div>报警等级:<b style="color:${color}">${levelTextStr}</b></div>
  427. <div>报警时间:${point.warningTime || '-'}</div>
  428. <div>监测值:<b style="color:${color}">${point.warningType || '-'}:${point.currentValue || '-'}</b></div>
  429. <div>阈值:${point.thresholdValue || '-'}</div>
  430. <div>审核状态:<b style="color:${statusColor}">${statusTextStr}</b></div>
  431. </div>`
  432. const infoWindow = new BMapGL.InfoWindow(infoContent, {
  433. width: 220,
  434. title: ''
  435. })
  436. mapInstance.openInfoWindow(infoWindow, bPoint)
  437. })
  438. }
  439. // ==================== 生命周期 ====================
  440. onMounted(async () => {
  441. await loadData()
  442. nextTick(() => {
  443. initMap()
  444. })
  445. })
  446. </script>
  447. <style scoped>
  448. .bjsh-page {
  449. display: flex;
  450. flex-direction: column;
  451. gap: 12px;
  452. }
  453. .map-section {
  454. width: 100%;
  455. }
  456. .map-container {
  457. width: 100%;
  458. height: 350px;
  459. border: 1px solid #e4e7ed;
  460. border-radius: 4px;
  461. }
  462. .table-section {
  463. width: 100%;
  464. }
  465. .table-section .search-form {
  466. margin-bottom: 12px;
  467. }
  468. /* 弹窗内部样式 */
  469. .section-title {
  470. font-size: 15px;
  471. font-weight: bold;
  472. margin-bottom: 0;
  473. }
  474. .upload-tip {
  475. font-size: 12px;
  476. color: #909399;
  477. margin-top: 4px;
  478. }
  479. .audit-form-section {
  480. margin-top: 12px;
  481. padding: 12px;
  482. background: #f8f9fb;
  483. border-radius: 6px;
  484. }
  485. .audit-timeline-section {
  486. margin-top: 16px;
  487. }
  488. .timeline-node {
  489. font-weight: 600;
  490. font-size: 14px;
  491. color: #303133;
  492. }
  493. .timeline-desc {
  494. font-size: 12px;
  495. color: #606266;
  496. margin-top: 2px;
  497. }
  498. </style>