statistics.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. <template>
  2. <!-- 第一行:统计卡片行 -->
  3. <el-row :gutter="16" class="mb-4">
  4. <el-col :span="24">
  5. <el-card class="chart-card" shadow="never">
  6. <el-form
  7. class="-mb-15px"
  8. :model="queryParams"
  9. ref="queryFormRef"
  10. :inline="true"
  11. label-width="68px"
  12. >
  13. <el-form-item label="所属部门" prop="project_name">
  14. <el-tree-select
  15. v-model="queryParams.deptId"
  16. :data="deptList"
  17. :props="defaultProps"
  18. check-strictly
  19. node-key="id"
  20. filterable
  21. placeholder="请选择所在部门"
  22. clearable
  23. style="width: 180px"
  24. />
  25. </el-form-item>
  26. <el-form-item label="创建时间" prop="createTime">
  27. <el-date-picker
  28. v-model="queryParams.createTime"
  29. value-format="YYYY-MM-DD HH:mm:ss"
  30. type="daterange"
  31. start-placeholder="开始日期"
  32. end-placeholder="结束日期"
  33. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  34. class="!w-220px"
  35. />
  36. </el-form-item>
  37. <!-- <el-form-item label="填写状态" prop="project_name">
  38. <el-select
  39. v-model="queryParams.status"
  40. placeholder="填写状态"
  41. clearable
  42. class="!w-240px"
  43. >
  44. <el-option
  45. v-for="dict in getIntDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
  46. :key="dict.value"
  47. :label="dict.label"
  48. :value="dict.value"
  49. />
  50. </el-select>
  51. </el-form-item>-->
  52. <el-form-item>
  53. <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
  54. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  55. </el-form-item>
  56. </el-form>
  57. </el-card>
  58. </el-col>
  59. </el-row>
  60. <!-- 第二行:图表行 -->
  61. <el-row :gutter="16" class="mb-4">
  62. <el-col :span="12">
  63. <el-card class="chart-card" shadow="never">
  64. <template #header>
  65. <div class="flex items-center">
  66. <span class="text-base font-medium text-gray-600">运行记录工单统计</span>
  67. </div>
  68. </template>
  69. <el-row class="h-[220px]" style="display: flex; justify-content: space-around;">
  70. <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,4,queryParams.createTime)">
  71. <div ref="reportingChartRef" class="h-[160px] w-full"></div>
  72. <div class="text-center mt-2" >
  73. <span class="text-sm text-gray-600">总数</span>
  74. </div>
  75. </el-col>
  76. <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,1,queryParams.createTime)">
  77. <div ref="dealFinishedChartRef" class="h-[160px] w-full"></div>
  78. <div class="text-center mt-2">
  79. <span class="text-sm text-gray-600">已填写</span>
  80. </div>
  81. </el-col>
  82. <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,0,queryParams.createTime)">
  83. <div ref="transOrderChartRef" class="h-[160px] w-full"></div>
  84. <div class="text-center mt-2">
  85. <span class="text-sm text-gray-600">未填写</span>
  86. </div>
  87. </el-col>
  88. <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,2,queryParams.createTime)">
  89. <div ref="writeChartRef" class="h-[160px] w-full"></div>
  90. <div class="text-center mt-2">
  91. <span class="text-sm text-gray-600">填写中</span>
  92. </div>
  93. </el-col>
  94. <el-col :span="4" class="flex flex-col items-center" @click="openFill(queryParams.deptId,3,queryParams.createTime)">
  95. <div ref="ignoreChartRef" class="h-[160px] w-full"></div>
  96. <div class="text-center mt-2">
  97. <span class="text-sm text-gray-600" >忽略</span>
  98. </div>
  99. </el-col>
  100. </el-row>
  101. </el-card>
  102. </el-col>
  103. <el-col :span="12">
  104. <el-card class="chart-card" shadow="never">
  105. <template #header>
  106. <div class="flex items-center">
  107. <span class="text-base font-medium text-gray-600">运行记录设备统计</span>
  108. </div>
  109. </template>
  110. <el-row class="h-[220px]" style="display: flex; justify-content: space-around;">
  111. <el-col :span="4" class="flex flex-col items-center" @click="openForm(queryParams.deptId,2,queryParams.createTime)">
  112. <div ref="reportingChartRef1" class="h-[160px] w-full"></div>
  113. <div class="text-center mt-2" >
  114. <span class="text-sm text-gray-600">总数</span>
  115. </div>
  116. </el-col>
  117. <el-col :span="4" class="flex flex-col items-center" @click="openForm(queryParams.deptId,1,queryParams.createTime)">
  118. <div ref="dealFinishedChartRef1" class="h-[160px] w-full"></div>
  119. <div class="text-center mt-2">
  120. <span class="text-sm text-gray-600">已填写</span>
  121. </div>
  122. </el-col>
  123. <el-col :span="4" class="flex flex-col items-center" @click="openForm(queryParams.deptId,0,queryParams.createTime)">
  124. <div ref="transOrderChartRef1" class="h-[160px] w-full"></div>
  125. <div class="text-center mt-2">
  126. <span class="text-sm text-gray-600">未填写</span>
  127. </div>
  128. </el-col>
  129. <el-col :span="4" class="flex flex-col items-center" @click="openForm(queryParams.deptId,3,queryParams.createTime)">
  130. <div ref="ignoreChartRef1" class="h-[160px] w-full"></div>
  131. <div class="text-center mt-2">
  132. <span class="text-sm text-gray-600">忽略</span>
  133. </div>
  134. </el-col>
  135. </el-row>
  136. </el-card>
  137. </el-col>
  138. </el-row>
  139. <!-- 第三行:消息统计行 -->
  140. <el-row>
  141. <el-col :span="24">
  142. <el-card class="chart-card" shadow="never">
  143. <template #header>
  144. <div class="flex items-center justify-between">
  145. <span class="text-base font-medium text-gray-600">运行记录工单统计</span>
  146. </div>
  147. </template>
  148. <div ref="chartContainer" class="h-[300px]"></div>
  149. </el-card>
  150. </el-col>
  151. </el-row>
  152. </template>
  153. <script setup lang="ts" name="Index">
  154. import * as echarts from 'echarts/core'
  155. import { BarChart } from 'echarts/charts'; // 显式导入柱状图模块
  156. import {
  157. GridComponent,
  158. LegendComponent,
  159. TitleComponent,
  160. ToolboxComponent,
  161. TooltipComponent
  162. } from 'echarts/components'
  163. import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
  164. import { LabelLayout, UniversalTransition } from 'echarts/features'
  165. import { CanvasRenderer } from 'echarts/renderers'
  166. import {
  167. IotStatisticsDeviceMessageSummaryRespVO,
  168. IotStatisticsSummaryRespVO
  169. } from '@/api/iot/statistics'
  170. import { formatDate } from '@/utils/formatTime'
  171. import { IotStatApi } from '@/api/pms/stat'
  172. import { ref, reactive, onMounted, onUnmounted } from "vue";
  173. import { defaultProps, handleTree } from "@/utils/tree";
  174. import * as DeptApi from "@/api/system/dept";
  175. import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
  176. import {useUserStore} from "@/store/modules/user";
  177. // 导入部门数据类型
  178. import { DeptTreeItem } from '@/api/system/dept'
  179. import StatisticsForm from './StatisticsForm.vue'
  180. const { push } = useRouter()
  181. // 初始化echarts
  182. echarts.use([
  183. TooltipComponent,
  184. LegendComponent,
  185. PieChart,
  186. CanvasRenderer,
  187. LabelLayout,
  188. TitleComponent,
  189. ToolboxComponent,
  190. GridComponent,
  191. LineChart,
  192. UniversalTransition,
  193. GaugeChart,
  194. BarChart
  195. ])
  196. const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
  197. const deptDataList = ref<DeptDataItem[]>([]) // 部门数据列表
  198. // 定义部门数据类型
  199. interface DeptDataItem {
  200. deptId: number;
  201. name: string;
  202. totalCount: number;
  203. filledCount: number;
  204. unfilledCount: number;
  205. fillingCount: number;
  206. }
  207. /** IoT 首页 */
  208. defineOptions({ name: 'iotOpeationSta' })
  209. const timeRange = ref('7d') // 修改默认选择为近一周
  210. const dateRange = ref<[Date, Date] | null>(null)
  211. const queryParams = reactive({
  212. startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
  213. endTime: Date.now(), // 设置默认结束时间为当前时间
  214. createTime: [],
  215. deptId: null, // 选中的部门ID
  216. status: null // 填写状态
  217. })
  218. const handleQuery = () => {
  219. // 重新获取数据
  220. getStats()
  221. getDevStats()
  222. initChart()
  223. }
  224. const formRef = ref()
  225. const openForm = (deptId?:string,isFill?:string,createTime?:[]) => {
  226. push({ name: 'FillOrderInfo2',params:{deptId,isFill,createTime}})
  227. }
  228. const openFill = (deptId?:string,orderStatus?:string,createTime?:[]) => {
  229. push({
  230. name: 'IotOpeationFill1',
  231. query: { // 使用query传递参数,而非params
  232. deptId,
  233. orderStatus,
  234. // 数组可以直接传递,会自动序列化
  235. createTime: createTime ? createTime.join(',') : undefined
  236. // 建议将数组转为字符串,避免路由参数格式问题
  237. }
  238. })
  239. }
  240. const resetQuery = () => {
  241. // 重置查询参数
  242. queryParams.startTime = Date.now() - 7 * 24 * 60 * 60 * 1000
  243. queryParams.endTime = Date.now()
  244. queryParams.deptId = useUserStore().getUser.deptId;
  245. queryParams.createTime = null
  246. // 重新获取数据
  247. getStats()
  248. initChart()
  249. }
  250. //运行记录工单统计
  251. const reportingChartRef = ref() // 在线设备统计的图表
  252. const dealFinishedChartRef = ref() // 离线设备统计的图表
  253. const transOrderChartRef = ref() // 待激活设备统计的图表
  254. const writeChartRef = ref() // 待填写图表
  255. const ignoreChartRef = ref() // 上下行消息量统计的图表
  256. //运行记录设备统计
  257. const reportingChartRef1 = ref() // 在线设备统计的图表
  258. const dealFinishedChartRef1 = ref() // 离线设备统计的图表
  259. const transOrderChartRef1 = ref() // 待激活设备统计的图表
  260. const ignoreChartRef1 = ref() // 上下行消息量统计的图表
  261. // 基础统计数据
  262. const statsData = ref<IotStatisticsSummaryRespVO>({
  263. productCategoryCount: 0,
  264. productCount: 0,
  265. deviceCount: 0,
  266. deviceMessageCount: 0,
  267. productCategoryTodayCount: 0,
  268. productTodayCount: 0,
  269. deviceTodayCount: 0,
  270. deviceMessageTodayCount: 0,
  271. deviceOnlineCount: 0,
  272. deviceOfflineCount: 0,
  273. deviceInactiveCount: 0,
  274. productCategoryDeviceCounts: {}
  275. })
  276. const day = ref({
  277. failureDay: undefined,
  278. maintainDay: undefined
  279. })
  280. const week = ref({
  281. failureWeek: undefined,
  282. maintainWeek: undefined
  283. })
  284. const month = ref({
  285. failureMonth: undefined,
  286. maintainMonth: undefined
  287. })
  288. const total = ref({
  289. failureTotal: undefined,
  290. maintainTotal: undefined
  291. })
  292. const status = ref<IotStatusItem[]>([])
  293. const status1 = ref<IotStatusItem[]>([])
  294. // 定义状态项接口
  295. interface IotStatusItem {
  296. totalCount: number
  297. filledCount: number
  298. unfilledCount: number
  299. fillingCount: number
  300. }
  301. // 消息统计数据
  302. const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
  303. upstreamCounts: {},
  304. downstreamCounts: {}
  305. })
  306. /** 处理快捷时间范围选择 */
  307. const handleTimeRangeChange = (timeRange: string) => {
  308. const now = Date.now()
  309. let startTime: number
  310. switch (timeRange) {
  311. case '1h':
  312. startTime = now - 60 * 60 * 1000
  313. break
  314. case '24h':
  315. startTime = now - 24 * 60 * 60 * 1000
  316. break
  317. case '7d':
  318. startTime = now - 7 * 24 * 60 * 60 * 1000
  319. break
  320. default:
  321. return
  322. }
  323. // 清空日期选择器
  324. dateRange.value = null
  325. // 更新查询参数
  326. queryParams.startTime = startTime
  327. queryParams.endTime = now
  328. // 重新获取数据
  329. getStats()
  330. initChart()
  331. }
  332. /** 处理自定义日期范围选择 */
  333. const handleDateRangeChange = (value: [Date, Date] | null) => {
  334. if (value) {
  335. // 清空快捷选项
  336. timeRange.value = ''
  337. // 更新查询参数
  338. queryParams.startTime = value[0].getTime()
  339. queryParams.endTime = value[1].getTime()
  340. // 重新获取数据
  341. getStats()
  342. initChart()
  343. }
  344. }
  345. /** 获取统计数据 */
  346. const getStats = async () => {
  347. // 获取基础统计数据
  348. // 获取部门统计数据
  349. IotStatApi.getDeptStatistics(queryParams).then((res) => {
  350. deptDataList.value = res.deptCountList || [];
  351. status.value = res.totalList || [];
  352. initChart()
  353. initCharts()
  354. })
  355. }
  356. const getDevStats = async () => {
  357. // 获取基础统计数据
  358. // 获取部门统计数据
  359. IotStatApi.getDevSta(queryParams).then((res) => {
  360. status1.value = res.totalList || [];
  361. initChart()
  362. initCharts()
  363. })
  364. }
  365. /** 初始化图表 */
  366. const initCharts = () => {
  367. // 使用数组的第一个元素(如果存在)
  368. const firstStatus = status.value[0] || {}
  369. const firstStatus1 = status1.value[0] || {}
  370. // 上报中
  371. initGaugeChart(
  372. reportingChartRef.value,
  373. firstStatus.totalCount === undefined ? 0 : firstStatus.totalCount,
  374. '#0d9'
  375. )
  376. initGaugeChart(
  377. reportingChartRef1.value,
  378. firstStatus1.totalCount === undefined ? 0 : firstStatus1.totalCount,
  379. '#0d9'
  380. )
  381. // 处理完成
  382. initGaugeChart(
  383. dealFinishedChartRef.value,
  384. firstStatus.filledCount === undefined ? 0 : firstStatus.filledCount,
  385. '#05b'
  386. )
  387. initGaugeChart(
  388. ignoreChartRef.value,
  389. firstStatus.ignoreCount === undefined ? 0 : firstStatus.ignoreCount,
  390. 'purple'
  391. )
  392. initGaugeChart(
  393. ignoreChartRef1.value,
  394. firstStatus1.ignoreCount === undefined ? 0 : firstStatus1.ignoreCount,
  395. 'purple'
  396. )
  397. initGaugeChart(
  398. dealFinishedChartRef1.value,
  399. firstStatus1.filledCount === undefined ? 0 : firstStatus1.filledCount,
  400. '#05b'
  401. )
  402. // 转工单
  403. initGaugeChart(
  404. transOrderChartRef.value,
  405. firstStatus.unfilledCount === undefined ? 0 : firstStatus.unfilledCount,
  406. '#f50'
  407. )
  408. initGaugeChart(
  409. transOrderChartRef1.value,
  410. firstStatus1.unfilledCount === undefined ? 0 : firstStatus1.unfilledCount,
  411. '#f50'
  412. )
  413. // 待填写
  414. initGaugeChart(
  415. writeChartRef.value,
  416. firstStatus.fillingCount === undefined ? 0 : firstStatus.fillingCount,
  417. '#05b'
  418. )
  419. }
  420. /** 初始化仪表盘图表 */
  421. const initGaugeChart = (el: any, value: number, color: string) => {
  422. echarts.init(el).setOption({
  423. series: [
  424. {
  425. type: 'gauge',
  426. startAngle: 360,
  427. endAngle: 0,
  428. min: 0,
  429. max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
  430. progress: {
  431. show: true,
  432. width: 12,
  433. itemStyle: {
  434. color: color
  435. }
  436. },
  437. axisLine: {
  438. lineStyle: {
  439. width: 12,
  440. color: [[1, '#E5E7EB']]
  441. }
  442. },
  443. axisTick: { show: false },
  444. splitLine: { show: false },
  445. axisLabel: { show: false },
  446. pointer: { show: false },
  447. anchor: { show: false },
  448. title: { show: false },
  449. detail: {
  450. valueAnimation: true,
  451. fontSize: 24,
  452. fontWeight: 'bold',
  453. fontFamily: 'Inter, sans-serif',
  454. color: color,
  455. offsetCenter: [0, '0'],
  456. formatter: (value: number) => {
  457. return `${value} `
  458. }
  459. },
  460. data: [{ value: value }]
  461. }
  462. ]
  463. })
  464. }
  465. const chartContainer = ref(null)
  466. let chartInstance = null
  467. // 初始化部门统计图表
  468. const initChart = () => {
  469. if (!chartContainer.value) return
  470. // 准备数据
  471. const deptNames = deptDataList.value.map(item => item.name)
  472. const totalCounts = deptDataList.value.map(item => item.totalCount)
  473. const filledCounts = deptDataList.value.map(item => item.filledCount)
  474. const unfilledCounts = deptDataList.value.map(item => item.unfilledCount)
  475. const fillingCounts = deptDataList.value.map(item => item.fillingCount)
  476. // ECharts配置
  477. const option = {
  478. tooltip: {
  479. trigger: 'axis',
  480. axisPointer: {
  481. type: 'shadow'
  482. },
  483. formatter: (params) => {
  484. let result = `<div class="font-bold">${params[0].axisValue}</div>`
  485. params.forEach(param => {
  486. result += `<div>${param.marker} ${param.seriesName}: ${param.value}</div>`
  487. })
  488. return result
  489. }
  490. },
  491. legend: {
  492. data: ['总数', '已填写', '未填写', '填写中'],
  493. top: 25
  494. },
  495. grid: {
  496. left: '3%',
  497. right: '4%',
  498. bottom: '3%',
  499. containLabel: true
  500. },
  501. xAxis: {
  502. type: 'category',
  503. data: deptNames,
  504. axisLabel: {
  505. rotate: 45,
  506. margin: 15,
  507. fontSize: 10
  508. }
  509. },
  510. yAxis: {
  511. type: 'value',
  512. axisLabel: {
  513. formatter: (value) => Math.floor(value).toString()
  514. }
  515. },
  516. series: [
  517. {
  518. name: '总数',
  519. type: 'bar',
  520. barGap: 0,
  521. itemStyle: {
  522. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  523. { offset: 0, color: '#188df0' },
  524. { offset: 1, color: '#188df0' }
  525. ])
  526. },
  527. emphasis: {
  528. focus: 'series'
  529. },
  530. data: totalCounts
  531. },
  532. {
  533. name: '已填写',
  534. type: 'bar',
  535. itemStyle: {
  536. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  537. { offset: 0, color: '#d3a137' },
  538. { offset: 1, color: '#d3a137' }
  539. ])
  540. },
  541. emphasis: {
  542. focus: 'series'
  543. },
  544. data: filledCounts
  545. },
  546. {
  547. name: '未填写',
  548. type: 'bar',
  549. itemStyle: {
  550. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  551. { offset: 0, color: 'green' },
  552. { offset: 1, color: 'green' }
  553. ])
  554. },
  555. emphasis: {
  556. focus: 'series'
  557. },
  558. data: unfilledCounts
  559. },
  560. {
  561. name: '填写中',
  562. type: 'bar',
  563. itemStyle: {
  564. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  565. { offset: 0, color: 'red' },
  566. { offset: 1, color: 'red' }
  567. ])
  568. },
  569. emphasis: {
  570. focus: 'series'
  571. },
  572. data: fillingCounts
  573. }
  574. ]
  575. }
  576. // 初始化图表
  577. chartInstance = echarts.init(chartContainer.value)
  578. chartInstance.setOption(option)
  579. // 窗口缩放监听
  580. window.addEventListener('resize', handleResize)
  581. handleResize()
  582. }
  583. // 自适应调整
  584. const handleResize = () => {
  585. chartInstance?.resize()
  586. }
  587. const router = useRouter()
  588. const route = useRoute()
  589. // 监听路由变化
  590. const removeBeforeEach = router.beforeEach((to, from, next) => {
  591. // 如果是从FillOrderInfo1页面返回
  592. if (from.name === 'FillOrderInfo1' && to.name === route.name) {
  593. // 恢复查询参数
  594. Object.assign(queryParams, initialQueryParams)
  595. // 阻止刷新,使用replace而不是push
  596. next({ ...to, replace: true })
  597. } else {
  598. next()
  599. }
  600. })
  601. /** 初始化 */
  602. onMounted(async () => {
  603. queryParams.deptId = useUserStore().getUser.deptId;
  604. // 计算近一周时间
  605. const end = new Date();
  606. const start = new Date();
  607. start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000);
  608. // 格式化日期为后端需要的格式
  609. const formatDate = (date) => {
  610. const year = date.getFullYear();
  611. const month = String(date.getMonth() + 1).padStart(2, '0');
  612. const day = String(date.getDate()).padStart(2, '0');
  613. const hours = String(date.getHours()).padStart(2, '0');
  614. const minutes = String(date.getMinutes()).padStart(2, '0');
  615. const seconds = String(date.getSeconds()).padStart(2, '0');
  616. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  617. };
  618. queryParams.createTime = [formatDate(start), formatDate(end)];
  619. getStats()
  620. getDevStats();
  621. deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  622. })
  623. </script>
  624. <style lang="scss" scoped>
  625. .chart-card {
  626. background-color: white;
  627. border-radius: 8px;
  628. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
  629. padding: 16px;
  630. }
  631. // 新增样式,隐藏滚动条但保留功能
  632. ::-webkit-scrollbar {
  633. display: none;
  634. }
  635. :host {
  636. overflow: hidden;
  637. }
  638. // 确保页面铺满屏幕并不出现滚动条
  639. html, body {
  640. height: 100%;
  641. margin: 0;
  642. padding: 0;
  643. overflow: hidden;
  644. }
  645. // 适配图表容器高度
  646. .el-row {
  647. max-height: calc(100vh - 32px); // 减去页面padding
  648. overflow: auto;
  649. -ms-overflow-style: none; // 隐藏IE滚动条
  650. scrollbar-width: none; // 隐藏Firefox滚动条
  651. }
  652. </style>