inspect.vue 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590
  1. <template>
  2. <div ref="wrapperRef" class="bg absolute top-0 left-0 size-full z-10">
  3. <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
  4. <div class="bg kb-screen" id="inspect-kb" :style="targetAreaStyle">
  5. <header class="header">{{ t('stat.inspectvue702065-0') }}</header>
  6. <main class="inspect-kb-content">
  7. <!-- 第一行:筛选条件 -->
  8. <el-row :gutter="16" class="filter-row">
  9. <el-col :span="24">
  10. <el-card class="filter-card" shadow="hover" style="border: none">
  11. <el-form
  12. class="-mb-15px"
  13. :model="queryParams"
  14. ref="queryFormRef"
  15. :inline="true"
  16. label-width="auto">
  17. <el-form-item :label="t('stat.inspectvue702065-1')" prop="project_name">
  18. <el-tree-select
  19. v-model="queryParams.deptId"
  20. :data="deptList"
  21. :props="defaultProps"
  22. check-strictly
  23. node-key="id"
  24. filterable
  25. :placeholder="t('stat.inspectvue702065-2')"
  26. clearable
  27. style="width: 200px" />
  28. </el-form-item>
  29. <el-form-item :label="t('stat.inspectvue702065-3')" prop="createTime">
  30. <el-date-picker
  31. v-model="queryParams.createTime"
  32. value-format="YYYY-MM-DD HH:mm:ss"
  33. type="daterange"
  34. start-:placeholder="t('stat.inspectvue702065-4')"
  35. end-:placeholder="t('stat.inspectvue702065-5')"
  36. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  37. class="!w-240px"
  38. @change="handleDateChange" />
  39. </el-form-item>
  40. <el-form-item>
  41. <el-button type="primary" @click="handleQuery">
  42. <Icon icon="ep:search" class="mr-5px" />{{
  43. t('stat.inspectvue702065-6')
  44. }}</el-button
  45. >
  46. <el-button @click="resetQuery" class="btn-default">
  47. <Icon icon="ep:refresh" class="mr-5px" />{{
  48. t('stat.inspectvue702065-7')
  49. }}</el-button
  50. >
  51. </el-form-item>
  52. </el-form>
  53. </el-card>
  54. </el-col>
  55. </el-row>
  56. <!-- 第二行:统计卡片行 -->
  57. <el-row :gutter="16" class="summary-row">
  58. <el-col :span="6">
  59. <el-card class="stat-card stat-card-neutral-1" shadow="hover" style="border: none">
  60. <div class="stat-content">
  61. <div class="stat-header">
  62. <div class="stat-icon-wrapper">
  63. <Icon icon="ep:calendar" class="stat-icon" />
  64. </div>
  65. <div class="stat-title">{{ t('stat.inspectvue702065-8') }}</div>
  66. </div>
  67. <el-divider class="stat-divider" />
  68. <div class="stat-body">
  69. <div class="stat-item">
  70. <div class="stat-sub-label">{{ t('stat.inspectvue702065-9') }}</div>
  71. <div class="stat-sub-value text-slate-700">{{ day.total }}</div>
  72. </div>
  73. <div class="stat-item">
  74. <div class="stat-sub-label">{{ t('stat.inspectvue702065-10') }}</div>
  75. <div class="stat-sub-value text-amber-600">{{ day.todo }}</div>
  76. </div>
  77. </div>
  78. </div>
  79. </el-card>
  80. </el-col>
  81. <el-col :span="6">
  82. <el-card class="stat-card stat-card-neutral-2" shadow="hover" style="border: none">
  83. <div class="stat-content">
  84. <div class="stat-header">
  85. <div class="stat-icon-wrapper">
  86. <Icon icon="ep:star" class="stat-icon" />
  87. </div>
  88. <div class="stat-title">{{ t('stat.inspectvue702065-11') }}</div>
  89. </div>
  90. <el-divider class="stat-divider" />
  91. <div class="stat-body">
  92. <div class="stat-item">
  93. <div class="stat-sub-label">{{ t('stat.inspectvue702065-9') }}</div>
  94. <div class="stat-sub-value text-slate-700">{{ week.total }}</div>
  95. </div>
  96. <div class="stat-item">
  97. <div class="stat-sub-label">{{ t('stat.inspectvue702065-10') }}</div>
  98. <div class="stat-sub-value text-amber-600">{{ week.todo }}</div>
  99. </div>
  100. </div>
  101. </div>
  102. </el-card>
  103. </el-col>
  104. <el-col :span="6">
  105. <el-card class="stat-card stat-card-neutral-3" shadow="hover" style="border: none">
  106. <div class="stat-content">
  107. <div class="stat-header">
  108. <div class="stat-icon-wrapper">
  109. <Icon icon="ep:trend-charts" class="stat-icon" />
  110. </div>
  111. <div class="stat-title">{{ t('stat.inspectvue702065-12') }}</div>
  112. </div>
  113. <el-divider class="stat-divider" />
  114. <div class="stat-body">
  115. <div class="stat-item">
  116. <div class="stat-sub-label">{{ t('stat.inspectvue702065-9') }}</div>
  117. <div class="stat-sub-value text-slate-700">{{ month.total }}</div>
  118. </div>
  119. <div class="stat-item">
  120. <div class="stat-sub-label">{{ t('stat.inspectvue702065-10') }}</div>
  121. <div class="stat-sub-value text-amber-600">{{ month.todo }}</div>
  122. </div>
  123. </div>
  124. </div>
  125. </el-card>
  126. </el-col>
  127. <el-col :span="6">
  128. <el-card class="stat-card stat-card-neutral-4" shadow="hover" style="border: none">
  129. <div class="stat-content">
  130. <div class="stat-header">
  131. <div class="stat-icon-wrapper">
  132. <Icon icon="ep:data-analysis" class="stat-icon" />
  133. </div>
  134. <div class="stat-title">{{ t('stat.inspectvue702065-13') }}</div>
  135. </div>
  136. <el-divider class="stat-divider" />
  137. <div class="stat-body">
  138. <div class="stat-item">
  139. <div class="stat-sub-label">{{ t('stat.inspectvue702065-9') }}</div>
  140. <div class="stat-sub-value text-slate-700">{{ total.total }}</div>
  141. </div>
  142. <div class="stat-item">
  143. <div class="stat-sub-label">{{ t('stat.inspectvue702065-10') }}</div>
  144. <div class="stat-sub-value text-amber-600">{{ total.todo }}</div>
  145. </div>
  146. </div>
  147. </div>
  148. </el-card>
  149. </el-col>
  150. </el-row>
  151. <!-- 第三行:图表行 -->
  152. <el-row :gutter="16" class="status-row">
  153. <el-col :span="12">
  154. <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
  155. <template #header>
  156. <div class="chart-header">
  157. <div class="chart-title-wrapper">
  158. <div class="chart-title-dot chart-title-dot-blue"></div>
  159. <span class="chart-title">{{ t('stat.inspectvue702065-14') }}</span>
  160. </div>
  161. </div>
  162. </template>
  163. <el-row class="chart-grid">
  164. <el-col :span="8" class="chart-item">
  165. <div ref="finishedChartRef" class="gauge-chart"></div>
  166. <div class="chart-label clickable" @click="clickStatus('工单已执行')">
  167. <span class="label-dot label-dot-blue"></span>
  168. <span class="label-text">{{ t('stat.inspectvue702065-15') }}</span>
  169. </div>
  170. </el-col>
  171. <el-col :span="8" class="chart-item">
  172. <div ref="writeChartRef" class="gauge-chart"></div>
  173. <div class="chart-label clickable" @click="clickStatus('工单待执行')">
  174. <span class="label-dot label-dot-amber"></span>
  175. <span class="label-text">{{ t('stat.inspectvue702065-16') }}</span>
  176. </div>
  177. </el-col>
  178. <el-col :span="8" class="chart-item">
  179. <div ref="ignoreChartRef" class="gauge-chart"></div>
  180. <div class="chart-label clickable" @click="clickStatus('工单忽略')">
  181. <span class="label-dot label-dot-gray"></span>
  182. <span class="label-text">{{ t('stat.inspectvue702065-17') }}</span>
  183. </div>
  184. </el-col>
  185. </el-row>
  186. </el-card>
  187. </el-col>
  188. <el-col :span="12">
  189. <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
  190. <template #header>
  191. <div class="chart-header">
  192. <div class="chart-title-wrapper">
  193. <div class="chart-title-dot chart-title-dot-green"></div>
  194. <span class="chart-title">{{ t('stat.inspectvue702065-18') }}</span>
  195. </div>
  196. </div>
  197. </template>
  198. <el-row class="chart-grid">
  199. <el-col :span="8" class="chart-item">
  200. <div ref="finishedTodayChartRef" class="gauge-chart"></div>
  201. <div class="chart-label clickable" @click="clickStatus('设备已执行')">
  202. <span class="label-dot label-dot-blue"></span>
  203. <span class="label-text">{{ t('stat.inspectvue702065-19') }}</span>
  204. </div>
  205. </el-col>
  206. <el-col :span="8" class="chart-item">
  207. <div ref="writeTodayChartRef" class="gauge-chart"></div>
  208. <div class="chart-label clickable" @click="clickStatus('设备待执行')">
  209. <span class="label-dot label-dot-amber"></span>
  210. <span class="label-text">{{ t('stat.inspectvue702065-20') }}</span>
  211. </div>
  212. </el-col>
  213. <el-col :span="8" class="chart-item">
  214. <div ref="ignoreTodayChartRef" class="gauge-chart"></div>
  215. <div class="chart-label clickable" @click="clickStatus('设备忽略')">
  216. <span class="label-dot label-dot-gray"></span>
  217. <span class="label-text">{{ t('stat.inspectvue702065-17') }}</span>
  218. </div>
  219. </el-col>
  220. </el-row>
  221. </el-card>
  222. </el-col>
  223. </el-row>
  224. <!-- 第四行:消息统计行 -->
  225. <el-row :gutter="16" class="trend-row">
  226. <el-col :span="8">
  227. <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
  228. <template #header>
  229. <div class="chart-header">
  230. <div class="chart-title-wrapper">
  231. <div class="chart-title-dot chart-title-dot-purple"></div>
  232. <span class="chart-title">{{ t('stat.inspectvue702065-21') }}</span>
  233. </div>
  234. </div>
  235. </template>
  236. <div ref="statusChartRef" class="pie-chart-container"></div>
  237. </el-card>
  238. </el-col>
  239. <el-col :span="16">
  240. <el-card class="chart-card-enhanced" shadow="hover" style="border: none">
  241. <template #header>
  242. <div class="chart-header">
  243. <div class="chart-title-wrapper">
  244. <div class="chart-title-dot chart-title-dot-indigo"></div>
  245. <span class="chart-title">{{ t('stat.inspectvue702065-22') }}</span>
  246. </div>
  247. </div>
  248. </template>
  249. <div ref="chartContainer" class="bar-chart-container"></div>
  250. </el-card>
  251. </el-col>
  252. </el-row>
  253. <!-- TODO 第五行:地图 -->
  254. </main>
  255. </div>
  256. </div>
  257. </div>
  258. </template>
  259. <script setup lang="ts" name="Index">
  260. import * as echarts from 'echarts/core'
  261. import { useI18n } from 'vue-i18n'
  262. import { BarChart } from 'echarts/charts' // 显式导入柱状图模块
  263. const { t } = useI18n()
  264. import {
  265. GridComponent,
  266. LegendComponent,
  267. TitleComponent,
  268. ToolboxComponent,
  269. TooltipComponent
  270. } from 'echarts/components'
  271. import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
  272. import { LabelLayout, UniversalTransition } from 'echarts/features'
  273. import { CanvasRenderer } from 'echarts/renderers'
  274. import {
  275. IotStatisticsDeviceMessageSummaryRespVO,
  276. IotStatisticsSummaryRespVO
  277. } from '@/api/iot/statistics'
  278. const { currentRoute, push } = useRouter()
  279. import { formatDate } from '@/utils/formatTime'
  280. import { IotStatApi } from '@/api/pms/stat'
  281. import { defaultProps, handleTree } from '@/utils/tree'
  282. import { ref } from 'vue'
  283. import { DeptTreeItem } from '@/api/system/dept'
  284. import * as DeptApi from '@/api/system/dept'
  285. import { useUserStore } from '@/store/modules/user'
  286. import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
  287. // TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
  288. /** IoT 首页 */
  289. defineOptions({ name: 'IotInspectStat' })
  290. const wrapperRef = ref<HTMLDivElement>()
  291. const scale = ref(1)
  292. const chartInstances = new Set<ReturnType<typeof echarts.init>>()
  293. let resizeObserver: ResizeObserver | null = null
  294. let resizeRaf = 0
  295. const targetWrapperStyle = computed(() => ({
  296. width: `${DESIGN_WIDTH * scale.value}px`,
  297. height: `${DESIGN_HEIGHT * scale.value}px`
  298. }))
  299. const targetAreaStyle = computed(() => ({
  300. '--kb-scale': scale.value,
  301. width: `${DESIGN_WIDTH * scale.value}px`,
  302. height: `${DESIGN_HEIGHT * scale.value}px`
  303. }))
  304. const getChartInstance = (el: HTMLElement | null) => {
  305. if (!el) return null
  306. const chart = echarts.getInstanceByDom(el) || echarts.init(el)
  307. chartInstances.add(chart)
  308. return chart
  309. }
  310. const resizeCharts = () => {
  311. chartInstances.forEach((chart) => chart.resize())
  312. }
  313. const updateScale = () => {
  314. cancelAnimationFrame(resizeRaf)
  315. resizeRaf = requestAnimationFrame(() => {
  316. const wrapper = wrapperRef.value
  317. if (!wrapper) return
  318. const { clientWidth, clientHeight } = wrapper
  319. if (!clientWidth || !clientHeight) return
  320. scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
  321. nextTick(resizeCharts)
  322. })
  323. }
  324. // TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
  325. echarts.use([
  326. TooltipComponent,
  327. LegendComponent,
  328. PieChart,
  329. CanvasRenderer,
  330. LabelLayout,
  331. TitleComponent,
  332. ToolboxComponent,
  333. GridComponent,
  334. LineChart,
  335. UniversalTransition,
  336. GaugeChart,
  337. BarChart
  338. ])
  339. const timeRange = ref('7d') // 修改默认选择为近一周
  340. const dateRange = ref<[Date, Date] | null>(null)
  341. const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
  342. const queryParams = reactive({
  343. deptId: undefined,
  344. createTime: undefined,
  345. startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
  346. endTime: Date.now() // 设置默认结束时间为当前时间
  347. })
  348. // const deviceCountChartRef = ref() // 设备数量统计的图表
  349. const reportingChartRef = ref() // 在线设备统计的图表
  350. const dealFinishedChartRef = ref() // 离线设备统计的图表
  351. const transOrderChartRef = ref() // 待激活设备统计的图表
  352. const orderFinishChartRef = ref()
  353. const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
  354. const writeChartRef = ref() // 上下行消息量统计的图表
  355. const ignoreChartRef = ref() // 上下行消息量统计的图表
  356. const finishedChartRef = ref() // 上下行消息量统计的图表
  357. const writeTodayChartRef = ref() // 上下行消息量统计的图表
  358. const ignoreTodayChartRef = ref() // 上下行消息量统计的图表
  359. const finishedTodayChartRef = ref() // 上下行消息量统计的图表
  360. const statusChartRef = ref() // 设备数量统计的图表
  361. // 基础统计数据
  362. // TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
  363. const statsData = ref<IotStatisticsSummaryRespVO>({
  364. productCategoryCount: 0,
  365. productCount: 0,
  366. deviceCount: 0,
  367. deviceMessageCount: 0,
  368. productCategoryTodayCount: 0,
  369. productTodayCount: 0,
  370. deviceTodayCount: 0,
  371. deviceMessageTodayCount: 0,
  372. deviceOnlineCount: 0,
  373. deviceOfflineCount: 0,
  374. deviceInactiveCount: 0,
  375. productCategoryDeviceCounts: {}
  376. })
  377. const handleQuery = () => {
  378. getStats()
  379. // initChart()
  380. // initCharts()
  381. }
  382. const resetQuery = () => {
  383. // 重置查询参数
  384. queryParams.startTime = Date.now() - 7 * 24 * 60 * 60 * 1000
  385. queryParams.endTime = Date.now()
  386. queryParams.deptId = useUserStore().getUser.deptId
  387. queryParams.createTime = null
  388. // 重新获取数据
  389. getStats()
  390. }
  391. const handleDateChange = () => {
  392. handleQuery()
  393. }
  394. const clickStatus = (type) => {
  395. let status = ''
  396. const createTime = queryParams.createTime
  397. const deptId = queryParams.deptId
  398. if (type === '工单待执行') {
  399. status = 'todo'
  400. push({ name: 'IotInspectOrdere', params: { deptId, status, createTime } })
  401. } else if (type === '工单已执行') {
  402. status = 'finished'
  403. push({ name: 'IotInspectOrdere', params: { deptId, status, createTime } })
  404. } else if (type === '工单忽略') {
  405. status = 'ignore'
  406. push({ name: 'IotInspectOrdere', params: { deptId, status, createTime } })
  407. } else if (type === '设备忽略') {
  408. status = 'ignore'
  409. push({ name: 'IotInspectOrderDetailStat', params: { deptId, status, createTime } })
  410. } else if (type === '设备待执行') {
  411. status = 'todo'
  412. push({ name: 'IotInspectOrderDetailStat', params: { deptId, status, createTime } })
  413. } else if (type === '设备已执行') {
  414. status = 'finished'
  415. push({ name: 'IotInspectOrderDetailStat', params: { deptId, status, createTime } })
  416. }
  417. }
  418. const day = ref({
  419. total: undefined,
  420. todo: undefined
  421. })
  422. const week = ref({
  423. total: undefined,
  424. todo: undefined
  425. })
  426. const month = ref({
  427. total: undefined,
  428. todo: undefined
  429. })
  430. const total = ref({
  431. total: undefined,
  432. todo: undefined
  433. })
  434. const status = ref({
  435. finished: 0,
  436. todo: 0,
  437. ignore: 0
  438. })
  439. const todayStatus = ref({
  440. finished: 0,
  441. todo: 0,
  442. ignore: 0
  443. })
  444. const typeData = ref({})
  445. // 消息统计数据
  446. const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
  447. upstreamCounts: {},
  448. downstreamCounts: {}
  449. })
  450. /** 处理快捷时间范围选择 */
  451. const handleTimeRangeChange = (timeRange: string) => {
  452. const now = Date.now()
  453. let startTime: number
  454. // TODO @super:这个的计算,看看能不能结合 dayjs 简化。因为 1h、24h、7d 感觉是比较标准的。如果没有,抽到 utils/formatTime.ts 作为一个工具方法
  455. switch (timeRange) {
  456. case '1h':
  457. startTime = now - 60 * 60 * 1000
  458. break
  459. case '24h':
  460. startTime = now - 24 * 60 * 60 * 1000
  461. break
  462. case '7d':
  463. startTime = now - 7 * 24 * 60 * 60 * 1000
  464. break
  465. default:
  466. return
  467. }
  468. // 清空日期选择器
  469. dateRange.value = null
  470. // 更新查询参数
  471. queryParams.startTime = startTime
  472. queryParams.endTime = now
  473. // 重新获取数据
  474. getStats()
  475. }
  476. /** 处理自定义日期范围选择 */
  477. const handleDateRangeChange = (value: [Date, Date] | null) => {
  478. if (value) {
  479. // 清空快捷选项
  480. timeRange.value = ''
  481. // 更新查询参数
  482. queryParams.startTime = value[0].getTime()
  483. queryParams.endTime = value[1].getTime()
  484. // 重新获取数据
  485. getStats()
  486. }
  487. }
  488. /** 获取统计数据 */
  489. const getStats = async () => {
  490. // 获取基础统计数据
  491. IotStatApi.getInspectDay().then((res) => {
  492. day.value = res
  493. })
  494. IotStatApi.getInspectWeek().then((res) => {
  495. week.value = res
  496. })
  497. IotStatApi.getInspectMonth().then((res) => {
  498. month.value = res
  499. })
  500. IotStatApi.getInspectTotal().then((res) => {
  501. total.value = res
  502. })
  503. IotStatApi.getInspectStatus(queryParams).then((res) => {
  504. status.value = res
  505. initChart()
  506. initCharts()
  507. })
  508. IotStatApi.getInspectDeviceStatus(queryParams).then((res) => {
  509. todayStatus.value = res
  510. initChart()
  511. initCharts()
  512. })
  513. IotStatApi.getInspectItemStatus(queryParams).then((res) => {
  514. typeData.value = res
  515. initChart()
  516. initCharts()
  517. })
  518. // statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
  519. //
  520. // // 获取消息统计数据
  521. // messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
  522. // 初始化图表
  523. }
  524. /** 初始化图表 */
  525. const initCharts = () => {
  526. const chart = getChartInstance(statusChartRef.value)
  527. if (!chart) return
  528. chart.setOption({
  529. tooltip: {
  530. trigger: 'item'
  531. },
  532. legend: {
  533. // top: '5%',
  534. // right: '10%',
  535. // align: 'center',
  536. orient: 'horizontal', // 水平排列图例项
  537. bottom: '0%', // 放置在底部
  538. icon: 'circle'
  539. },
  540. series: [
  541. {
  542. name: '',
  543. type: 'pie',
  544. radius: ['0%', '70%'],
  545. avoidLabelOverlap: false,
  546. center: ['50%', '45%'],
  547. label: {
  548. show: false,
  549. position: 'outside'
  550. },
  551. emphasis: {
  552. label: {
  553. show: true,
  554. fontSize: 15,
  555. fontWeight: 'bold'
  556. }
  557. },
  558. labelLine: {
  559. show: true
  560. },
  561. data: typeData.value
  562. }
  563. ]
  564. })
  565. chart.off('click')
  566. chart.on('click', (params) => {
  567. console.log('点击的数据值为:', params.value)
  568. console.log('点击的数据类型为:', params.data.type)
  569. debugger
  570. const createTime = queryParams.createTime
  571. const deptId = queryParams.deptId
  572. const status = params.data.name
  573. push({ name: 'IotInspectItemStat', params: { deptId, status, createTime } })
  574. })
  575. // echarts.init(statusChartRef.value).setOption({
  576. // })
  577. //待执行
  578. initGaugeChart(
  579. writeTodayChartRef.value,
  580. todayStatus.value.todo === undefined ? 0 : todayStatus.value.todo,
  581. '#f50',
  582. '设备待执行'
  583. )
  584. initGaugeChart(
  585. ignoreTodayChartRef.value,
  586. todayStatus.value.ignore === undefined ? 0 : todayStatus.value.ignore,
  587. 'purple',
  588. '设备忽略'
  589. )
  590. //已执行
  591. initGaugeChart(
  592. finishedTodayChartRef.value,
  593. todayStatus.value.finished === undefined ? 0 : todayStatus.value.finished,
  594. '#05b',
  595. '设备已执行'
  596. )
  597. // 工单待执行
  598. initGaugeChart(
  599. writeChartRef.value,
  600. status.value.todo === undefined ? 0 : status.value.todo,
  601. '#f50',
  602. '工单待执行'
  603. )
  604. // 忽略
  605. initGaugeChart(
  606. ignoreChartRef.value,
  607. status.value.ignore === undefined ? 0 : status.value.ignore,
  608. 'purple',
  609. '工单忽略'
  610. )
  611. //工单已执行
  612. initGaugeChart(
  613. finishedChartRef.value,
  614. status.value.finished === undefined ? 0 : status.value.finished,
  615. '#05b',
  616. '工单已执行'
  617. )
  618. // 消息量统计
  619. //initMessageChart()
  620. }
  621. /** 初始化仪表盘图表 */
  622. const initGaugeChart = (el: any, value: number, color: string, type: string) => {
  623. const chart = getChartInstance(el)
  624. if (!chart) return
  625. chart.setOption({
  626. series: [
  627. {
  628. type: 'gauge',
  629. startAngle: 360,
  630. endAngle: 0,
  631. min: 0,
  632. max: value, // 使用设备总数作为最大值
  633. progress: {
  634. show: true,
  635. width: 15,
  636. itemStyle: {
  637. color: color
  638. }
  639. },
  640. axisLine: {
  641. lineStyle: {
  642. width: 15,
  643. color: [[1, '#E5E7EB']]
  644. }
  645. },
  646. axisTick: { show: false },
  647. splitLine: { show: false },
  648. axisLabel: { show: false },
  649. pointer: { show: false },
  650. anchor: { show: false },
  651. title: { show: false },
  652. detail: {
  653. valueAnimation: true,
  654. fontSize: 20,
  655. fontWeight: 'bold',
  656. fontFamily: 'Inter, sans-serif',
  657. color: color,
  658. offsetCenter: [0, '0'],
  659. formatter: (value: number) => {
  660. return `${value} `
  661. }
  662. },
  663. data: [{ value: value, type: type }]
  664. }
  665. ]
  666. })
  667. // 添加点击事件监听器
  668. chart.off('click')
  669. chart.on('click', (params) => {
  670. console.log('点击的数据值为:', params.value)
  671. console.log('点击的数据类型为:', params.data.type)
  672. const createTime = queryParams.createTime
  673. const deptId = queryParams.deptId
  674. let status = ''
  675. if (params.data.type === '工单待执行') {
  676. status = 'todo'
  677. push({ name: 'IotInspectOrdere', params: { deptId, status, createTime } })
  678. } else if (params.data.type === '工单已执行') {
  679. status = 'finished'
  680. push({ name: 'IotInspectOrdere', params: { deptId, status, createTime } })
  681. } else if (params.data.type === '工单忽略') {
  682. status = 'ignore'
  683. push({ name: 'IotInspectOrdere', params: { deptId, status, createTime } })
  684. } else if (params.data.type === '设备待执行') {
  685. status = 'todo'
  686. push({ name: 'IotInspectOrderDetailStat', params: { deptId, status, createTime } })
  687. } else if (params.data.type === '设备已执行') {
  688. status = 'finished'
  689. push({ name: 'IotInspectOrderDetailStat', params: { deptId, status, createTime } })
  690. } else if (params.data.type === '设备忽略') {
  691. status = 'ignore'
  692. push({ name: 'IotInspectOrderDetailStat', params: { deptId, status, createTime } })
  693. }
  694. })
  695. }
  696. /** 初始化消息统计图表 */
  697. const initMessageChart = () => {
  698. // 获取所有时间戳并排序
  699. // TODO @super:一些 idea 里的红色报错,要去处理掉噢。
  700. const timestamps = Array.from(
  701. new Set([
  702. ...messageStats.value.upstreamCounts.map((item) => Number(Object.keys(item)[0])),
  703. ...messageStats.value.downstreamCounts.map((item) => Number(Object.keys(item)[0]))
  704. ])
  705. ).sort((a, b) => a - b) // 确保时间戳从小到大排序
  706. // 准备数据
  707. const xdata = timestamps.map((ts) => formatDate(ts, 'YYYY-MM-DD HH:mm'))
  708. const upData = timestamps.map((ts) => {
  709. const item = messageStats.value.upstreamCounts.find(
  710. (count) => Number(Object.keys(count)[0]) === ts
  711. )
  712. return item ? Object.values(item)[0] : 0
  713. })
  714. const downData = timestamps.map((ts) => {
  715. const item = messageStats.value.downstreamCounts.find(
  716. (count) => Number(Object.keys(count)[0]) === ts
  717. )
  718. return item ? Object.values(item)[0] : 0
  719. })
  720. // 配置图表
  721. getChartInstance(deviceMessageCountChartRef.value)?.setOption({
  722. tooltip: {
  723. trigger: 'axis',
  724. backgroundColor: 'rgba(255, 255, 255, 0.9)',
  725. borderColor: '#E5E7EB',
  726. textStyle: {
  727. color: '#374151'
  728. }
  729. },
  730. legend: {
  731. data: ['上行消息量', '下行消息量'],
  732. textStyle: {
  733. color: '#374151',
  734. fontWeight: 500
  735. }
  736. },
  737. grid: {
  738. left: '3%',
  739. right: '4%',
  740. bottom: '3%',
  741. containLabel: true
  742. },
  743. xAxis: {
  744. type: 'category',
  745. boundaryGap: false,
  746. data: xdata,
  747. axisLine: {
  748. lineStyle: {
  749. color: '#E5E7EB'
  750. }
  751. },
  752. axisLabel: {
  753. color: '#6B7280'
  754. }
  755. },
  756. yAxis: {
  757. type: 'value',
  758. axisLine: {
  759. lineStyle: {
  760. color: '#E5E7EB'
  761. }
  762. },
  763. axisLabel: {
  764. color: '#6B7280'
  765. },
  766. splitLine: {
  767. lineStyle: {
  768. color: '#F3F4F6'
  769. }
  770. }
  771. },
  772. series: [
  773. {
  774. name: '上行消息量',
  775. type: 'line',
  776. smooth: true, // 添加平滑曲线
  777. data: upData,
  778. itemStyle: {
  779. color: '#3B82F6'
  780. },
  781. lineStyle: {
  782. width: 2
  783. },
  784. areaStyle: {
  785. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  786. { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
  787. { offset: 1, color: 'rgba(59, 130, 246, 0)' }
  788. ])
  789. }
  790. },
  791. {
  792. name: '下行消息量',
  793. type: 'line',
  794. smooth: true, // 添加平滑曲线
  795. data: downData,
  796. itemStyle: {
  797. color: '#10B981'
  798. },
  799. lineStyle: {
  800. width: 2
  801. },
  802. areaStyle: {
  803. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  804. { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
  805. { offset: 1, color: 'rgba(16, 185, 129, 0)' }
  806. ])
  807. }
  808. }
  809. ]
  810. })
  811. }
  812. const chartContainer = ref(null)
  813. let chartInstance: ReturnType<typeof echarts.init> | null = null
  814. // 生成过去12个月份的标签 (格式: YYYY-MM)
  815. const generateMonthLabels = () => {
  816. const months = []
  817. const date = new Date()
  818. for (let i = 11; i >= 0; i--) {
  819. const tempDate = new Date(date.getFullYear(), date.getMonth() - i, 1)
  820. const year = tempDate.getFullYear()
  821. const month = String(tempDate.getMonth() + 1).padStart(2, '0')
  822. months.push(`${year}-${month}`)
  823. }
  824. return months
  825. }
  826. // 模拟数据获取
  827. const fetchChartData = async () => {
  828. // 模拟异步请求
  829. return new Promise((resolve) => {
  830. setTimeout(() => {
  831. resolve({
  832. months: generateMonthLabels(),
  833. faults: [20, 30, 100, 40, 20, 50, 70, 80, 60, 90, 100, 100],
  834. repairs: [10, 30, 90, 30, 10, 20, 60, 50, 22, 34, 70, 85]
  835. })
  836. }, 300)
  837. })
  838. }
  839. // 初始化图表配置
  840. const initChart = async () => {
  841. if (!chartContainer.value) return
  842. // 获取数据
  843. const { months, faults, repairs } = await fetchChartData()
  844. debugger
  845. // ECharts配置
  846. const option = {
  847. tooltip: {
  848. trigger: 'axis',
  849. axisPointer: {
  850. type: 'shadow'
  851. },
  852. formatter: (params) => {
  853. return `${params[0].axisValue}<br/>
  854. ${params[0].marker} ${params[0].seriesName}: ${params[0].value}`
  855. }
  856. },
  857. legend: {
  858. data: ['巡检工单数量'],
  859. top: 25
  860. },
  861. grid: {
  862. left: '3%',
  863. right: '4%',
  864. bottom: '3%',
  865. containLabel: true
  866. },
  867. xAxis: {
  868. type: 'category',
  869. data: months,
  870. axisLabel: {
  871. rotate: 45,
  872. margin: 15
  873. }
  874. },
  875. yAxis: {
  876. type: 'value',
  877. axisLabel: {
  878. formatter: (value) => Math.floor(value).toString()
  879. }
  880. },
  881. series: [
  882. {
  883. name: '巡检工单数量',
  884. type: 'bar',
  885. itemStyle: {
  886. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  887. { offset: 0, color: '#2196df' },
  888. { offset: 1, color: '#2196df' }
  889. ])
  890. },
  891. emphasis: {
  892. focus: 'series'
  893. },
  894. data: repairs
  895. }
  896. ]
  897. }
  898. // 初始化图表
  899. chartInstance = getChartInstance(chartContainer.value)
  900. if (!chartInstance) return
  901. chartInstance.setOption(option)
  902. handleResize()
  903. }
  904. // 自适应调整
  905. const handleResize = () => {
  906. chartInstance?.resize()
  907. }
  908. /** 初始化 */
  909. onMounted(async () => {
  910. nextTick(updateScale)
  911. resizeObserver = new ResizeObserver(updateScale)
  912. if (wrapperRef.value) {
  913. resizeObserver.observe(wrapperRef.value)
  914. }
  915. window.addEventListener('resize', updateScale)
  916. deptList.value = handleTree(await DeptApi.getSimpleDeptList())
  917. queryParams.deptId = useUserStore().getUser.deptId
  918. await getStats()
  919. nextTick(resizeCharts)
  920. // await initChart()
  921. // await initCharts()
  922. })
  923. onUnmounted(() => {
  924. resizeObserver?.disconnect()
  925. window.removeEventListener('resize', updateScale)
  926. cancelAnimationFrame(resizeRaf)
  927. chartInstances.forEach((chart) => chart.dispose())
  928. chartInstances.clear()
  929. })
  930. </script>
  931. <style lang="scss" scoped>
  932. @import url('@/styles/kb.scss');
  933. // 响应式优化
  934. @media (width <= 1200px) {
  935. .stat-sub-value {
  936. font-size: 24px;
  937. }
  938. }
  939. @media (width <= 768px) {
  940. .stat-card {
  941. margin-bottom: 16px;
  942. }
  943. .chart-item {
  944. padding: 8px;
  945. }
  946. .gauge-chart {
  947. height: 140px;
  948. }
  949. }
  950. // 筛选卡片样式
  951. .filter-card {
  952. background: linear-gradient(to bottom, #fafafa, #fff);
  953. border: 1px solid #e5e7eb;
  954. border-radius: 12px;
  955. transition: all 0.3s ease;
  956. &:hover {
  957. box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
  958. }
  959. :deep(.el-card__body) {
  960. padding: 20px;
  961. }
  962. :deep(.el-form-item__label) {
  963. font-weight: 500;
  964. color: #6b7280;
  965. }
  966. }
  967. .btn-primary {
  968. background: linear-gradient(135deg, #64748b 0%, #475569 100%);
  969. border: none;
  970. transition: all 0.3s ease;
  971. &:hover {
  972. background: linear-gradient(135deg, #475569 0%, #334155 100%);
  973. transform: translateY(-1px);
  974. box-shadow: 0 4px 8px rgb(100 116 139 / 30%);
  975. }
  976. }
  977. .btn-default {
  978. color: #6b7280;
  979. border-color: #d1d5db;
  980. transition: all 0.3s ease;
  981. &:hover {
  982. color: #4b5563;
  983. background: #f9fafb;
  984. border-color: #9ca3af;
  985. }
  986. }
  987. // 统计卡片基础样式
  988. .stat-card {
  989. position: relative;
  990. overflow: hidden;
  991. background: #fff;
  992. border: 1px solid #e5e7eb;
  993. border-radius: 12px;
  994. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  995. &:hover {
  996. border-color: #d1d5db;
  997. transform: translateY(-4px);
  998. box-shadow: 0 12px 24px -8px rgb(0 0 0 / 10%);
  999. }
  1000. :deep(.el-card__body) {
  1001. padding: 0;
  1002. }
  1003. }
  1004. // 中性色渐变背景(低饱和度)
  1005. .stat-card-neutral-1 {
  1006. background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  1007. }
  1008. .stat-card-neutral-2 {
  1009. background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
  1010. }
  1011. .stat-card-neutral-3 {
  1012. background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
  1013. }
  1014. .stat-card-neutral-4 {
  1015. background: linear-gradient(135deg, #f1f5f9 0%, #cbd5e1 100%);
  1016. }
  1017. // 统计内容区域
  1018. .stat-content {
  1019. padding: 20px;
  1020. }
  1021. .stat-header {
  1022. display: flex;
  1023. align-items: center;
  1024. gap: 12px;
  1025. margin-bottom: 16px;
  1026. }
  1027. .stat-icon-wrapper {
  1028. display: flex;
  1029. width: 48px;
  1030. height: 48px;
  1031. background: #0090ff;
  1032. border-radius: 12px;
  1033. box-shadow: 0 4px 8px rgb(100 116 139 / 20%);
  1034. align-items: center;
  1035. justify-content: center;
  1036. }
  1037. .stat-icon {
  1038. font-size: 24px;
  1039. color: white;
  1040. }
  1041. .stat-title {
  1042. font-size: 15px;
  1043. font-weight: 600;
  1044. color: #475569;
  1045. }
  1046. .stat-divider {
  1047. margin: 12px 0;
  1048. border-color: #e2e8f0;
  1049. }
  1050. .stat-body {
  1051. display: flex;
  1052. flex-direction: column;
  1053. gap: 12px;
  1054. }
  1055. .stat-item {
  1056. display: flex;
  1057. justify-content: space-between;
  1058. align-items: center;
  1059. padding: 8px 12px;
  1060. background: rgb(255 255 255 / 60%);
  1061. border-radius: 8px;
  1062. transition: all 0.2s ease;
  1063. &:hover {
  1064. background: rgb(255 255 255 / 90%);
  1065. }
  1066. }
  1067. .stat-sub-label {
  1068. font-size: 13px;
  1069. font-weight: 500;
  1070. color: #64748b;
  1071. }
  1072. .stat-sub-value {
  1073. font-size: 28px;
  1074. font-weight: 700;
  1075. line-height: 1;
  1076. }
  1077. // 图表卡片增强样式
  1078. .chart-card-enhanced {
  1079. background: #fff;
  1080. border: 1px solid #e5e7eb;
  1081. border-radius: 12px;
  1082. transition: all 0.3s ease;
  1083. &:hover {
  1084. border-color: #d1d5db;
  1085. box-shadow: 0 8px 24px rgb(0 0 0 / 8%);
  1086. }
  1087. :deep(.el-card__header) {
  1088. padding: 16px 20px;
  1089. background: linear-gradient(to right, #fafafa, #fff);
  1090. border-bottom: 1px solid #f1f5f9;
  1091. }
  1092. :deep(.el-card__body) {
  1093. padding: 20px;
  1094. }
  1095. }
  1096. .chart-header {
  1097. display: flex;
  1098. align-items: center;
  1099. justify-content: space-between;
  1100. }
  1101. .chart-title-wrapper {
  1102. display: flex;
  1103. align-items: center;
  1104. gap: 8px;
  1105. }
  1106. .chart-title-dot {
  1107. width: 8px;
  1108. height: 8px;
  1109. border-radius: 50%;
  1110. box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
  1111. }
  1112. .chart-title-dot-blue {
  1113. background: #05b;
  1114. }
  1115. .chart-title-dot-green {
  1116. background: #05b;
  1117. }
  1118. .chart-title-dot-purple {
  1119. background: #05b;
  1120. }
  1121. .chart-title-dot-indigo {
  1122. background: #05b;
  1123. }
  1124. .chart-title {
  1125. font-size: 16px;
  1126. font-weight: 600;
  1127. letter-spacing: 0.3px;
  1128. color: #334155;
  1129. }
  1130. .chart-grid {
  1131. min-height: 220px;
  1132. }
  1133. .chart-item {
  1134. display: flex;
  1135. flex-direction: column;
  1136. align-items: center;
  1137. padding: 12px;
  1138. transition: all 0.2s ease;
  1139. &:hover {
  1140. background: #f8fafc;
  1141. border-radius: 8px;
  1142. }
  1143. }
  1144. .gauge-chart {
  1145. width: 100%;
  1146. height: 160px;
  1147. }
  1148. .chart-label {
  1149. display: flex;
  1150. padding: 6px 12px;
  1151. margin-top: 8px;
  1152. background: #f1f5f9;
  1153. border-radius: 20px;
  1154. transition: all 0.2s ease;
  1155. align-items: center;
  1156. gap: 6px;
  1157. .chart-item:hover & {
  1158. background: #e2e8f0;
  1159. }
  1160. &.clickable {
  1161. cursor: pointer;
  1162. &:hover {
  1163. background: #cbd5e1;
  1164. transform: scale(1.05);
  1165. }
  1166. }
  1167. }
  1168. .label-dot {
  1169. display: inline-block;
  1170. width: 8px;
  1171. height: 8px;
  1172. border-radius: 50%;
  1173. }
  1174. .label-dot-blue {
  1175. background: #64748b;
  1176. box-shadow: 0 0 0 2px rgb(100 116 139 / 20%);
  1177. }
  1178. .label-dot-amber {
  1179. background: #d97706;
  1180. box-shadow: 0 0 0 2px rgb(217 119 6 / 20%);
  1181. }
  1182. .label-dot-gray {
  1183. background: #78716c;
  1184. box-shadow: 0 0 0 2px rgb(120 113 108 / 20%);
  1185. }
  1186. .label-text {
  1187. font-size: 13px;
  1188. font-weight: 500;
  1189. color: #64748b;
  1190. }
  1191. .pie-chart-container {
  1192. width: 100%;
  1193. height: 300px;
  1194. }
  1195. .bar-chart-container {
  1196. width: 100%;
  1197. height: 300px;
  1198. }
  1199. .kb-screen {
  1200. overflow: hidden;
  1201. }
  1202. .inspect-kb-content {
  1203. display: grid;
  1204. height: calc(100% - 52px * var(--kb-scale));
  1205. padding: calc(16px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
  1206. gap: calc(12px * var(--kb-scale));
  1207. grid-template-rows:
  1208. calc(74px * var(--kb-scale)) calc(174px * var(--kb-scale))
  1209. calc(300px * var(--kb-scale)) minmax(0, 1fr);
  1210. }
  1211. .inspect-kb-content > .el-row {
  1212. height: 100%;
  1213. margin-bottom: 0 !important;
  1214. }
  1215. .inspect-kb-content > .el-row > .el-col {
  1216. height: 100%;
  1217. }
  1218. .filter-card,
  1219. .stat-card,
  1220. .chart-card-enhanced {
  1221. height: 100%;
  1222. background: linear-gradient(180deg, rgb(235 243 255 / 76%) 0%, rgb(221 233 251 / 58%) 100%);
  1223. border: 1px solid rgb(255 255 255 / 68%) !important;
  1224. border-radius: calc(8px * var(--kb-scale));
  1225. box-shadow:
  1226. inset 0 1px 0 rgb(255 255 255 / 78%),
  1227. 0 calc(10px * var(--kb-scale)) calc(24px * var(--kb-scale)) rgb(46 90 164 / 10%);
  1228. backdrop-filter: blur(12px);
  1229. }
  1230. .filter-card:hover,
  1231. .stat-card:hover,
  1232. .chart-card-enhanced:hover {
  1233. border-color: rgb(255 255 255 / 84%) !important;
  1234. transform: none;
  1235. box-shadow:
  1236. inset 0 1px 0 rgb(255 255 255 / 86%),
  1237. 0 calc(12px * var(--kb-scale)) calc(26px * var(--kb-scale)) rgb(46 90 164 / 14%);
  1238. }
  1239. .filter-card :deep(.el-card__body) {
  1240. display: flex;
  1241. height: 100%;
  1242. padding: calc(12px * var(--kb-scale)) calc(18px * var(--kb-scale));
  1243. align-items: center;
  1244. }
  1245. .filter-card :deep(.el-form) {
  1246. display: flex;
  1247. width: 100%;
  1248. margin-bottom: 0 !important;
  1249. align-items: center;
  1250. gap: calc(16px * var(--kb-scale));
  1251. }
  1252. .filter-card :deep(.el-form-item) {
  1253. margin-right: 0;
  1254. margin-bottom: 0;
  1255. }
  1256. .filter-card :deep(.el-form-item__label) {
  1257. height: calc(32px * var(--kb-scale));
  1258. padding-right: calc(10px * var(--kb-scale));
  1259. font-size: calc(14px * var(--kb-scale));
  1260. line-height: calc(32px * var(--kb-scale));
  1261. color: #31527e;
  1262. }
  1263. .filter-card :deep(.el-tree-select) {
  1264. width: calc(220px * var(--kb-scale)) !important;
  1265. }
  1266. .filter-card :deep(.el-date-editor) {
  1267. width: calc(320px * var(--kb-scale)) !important;
  1268. }
  1269. .filter-card :deep(.el-input__wrapper),
  1270. .filter-card :deep(.el-select__wrapper) {
  1271. min-height: calc(32px * var(--kb-scale));
  1272. padding: 0 calc(10px * var(--kb-scale));
  1273. background: rgb(255 255 255 / 72%);
  1274. }
  1275. .filter-card :deep(.el-range-input),
  1276. .filter-card :deep(.el-range-separator),
  1277. .filter-card :deep(.el-input__inner) {
  1278. font-size: calc(13px * var(--kb-scale));
  1279. }
  1280. .filter-card :deep(.el-button) {
  1281. height: calc(32px * var(--kb-scale));
  1282. padding: 0 calc(14px * var(--kb-scale));
  1283. margin-left: calc(8px * var(--kb-scale));
  1284. font-size: calc(13px * var(--kb-scale));
  1285. }
  1286. .stat-card {
  1287. overflow: hidden;
  1288. }
  1289. .stat-card :deep(.el-card__body) {
  1290. height: 100%;
  1291. padding: 0;
  1292. }
  1293. .stat-card-neutral-1,
  1294. .stat-card-neutral-2,
  1295. .stat-card-neutral-3,
  1296. .stat-card-neutral-4 {
  1297. background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 42%) 100%);
  1298. }
  1299. .stat-content {
  1300. display: flex;
  1301. height: 100%;
  1302. padding: calc(16px * var(--kb-scale));
  1303. flex-direction: column;
  1304. }
  1305. .stat-header {
  1306. margin-bottom: calc(8px * var(--kb-scale));
  1307. gap: calc(10px * var(--kb-scale));
  1308. }
  1309. .stat-icon-wrapper {
  1310. width: calc(38px * var(--kb-scale));
  1311. height: calc(38px * var(--kb-scale));
  1312. background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(235 244 255 / 92%) 100%);
  1313. border: 1px solid rgb(255 255 255 / 88%);
  1314. border-radius: calc(8px * var(--kb-scale));
  1315. box-shadow: 0 calc(5px * var(--kb-scale)) calc(10px * var(--kb-scale)) rgb(57 111 194 / 12%);
  1316. }
  1317. .stat-icon {
  1318. font-size: calc(19px * var(--kb-scale));
  1319. color: #2474dd;
  1320. }
  1321. .stat-title {
  1322. font-family: YouSheBiaoTiHei, sans-serif;
  1323. font-size: calc(20px * var(--kb-scale));
  1324. font-weight: normal;
  1325. letter-spacing: 0;
  1326. color: #03409b;
  1327. }
  1328. .stat-divider {
  1329. margin: 0 0 calc(8px * var(--kb-scale));
  1330. border-color: rgb(3 64 155 / 12%);
  1331. }
  1332. .stat-body {
  1333. display: grid;
  1334. min-height: 0;
  1335. flex: 1;
  1336. gap: calc(10px * var(--kb-scale));
  1337. grid-template-columns: repeat(2, minmax(0, 1fr));
  1338. }
  1339. .stat-item {
  1340. padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale));
  1341. background: rgb(255 255 255 / 38%);
  1342. border: 1px solid rgb(255 255 255 / 46%);
  1343. border-radius: calc(6px * var(--kb-scale));
  1344. }
  1345. .stat-sub-label {
  1346. font-size: calc(13px * var(--kb-scale));
  1347. color: #55708f;
  1348. }
  1349. .stat-sub-value {
  1350. font-family: YouSheBiaoTiHei, sans-serif;
  1351. font-size: calc(28px * var(--kb-scale));
  1352. font-weight: normal;
  1353. color: #1762c8;
  1354. }
  1355. .stat-sub-value.text-amber-600 {
  1356. color: #e87b1e;
  1357. }
  1358. .chart-card-enhanced {
  1359. display: flex;
  1360. overflow: hidden;
  1361. flex-direction: column;
  1362. }
  1363. .chart-card-enhanced :deep(.el-card__header) {
  1364. height: calc(42px * var(--kb-scale));
  1365. padding: 0 calc(16px * var(--kb-scale)) 0 calc(36px * var(--kb-scale));
  1366. background: transparent;
  1367. border-bottom: 1px solid rgb(3 64 155 / 12%);
  1368. }
  1369. .chart-card-enhanced :deep(.el-card__body) {
  1370. min-height: 0;
  1371. padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale));
  1372. flex: 1;
  1373. }
  1374. .chart-header,
  1375. .chart-title-wrapper {
  1376. height: 100%;
  1377. }
  1378. .chart-title-wrapper {
  1379. position: relative;
  1380. }
  1381. .chart-title-dot {
  1382. position: absolute;
  1383. left: calc(-22px * var(--kb-scale));
  1384. width: calc(4px * var(--kb-scale));
  1385. height: calc(18px * var(--kb-scale));
  1386. background: #03409b;
  1387. border-radius: calc(2px * var(--kb-scale));
  1388. box-shadow: none;
  1389. }
  1390. .chart-title {
  1391. font-family: YouSheBiaoTiHei, sans-serif;
  1392. font-size: calc(22px * var(--kb-scale));
  1393. font-weight: normal;
  1394. letter-spacing: 0;
  1395. color: #03409b;
  1396. }
  1397. .chart-grid {
  1398. height: 100%;
  1399. min-height: 0;
  1400. }
  1401. .chart-item {
  1402. height: 100%;
  1403. padding: calc(4px * var(--kb-scale));
  1404. justify-content: center;
  1405. }
  1406. .chart-item:hover {
  1407. background: transparent;
  1408. }
  1409. .gauge-chart {
  1410. height: calc(190px * var(--kb-scale));
  1411. }
  1412. .chart-label {
  1413. min-height: calc(28px * var(--kb-scale));
  1414. padding: calc(4px * var(--kb-scale)) calc(10px * var(--kb-scale));
  1415. margin-top: 0;
  1416. background: rgb(255 255 255 / 52%);
  1417. border-radius: calc(5px * var(--kb-scale));
  1418. gap: calc(6px * var(--kb-scale));
  1419. }
  1420. .chart-label.clickable:hover {
  1421. background: rgb(255 255 255 / 78%);
  1422. transform: none;
  1423. }
  1424. .label-dot {
  1425. width: calc(7px * var(--kb-scale));
  1426. height: calc(7px * var(--kb-scale));
  1427. }
  1428. .label-text {
  1429. font-size: calc(13px * var(--kb-scale));
  1430. color: #466484;
  1431. }
  1432. .pie-chart-container,
  1433. .bar-chart-container {
  1434. width: 100%;
  1435. height: 100%;
  1436. }
  1437. </style>