HistoricalDetailsModal.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <template>
  2. <Modal v-model:open="visible" title="密闭监测详情" width="1200px" @ok="handleOk" @cancel="handleCancel" prefixCls="custom-modal">
  3. <div class="filter-area">
  4. <!-- 时间选择 -->
  5. <div class="filter-section">
  6. <span class="filter-label">时间选择:</span>
  7. <RangePicker
  8. v-model="dateRange"
  9. format="YYYY-MM-DD HH:mm:ss"
  10. :placeholder="['开始时间', '结束时间']"
  11. style="width: 320px"
  12. :show-time="{ format: 'HH:mm:ss' }"
  13. />
  14. </div>
  15. <!-- 参数选择 -->
  16. <div class="filter-section param-section">
  17. <span class="filter-label">参数选择:</span>
  18. <div class="param-selector">
  19. <Input v-model="selectedParamsText" placeholder="请选择监测参数" readonly style="width: 300px" />
  20. <Button type="primary" @click="showTree = !showTree">+</Button>
  21. <!-- 树形选择器 -->
  22. <div class="tree-popup" v-if="showTree">
  23. <BasicTree :treeData="treeData" :checkable="true" defaultExpandAll @check="handleTreeCheck" :checkedKeys="checkedTreeKeys" />
  24. </div>
  25. </div>
  26. </div>
  27. <!-- 生成按钮 -->
  28. <div class="filter-section">
  29. <Button type="primary" @click="generateChart">生成</Button>
  30. </div>
  31. </div>
  32. <!-- 动态图表区域-->
  33. <div class="chart-area">
  34. <div class="chart-item" style="flex: 1 1 100%">
  35. <div class="chart-placeholder">
  36. <template v-if="generatedChartData.length">
  37. <CustomChart :chart-data="generatedChartData" :chart-config="generatedChartConfig" style="height: 100%; width: 100%" />
  38. </template>
  39. <template v-else-if="isChartGenerated">
  40. <div class="empty-chart">暂无匹配数据,请调整筛选条件</div>
  41. </template>
  42. <template v-else>
  43. <div class="empty-chart">请选择时间范围和参数,点击"生成"查看数据</div>
  44. </template>
  45. </div>
  46. </div>
  47. </div>
  48. </Modal>
  49. </template>
  50. <script lang="ts" setup>
  51. import { ref, computed } from 'vue';
  52. import { Modal, DatePicker, Button, message, Input } from 'ant-design-vue';
  53. import { BasicTree } from '/@/components/Tree/index';
  54. import CustomChart from '/@/components/Configurable/detail/CustomChart.vue';
  55. import { treeData } from '../monitor.data'; // 引入模拟数据
  56. import dayjs from 'dayjs';
  57. // import isBetween from 'dayjs/plugin/isBetween'; // 引入 isBetween 插件
  58. // // 关键:使用 dayjs 插件
  59. // dayjs.extend(isBetween);
  60. // 组件注册
  61. const RangePicker = DatePicker.RangePicker;
  62. // 弹框控制
  63. const visible = ref(false);
  64. const dataRef = ref<any>();
  65. // 外部调用显示弹框
  66. const showModal = (record: any) => {
  67. visible.value = true;
  68. dataRef.value = record;
  69. };
  70. const hideModal = () => (visible.value = false);
  71. const handleOk = () => hideModal();
  72. const handleCancel = () => hideModal();
  73. // 筛选相关响应式数据
  74. const dateRange = ref([dayjs().subtract(1, 'day').toDate(), dayjs().toDate()]); // 默认时间范围(近1天)
  75. const selectedParams = ref([]); // 选中的参数(实际用于图表)
  76. const selectedParamsText = ref(''); // 参数选择框显示文本
  77. const showTree = ref(false); // 控制树形选择器显示/隐藏
  78. const checkedTreeKeys = ref([]); // 树形选中的key
  79. const generatedChartData = ref([]); // 生成的图表数据
  80. const generatedChartConfig = ref({}); // 生成的图表配置
  81. const isChartGenerated = ref(false); // 是否已点击生成
  82. // Tree Key 与参数名映射(关键:关联树形节点和实际参数)
  83. const treeKeyToParamMap = computed(() => ({
  84. '0-0-0': 'CO',
  85. '0-0-1': 'CH4',
  86. '0-0-2': 'C2H4',
  87. '0-0-3': 'C2H2',
  88. '0-0-4': 'CO2',
  89. '0-0-5': 'O2',
  90. '1-1-0': 'innerPressure',
  91. '1-1-1': 'outerPressure',
  92. '1-1-2': 'pressureDiff',
  93. '2-2': 'temperature',
  94. }));
  95. // 参数名反向映射到 Tree Key
  96. const paramToTreeKeyMap = computed(() => {
  97. return Object.fromEntries(Object.entries(treeKeyToParamMap.value).map(([key, val]) => [val, key]));
  98. });
  99. // 树形选择事件处理
  100. const handleTreeCheck = (checkedKeys) => {
  101. checkedTreeKeys.value = checkedKeys;
  102. // 转换为实际参数名
  103. const params = checkedKeys.map((key) => treeKeyToParamMap.value[key]).filter((param) => param); // 过滤无效参数
  104. selectedParams.value = params;
  105. // 更新输入框显示文本
  106. const paramLabels = params.map((param) => paramLabelMap.value[param]);
  107. selectedParamsText.value = paramLabels.join('、');
  108. };
  109. // 参数颜色映射
  110. const paramColorMap = computed(() => ({
  111. CO: '#f5222d', // 红色
  112. CH4: '#1890ff', // 蓝色
  113. C2H4: '#faad14', // 橙色
  114. C2H2: '#52c41a', // 绿色
  115. CO2: '#722ed1', // 紫色
  116. O2: '#13c2c2', // 青色
  117. innerPressure: '#ff4d4f', // 浅红
  118. outerPressure: '#40a9ff', // 浅蓝
  119. pressureDiff: '#fa8c16', // 浅橙
  120. temperature: '#9254de', // 浅紫
  121. }));
  122. // 参数标签映射(图表系列名称)
  123. const paramLabelMap = computed(() => ({
  124. CO: 'CO浓度(ppm)',
  125. CH4: 'CH4浓度(%)',
  126. C2H4: 'C2H4浓度(ppm)',
  127. C2H2: 'C2H2浓度(ppm)',
  128. CO2: 'CO2浓度(%)',
  129. O2: 'O2浓度(%)',
  130. innerPressure: '内压力(kPa)',
  131. outerPressure: '外压力(kPa)',
  132. pressureDiff: '压差(kPa)',
  133. temperature: '温度(℃)',
  134. }));
  135. // 生成折线图核心逻辑
  136. const generateChart = () => {
  137. // 校验筛选条件
  138. if (!dateRange.value[0] || !dateRange.value[1]) {
  139. message.warning('请选择完整的时间范围');
  140. return;
  141. }
  142. if (selectedParams.value.length === 0) {
  143. message.warning('请至少选择一个监测参数');
  144. return;
  145. }
  146. isChartGenerated.value = true;
  147. const start = dayjs(dateRange.value[0]); // 转为 dayjs 实例
  148. const end = dayjs(dateRange.value[1]); // 转为 dayjs 实例
  149. // 1. 筛选时间范围内的模拟数据(修复核心漏洞)
  150. const filteredData = [];
  151. // 2. 构建图表数据结构(适配 CustomChart 的 line 类型)
  152. const timeMap = new Map();
  153. // 修复变量名:filteredRawData → filteredData(之前未定义)
  154. filteredData.forEach((item) => {
  155. const timeStr = dayjs(item.time).format('YYYY-MM-DD HH:mm:ss');
  156. if (!timeMap.has(timeStr)) {
  157. timeMap.set(timeStr, { time: timeStr });
  158. }
  159. // 只保留选中的参数数据
  160. selectedParams.value.forEach((param) => {
  161. if (item[param] !== undefined) {
  162. timeMap.get(timeStr)[param] = item[param];
  163. }
  164. });
  165. });
  166. // 转换为数组并按时间排序
  167. const chartData = Array.from(timeMap.values()).sort((a, b) => {
  168. return dayjs(a.time).valueOf() - dayjs(b.time).valueOf();
  169. });
  170. generatedChartData.value = chartData;
  171. // 3. 构建图表配置(折线图类型,完善适配逻辑)
  172. generatedChartConfig.value = {
  173. type: 'line', // 折线图
  174. clear: true, // 每次生成清空之前的图表
  175. legend: { show: true, top: 10, right: 10 },
  176. xAxis: [
  177. {
  178. type: 'category',
  179. axisLabel: {
  180. rotate: 30,
  181. formatter: (value) => dayjs(value).format('HH:mm:ss'),
  182. interval: Math.max(1, Math.floor(chartData.length / 10)), // 控制x轴标签密度
  183. },
  184. },
  185. ],
  186. yAxis: selectedParams.value.map((param) => ({
  187. type: 'value',
  188. name: paramLabelMap.value[param].split('(')[1].replace(')', ''), // 显示单位
  189. nameTextStyle: { color: paramColorMap.value[param] },
  190. axisLine: { lineStyle: { color: paramColorMap.value[param] } },
  191. splitLine: { lineStyle: { opacity: 0.1 } },
  192. })),
  193. series: selectedParams.value.map((param, index) => ({
  194. name: paramLabelMap.value[param],
  195. type: 'line',
  196. readFrom: '', // 适配 CustomChart 的 baseSeries 读取逻辑
  197. label: paramLabelMap.value[param],
  198. xprop: 'time', // 对应图表数据的 x 字段(时间)
  199. yprop: param, // 对应图表数据的 y 字段(参数值)
  200. color: paramColorMap.value[param],
  201. smooth: true,
  202. symbol: 'circle',
  203. symbolSize: 4,
  204. yAxisIndex: index,
  205. })),
  206. tooltip: {
  207. trigger: 'axis',
  208. axisPointer: { type: 'cross' },
  209. formatter: (params) => {
  210. let tooltipHtml = `<div>${dayjs(params[0].axisValue).format('YYYY-MM-DD HH:mm:ss')}</div>`;
  211. params.forEach((param) => {
  212. tooltipHtml += `<div style="color: ${param.color}">${param.seriesName}: ${param.value[1]} ${param.seriesName.split('(')[1].replace(')', '')}</div>`;
  213. });
  214. return tooltipHtml;
  215. },
  216. },
  217. grid: { left: 60, top: 40, right: 60, bottom: 60 },
  218. };
  219. // 无数据提示
  220. if (chartData.length === 0) {
  221. message.info('当前筛选条件下无数据');
  222. }
  223. };
  224. // 暴露方法给父组件
  225. defineExpose({
  226. showModal,
  227. hideModal,
  228. });
  229. </script>
  230. <style scoped>
  231. .param-selector {
  232. display: flex;
  233. align-items: center;
  234. gap: 8px;
  235. position: relative;
  236. }
  237. .tree-popup {
  238. position: absolute;
  239. top: 100%;
  240. left: 0;
  241. margin-top: 8px;
  242. width: 300px;
  243. max-height: 300px;
  244. overflow-y: auto;
  245. background: #fff;
  246. border: 1px solid #e8e8e8;
  247. border-radius: 4px;
  248. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  249. z-index: 100;
  250. padding: 8px;
  251. }
  252. .filter-area {
  253. display: flex;
  254. flex-wrap: wrap;
  255. gap: 16px;
  256. margin-bottom: 20px;
  257. padding: 20px;
  258. border: 1px solid #f0f0f0;
  259. border-radius: 10px;
  260. background: #f8f9fc;
  261. align-items: center;
  262. }
  263. .filter-section {
  264. display: flex;
  265. align-items: center;
  266. gap: 8px;
  267. }
  268. .filter-label {
  269. color: #666;
  270. min-width: 80px;
  271. flex-shrink: 0;
  272. font-weight: 500;
  273. }
  274. .param-section {
  275. flex: 1;
  276. min-width: 300px;
  277. }
  278. .chart-area {
  279. display: flex;
  280. flex-wrap: wrap;
  281. gap: 16px;
  282. padding: 20px;
  283. border: 1px solid #f0f0f0;
  284. border-radius: 10px;
  285. background: #f8f9fc;
  286. }
  287. .chart-item {
  288. flex: 1;
  289. min-width: 200px;
  290. }
  291. .chart-title {
  292. font-size: 16px;
  293. font-weight: 500;
  294. margin-bottom: 12px;
  295. color: #333;
  296. display: flex;
  297. align-items: center;
  298. gap: 8px;
  299. }
  300. .chart-desc {
  301. font-size: 12px;
  302. color: #666;
  303. font-weight: normal;
  304. }
  305. .chart-placeholder {
  306. width: 100%;
  307. height: 300px;
  308. border-radius: 4px;
  309. overflow: hidden;
  310. background: #333;
  311. border: 1px solid #eee;
  312. }
  313. .empty-chart {
  314. width: 100%;
  315. height: 100%;
  316. display: flex;
  317. align-items: center;
  318. justify-content: center;
  319. color: #999;
  320. font-size: 14px;
  321. }
  322. @media (max-width: 1200px) {
  323. .param-section {
  324. min-width: 100%;
  325. margin-top: 8px;
  326. }
  327. .filter-area {
  328. gap: 12px;
  329. }
  330. }
  331. </style>