index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  1. <template>
  2. <div class="app-container">
  3. <!-- 统计数据卡片 -->
  4. <el-row :gutter="20" class="mb20">
  5. <el-col :span="4">
  6. <el-card class="stat-card total-people">
  7. <div class="stat-item">
  8. <div class="stat-number">{{ statistics.totalPeople }}</div>
  9. <div class="stat-label">总人数</div>
  10. </div>
  11. </el-card>
  12. </el-col>
  13. <el-col :span="4">
  14. <el-card class="stat-card total-entries">
  15. <div class="stat-item">
  16. <div class="stat-number">{{ statistics.totalEntries }}</div>
  17. <div class="stat-label">总次数</div>
  18. </div>
  19. </el-card>
  20. </el-col>
  21. <el-col :span="4">
  22. <el-card class="stat-card avg-frequency">
  23. <div class="stat-item">
  24. <div class="stat-number">{{ statistics.avgFrequency }}</div>
  25. <div class="stat-label">平均频次</div>
  26. </div>
  27. </el-card>
  28. </el-col>
  29. <el-col :span="4">
  30. <el-card class="stat-card max-frequency">
  31. <div class="stat-item">
  32. <div class="stat-number">{{ statistics.maxFrequency }}</div>
  33. <div class="stat-label">最高频次</div>
  34. </div>
  35. </el-card>
  36. </el-col>
  37. <el-col :span="4">
  38. <el-card class="stat-card night-entries">
  39. <div class="stat-item">
  40. <div class="stat-number">{{ statistics.nightPeople }}</div>
  41. <div class="stat-label">夜间出入境人数</div>
  42. </div>
  43. </el-card>
  44. </el-col>
  45. <el-col :span="4">
  46. <el-card class="stat-card holiday-entries">
  47. <div class="stat-item">
  48. <div class="stat-number">{{ statistics.holidayPeople }}</div>
  49. <div class="stat-label">节假日出入境人数</div>
  50. </div>
  51. </el-card>
  52. </el-col>
  53. </el-row>
  54. <!-- 图表区域 -->
  55. <el-row :gutter="20" class="mb20">
  56. <el-col :span="12">
  57. <el-card class="chart-card">
  58. <template #header>
  59. <div class="chart-header">
  60. <span>出入境频次分布</span>
  61. </div>
  62. </template>
  63. <div class="chart-container" ref="pieChartContainer" style="height: 300px;"></div>
  64. </el-card>
  65. </el-col>
  66. <el-col :span="12">
  67. <el-card class="chart-card">
  68. <template #header>
  69. <div class="chart-header">
  70. <span>节假日出入境</span>
  71. </div>
  72. </template>
  73. <div class="holiday-container" style="height: 300px; overflow-y: auto; padding: 20px;">
  74. <div v-loading="holidayLoading" class="holiday-list">
  75. <div v-for="(item, index) in holidayList" :key="index" class="holiday-item">
  76. <div class="holiday-content">
  77. <span class="holiday-name">{{ item.holidayName }}</span>
  78. <span class="holiday-dates">({{ item.startDate }}至{{ item.endDate }})</span>
  79. <span class="holiday-count">{{ item.inOutCount }}次</span>
  80. </div>
  81. </div>
  82. <div v-if="holidayList.length === 0 && holidayLoading" class="no-data">
  83. <span>加载中...</span>
  84. </div>
  85. <div v-if="holidayList.length === 0 && !holidayLoading" class="no-data">
  86. <span>暂无数据</span>
  87. </div>
  88. </div>
  89. </div>
  90. </el-card>
  91. </el-col>
  92. </el-row>
  93. <el-row :gutter="20" class="mb20">
  94. <el-col :span="24">
  95. <el-card class="chart-card">
  96. <template #header>
  97. <div class="chart-header">
  98. <span>夜间出入境趋势</span>
  99. </div>
  100. </template>
  101. <div class="chart-container" ref="nightChartContainer" style="height: 300px;"></div>
  102. </el-card>
  103. </el-col>
  104. </el-row>
  105. <!-- 快捷入口 -->
  106. <el-row :gutter="20">
  107. <el-col :span="24">
  108. <el-card class="quick-access-card">
  109. <template #header>
  110. <div class="card-header">
  111. <span>快捷入口</span>
  112. </div>
  113. </template>
  114. <div class="quick-access-grid">
  115. <div class="quick-item" @click="goToPage('/anal/high')">
  116. <div class="quick-icon bg-blue">
  117. <el-icon><TrendCharts /></el-icon>
  118. </div>
  119. <div class="quick-text">高频出入境</div>
  120. </div>
  121. <div class="quick-item" @click="goToPage('/anal/night')">
  122. <div class="quick-icon bg-purple">
  123. <el-icon><Moon /></el-icon>
  124. </div>
  125. <div class="quick-text">夜间出入境</div>
  126. </div>
  127. <div class="quick-item" @click="goToPage('/anal/holiday')">
  128. <div class="quick-icon bg-green">
  129. <el-icon><Calendar /></el-icon>
  130. </div>
  131. <div class="quick-text">节假日出入境</div>
  132. </div>
  133. <div class="quick-item" @click="goToPage('/anal/rapid')">
  134. <div class="quick-icon bg-orange">
  135. <el-icon><Lightning /></el-icon>
  136. </div>
  137. <div class="quick-text">短期往返分析</div>
  138. </div>
  139. <div class="quick-item" @click="goToPage('/anal/low')">
  140. <div class="quick-icon bg-red">
  141. <el-icon><Document /></el-icon>
  142. </div>
  143. <div class="quick-text">低频出入境</div>
  144. </div>
  145. <div class="quick-item" @click="goToPage('/anal/overstay')">
  146. <div class="quick-icon bg-cyan">
  147. <el-icon><Timer /></el-icon>
  148. </div>
  149. <div class="quick-text">境外滞留分析</div>
  150. </div>
  151. </div>
  152. </el-card>
  153. </el-col>
  154. </el-row>
  155. </div>
  156. </template>
  157. <script setup name="Index">
  158. import * as echarts from 'echarts'
  159. import { onMounted, onUnmounted, ref, reactive, toRefs, nextTick } from 'vue'
  160. import { House, TrendCharts, Moon, Calendar, Lightning, Document, Timer } from '@element-plus/icons-vue'
  161. import { useRouter } from 'vue-router'
  162. import { getInOutStats, listHighFrequencyInOut, getNightInOutStats, getHolidayInOutStats } from "@/api/biz/anal";
  163. const router = useRouter()
  164. const holidayList = ref([]);
  165. const holidayLoading = ref(false);
  166. const pieChartContainer = ref(null);
  167. const nightChartContainer = ref(null);
  168. const statistics = ref({
  169. totalPeople: 0,
  170. totalEntries: 0,
  171. avgFrequency: 0,
  172. maxFrequency: 0,
  173. nightPeople: 0,
  174. holidayPeople: 0
  175. });
  176. // 直接使用当年查询,不再需要查询表单
  177. const data = reactive({
  178. queryParams: {
  179. pageNum: 1,
  180. pageSize: 10,
  181. year: new Date().getFullYear().toString() // 直接使用当前年份
  182. }
  183. });
  184. const { queryParams } = toRefs(data);
  185. /** 查询数据 */
  186. function getList() {
  187. // 准备查询参数 - 直接使用当前年份
  188. const params = {
  189. year: queryParams.value.year
  190. };
  191. // 设置加载状态
  192. holidayLoading.value = true;
  193. // 并行调用所有需要的API
  194. Promise.all([
  195. getInOutStats(params), // 基础统计
  196. getNightInOutStats(params), // 夜间统计
  197. getHolidayInOutStats(params), // 节假日统计
  198. listHighFrequencyInOut(params) // 高频数据用于频次分布
  199. ]).then(responses => {
  200. console.log('API响应数据:', responses); // 调试信息
  201. // 处理基础统计信息
  202. const statsResponse = responses[0];
  203. if (statsResponse.code === 200 && statsResponse.data) {
  204. const stats = statsResponse.data;
  205. statistics.value.totalPeople = stats.totalPeople || 0;
  206. statistics.value.totalEntries = stats.totalCount || 0;
  207. statistics.value.avgFrequency = Number(stats.avgFrequency).toFixed(2) || 0;
  208. statistics.value.maxFrequency = stats.maxFrequency || 0;
  209. }
  210. // 处理夜间统计信息
  211. const nightResponse = responses[1];
  212. if (nightResponse.code === 200 && nightResponse.data) {
  213. const nightStats = nightResponse.data;
  214. statistics.value.nightPeople = nightStats.totalPeople || 0;
  215. }
  216. // 处理节假日统计信息
  217. const holidayResponse = responses[2];
  218. console.log('节假日统计响应:', holidayResponse); // 调试信息
  219. if (holidayResponse.code === 200) {
  220. // 节假日统计数据结构为数组,包含节假日名称、出入境次数等信息
  221. const holidayData = Array.isArray(holidayResponse.data) ? holidayResponse.data : [];
  222. console.log('节假日统计数据:', holidayData); // 调试信息
  223. // 计算总人数
  224. const totalHolidayCount = holidayData.reduce((sum, item) => sum + (item.inOutCount || 0), 0);
  225. statistics.value.holidayPeople = totalHolidayCount;
  226. // 处理节假日列表数据
  227. holidayList.value = holidayData.map(item => ({
  228. holidayName: item.holidayName,
  229. inOutCount: item.inOutCount,
  230. startDate: item.startDate,
  231. endDate: item.endDate
  232. }));
  233. } else {
  234. console.error('节假日数据获取失败:', holidayResponse);
  235. holidayList.value = [];
  236. statistics.value.holidayPeople = 0;
  237. }
  238. // 更新图表
  239. nextTick(() => {
  240. renderPieChart(params); // 频次分布饼图
  241. renderNightChart(params); // 夜间趋势图
  242. });
  243. }).catch(error => {
  244. console.error('获取统计数据失败:', error);
  245. holidayList.value = [];
  246. statistics.value.holidayPeople = 0;
  247. }).finally(() => {
  248. holidayLoading.value = false;
  249. });
  250. }
  251. /** 渲染出入境频次分布饼图 */
  252. function renderPieChart(params) {
  253. if (!pieChartContainer.value) return;
  254. const myChart = echarts.init(pieChartContainer.value);
  255. // 调用后端接口获取频次分布数据
  256. getInOutStats(params).then(response => {
  257. let distributionData = [];
  258. if (response.code === 200 && response.data) {
  259. const stats = response.data;
  260. // 检查是否有频次分布数据
  261. if (stats.frequencyDistribution && typeof stats.frequencyDistribution === 'object') {
  262. const dist = stats.frequencyDistribution;
  263. distributionData = [];
  264. // 将对象格式转换为饼图需要的数组格式,并按区间排序
  265. const rangeOrder = ['1-5', '5-10', '10-20', '20-30', '30+'];
  266. // 先处理预定义的区间
  267. for (const range of rangeOrder) {
  268. if (dist.hasOwnProperty(range)) {
  269. // 根据区间范围选择颜色
  270. let color;
  271. if (range.startsWith('1-5')) color = '#5470c6';
  272. else if (range.startsWith('5-10')) color = '#91cc75';
  273. else if (range.startsWith('10-20')) color = '#fac858';
  274. else if (range.startsWith('20-30')) color = '#ee6666';
  275. else if (range.startsWith('30')) color = '#73c0de';
  276. else color = '#9da4b0';
  277. distributionData.push({
  278. value: dist[range],
  279. name: range,
  280. itemStyle: { color: color }
  281. });
  282. }
  283. }
  284. // 处理不在预定义区间中的其他区间
  285. for (const [range, count] of Object.entries(dist)) {
  286. if (!rangeOrder.includes(range)) {
  287. let color;
  288. if (range.startsWith('1-5')) color = '#5470c6';
  289. else if (range.startsWith('5-10')) color = '#91cc75';
  290. else if (range.startsWith('10-20')) color = '#fac858';
  291. else if (range.startsWith('20-30')) color = '#ee6666';
  292. else if (range.startsWith('30')) color = '#73c0de';
  293. else color = '#9da4b0';
  294. distributionData.push({
  295. value: count,
  296. name: range,
  297. itemStyle: { color: color }
  298. });
  299. }
  300. }
  301. }
  302. }
  303. const option = {
  304. tooltip: {
  305. trigger: 'item',
  306. formatter: '{a} <br/>{b}: {c}人 ({d}%)'
  307. },
  308. legend: {
  309. orient: 'vertical',
  310. left: 'right'
  311. },
  312. series: [{
  313. name: '人数',
  314. type: 'pie',
  315. radius: [20, 120], // 设置内外半径,形成南丁格尔图效果
  316. center: ['50%', '50%'],
  317. roseType: 'area', // 启用南丁格尔图模式
  318. itemStyle: {
  319. borderRadius: 8
  320. },
  321. label: {
  322. show: true,
  323. formatter: '{b}: {c}',
  324. fontSize: 10,
  325. position: 'outside'
  326. },
  327. data: distributionData,
  328. emphasis: {
  329. itemStyle: {
  330. shadowBlur: 10,
  331. shadowOffsetX: 0,
  332. shadowColor: 'rgba(0, 0, 0, 0.5)'
  333. }
  334. }
  335. }]
  336. };
  337. myChart.setOption(option);
  338. }).catch(() => {
  339. const option = {
  340. tooltip: {
  341. trigger: 'item'
  342. },
  343. legend: {
  344. orient: 'vertical',
  345. left: 'right'
  346. },
  347. series: [{
  348. name: '人数',
  349. type: 'pie',
  350. radius: [20, 120],
  351. center: ['50%', '50%'],
  352. roseType: 'area',
  353. itemStyle: {
  354. borderRadius: 8
  355. },
  356. label: {
  357. show: true,
  358. formatter: '{b}: {c}',
  359. fontSize: 10,
  360. position: 'outside'
  361. },
  362. data: [],
  363. emphasis: {
  364. itemStyle: {
  365. shadowBlur: 10,
  366. shadowOffsetX: 0,
  367. shadowColor: 'rgba(0, 0, 0, 0.5)'
  368. }
  369. }
  370. }]
  371. };
  372. myChart.setOption(option);
  373. });
  374. }
  375. /** 渲染夜间出入境趋势图 */
  376. function renderNightChart(params) {
  377. if (!nightChartContainer.value) return;
  378. const myChart = echarts.init(nightChartContainer.value);
  379. getNightInOutStats(params).then(response => {
  380. let xAxisData = [];
  381. let seriesData = [];
  382. if (response.code === 200 && response.data) {
  383. const stats = response.data;
  384. // 使用趋势数据
  385. if (stats.trendData && Array.isArray(stats.trendData)) {
  386. // 按年查询,显示每月趋势
  387. xAxisData = stats.trendData.map(item => {
  388. if (item.period) {
  389. const parts = item.period.split('-');
  390. if (parts.length === 2) {
  391. return parts[1] + '月';
  392. }
  393. }
  394. return '未知';
  395. });
  396. // 获取人数或次数数据
  397. seriesData = stats.trendData.map(item => item.inOutCount !== undefined ? item.inOutCount : 0);
  398. }
  399. }
  400. const option = {
  401. tooltip: {
  402. trigger: 'axis',
  403. axisPointer: {
  404. type: 'shadow'
  405. },
  406. formatter: (params) => {
  407. const param = params[0];
  408. if (param.name === '暂无数据') {
  409. return '暂无数据';
  410. }
  411. const date = xAxisData[param.dataIndex];
  412. const value = param.value;
  413. return `${date}<br/>夜间出入境人数:${value}`;
  414. }
  415. },
  416. grid: {
  417. show: false
  418. },
  419. xAxis: {
  420. type: 'category',
  421. data: xAxisData,
  422. axisLabel: {
  423. rotate: 45,
  424. fontSize: 12
  425. },
  426. boundaryGap: false
  427. },
  428. yAxis: {
  429. type: 'value',
  430. name: '人数',
  431. show: true
  432. },
  433. series: [{
  434. data: seriesData,
  435. type: 'line',
  436. smooth: true,
  437. symbol: 'emptyCircle',
  438. symbolSize: 6,
  439. lineStyle: {
  440. color: '#73c0de',
  441. width: 2
  442. },
  443. itemStyle: {
  444. color: '#3ba272'
  445. },
  446. areaStyle: {
  447. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  448. { offset: 0, color: 'rgba(115, 192, 222, 0.3)' },
  449. { offset: 1, color: 'rgba(115, 192, 222, 0.05)' }
  450. ])
  451. }
  452. }]
  453. };
  454. myChart.setOption(option);
  455. }).catch(() => {
  456. const option = {
  457. tooltip: {
  458. trigger: 'axis',
  459. axisPointer: {
  460. type: 'shadow'
  461. }
  462. },
  463. grid: {
  464. show: false
  465. },
  466. xAxis: {
  467. type: 'category',
  468. data: ['暂无数据'],
  469. axisLabel: {
  470. rotate: 45,
  471. fontSize: 12
  472. },
  473. boundaryGap: false
  474. },
  475. yAxis: {
  476. type: 'value',
  477. name: '人数',
  478. show: true
  479. },
  480. series: [{
  481. data: [0],
  482. type: 'line',
  483. smooth: true,
  484. symbol: 'emptyCircle',
  485. symbolSize: 6,
  486. lineStyle: {
  487. color: '#73c0de',
  488. width: 2
  489. },
  490. itemStyle: {
  491. color: '#3ba272'
  492. }
  493. }]
  494. };
  495. myChart.setOption(option);
  496. });
  497. }
  498. /** 跳转到指定页面 */
  499. function goToPage(path) {
  500. router.push(path)
  501. }
  502. // 页面加载时获取数据 - 直接查询当年数据
  503. onMounted(() => {
  504. getList();
  505. });
  506. // 组件卸载时销毁图表
  507. onUnmounted(() => {
  508. if (pieChartContainer.value) {
  509. const pieChart = echarts.getInstanceByDom(pieChartContainer.value);
  510. if (pieChart) pieChart.dispose();
  511. }
  512. if (nightChartContainer.value) {
  513. const nightChart = echarts.getInstanceByDom(nightChartContainer.value);
  514. if (nightChart) nightChart.dispose();
  515. }
  516. });
  517. </script>
  518. <style lang="scss" scoped>
  519. .app-container {
  520. padding: 20px;
  521. background-color: #f5f7f9;
  522. }
  523. .stat-card {
  524. text-align: center;
  525. border-radius: 12px;
  526. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  527. transition: all 0.3s ease;
  528. height: 100px;
  529. display: flex;
  530. align-items: center;
  531. justify-content: center;
  532. }
  533. .stat-card:hover {
  534. transform: translateY(-3px);
  535. box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
  536. }
  537. .stat-card.total-people {
  538. background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
  539. color: #1890ff;
  540. }
  541. .stat-card.total-entries {
  542. background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
  543. color: #52c41a;
  544. }
  545. .stat-card.avg-frequency {
  546. background: linear-gradient(135deg, #fffbe6 0%, #fff5a0 100%);
  547. color: #faad14;
  548. }
  549. .stat-card.max-frequency {
  550. background: linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%);
  551. color: #f5222d;
  552. }
  553. .stat-card.night-entries {
  554. background: linear-gradient(135deg, #f0f9ff 0%, #c0e8ff 100%);
  555. color: #409eff;
  556. }
  557. .stat-card.holiday-entries {
  558. background: linear-gradient(135deg, #fff0f5 0%, #ffd6e7 100%);
  559. color: #f759ab;
  560. }
  561. .stat-item {
  562. width: 100%;
  563. padding: 10px;
  564. }
  565. .stat-number {
  566. font-size: 24px;
  567. font-weight: bold;
  568. margin-bottom: 5px;
  569. }
  570. .stat-label {
  571. font-size: 13px;
  572. color: #606266;
  573. font-weight: 500;
  574. }
  575. .chart-card {
  576. border-radius: 12px;
  577. overflow: hidden;
  578. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  579. margin-bottom: 20px;
  580. }
  581. .chart-header {
  582. font-weight: 600;
  583. font-size: 16px;
  584. }
  585. .chart-container {
  586. width: 100%;
  587. height: 100%;
  588. }
  589. .table-container {
  590. width: 100%;
  591. padding: 0 20px 20px 20px;
  592. }
  593. .holiday-container {
  594. width: 100%;
  595. }
  596. .holiday-list {
  597. display: flex;
  598. flex-direction: column;
  599. gap: 12px;
  600. }
  601. .holiday-item {
  602. padding: 12px 16px;
  603. border: 1px solid #c0e8ff;
  604. border-radius: 6px;
  605. background-color: #f0f9ff;
  606. transition: all 0.3s ease;
  607. box-shadow: 0 1px 4px rgba(192, 232, 255, 0.3);
  608. }
  609. .holiday-item:hover {
  610. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  611. border-color: #c0c4cc;
  612. transform: translateY(-2px);
  613. }
  614. .holiday-content {
  615. display: flex;
  616. align-items: center;
  617. width: 100%;
  618. }
  619. .holiday-name {
  620. font-weight: 600;
  621. font-size: 14px;
  622. color: #1890ff;
  623. margin-right: 6px;
  624. white-space: nowrap;
  625. overflow: hidden;
  626. text-overflow: ellipsis;
  627. }
  628. .holiday-dates {
  629. font-size: 12px;
  630. color: #52c41a;
  631. margin-right: 12px;
  632. white-space: nowrap;
  633. }
  634. .holiday-count {
  635. font-weight: 600;
  636. font-size: 14px;
  637. color: #f5222d;
  638. margin-left: auto;
  639. white-space: nowrap;
  640. }
  641. .no-data {
  642. text-align: center;
  643. padding: 40px 0;
  644. color: #909399;
  645. font-style: italic;
  646. }
  647. .data-card {
  648. border-radius: 12px;
  649. overflow: hidden;
  650. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  651. }
  652. .card-header {
  653. font-weight: 600;
  654. font-size: 16px;
  655. }
  656. .quick-access-card {
  657. border-radius: 12px;
  658. overflow: hidden;
  659. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  660. }
  661. .quick-access-grid {
  662. display: grid;
  663. grid-template-columns: repeat(6, 1fr);
  664. gap: 20px;
  665. }
  666. .quick-item {
  667. text-align: center;
  668. cursor: pointer;
  669. padding: 20px 10px;
  670. border-radius: 8px;
  671. transition: all 0.3s ease;
  672. background-color: #fafafa;
  673. }
  674. .quick-item:hover {
  675. transform: translateY(-3px);
  676. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  677. background-color: #fff;
  678. }
  679. .quick-icon {
  680. width: 60px;
  681. height: 60px;
  682. display: flex;
  683. align-items: center;
  684. justify-content: center;
  685. margin: 0 auto 10px;
  686. border-radius: 10px;
  687. font-size: 24px;
  688. color: white;
  689. }
  690. .bg-blue {
  691. background: linear-gradient(135deg, #409eff 0%, #4a9eff 100%);
  692. }
  693. .bg-purple {
  694. background: linear-gradient(135deg, #7265e3 0%, #7a6de3 100%);
  695. }
  696. .bg-green {
  697. background: linear-gradient(135deg, #20c997 0%, #26d19c 100%);
  698. }
  699. .bg-orange {
  700. background: linear-gradient(135deg, #f56c6c 0%, #f67878 100%);
  701. }
  702. .bg-red {
  703. background: linear-gradient(135deg, #e6a23c 0%, #eca742 100%);
  704. }
  705. .bg-cyan {
  706. background: linear-gradient(135deg, #17b3a3 0%, #19c0b2 100%);
  707. }
  708. .quick-text {
  709. font-size: 14px;
  710. color: #303133;
  711. font-weight: 500;
  712. }
  713. .mb20 {
  714. margin-bottom: 20px;
  715. }
  716. @media (max-width: 1200px) {
  717. .quick-access-grid {
  718. grid-template-columns: repeat(3, 1fr);
  719. }
  720. }
  721. @media (max-width: 768px) {
  722. .quick-access-grid {
  723. grid-template-columns: repeat(2, 1fr);
  724. }
  725. .el-col-4 {
  726. width: 50%;
  727. margin-bottom: 15px;
  728. }
  729. .el-col-12 {
  730. width: 100%;
  731. }
  732. .el-row {
  733. margin-bottom: 15px;
  734. }
  735. }
  736. </style>