index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. <template>
  2. <div class="app-container home">
  3. <!-- 欢迎标题区 -->
  4. <div class="welcome-header mb-[20px]">
  5. <h2 class="text-xl font-bold text-gray-800">欢迎回来,管理员</h2>
  6. <p class="text-sm text-gray-500 mt-1">今日是,今天也是安全的一天</p>
  7. </div>
  8. <!-- 今日数据卡片 -->
  9. <el-row :gutter="16" class="mb-[20px]">
  10. <el-col :xs="24" :sm="12" :lg="6">
  11. <div class="stat-card stat-card-blue" @click="goToHazard()">
  12. <div class="stat-card-content">
  13. <div class="stat-info">
  14. <p class="stat-label">安全隐患总数</p>
  15. <p class="stat-value">{{ dashboardData.hazardTotal }}</p>
  16. <p class="stat-trend">
  17. <span class="trend-up">↑ 较前日 +3.2%</span>
  18. </p>
  19. </div>
  20. <div class="stat-icon">
  21. <svg viewBox="0 0 24 24" width="32" height="32">
  22. <path fill="currentColor" d="M12 2L2 7v6c0 5.5 3.8 10.7 9 12 5.2-1.3 9-6.5 9-12V7l-10-5z"/>
  23. </svg>
  24. </div>
  25. </div>
  26. </div>
  27. </el-col>
  28. <el-col :xs="24" :sm="12" :lg="6">
  29. <div class="stat-card stat-card-green" @click="goToHazard('已处理')">
  30. <div class="stat-card-content">
  31. <div class="stat-info">
  32. <p class="stat-label">已处理隐患</p>
  33. <p class="stat-value">{{ dashboardData.hazardProcessed }}</p>
  34. <p class="stat-trend">
  35. <span class="trend-normal">↑ 处理率 75%</span>
  36. </p>
  37. </div>
  38. <div class="stat-icon">
  39. <svg viewBox="0 0 24 24" width="32" height="32">
  40. <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
  41. </svg>
  42. </div>
  43. </div>
  44. </div>
  45. </el-col>
  46. <el-col :xs="24" :sm="12" :lg="6">
  47. <div class="stat-card stat-card-orange" @click="goToHazard('待处理')">
  48. <div class="stat-card-content">
  49. <div class="stat-info">
  50. <p class="stat-label">待处理隐患</p>
  51. <p class="stat-value">{{ dashboardData.hazardPending }}</p>
  52. <p class="stat-trend">
  53. <span class="trend-warn">需尽快处理</span>
  54. </p>
  55. </div>
  56. <div class="stat-icon">
  57. <svg viewBox="0 0 24 24" width="32" height="32">
  58. <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
  59. </svg>
  60. </div>
  61. </div>
  62. </div>
  63. </el-col>
  64. <el-col :xs="24" :sm="12" :lg="6">
  65. <div class="stat-card stat-card-purple" @click="goToEquipment()">
  66. <div class="stat-card-content">
  67. <div class="stat-info">
  68. <p class="stat-label">特种设备数量</p>
  69. <p class="stat-value">{{ dashboardData.equipmentTotal }}</p>
  70. <p class="stat-trend">
  71. <span class="trend-normal">✓ 全部检验合格</span>
  72. </p>
  73. </div>
  74. <div class="stat-icon">
  75. <svg viewBox="0 0 24 24" width="32" height="32">
  76. <path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
  77. </svg>
  78. </div>
  79. </div>
  80. </div>
  81. </el-col>
  82. </el-row>
  83. <!-- 图表区域 -->
  84. <el-row :gutter="16" class="mb-[20px]">
  85. <el-col :xs="24" :lg="16">
  86. <el-card shadow="never" class="chart-card">
  87. <template #header>
  88. <div class="chart-header">
  89. <span class="chart-title">月度隐患处理趋势</span>
  90. </div>
  91. </template>
  92. <div ref="trendChartRef" style="width: 100%; height: 320px;"></div>
  93. </el-card>
  94. </el-col>
  95. <el-col :xs="24" :lg="8">
  96. <el-card shadow="never" class="chart-card">
  97. <template #header>
  98. <div class="chart-header">
  99. <span class="chart-title">隐患类型分布</span>
  100. </div>
  101. </template>
  102. <div ref="pieChartRef" style="width: 100%; height: 320px;"></div>
  103. </el-card>
  104. </el-col>
  105. </el-row>
  106. <!-- 最新安全隐患 -->
  107. <el-row class="mb-[20px]">
  108. <el-col :span="24">
  109. <el-card shadow="never" class="list-card">
  110. <template #header>
  111. <div class="list-header">
  112. <span class="list-title">最新安全隐患</span>
  113. <a class="view-all" @click="goToHazard()">查看全部→</a>
  114. </div>
  115. </template>
  116. <el-table :data="latestHazards" stripe style="width: 100%" :show-overflow-tooltip="true">
  117. <el-table-column prop="hazardNo" label="隐患编号" min-width="120" />
  118. <el-table-column prop="hazardLocation" label="隐患位置" min-width="140" />
  119. <el-table-column prop="hazardType" label="隐患类型" min-width="120" />
  120. <el-table-column prop="riskLevel" label="风险等级" min-width="100">
  121. <template #default="scope">
  122. <el-tag :type="getRiskLevelType(scope.row.riskLevel)" size="small">{{ scope.row.riskLevel }}</el-tag>
  123. </template>
  124. </el-table-column>
  125. <el-table-column prop="discoverTime" label="上报时间" min-width="120" />
  126. <el-table-column prop="processStatus" label="状态" min-width="100">
  127. <template #default="scope">
  128. <el-tag :type="getStatusType(scope.row.processStatus)" size="small">{{ scope.row.processStatus }}</el-tag>
  129. </template>
  130. </el-table-column>
  131. </el-table>
  132. </el-card>
  133. </el-col>
  134. </el-row>
  135. <!-- 底部列表 -->
  136. <el-row :gutter="16">
  137. <el-col :xs="24" :lg="8">
  138. <el-card shadow="never" class="list-card">
  139. <template #header>
  140. <div class="list-header">
  141. <span class="list-title">近期安全培训</span>
  142. <a class="view-all" @click="goToTraining()">查看全部→</a>
  143. </div>
  144. </template>
  145. <div class="training-list">
  146. <div v-for="(item, index) in trainingList" :key="index" class="training-item" @click="goToTraining()">
  147. <div class="training-info">
  148. <p class="training-title">{{ item.trainingTitle }}</p>
  149. <p class="training-time">{{ item.trainingTime }}</p>
  150. </div>
  151. </div>
  152. <div v-if="!trainingList.length" class="empty-hint">暂无培训安排</div>
  153. </div>
  154. </el-card>
  155. </el-col>
  156. <el-col :xs="24" :lg="16">
  157. <el-card shadow="never" class="list-card">
  158. <template #header>
  159. <div class="list-header">
  160. <span class="list-title">最新公告</span>
  161. <a class="view-all" @click="goToNotice()">查看全部→</a>
  162. </div>
  163. </template>
  164. <div class="notice-list">
  165. <div v-for="(item, index) in noticeList" :key="index" class="notice-item" @click="goToNotice()">
  166. <div class="notice-content">
  167. <p class="notice-title">{{ item.noticeTitle }}</p>
  168. <p class="notice-desc">{{ item.noticeContent }}</p>
  169. </div>
  170. <span class="notice-date">{{ item.createTime }}</span>
  171. </div>
  172. <div v-if="!noticeList.length" class="empty-hint">暂无公告</div>
  173. </div>
  174. </el-card>
  175. </el-col>
  176. </el-row>
  177. </div>
  178. </template>
  179. <script setup name="Dashboard" lang="ts">
  180. import { ref, reactive, onMounted } from 'vue';
  181. import { useRouter } from 'vue-router';
  182. import * as echarts from 'echarts';
  183. import { getHazardStats, getHazardTrend, getHazardDistribution, getHazardLatest } from '@/api/riskctrl/hazard';
  184. import { getEquipmentStats } from '@/api/riskctrl/specialEquipment';
  185. import { getLatestTrainings } from '@/api/riskctrl/safetyTraining';
  186. import { getPublishedNotices } from '@/api/riskctrl/noticeAnnouncement';
  187. import type { HazardVO } from '@/api/riskctrl/hazard/types';
  188. const router = useRouter();
  189. // 图表引用
  190. const trendChartRef = ref<HTMLDivElement>();
  191. const pieChartRef = ref<HTMLDivElement>();
  192. let trendChart: echarts.ECharts | null = null;
  193. let pieChart: echarts.ECharts | null = null;
  194. // 数据(与截图保持一致)
  195. const dashboardData = reactive({
  196. hazardTotal: 128,
  197. hazardProcessed: 96,
  198. hazardPending: 32,
  199. equipmentTotal: 47
  200. });
  201. // 月度隐患处理趋势数据(与截图保持一致)
  202. const trendData = ref<any[]>([
  203. { month: '1月', total: 10, closed: 10 },
  204. { month: '2月', total: 15, closed: 12 },
  205. { month: '3月', total: 18, closed: 14 },
  206. { month: '4月', total: 13, closed: 12 },
  207. { month: '5月', total: 22, closed: 18 },
  208. { month: '6月', total: 18, closed: 15 }
  209. ]);
  210. // 隐患类型分布数据(与截图保持一致)
  211. const pieData = ref<any[]>([
  212. { type: '电气安全', count: 35 },
  213. { type: '消防隐患', count: 25 },
  214. { type: '设备安全', count: 20 },
  215. { type: '环境安全', count: 18 },
  216. { type: '人员操作', count: 30 }
  217. ]);
  218. const latestHazards = ref<HazardVO[]>([]);
  219. // 近期安全培训
  220. const trainingList = ref<Array<{ trainingTitle: string; trainingTime: string; trainingId: string | number }>>([]);
  221. // 最新公告
  222. const noticeList = ref<Array<{ noticeTitle: string; noticeContent: string; createTime: string; noticeId: string | number }>>([]);
  223. // 风险等级标签样式
  224. const getRiskLevelType = (level: string) => {
  225. const map: Record<string, string> = {
  226. '重大': 'danger',
  227. '较大': 'warning',
  228. '一般': ''
  229. };
  230. return map[level] || 'info';
  231. };
  232. // 状态标签样式
  233. const getStatusType = (status: string) => {
  234. const map: Record<string, string> = {
  235. '待处理': 'danger',
  236. '处理中': 'warning',
  237. '已处理': 'success'
  238. };
  239. return map[status] || 'info';
  240. };
  241. // 加载统计数据
  242. const loadStats = async () => {
  243. try {
  244. const [hazardRes, equipRes] = await Promise.all([
  245. getHazardStats(),
  246. getEquipmentStats()
  247. ]);
  248. const hdata = hazardRes.data as any;
  249. if (hdata.total) dashboardData.hazardTotal = hdata.total;
  250. if (hdata.processed) dashboardData.hazardProcessed = hdata.processed;
  251. if (hdata.unprocessed) dashboardData.hazardPending = hdata.unprocessed;
  252. if ((equipRes.data as any)?.total) dashboardData.equipmentTotal = (equipRes.data as any).total;
  253. } catch (e) {
  254. console.error('加载统计失败', e);
  255. }
  256. };
  257. // 加载最新隐患
  258. const loadLatestHazards = async () => {
  259. try {
  260. const res = await getHazardLatest(5);
  261. latestHazards.value = res.data || [];
  262. } catch (e) {
  263. console.error('加载最新隐患失败', e);
  264. }
  265. };
  266. // 加载趋势
  267. const loadTrend = async () => {
  268. try {
  269. const res = await getHazardTrend(6);
  270. trendData.value = res.data || [];
  271. initTrendChart();
  272. } catch (e) {
  273. console.error('加载趋势失败', e);
  274. }
  275. };
  276. // 加载分布
  277. const loadDistribution = async () => {
  278. try {
  279. const res = await getHazardDistribution();
  280. pieData.value = res.data || [];
  281. initPieChart();
  282. } catch (e) {
  283. console.error('加载分布失败', e);
  284. }
  285. };
  286. // 初始化趋势图
  287. const initTrendChart = () => {
  288. if (!trendChartRef.value) return;
  289. if (trendChart) trendChart.dispose();
  290. trendChart = echarts.init(trendChartRef.value);
  291. const months = trendData.value.map((item: any) => item.month);
  292. const totals = trendData.value.map((item: any) => item.total || 0);
  293. const closeds = trendData.value.map((item: any) => item.closed || 0);
  294. const option: echarts.EChartsOption = {
  295. tooltip: {
  296. trigger: 'axis',
  297. axisPointer: { type: 'cross' }
  298. },
  299. legend: {
  300. data: ['上报隐患', '已处理'],
  301. bottom: 0,
  302. icon: 'circle',
  303. itemWidth: 8,
  304. itemHeight: 8
  305. },
  306. grid: {
  307. left: '3%',
  308. right: '4%',
  309. bottom: '15%',
  310. top: '10%',
  311. containLabel: true
  312. },
  313. xAxis: {
  314. type: 'category',
  315. boundaryGap: false,
  316. data: months,
  317. axisLine: { lineStyle: { color: '#e0e0e0' } },
  318. axisLabel: { color: '#666', fontSize: 12 }
  319. },
  320. yAxis: {
  321. type: 'value',
  322. axisLine: { show: false },
  323. splitLine: { lineStyle: { color: '#f0f0f0' } },
  324. axisLabel: { color: '#666', fontSize: 12 }
  325. },
  326. series: [
  327. {
  328. name: '上报隐患',
  329. type: 'line',
  330. smooth: true,
  331. data: totals,
  332. itemStyle: { color: '#3b82f6' },
  333. lineStyle: { width: 2 },
  334. symbol: 'circle',
  335. symbolSize: 6
  336. },
  337. {
  338. name: '已处理',
  339. type: 'line',
  340. smooth: true,
  341. data: closeds,
  342. itemStyle: { color: '#22c55e' },
  343. lineStyle: { width: 2 },
  344. symbol: 'circle',
  345. symbolSize: 6
  346. }
  347. ]
  348. };
  349. trendChart.setOption(option);
  350. };
  351. // 初始化饼图
  352. const initPieChart = () => {
  353. if (!pieChartRef.value) return;
  354. if (pieChart) pieChart.dispose();
  355. pieChart = echarts.init(pieChartRef.value);
  356. const data = pieData.value.map((item: any) => ({
  357. name: item.type,
  358. value: item.count
  359. }));
  360. const colors = ['#3b82f6', '#ef4444', '#f59e0b', '#22c55e', '#8b5cf6'];
  361. const option: echarts.EChartsOption = {
  362. tooltip: {
  363. trigger: 'item',
  364. formatter: '{b}: {c} ({d}%)'
  365. },
  366. legend: {
  367. orient: 'vertical',
  368. right: '5%',
  369. top: 'center',
  370. itemWidth: 10,
  371. itemHeight: 10,
  372. textStyle: { fontSize: 12, color: '#666' },
  373. data: ['电气安全', '消防隐患', '设备安全', '环境安全', '人员操作']
  374. },
  375. color: colors,
  376. series: [
  377. {
  378. name: '隐患类型',
  379. type: 'pie',
  380. radius: ['45%', '70%'],
  381. center: ['35%', '50%'],
  382. avoidLabelOverlap: false,
  383. itemStyle: {
  384. borderRadius: 6,
  385. borderColor: '#fff',
  386. borderWidth: 2
  387. },
  388. label: {
  389. show: false
  390. },
  391. emphasis: {
  392. label: {
  393. show: true,
  394. fontSize: 14,
  395. fontWeight: 'bold'
  396. }
  397. },
  398. labelLine: {
  399. show: false
  400. },
  401. data: data.length ? data : [{ name: '暂无数据', value: 0 }]
  402. }
  403. ]
  404. };
  405. pieChart.setOption(option);
  406. };
  407. // 路由跳转
  408. const goToHazard = (status?: string) => {
  409. const query: any = {};
  410. if (status === '已处理') query.processStatus = '已处理';
  411. if (status === '待处理') query.processStatus = '待处理';
  412. router.push({ path: '/riskctrl/hazard', query });
  413. };
  414. const goToEquipment = () => {
  415. router.push('/riskctrl/specialEquipment');
  416. };
  417. const goToTraining = () => {
  418. router.push('/riskctrl/safetyTraining');
  419. };
  420. const goToNotice = () => {
  421. router.push('/riskctrl/noticeAnnouncement');
  422. };
  423. // 加载培训和公告数据
  424. const loadTrainingsAndNotices = async () => {
  425. try {
  426. const [trainingRes, noticeRes] = await Promise.all([
  427. getLatestTrainings(3),
  428. getPublishedNotices(3)
  429. ]);
  430. if (trainingRes.data) {
  431. trainingList.value = (trainingRes.data as any) || [];
  432. }
  433. if (noticeRes.data) {
  434. noticeList.value = (noticeRes.data as any) || [];
  435. }
  436. } catch (e) {
  437. console.error('加载培训/公告失败', e);
  438. }
  439. };
  440. // 窗口大小变化时重绘图表
  441. const handleResize = () => {
  442. trendChart?.resize();
  443. pieChart?.resize();
  444. };
  445. onMounted(() => {
  446. // 先初始化图表(使用静态数据)
  447. setTimeout(() => {
  448. initTrendChart();
  449. initPieChart();
  450. }, 100);
  451. // 尝试从后端加载数据(失败则保持静态数据)
  452. loadStats();
  453. loadLatestHazards();
  454. loadTrend();
  455. loadDistribution();
  456. loadTrainingsAndNotices();
  457. window.addEventListener('resize', handleResize);
  458. });
  459. </script>
  460. <style lang="scss" scoped>
  461. .home {
  462. padding: 20px;
  463. background-color: #f5f7fb;
  464. min-height: 100%;
  465. }
  466. .welcome-header {
  467. h2 {
  468. font-size: 20px;
  469. font-weight: 600;
  470. color: #1f2937;
  471. }
  472. p {
  473. font-size: 13px;
  474. color: #9ca3af;
  475. }
  476. }
  477. .stat-card {
  478. background: #fff;
  479. border-radius: 12px;
  480. padding: 20px;
  481. cursor: pointer;
  482. transition: all 0.3s ease;
  483. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  484. &:hover {
  485. transform: translateY(-2px);
  486. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
  487. }
  488. .stat-card-content {
  489. display: flex;
  490. align-items: center;
  491. justify-content: space-between;
  492. }
  493. .stat-info {
  494. flex: 1;
  495. }
  496. .stat-label {
  497. font-size: 13px;
  498. color: #6b7280;
  499. margin-bottom: 8px;
  500. }
  501. .stat-value {
  502. font-size: 28px;
  503. font-weight: 700;
  504. color: #1f2937;
  505. line-height: 1.2;
  506. margin-bottom: 6px;
  507. }
  508. .stat-trend {
  509. font-size: 12px;
  510. .trend-up {
  511. color: #22c55e;
  512. }
  513. .trend-normal {
  514. color: #22c55e;
  515. }
  516. .trend-warn {
  517. color: #f59e0b;
  518. }
  519. }
  520. .stat-icon {
  521. width: 48px;
  522. height: 48px;
  523. border-radius: 10px;
  524. display: flex;
  525. align-items: center;
  526. justify-content: center;
  527. flex-shrink: 0;
  528. }
  529. &.stat-card-blue .stat-icon {
  530. background-color: #eff6ff;
  531. color: #3b82f6;
  532. }
  533. &.stat-card-green .stat-icon {
  534. background-color: #f0fdf4;
  535. color: #22c55e;
  536. }
  537. &.stat-card-orange .stat-icon {
  538. background-color: #fffbeb;
  539. color: #f59e0b;
  540. }
  541. &.stat-card-purple .stat-icon {
  542. background-color: #faf5ff;
  543. color: #8b5cf6;
  544. }
  545. }
  546. .chart-card {
  547. border-radius: 12px;
  548. border: none;
  549. :deep(.el-card__header) {
  550. padding: 16px 20px;
  551. border-bottom: 1px solid #f3f4f6;
  552. }
  553. :deep(.el-card__body) {
  554. padding: 16px 20px;
  555. }
  556. .chart-header {
  557. display: flex;
  558. align-items: center;
  559. justify-content: space-between;
  560. }
  561. .chart-title {
  562. font-size: 15px;
  563. font-weight: 600;
  564. color: #1f2937;
  565. }
  566. }
  567. .list-card {
  568. border-radius: 12px;
  569. border: none;
  570. :deep(.el-card__header) {
  571. padding: 16px 20px;
  572. border-bottom: 1px solid #f3f4f6;
  573. }
  574. :deep(.el-card__body) {
  575. padding: 0;
  576. }
  577. .list-header {
  578. display: flex;
  579. align-items: center;
  580. justify-content: space-between;
  581. }
  582. .list-title {
  583. font-size: 15px;
  584. font-weight: 600;
  585. color: #1f2937;
  586. }
  587. .view-all {
  588. font-size: 13px;
  589. color: #3b82f6;
  590. cursor: pointer;
  591. &:hover {
  592. color: #2563eb;
  593. }
  594. }
  595. }
  596. .training-list {
  597. padding: 12px 20px;
  598. .training-item {
  599. padding: 12px 0;
  600. border-bottom: 1px solid #f3f4f6;
  601. cursor: pointer;
  602. &:last-child {
  603. border-bottom: none;
  604. }
  605. &:hover {
  606. .training-title {
  607. color: #3b82f6;
  608. }
  609. }
  610. .training-title {
  611. font-size: 14px;
  612. color: #374151;
  613. margin-bottom: 4px;
  614. transition: color 0.2s;
  615. }
  616. .training-time {
  617. font-size: 12px;
  618. color: #9ca3af;
  619. }
  620. }
  621. }
  622. .notice-list {
  623. padding: 12px 20px;
  624. .notice-item {
  625. display: flex;
  626. align-items: flex-start;
  627. justify-content: space-between;
  628. padding: 12px 0;
  629. border-bottom: 1px solid #f3f4f6;
  630. cursor: pointer;
  631. &:last-child {
  632. border-bottom: none;
  633. }
  634. &:hover {
  635. .notice-title {
  636. color: #3b82f6;
  637. }
  638. }
  639. .notice-content {
  640. flex: 1;
  641. margin-right: 16px;
  642. }
  643. .notice-title {
  644. font-size: 14px;
  645. font-weight: 500;
  646. color: #374151;
  647. margin-bottom: 4px;
  648. transition: color 0.2s;
  649. }
  650. .notice-desc {
  651. font-size: 12px;
  652. color: #9ca3af;
  653. line-height: 1.5;
  654. overflow: hidden;
  655. text-overflow: ellipsis;
  656. display: -webkit-box;
  657. -webkit-line-clamp: 1;
  658. -webkit-box-orient: vertical;
  659. }
  660. .notice-date {
  661. font-size: 12px;
  662. color: #9ca3af;
  663. white-space: nowrap;
  664. flex-shrink: 0;
  665. }
  666. }
  667. }
  668. .empty-hint {
  669. text-align: center;
  670. padding: 20px;
  671. color: #9ca3af;
  672. font-size: 13px;
  673. }
  674. </style>