LeafPage.vue 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311
  1. <template>
  2. <div class="dashboard-container">
  3. <Skeleton v-if="loading" active class="p-20px"></Skeleton>
  4. <!-- 内容区域 -->
  5. <div class="content-area" v-if="!loading">
  6. <!-- 顶部信息卡片 -->
  7. <div class="top-info">
  8. <div class="info-card">
  9. <div class="card-title">
  10. <div class="info-details">
  11. <div class="label">{{ basicInfo.mineName }}</div>
  12. <!-- <div class="value">西川煤矿分公司</div> -->
  13. <div class="tag" :style="{ backgroundColor: getAlarmColor(basicInfo.alarmLevel), color: '#fff' }">{{
  14. getRiskText(basicInfo.alarmLevel)
  15. }}</div>
  16. </div>
  17. <div>
  18. <a-button class="btn" @click="handleGoToPageQuery('/warningAnalysis/connectAnalysis')">数据看板</a-button>
  19. </div>
  20. <div class="info-border">
  21. <div class="info-border-icon"></div>
  22. <div class="info-border-line"></div>
  23. <div class="info-border-icon"></div>
  24. </div>
  25. </div>
  26. <div class="card-details">
  27. <div v-for="item in basicInfoColumns" :key="item.key" class="info-card-item">
  28. <div class="card-label">{{ item.label }}</div>
  29. <div class="card-value-wrapper">
  30. <span
  31. v-if="['accessStatus', 'status', 'gasLevelName', 'alarmLevel', 'gjMineStatus'].includes(item.key)"
  32. class="status-dot"
  33. :style="{ backgroundColor: getBasicInfoColor(item.key), boxShadow: `0 0 10px ${getBasicInfoColor(item.key)}` }"
  34. ></span>
  35. <span class="card-value-text" :style="{ color: getBasicInfoColor(item.key) }">
  36. {{ getBasicInfoValue(item.key) }}
  37. </span>
  38. </div>
  39. <div class="card-border">
  40. <div class="card-border-icon"></div>
  41. <div class="card-border-line"></div>
  42. </div>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. <!-- 预警统计 -->
  48. <div class="warning-statistics">
  49. <div class="statistics-header">
  50. <div class="title">预警统计</div>
  51. <div class="btn-group">
  52. <!-- <button class="btn">预警级别</button> -->
  53. </div>
  54. </div>
  55. <div class="statistics-content">
  56. <div class="stat-item high-risk">
  57. <div class="icon">
  58. <div class="icon-top"></div>
  59. <div class="icon-bottom"></div>
  60. </div>
  61. <div class="text">高风险<span class="text-sub">(需立即处理)</span></div>
  62. <div class="count">{{ get(alarmStatistics, 'alarm4', '-') }}</div>
  63. </div>
  64. <div class="stat-item medium-risk">
  65. <div class="icon">
  66. <div class="icon-top"></div>
  67. <div class="icon-bottom"></div>
  68. </div>
  69. <div class="text">较高风险<span class="text-sub"></span></div>
  70. <div class="count">{{ get(alarmStatistics, 'alarm3', '-') }}</div>
  71. </div>
  72. <div class="stat-item normal-risk">
  73. <div class="icon">
  74. <div class="icon-top"></div>
  75. <div class="icon-bottom"></div>
  76. </div>
  77. <div class="text">一般风险<span class="text-sub"></span></div>
  78. <div class="count">{{ get(alarmStatistics, 'alarm2', '-') }}</div>
  79. </div>
  80. <div class="stat-item low-risk">
  81. <div class="icon">
  82. <div class="icon-top"></div>
  83. <div class="icon-bottom"></div>
  84. </div>
  85. <div class="text">低风险<span class="text-sub"></span></div>
  86. <div class="count">{{ get(alarmStatistics, 'alarm1', '-') }}</div>
  87. </div>
  88. </div>
  89. </div>
  90. <!-- 预警数据详情 -->
  91. <div class="warning-details">
  92. <div class="details-header">
  93. <div class="title">预警数据详情</div>
  94. <div class="btn-group">
  95. <button class="btn" @click="collapseAll">收起所有分组</button>
  96. <button class="btn" @click="expandAll">展开详情</button>
  97. </div>
  98. </div>
  99. <div class="details-content">
  100. <Empty v-if="!goafMonitorData.length"></Empty>
  101. <!-- 第一个预警项 -->
  102. <div v-for="(item, index) in goafMonitorData" :key="index" class="warning-item">
  103. <div class="item-header">
  104. <div class="header-left">
  105. <div class="icon"></div>
  106. <div class="name">老空区密闭名称: {{ item.devicePos }}</div>
  107. <div class="risk-level" :style="{ backgroundColor: getAlarmColor(item.alarmLevel), color: '#fff' }">
  108. {{ getRiskText(item.alarmLevel) }}
  109. </div>
  110. </div>
  111. <a-button class="expand-btn" @click="toggleExpand(index)">
  112. {{ isExpanded(index) ? '收起' : '展开' }}
  113. <div class="arrow-icon-wrapper">
  114. <div class="arrow-icon" :class="{ rotate: isExpanded(index) }"></div>
  115. </div>
  116. </a-button>
  117. </div>
  118. <div class="item-body">
  119. <div class="data-row">
  120. <div v-for="(col, colIndex) in dataColumns" :key="colIndex" class="data-col">
  121. <div class="col-title">{{ col.label }}</div>
  122. <div class="col-value" :style="{ color: getColValueColor(col, item) }">
  123. <span
  124. v-if="col.type !== 'text'"
  125. class="status-dot"
  126. :style="{
  127. backgroundColor: getColValueColor(col, item),
  128. boxShadow: `0 0 10px ${getColValueColor(col, item)}`,
  129. }"
  130. ></span>
  131. {{ getColDisplayText(col, item) }}
  132. </div>
  133. <div class="col-icon"></div>
  134. </div>
  135. </div>
  136. </div>
  137. <div class="monitoring-indicators" v-show="isExpanded(index)">
  138. <div class="indicator-group">
  139. <div class="group-title">
  140. <div class="title-icon-top"></div>
  141. <div class="title-icon-bottom"></div>
  142. <div class="title-content">
  143. 密闭墙内<br />
  144. 监测指标
  145. </div>
  146. </div>
  147. <div class="indicator-area">
  148. <div v-for="(ind, indIndex) in indicatorColumns" :key="'inner-' + indIndex" class="indicator-item">
  149. <div class="label">{{ ind.label }}:</div>
  150. <!-- 墙内数据直接取 item[key],例如 item.coVal -->
  151. <div class="value" :title="getDataValue(item, ind.key)">{{ getDataValue(item, ind.key) }} {{ ind.unit }}</div>
  152. </div>
  153. </div>
  154. </div>
  155. <div class="indicator-group">
  156. <div class="group-title">
  157. <div class="title-icon-top icon-outer"></div>
  158. <div class="title-icon-bottom"></div>
  159. <div class="title-content">
  160. 密闭墙外<br />
  161. 监测指标
  162. </div>
  163. </div>
  164. <div class="indicator-area">
  165. <div v-for="(ind, indIndex) in indicatorColumns" :key="'outer-' + indIndex" class="indicator-item">
  166. <div class="label">{{ ind.label }}:</div>
  167. <!-- 墙外数据+ Out item[key],例如 item.coValOut -->
  168. <div class="value" :title="getDataValue(item, ind.key + 'Out')">{{ getDataValue(item, ind.key + 'Out') }} {{ ind.unit }}</div>
  169. </div>
  170. </div>
  171. </div>
  172. <!-- <div class="indicator-group">
  173. <div class="group-title">
  174. <div class="title-icon-top icon-result"></div>
  175. <div class="title-icon-bottom"></div>
  176. <div class="title-content"> 预警结果 </div>
  177. </div>
  178. <div class="indicator-area indicator-result">
  179. <div v-for="(res, resIndex) in resultColumns" :key="'result-' + resIndex" class="indicator-item">
  180. <div class="label">{{ res.label }}:</div>
  181. <div v-if="res.key == 'alarmLevel'" :title="getRiskText(item.alarmLevel)" class="value">{{ getRiskText(item.alarmLevel) }}</div>
  182. <div v-else class="value" :title="getDataValue(item, res.key)">{{ getDataValue(item, res.key) }}</div>
  183. </div>
  184. <div class="indicator-item placeholder-item"></div>
  185. </div>
  186. </div> -->
  187. </div>
  188. </div>
  189. </div>
  190. </div>
  191. <!-- 异常超限报警 -->
  192. <div class="abnormal-alarm">
  193. <div class="alarm-header">
  194. <div class="title">异常超限报警</div>
  195. <div class="btn-group">
  196. <!-- <button class="btn">收起图表</button> -->
  197. </div>
  198. </div>
  199. <div class="alarm-content">
  200. <div v-for="(alarm, index) in alarmColumns" :key="index" class="alarm-item">
  201. <div class="alarm-name">{{ alarm.label }}</div>
  202. <div class="alarm-count">{{ get(mineAlarmData, alarm.key, '-') }}</div>
  203. <div class="alarm-border">
  204. <div class="alarm-border-icon"></div>
  205. <div class="alarm-border-line"></div>
  206. </div>
  207. <div class="alarm-border-bottom"></div>
  208. </div>
  209. </div>
  210. </div>
  211. <!-- 历史数据折线图 -->
  212. <div class="historical-data">
  213. <div class="chart-header">
  214. <div class="title">历史数据分析图表</div>
  215. <div class="btn-group">
  216. <button class="btn" @click="toggleChartVisibility">
  217. {{ isChartVisible ? '收起图表' : '展开图表' }}
  218. </button>
  219. </div>
  220. </div>
  221. <div class="chart-content" v-show="isChartVisible">
  222. <ConnectAnalysis ref="connectAnalysisRef" />
  223. </div>
  224. </div>
  225. </div>
  226. </div>
  227. </template>
  228. <script setup lang="ts">
  229. import { useRouter } from 'vue-router';
  230. // import { useAppStore } from '/@/store/modules/app';
  231. import { ref, watch } from 'vue';
  232. import { useMineDepartmentStore } from '/@/store/modules/mine';
  233. import { getGoafAlarmNum, getGoafData, getMineData /*, getProvinceAlarm */ } from '../overhaul.api';
  234. import { StatusColorEnum } from '/@/enums/jeecgEnum';
  235. import ConnectAnalysis from '/@/views/analysis/warningAnalysis/connectAnalysis/index.vue';
  236. import { getOverLimitNum } from '../../SealedGoaf/sealedGoaf.api';
  237. import { get } from 'lodash-es';
  238. import { Empty, Skeleton } from 'ant-design-vue';
  239. // const appStore = useAppStore();
  240. const mineStore = useMineDepartmentStore();
  241. // 路由实例
  242. const router = useRouter();
  243. // 1. 定义数据类型接口
  244. interface GoafMonitorItem {
  245. devicePos?: string;
  246. [key: string]: any; // 允许其他动态属性
  247. }
  248. /**
  249. * 风险等级映射配置
  250. */
  251. const RISK_LEVEL_MAP: Record<string, { text: string; color: string }> = {
  252. '4': { text: '高风险', color: StatusColorEnum.red },
  253. '3': { text: '较高风险', color: StatusColorEnum.gold },
  254. '2': { text: '一般风险', color: StatusColorEnum.yellow },
  255. '1': { text: '低风险', color: StatusColorEnum.blue },
  256. '0': { text: '无风险', color: '#333' }, // 假设 0 为无风险或默认
  257. };
  258. // 顶部的基础信息部分
  259. const basicInfoColumns = [
  260. { label: '组织机构', key: 'parentArea' },
  261. { label: '瓦斯等级', key: 'gasLevelName' },
  262. { label: '煤层自燃倾向性', key: 'coalSeamLevel' },
  263. { label: '生产状态', key: 'gjMineStatus' },
  264. { label: '预警结果', key: 'alarmLevel' },
  265. { label: '预警时间', key: 'earliestTime' },
  266. { label: '持续时间', key: 'chixuTime' },
  267. { label: '接入状态', key: 'accessStatus' },
  268. { label: '数据联网状态', key: 'status' },
  269. { label: '设备数量', key: 'accessGoafNum' },
  270. ];
  271. // 1. 预警数据详情 - 顶部指标列配置
  272. const dataColumns = [
  273. { label: '密闭墙内自燃发火隐患', key: 'fireAlarm.alarmName', colorKey: 'fireAlarm.alarmLevel', type: 'alarm' },
  274. { label: '密闭墙外自燃发火隐患', key: 'fireAlarmOut.alarmName', colorKey: 'fireAlarmOut.alarmLevel', type: 'alarm' },
  275. { label: '爆炸三角', key: 'explosionAlarm.alarmName', colorKey: 'explosionAlarm.alarmLevel', type: 'alarm' },
  276. { label: '压差预警', key: 'sourcePressureAlarm.alarmName', colorKey: 'sourcePressureAlarm.alarmLevel', type: 'alarm' },
  277. { label: '漏风状态判定', key: 'leakageAlarm.alarmName', colorKey: 'leakageAlarm.alarmLevel', type: 'alarm' },
  278. { label: '设备状态', key: 'linkStatus', type: 'status' },
  279. { label: '检查时间', key: 'readTime', type: 'text' },
  280. ];
  281. // 2. 监测指标通用配置 (用于墙内和墙外)
  282. const indicatorColumns = [
  283. { label: 'CO', key: 'coVal', unit: 'ppm' },
  284. { label: 'CO2', key: 'co2Val', unit: '%' },
  285. { label: 'CH4', key: 'ch4Val', unit: '%' },
  286. { label: 'O2', key: 'o2Val', unit: '%' },
  287. { label: 'C2H2', key: 'c2h2Val', unit: 'ppm' },
  288. { label: 'C2H4', key: 'c2h4Val', unit: 'ppm' },
  289. { label: '温度', key: 'temperature', unit: '°C' },
  290. ];
  291. // 3. 预警结果配置(已注释,对应模板块已隐藏)
  292. // const resultColumns = [
  293. // { label: '数据来源', key: 'readTime', value: '最近2天' },
  294. // { label: '预警结果', key: 'alarmLevel', value: '异常' },
  295. // ];
  296. // 4. 异常超限报警配置
  297. const alarmColumns = [
  298. { label: '甲烷超限', key: 'ch4Val' },
  299. { label: '氧气超限', key: 'o2Val' },
  300. { label: '一氧化碳超限', key: 'coVal' },
  301. { label: '二氧化碳超限', key: 'co2Val' },
  302. { label: '乙烯超限', key: 'c2h4Val' },
  303. { label: '乙炔超限', key: 'c2h2Val' },
  304. { label: '压差超限', key: 'sourcePressure' },
  305. { label: '温度超限', key: 'temperature' },
  306. ];
  307. const basicInfo = ref<any>({}); //煤矿基础信息数据
  308. const alarmStatistics = ref<any>({}); //预警统计数据
  309. const goafMonitorData = ref<GoafMonitorItem[]>([]); //密闭监测详情数据
  310. const mineAlarmData = ref<any>({}); // 煤矿预警数据
  311. // const connectAnalysisRef = ref<any>(null); // 连接分析组件实例
  312. const isChartVisible = ref(true); // 控制图表显示/隐藏的状态
  313. const loading = ref(false);
  314. // 1. 用于存储展开状态的索引集合 (key: index, value: boolean)
  315. const expandedIndices = ref<Record<number, boolean>>({});
  316. // 2. 切换展开/收起状态的方法
  317. const toggleExpand = (index: number) => {
  318. expandedIndices.value[index] = !expandedIndices.value[index];
  319. };
  320. // 3. 判断是否展开
  321. const isExpanded = (index: number) => {
  322. return !!expandedIndices.value[index];
  323. };
  324. // 4. 展开所有详情
  325. const expandAll = () => {
  326. goafMonitorData.value.forEach((_, index) => {
  327. expandedIndices.value[index] = true;
  328. });
  329. };
  330. // 5. 收起所有分组
  331. const collapseAll = () => {
  332. // 清空对象即可,或者遍历设为 false
  333. expandedIndices.value = {};
  334. };
  335. // 切换图表显示状态
  336. const toggleChartVisibility = () => {
  337. isChartVisible.value = !isChartVisible.value;
  338. };
  339. /**
  340. * 获取设备状态信息 (文本和颜色)
  341. * @param status 0: 断开, 1: 正常
  342. */
  343. const getlinkStatusInfo = (status?: number | string) => {
  344. if (status === 1 || status === '1') {
  345. return { text: '正常', color: '#52c41a' }; // 绿色
  346. }
  347. return { text: '断开', color: '#ff4d4f' }; // 红色
  348. };
  349. /**
  350. * 获取列显示的文本
  351. */
  352. const getColDisplayText = (col: any, item: any) => {
  353. const rawValue = getDataValue(item, col.key);
  354. if (col.type === 'status') {
  355. return getlinkStatusInfo(rawValue).text;
  356. }
  357. // 默认直接返回原始值或经过 getDataValue 处理后的值
  358. return rawValue;
  359. };
  360. /**
  361. * 获取列显示的颜色
  362. */
  363. const getColValueColor = (col: any, item: any) => {
  364. // 如果是报警类型,使用原有的报警颜色逻辑
  365. if (col.type === 'alarm' && col.colorKey) {
  366. return getAlarmColor(getDataValue(item, col.colorKey));
  367. }
  368. // 如果是设备状态类型,使用状态颜色逻辑
  369. if (col.type === 'status') {
  370. return getlinkStatusInfo(getDataValue(item, col.key)).color;
  371. }
  372. // 默认黑色
  373. return '#333';
  374. };
  375. /**
  376. * 获取风险等级对应的文本
  377. * @param level 报警级别 '1', '2', '3', '4'
  378. */
  379. const getRiskText = (level?: string | number) => {
  380. if (!level && level !== 0) return '-';
  381. return RISK_LEVEL_MAP[String(level)]?.text || '-';
  382. };
  383. /**
  384. * 根据点分字符串路径获取对象值 (例如: 'fireAlarm.alarmName')
  385. * @param item 数据对象
  386. * @param path 路径字符串
  387. * @param defaultValue 默认值
  388. */
  389. const getDataValue = (item: any, path: string, defaultValue: string = '-') => {
  390. if (!item || !path) return defaultValue;
  391. const val = path.split('.').reduce((acc, part) => acc && acc[part], item);
  392. return val ?? defaultValue;
  393. };
  394. /**
  395. * 获取报警颜色
  396. * @param level 报警级别 '1', '2', '3', '4'
  397. */
  398. const getAlarmColor = (level?: string | number) => {
  399. if (!level && level !== 0) return '#333'; // 默认颜色
  400. return RISK_LEVEL_MAP[String(level)]?.color;
  401. };
  402. /**
  403. * 获取基础信息的显示内容(包含文本和颜色)
  404. */
  405. const getBasicInfoDisplay = (key: string) => {
  406. const value = basicInfo.value[key];
  407. // 默认返回黑色文本
  408. const defaultResult = { text: value ?? '-', color: '#333' };
  409. if (key === 'accessStatus') {
  410. // 1: 已接入 (绿色), 其他: 未接入 (红色)
  411. return value === 1 ? { text: '已接入', color: '#52c41a' } : { text: '未接入', color: StatusColorEnum.red };
  412. }
  413. if (key === 'status') {
  414. // 1: 在线 (绿色), 其他: 离线 (红色)
  415. return value === 1 ? { text: '在线', color: '#52c41a' } : { text: '离线', color: StatusColorEnum.red };
  416. }
  417. if (key === 'alarmLevel') {
  418. // 根据风险等级获取颜色
  419. const levelStr = String(value);
  420. const riskInfo = RISK_LEVEL_MAP[levelStr];
  421. if (riskInfo) {
  422. return { text: riskInfo.text, color: riskInfo.color };
  423. }
  424. return { text: '-', color: '#333' };
  425. }
  426. if (key === 'gasLevelName') {
  427. if (!value) return { text: value ?? '-', color: '#333' };
  428. // 瓦斯等级特殊处理
  429. const val = String(value);
  430. if (val.includes('低瓦斯')) {
  431. return { text: val, color: StatusColorEnum.blue }; // 蓝色
  432. } else if (val.includes('高瓦斯')) {
  433. return { text: val, color: StatusColorEnum.gold }; // 橙色/金色
  434. } else if (val.includes('突出')) {
  435. return { text: val, color: StatusColorEnum.red }; // 红色
  436. }
  437. return { text: val, color: '#333' };
  438. }
  439. if (key === 'gjMineStatus') {
  440. if (!value) return { text: value ?? '-', color: '#333' };
  441. // 生产状态特殊处理
  442. const val = String(value);
  443. if (val.includes('正常生产')) {
  444. return { text: val, color: '#52c41a' }; // 绿色
  445. } else if (val.includes('拟建')) {
  446. return { text: val, color: StatusColorEnum.blue }; // 蓝色
  447. } else {
  448. // 其余状态均显示绿色
  449. return { text: val ?? '-', color: StatusColorEnum.red };
  450. }
  451. }
  452. return defaultResult;
  453. };
  454. /**
  455. * 获取基础信息值的文本(兼容旧逻辑,如果只需要文本)
  456. */
  457. const getBasicInfoValue = (key: string) => {
  458. return getBasicInfoDisplay(key).text;
  459. };
  460. /**
  461. * 获取基础信息值的颜色
  462. */
  463. const getBasicInfoColor = (key: string) => {
  464. return getBasicInfoDisplay(key).color;
  465. };
  466. /**
  467. * 通用页面跳转方法
  468. * @param record 当前行数据
  469. * @param path 目标路径
  470. */
  471. function handleGoToPageQuery(path: string) {
  472. router
  473. .push({
  474. path,
  475. })
  476. .catch((err) => {
  477. console.error('跳转失败:', err);
  478. });
  479. }
  480. /**
  481. * 计算持续时间
  482. * @param earliestTimeStr 开始时间字符串
  483. * @param latestReadTimeStr 结束时间字符串
  484. * @returns 格式化后的持续时间字符串,如 "1天2小时30分钟",无效则返回 "-"
  485. */
  486. const calculateDuration = (earliestTimeStr?: string, latestReadTimeStr?: string): string => {
  487. if (!earliestTimeStr || !latestReadTimeStr) {
  488. return '-';
  489. }
  490. const earliestTime = new Date(earliestTimeStr).getTime();
  491. const latestReadTime = new Date(latestReadTimeStr).getTime();
  492. if (isNaN(earliestTime) || isNaN(latestReadTime)) {
  493. return '-';
  494. }
  495. const diffMs = latestReadTime - earliestTime;
  496. // 如果时间差为负数,返回 '-'
  497. if (diffMs < 0) {
  498. return '-';
  499. }
  500. // 转换为天、小时、分钟
  501. const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  502. const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  503. const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
  504. let timeStr = '';
  505. if (days > 0) {
  506. timeStr += `${days}天`;
  507. }
  508. if (hours > 0 || days > 0) {
  509. timeStr += `${hours}小时`;
  510. }
  511. // 如果只有分钟,或者有天/小时,都加上分钟
  512. timeStr += `${minutes}分钟`;
  513. return timeStr;
  514. };
  515. async function fetchLeafData() {
  516. try {
  517. // 异步获取所有接口数据(并行请求提升性能)
  518. const [goafAlarmNum, goafData, mineData, alarmData] = await Promise.all([
  519. getGoafAlarmNum({ deptId: mineStore.getDepartId }), // 预警统计
  520. getGoafData({ deptId: mineStore.getDepartId }), // 密闭监测数据
  521. getMineData({ deptId: mineStore.getDepartId }), // 煤矿基础信息数据
  522. // getProvinceAlarm({ deptId: mineStore.getDepartId }), // 预警信息列表
  523. getOverLimitNum({ deptId: mineStore.getDepartId }),
  524. ]);
  525. alarmStatistics.value = goafAlarmNum[0] || {};
  526. goafMonitorData.value = goafData.records;
  527. // basicInfo.value = mineData.records[0];
  528. mineAlarmData.value = alarmData.reduce((obj, e) => {
  529. obj[e.alarmFiled] = e.num;
  530. return obj;
  531. }, {});
  532. // 处理煤矿基础信息并计算持续时间
  533. const mineRecord = mineData.records[0];
  534. if (mineRecord) {
  535. // 获取最早预警时间
  536. const earliestTimeStr = mineRecord.earliestTime;
  537. // 获取最新的一条密闭监测数据的读取时间
  538. const latestReadTimeStr = goafData.records?.[0]?.readTime;
  539. // 调用抽取的方法计算持续时间
  540. mineRecord.chixuTime = calculateDuration(earliestTimeStr, latestReadTimeStr);
  541. basicInfo.value = mineRecord;
  542. } else {
  543. basicInfo.value = {};
  544. }
  545. // nextTick(() => {
  546. // if (connectAnalysisRef.value && typeof connectAnalysisRef.value.generateChart === 'function') {
  547. // connectAnalysisRef.value.generateChart();
  548. // }
  549. // });
  550. } catch (error) {
  551. console.error('数据获取/配置更新失败:', error);
  552. }
  553. }
  554. watch(
  555. () => mineStore.getDepartId,
  556. async () => {
  557. loading.value = true;
  558. await fetchLeafData();
  559. loading.value = false;
  560. },
  561. { immediate: true }
  562. );
  563. // 页面逻辑代码
  564. </script>
  565. <style scoped>
  566. .dashboard-container {
  567. display: flex;
  568. flex-direction: column;
  569. height: 100%;
  570. overflow: scroll;
  571. }
  572. .content-area {
  573. flex: 1;
  574. padding: 20px;
  575. background-color: #f0f8ff;
  576. }
  577. .top-info {
  578. width: 100%;
  579. height: 200px;
  580. margin-bottom: 20px;
  581. }
  582. .info-card {
  583. width: 100%;
  584. height: 100%;
  585. display: flex;
  586. flex-direction: row;
  587. background-image: url('/@/assets/images/overHaul/leafPage/info-card-bg.png');
  588. background-repeat: no-repeat;
  589. background-size: 100%;
  590. border-radius: 8px;
  591. padding: 20px;
  592. }
  593. .card-title {
  594. width: 20%;
  595. position: relative;
  596. display: flex;
  597. flex-direction: column;
  598. justify-content: space-between;
  599. padding: 8px 10px 8px 50px;
  600. .btn {
  601. color: #fff;
  602. background-color: #2b6ff0;
  603. &:hover {
  604. background-color: #397af3;
  605. }
  606. }
  607. }
  608. .info-border {
  609. position: absolute;
  610. top: 20px;
  611. left: 20px;
  612. .info-border-icon {
  613. width: 16px;
  614. height: 16px;
  615. background-image: url('/@/assets/images/overHaul/leafPage/info-card-icon1.svg');
  616. background-repeat: no-repeat;
  617. background-size: 100%;
  618. }
  619. .info-border-line {
  620. width: 1px;
  621. height: 95px;
  622. border-left: 2px dotted #397af3;
  623. margin-left: 7px;
  624. }
  625. }
  626. .info-details {
  627. display: flex;
  628. flex-direction: column;
  629. justify-content: space-between;
  630. padding: 8px 0;
  631. .label {
  632. font-size: 16px;
  633. color: #000;
  634. }
  635. .value {
  636. font-size: 16px;
  637. color: #000;
  638. }
  639. .tag {
  640. width: fit-content;
  641. height: 24px;
  642. padding: 2px 8px;
  643. border-radius: 15px;
  644. font-size: 12px;
  645. background-color: #ff6b6b;
  646. color: white;
  647. }
  648. }
  649. .card-details {
  650. width: 80%;
  651. display: grid;
  652. grid-template-columns: repeat(5, 1fr);
  653. gap: 8px;
  654. }
  655. .info-card-item {
  656. max-width: 245px;
  657. height: 70px;
  658. position: relative;
  659. padding: 8px 10px 8px 40px;
  660. background-color: #f1f7ff;
  661. border-radius: 4px;
  662. text-align: left;
  663. }
  664. .card-border {
  665. width: 20px;
  666. height: 50px;
  667. position: absolute;
  668. top: 14px;
  669. left: 15px;
  670. .card-border-icon {
  671. width: 16px;
  672. height: 16px;
  673. background-image: url('/@/assets/images/overHaul/leafPage/info-card-icon.svg');
  674. background-repeat: no-repeat;
  675. background-size: 100%;
  676. }
  677. .card-border-line {
  678. width: 1px;
  679. height: 30px;
  680. border-left: 2px dotted #397af3;
  681. margin-left: 7px;
  682. }
  683. }
  684. .card-label {
  685. font-size: 16px;
  686. color: #000;
  687. font-weight: bold;
  688. margin-bottom: 8px;
  689. }
  690. .card-value-wrapper {
  691. display: flex;
  692. align-items: center;
  693. gap: 6px;
  694. }
  695. .status-dot {
  696. width: 8px;
  697. height: 8px;
  698. border-radius: 50%;
  699. display: inline-block;
  700. flex-shrink: 0;
  701. }
  702. .card-value-text {
  703. font-size: 14px;
  704. color: #333;
  705. font-weight: bold;
  706. }
  707. .warning-statistics {
  708. background-color: #ffffff;
  709. margin-bottom: 20px;
  710. padding: 10px 12px;
  711. border-radius: 4px;
  712. }
  713. .statistics-header {
  714. display: flex;
  715. justify-content: space-between;
  716. align-items: center;
  717. margin-bottom: 10px;
  718. }
  719. .title {
  720. font-size: 16px;
  721. font-weight: bold;
  722. color: #333;
  723. }
  724. .btn-group {
  725. display: flex;
  726. gap: 8px;
  727. }
  728. .btn {
  729. color: #2b6ff0;
  730. padding: 6px 12px;
  731. border: 1px solid #2b6ff0;
  732. border-radius: 4px;
  733. background-color: #e9f0fe;
  734. cursor: pointer;
  735. font-size: 12px;
  736. }
  737. .btn:hover {
  738. background-color: #f5f5f5;
  739. }
  740. .statistics-content {
  741. display: flex;
  742. gap: 50px;
  743. }
  744. .stat-item {
  745. flex: 1;
  746. min-height: 80px;
  747. padding: 16px 50px 10px 90px;
  748. text-align: left;
  749. position: relative;
  750. overflow: hidden;
  751. background-image: url('/@/assets/images/overHaul/leafPage/high-risk-bg.png');
  752. background-repeat: no-repeat;
  753. background-size: 100%;
  754. .icon {
  755. position: absolute;
  756. top: 15px;
  757. left: 20px;
  758. }
  759. .text {
  760. font-size: 16px;
  761. color: #000;
  762. font-weight: bold;
  763. .text-sub {
  764. font-size: 12px;
  765. color: #989587;
  766. }
  767. }
  768. .count {
  769. font-size: 24px;
  770. font-weight: bold;
  771. }
  772. .icon-top {
  773. width: 40px;
  774. height: 40px;
  775. background-image: url('/@/assets/images/overHaul/leafPage/high-risk-icon1.svg');
  776. background-repeat: no-repeat;
  777. background-size: 100%;
  778. }
  779. .icon-bottom {
  780. width: 40px;
  781. height: 20px;
  782. background-image: url('/@/assets/images/overHaul/leafPage/high-risk-icon2.png');
  783. background-repeat: no-repeat;
  784. background-size: 100%;
  785. margin-top: -5px;
  786. }
  787. }
  788. .stat-item.high-risk {
  789. background-image: url('/@/assets/images/overHaul/leafPage/high-risk-bg.png');
  790. .icon-top {
  791. background-image: url('/@/assets/images/overHaul/leafPage/high-risk-icon1.svg');
  792. }
  793. .icon-bottom {
  794. background-image: url('/@/assets/images/overHaul/leafPage/high-risk-icon2.png');
  795. }
  796. .count {
  797. color: #ff0000;
  798. }
  799. }
  800. .stat-item.medium-risk {
  801. background-image: url('/@/assets/images/overHaul/leafPage/medium-risk-bg.png');
  802. .icon-top {
  803. background-image: url('/@/assets/images/overHaul/leafPage/medium-risk-icon1.svg');
  804. }
  805. .icon-bottom {
  806. background-image: url('/@/assets/images/overHaul/leafPage/medium-risk-icon2.png');
  807. }
  808. .count {
  809. color: #ff7700;
  810. }
  811. }
  812. .stat-item.normal-risk {
  813. background-image: url('/@/assets/images/overHaul/leafPage/normal-risk-bg.png');
  814. .icon-top {
  815. background-image: url('/@/assets/images/overHaul/leafPage/normal-risk-icon1.svg');
  816. }
  817. .icon-bottom {
  818. background-image: url('/@/assets/images/overHaul/leafPage/normal-risk-icon2.png');
  819. }
  820. .count {
  821. color: #d5c702;
  822. }
  823. }
  824. .stat-item.low-risk {
  825. background-image: url('/@/assets/images/overHaul/leafPage/low-risk-bg.png');
  826. .icon-top {
  827. background-image: url('/@/assets/images/overHaul/leafPage/low-risk-icon1.svg');
  828. }
  829. .icon-bottom {
  830. background-image: url('/@/assets/images/overHaul/leafPage/low-risk-icon2.png');
  831. }
  832. .count {
  833. color: #0070ff;
  834. }
  835. }
  836. .warning-details {
  837. background-color: #ffffff;
  838. margin-bottom: 20px;
  839. padding: 10px 12px;
  840. border-radius: 4px;
  841. }
  842. .details-header {
  843. display: flex;
  844. justify-content: space-between;
  845. align-items: center;
  846. margin-bottom: 16px;
  847. }
  848. .details-content {
  849. max-height: 350px;
  850. overflow-y: auto;
  851. scroll-snap-type: y mandatory;
  852. }
  853. .warning-item {
  854. margin-bottom: 16px;
  855. background-color: rgba(218, 234, 251, 0.5);
  856. border-radius: 8px;
  857. padding: 16px 10px;
  858. scroll-snap-align: start;
  859. }
  860. .item-header {
  861. display: flex;
  862. justify-content: space-between;
  863. align-items: center;
  864. padding-bottom: 10px;
  865. margin-bottom: 14px;
  866. border-bottom: 1px solid rgba(43, 111, 240, 0.2);
  867. .icon {
  868. width: 16px;
  869. height: 16px;
  870. background-image: url('/@/assets/images/overHaul/leafPage/info-card-icon1.svg');
  871. background-repeat: no-repeat;
  872. background-size: 100%;
  873. }
  874. .name {
  875. font-size: 16px;
  876. font-weight: bold;
  877. color: #333;
  878. margin: 0 15px 0px 5px;
  879. }
  880. .risk-level {
  881. width: fit-content;
  882. height: 24px;
  883. padding: 2px 8px;
  884. border-radius: 15px;
  885. font-size: 12px;
  886. background-color: #ff6b6b;
  887. color: white;
  888. }
  889. .header-left {
  890. display: flex;
  891. align-items: center;
  892. }
  893. .expand-btn {
  894. color: #2b6ff0;
  895. display: flex;
  896. align-items: center;
  897. gap: 4px;
  898. padding: 6px 12px;
  899. border: 1px solid #2b6ff0;
  900. border-radius: 4px;
  901. background-color: #e9f0fe;
  902. cursor: pointer;
  903. font-size: 12px;
  904. }
  905. .expand-btn:hover {
  906. color: #1a5cc8;
  907. }
  908. .arrow-icon-wrapper {
  909. width: 16px;
  910. height: 16px;
  911. position: relative;
  912. display: inline-block;
  913. }
  914. .arrow-icon {
  915. width: 16px;
  916. height: 16px;
  917. position: absolute;
  918. top: 50%;
  919. left: 50%;
  920. transform: translate(-50%, -50%);
  921. background-image: url('/@/assets/images/overHaul/leafPage/icon-down1.png');
  922. background-repeat: no-repeat;
  923. background-size: 100% 100%;
  924. transition: transform 0.3s ease;
  925. }
  926. .arrow-icon.rotate {
  927. transform: translate(-50%, -50%) rotate(180deg);
  928. }
  929. }
  930. .data-row {
  931. display: grid;
  932. grid-template-columns: repeat(7, 1fr);
  933. gap: 12px;
  934. }
  935. .data-col {
  936. width: 100%;
  937. height: 80px;
  938. position: relative;
  939. display: flex;
  940. flex-direction: column;
  941. align-items: start;
  942. /* justify-content: center; */
  943. padding: 0 16px 0 30px;
  944. background-image: url('/@/assets/images/overHaul/leafPage/warning-item-bg.png');
  945. background-repeat: no-repeat;
  946. background-size: 100% 100%;
  947. border-radius: 4px;
  948. font-size: 12px;
  949. .col-title {
  950. color: #000;
  951. height: 24px;
  952. line-height: 30px;
  953. font-size: 14px;
  954. margin-bottom: 5px;
  955. }
  956. .col-value {
  957. color: #000;
  958. font-size: 14px;
  959. .status-dot {
  960. width: 8px;
  961. height: 8px;
  962. border-radius: 50%;
  963. display: inline-block;
  964. }
  965. }
  966. .col-icon {
  967. position: absolute;
  968. top: 8px;
  969. left: 10px;
  970. width: 18px;
  971. height: 18px;
  972. background-image: url('/@/assets/images/overHaul/leafPage/info-card-icon.svg');
  973. background-repeat: no-repeat;
  974. background-size: 100%;
  975. }
  976. }
  977. .monitoring-indicators {
  978. display: flex;
  979. gap: 20px;
  980. margin-top: 20px;
  981. }
  982. .indicator-group {
  983. display: flex;
  984. background-color: white;
  985. border-radius: 8px;
  986. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  987. min-width: 0;
  988. flex: 1;
  989. }
  990. .indicator-group:nth-child(1) {
  991. flex: 4;
  992. }
  993. .indicator-group:nth-child(2) {
  994. flex: 4;
  995. }
  996. .indicator-group:nth-child(3) {
  997. flex: 2;
  998. }
  999. .group-title {
  1000. width: 120px;
  1001. display: flex;
  1002. flex-direction: column;
  1003. align-items: center;
  1004. text-align: center;
  1005. justify-content: center;
  1006. font-size: 14px;
  1007. font-weight: bold;
  1008. color: #000;
  1009. background-color: rgba(192, 222, 255, 0.6);
  1010. .title-icon-top {
  1011. width: 30px;
  1012. height: 30px;
  1013. background-image: url('/@/assets/images/overHaul/leafPage/icon-top1.svg');
  1014. background-repeat: no-repeat;
  1015. background-size: 100%;
  1016. margin-bottom: -5px;
  1017. }
  1018. .icon-outer {
  1019. background-image: url('/@/assets/images/overHaul/leafPage/icon-top2.svg');
  1020. }
  1021. .icon-result {
  1022. background-image: url('/@/assets/images/overHaul/leafPage/icon-top3.svg');
  1023. }
  1024. .title-icon-bottom {
  1025. width: 50px;
  1026. height: 23px;
  1027. background-image: url('/@/assets/images/overHaul/leafPage/icon-bottom.png');
  1028. background-repeat: no-repeat;
  1029. background-size: 100%;
  1030. }
  1031. }
  1032. .indicator-area {
  1033. width: calc(100% - 120px);
  1034. display: grid;
  1035. grid-template-columns: repeat(3, 1fr);
  1036. gap: 10px;
  1037. padding: 10px 16px;
  1038. }
  1039. .indicator-result {
  1040. grid-template-columns: repeat(1, 1fr);
  1041. }
  1042. .indicator-item {
  1043. height: 30px;
  1044. line-height: 30px;
  1045. display: flex;
  1046. justify-content: space-between;
  1047. background: linear-gradient(90deg, rgba(192, 222, 255, 0.5) 0%, rgba(192, 222, 255, 0.2) 100%);
  1048. border-radius: 4px;
  1049. margin-bottom: 8px;
  1050. padding: 0 4px;
  1051. .value {
  1052. overflow: hidden;
  1053. text-overflow: ellipsis;
  1054. white-space: nowrap;
  1055. min-width: 0;
  1056. flex: 1;
  1057. text-align: right;
  1058. }
  1059. }
  1060. /* 预警结果只有两行,用来占位的 item */
  1061. .placeholder-item {
  1062. visibility: hidden;
  1063. height: 30px;
  1064. margin-bottom: 8px;
  1065. background: transparent;
  1066. pointer-events: none;
  1067. }
  1068. .indicator-result .indicator-item:last-of-type {
  1069. margin-bottom: 8px;
  1070. }
  1071. .indicator-item:last-child {
  1072. border-bottom: none;
  1073. }
  1074. .abnormal-alarm {
  1075. background-color: #fff;
  1076. margin-bottom: 20px;
  1077. padding: 10px 12px;
  1078. border-radius: 4px;
  1079. }
  1080. .alarm-header {
  1081. display: flex;
  1082. justify-content: space-between;
  1083. align-items: center;
  1084. margin-bottom: 16px;
  1085. }
  1086. .alarm-content {
  1087. display: flex;
  1088. flex-wrap: wrap;
  1089. gap: 16px;
  1090. }
  1091. .alarm-item {
  1092. flex: 1;
  1093. min-width: 120px;
  1094. height: 70px;
  1095. position: relative;
  1096. padding: 8px 10px 8px 40px;
  1097. background-color: rgba(215, 233, 253, 0.9);
  1098. border-radius: 4px;
  1099. text-align: left;
  1100. }
  1101. .alarm-name {
  1102. font-size: 16px;
  1103. color: #000;
  1104. font-weight: bold;
  1105. margin-bottom: 8px;
  1106. }
  1107. .alarm-count {
  1108. font-size: 14px;
  1109. color: #333;
  1110. font-weight: bold;
  1111. }
  1112. .alarm-border {
  1113. width: 20px;
  1114. height: 50px;
  1115. position: absolute;
  1116. top: 14px;
  1117. left: 15px;
  1118. .alarm-border-icon {
  1119. width: 16px;
  1120. height: 16px;
  1121. background-image: url('/@/assets/images/overHaul/leafPage/info-card-icon.svg');
  1122. background-repeat: no-repeat;
  1123. background-size: 100%;
  1124. }
  1125. .alarm-border-line {
  1126. width: 1px;
  1127. height: 30px;
  1128. border-left: 2px dotted #397af3;
  1129. margin-left: 7px;
  1130. }
  1131. }
  1132. .alarm-border-bottom {
  1133. width: 30px;
  1134. height: 30px;
  1135. background-image: url('/@/assets/images/overHaul/leafPage/alarm-card-icon.png');
  1136. background-repeat: no-repeat;
  1137. background-size: 100%;
  1138. position: absolute;
  1139. right: 0;
  1140. bottom: 0;
  1141. }
  1142. .historical-data {
  1143. background-color: #fff;
  1144. margin-bottom: 20px;
  1145. padding: 10px 12px;
  1146. border-radius: 4px;
  1147. }
  1148. .chart-header {
  1149. display: flex;
  1150. justify-content: space-between;
  1151. align-items: center;
  1152. }
  1153. .chart-content {
  1154. padding: 0;
  1155. height: 600px;
  1156. overflow: hidden;
  1157. position: relative;
  1158. }
  1159. /* 深度选择器:重置 ConnectAnalysis 的样式以适配嵌入环境 */
  1160. :deep(.connectAnalysis) {
  1161. margin: 0 !important;
  1162. width: 100% !important;
  1163. height: 100% !important;
  1164. border: none !important;
  1165. box-shadow: none !important;
  1166. padding: 0 !important;
  1167. display: flex;
  1168. flex-direction: column;
  1169. }
  1170. :deep(.filter-area) {
  1171. width: 100%;
  1172. }
  1173. :deep(.echart-box) {
  1174. /* 让图表区域占据剩余所有高度 */
  1175. flex: 1;
  1176. height: auto !important;
  1177. display: flex;
  1178. flex-direction: column;
  1179. padding: 10px;
  1180. }
  1181. :deep(.echart-content) {
  1182. flex: 1;
  1183. height: auto !important;
  1184. min-height: 300px;
  1185. }
  1186. </style>