internetDetail.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <u-popup
  3. :show="visible"
  4. mode="bottom"
  5. :round="20"
  6. :closeable="true"
  7. :close-on-click-overlay="false"
  8. @close="closePopup"
  9. >
  10. <view class="internet-detail-container">
  11. <!-- 标题栏 -->
  12. <view class="popup-header">
  13. <text class="popup-title">设备详情</text>
  14. </view>
  15. <scroll-view scroll-y class="popup-body">
  16. <!-- 仪表盘区域 -->
  17. <view class="gauge-section">
  18. <view class="section-title">实时数据</view>
  19. <view class="gauge-grid">
  20. <view
  21. class="gauge-item"
  22. v-for="(item, index) in getGaugeData"
  23. :key="index"
  24. >
  25. <view
  26. class="gauge-card"
  27. v-for="(listItem, j) in item.list"
  28. :key="j"
  29. >
  30. <GaugeChart
  31. height="250px"
  32. :value="listItem.value"
  33. :min="listItem.min"
  34. :max="listItem.max"
  35. :title="listItem.name"
  36. :unit="listItem.unit"
  37. :colors="['#5470C6', '#91CC75', '#FAC858', '#EE6666']"
  38. :options="{ pointer: { itemStyle: { color: '#5470C6' } } }"
  39. :ref="'gaugeChartRef'"
  40. />
  41. </view>
  42. </view>
  43. </view>
  44. </view>
  45. <!-- 实时数据列表 -->
  46. <view class="data-section">
  47. <view class="section-title">运行状态</view>
  48. <view class="data-grid">
  49. <view class="data-item" v-for="(obj, key) in realData" :key="key">
  50. <view class="data-label">{{ obj.name }}:</view>
  51. <view
  52. class="data-value"
  53. v-if="
  54. obj.dataType &&
  55. (obj.dataType.type == 'enum' || obj.dataType.type == 'bool')
  56. "
  57. >
  58. {{ obj.specs[obj.value] }}
  59. </view>
  60. <view class="data-value" v-else>
  61. {{ obj.value }}
  62. <text v-if="obj.unit" class="data-unit">{{ obj.unit }}</text>
  63. </view>
  64. </view>
  65. </view>
  66. </view>
  67. <!-- 历史折线图 -->
  68. <view class="history-section">
  69. <view class="section-title">历史趋势</view>
  70. <view
  71. class="chart-card"
  72. v-for="(item, index) in historyData"
  73. :key="index"
  74. >
  75. <view class="chart-header">
  76. <text class="chart-name">{{ item.name }}</text>
  77. <view class="chart-stats">
  78. <text class="stat-item"
  79. >最高值<text class="stat-value"
  80. >{{ item.maxValue }} {{ item.unit }}</text
  81. ></text
  82. >
  83. <text class="stat-item"
  84. >最低值<text class="stat-value"
  85. >{{ item.minValue }} {{ item.unit }}</text
  86. ></text
  87. >
  88. <text class="stat-item"
  89. >平均值<text class="stat-value avg"
  90. >{{ item.avgValue }} {{ item.unit }}</text
  91. ></text
  92. >
  93. </view>
  94. </view>
  95. <view class="chart-tools">
  96. <dy-date-time-picker
  97. v-model="item.chartTime"
  98. :typeEnabled="{
  99. date: item.timeType == 'date',
  100. month: item.timeType == 'month',
  101. year: item.timeType == 'year',
  102. week: false,
  103. }"
  104. placeholder=""
  105. />
  106. <view class="time-type">
  107. <text
  108. :class="{ active: item.timeType === 'date' }"
  109. @click="dateClick(item, 'date')"
  110. >日</text
  111. >
  112. <text
  113. :class="{ active: item.timeType === 'month' }"
  114. @click="dateClick(item, 'month')"
  115. >月</text
  116. >
  117. <text
  118. :class="{ active: item.timeType === 'year' }"
  119. @click="dateClick(item, 'year')"
  120. >年</text
  121. >
  122. </view>
  123. <u-button
  124. type="primary"
  125. size="mini"
  126. @click="refreshSingleChart(item)"
  127. >查询</u-button
  128. >
  129. </view>
  130. <LineChart
  131. :data="item.trendData"
  132. title=""
  133. xAxisName="时间"
  134. :yAxisName="item.unit"
  135. height="300px"
  136. :options="{}"
  137. />
  138. </view>
  139. </view>
  140. </scroll-view>
  141. </view>
  142. </u-popup>
  143. </template>
  144. <script>
  145. import GaugeChart from "./GaugeChart.vue";
  146. import LineChart from "./LineChart.vue";
  147. import {
  148. getRealData,
  149. getHistoryData,
  150. getPhysicalModel,
  151. } from "@/api/ledgerAssets/equipment.js";
  152. import { getMonday } from "@/utils/utils.js";
  153. import { getAssetInfo } from "@/api/repair";
  154. import dayjs from "dayjs";
  155. export default {
  156. name: "InternetDetail",
  157. components: {
  158. GaugeChart,
  159. LineChart,
  160. },
  161. props: {},
  162. data() {
  163. return {
  164. visible: false,
  165. loading: false,
  166. realData: [],
  167. iotDashboardPoint: [],
  168. info: null,
  169. dict: {
  170. chartTime: {
  171. date: 3,
  172. month: 2,
  173. year: 1,
  174. },
  175. },
  176. gaugeData: [],
  177. historyData: [],
  178. id: null,
  179. };
  180. },
  181. computed: {
  182. getGaugeData() {
  183. let list = [];
  184. let gaugeData = this.iotDashboardPoint.length
  185. ? this.gaugeData.filter((item) =>
  186. this.iotDashboardPoint.find(
  187. (Point) => Point.identifier == item.identifier && Point.checked,
  188. ),
  189. )
  190. : this.gaugeData.filter((item, index) => index < 6);
  191. const perRow = 1;
  192. for (let i = 0; i < gaugeData.length; i += perRow) {
  193. list.push({
  194. list: gaugeData.slice(i, i + perRow),
  195. });
  196. }
  197. // this.$nextTick(() => {
  198. // console.log(this.$refs)
  199. // return
  200. // gaugeData.forEach((item, index) => {
  201. // if (this.$refs.gaugeChartRef[index]) {
  202. // this.$refs.gaugeChartRef[index].handleResize();
  203. // }
  204. // });
  205. // });
  206. return list;
  207. },
  208. },
  209. methods: {
  210. closePopup() {
  211. this.visible = false;
  212. },
  213. async open(id) {
  214. this.id = id;
  215. const { iotDashboardPoint } = await getAssetInfo(id);
  216. this.iotDashboardPoint = iotDashboardPoint;
  217. this.info = await getPhysicalModel(id);
  218. await this.getRealData();
  219. this.visible = true;
  220. },
  221. async getRealData() {
  222. this.loading = true;
  223. try {
  224. const res = await getRealData(this.id);
  225. this.realData = res;
  226. const tempData = this.info.properties || [];
  227. this.realData.forEach((item) => {
  228. tempData?.forEach((property) => {
  229. if (property.identifier == item.identifier) {
  230. item.max = property.dataType.specs.max;
  231. item.min = property.dataType.specs.min;
  232. item.propertyUnit = property.dataType.specs.unit;
  233. item.unitName = property.dataType.specs.unitName;
  234. item.specs = property.dataType.specs;
  235. item.dataType = property.dataType;
  236. item.propertyName = property.name;
  237. item.accessMode = property.accessMode;
  238. item.saveHistory = property.saveHistory;
  239. item.required = property.required;
  240. item.chartTime = dayjs(new Date()).format("YYYY-MM");
  241. // item.chartTime =dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss') ;
  242. item.timeType = "month";
  243. }
  244. });
  245. });
  246. const gaugeData = this.realData.filter(
  247. (item) => item.max !== undefined && item.min !== undefined,
  248. );
  249. this.gaugeData = gaugeData;
  250. const historyPromises = this.gaugeData.map((item) =>
  251. item.saveHistory ? this.getDefaultHistoryData(item) : null,
  252. );
  253. this.historyData = await Promise.all(historyPromises);
  254. } catch (error) {
  255. console.error("获取数据失败:", error);
  256. } finally {
  257. this.loading = false;
  258. }
  259. },
  260. async getDefaultHistoryData(item) {
  261. const chartTime = item.chartTime;
  262. const timeType = item.timeType || "month";
  263. let time = this.getTimeList(chartTime, timeType);
  264. // if (!time || !time.startTime || !time.endTime) {
  265. // const defaultDate = new Date().toISOString().split('T')[0];
  266. // time = {
  267. // startTime: defaultDate + ' 00:00:00',
  268. // endTime: defaultDate + ' 23:59:59'
  269. // };
  270. // }
  271. return {
  272. identifier: item.identifier || "",
  273. name: item.name || "",
  274. unit: item.unit || "",
  275. unitName: item.unitName || "",
  276. chartTime,
  277. timeType,
  278. trendData: {
  279. xAxis: [],
  280. series: [
  281. {
  282. name: "",
  283. data: [],
  284. color: "#5470C6",
  285. },
  286. ],
  287. },
  288. timeHistoryList: [],
  289. };
  290. },
  291. async getHistoryDatas(item) {
  292. try {
  293. const chartTime = item.chartTime;
  294. const timeType = item.timeType || "month";
  295. let time = this.getTimeList(chartTime, timeType);
  296. // if (!time || !time.startTime || !time.endTime) {
  297. // const defaultDate = new Date().toISOString().split('T')[0];
  298. // time = {
  299. // startTime: defaultDate + ' 00:00:00',
  300. // endTime: defaultDate + ' 23:59:59'
  301. // };
  302. // }
  303. // const apiTimeType = this.dict?.chartTime?.[timeType] || 'month';
  304. const res = await getHistoryData({
  305. startTime: time.startTime,
  306. endTime: time.endTime,
  307. property: item.identifier,
  308. substanceId: this.id,
  309. timeType: this.dict.chartTime[timeType],
  310. });
  311. const response = res || {};
  312. const trendData = {
  313. xAxis: [],
  314. series: [
  315. {
  316. name: response.name || item.name,
  317. data: [],
  318. color: "#5470C6",
  319. },
  320. ],
  321. };
  322. const timeHistoryList = Array.isArray(response.timeHistoryList)
  323. ? response.timeHistoryList
  324. : [];
  325. timeHistoryList.forEach((i) => {
  326. if (i && i.time !== undefined && i.value !== undefined) {
  327. trendData.xAxis.push(i.time);
  328. trendData.series[0].data.push(i.value);
  329. }
  330. });
  331. return {
  332. ...response,
  333. identifier: item.identifier || "",
  334. name: item.name || "",
  335. unit: item.unit || "",
  336. unitName: item.unitName || "",
  337. chartTime,
  338. timeType,
  339. trendData,
  340. timeHistoryList,
  341. };
  342. } catch (error) {
  343. console.error(
  344. `获取属性 ${item.name || item.identifier} 的历史数据失败:`,
  345. error,
  346. );
  347. return {
  348. identifier: item.identifier || "",
  349. name: item.name || "",
  350. unit: item.unit || "",
  351. unitName: item.unitName || "",
  352. chartTime: item.chartTime || new Date(),
  353. timeType: item.timeType || "month",
  354. trendData: {
  355. xAxis: [],
  356. series: [
  357. {
  358. name: item.name || "",
  359. data: [],
  360. color: "#5470C6",
  361. },
  362. ],
  363. },
  364. timeHistoryList: [],
  365. };
  366. }
  367. },
  368. async refreshSingleChart(item) {
  369. try {
  370. const updatedData = await this.getHistoryDatas(item);
  371. const index = this.historyData.findIndex(
  372. (d) => d.identifier === item.identifier,
  373. );
  374. if (index !== -1) {
  375. this.$set(this.historyData, index, updatedData);
  376. }
  377. } catch (error) {
  378. console.error(
  379. `刷新图表 ${item.name || item.identifier} 数据失败:`,
  380. error,
  381. );
  382. }
  383. },
  384. dateClick(item, dateType) {
  385. switch (dateType) {
  386. case "date":
  387. item.chartTime = dayjs(new Date()).format("YYYY-MM-DD");
  388. break;
  389. case "month":
  390. item.chartTime = dayjs(new Date()).format("YYYY-MM");
  391. break;
  392. case "year":
  393. item.chartTime = dayjs(new Date()).format("YYYY");
  394. break;
  395. default:
  396. break;
  397. }
  398. item.timeType = dateType;
  399. },
  400. getTimeList(date, timeType) {
  401. console.log(date, "date");
  402. // const fnNum = (num) => num < 10 ? '0' + num : num;
  403. let startTime, endTime;
  404. // const ndate = date ? new Date(date) : new Date();
  405. // const year = ndate.getFullYear();
  406. // const month = ndate.getMonth() + 1;
  407. // const day = ndate.getDate();
  408. switch (timeType) {
  409. case "date":
  410. startTime = (date.date || date) + " 00:00:00";
  411. endTime = (date.date || date) + " 23:59:59";
  412. break;
  413. case "month":
  414. const mon = getMonday(date.month || date);
  415. startTime = mon[0] + " 00:00:00";
  416. endTime = mon[1] + " 23:59:59";
  417. break;
  418. case "year":
  419. startTime = (date.year || date) + "-01-01 00:00:00";
  420. endTime = (date.year || date) + "-12-31 23:59:59";
  421. break;
  422. default:
  423. break;
  424. }
  425. return {
  426. startTime,
  427. endTime,
  428. };
  429. },
  430. },
  431. };
  432. </script>
  433. <style scoped lang="scss">
  434. .internet-detail-container {
  435. // display: flex;
  436. // flex-direction: column;
  437. height: calc(100vh - 100rpx);
  438. background: #f5f7fa;
  439. }
  440. .popup-header {
  441. display: flex;
  442. justify-content: center;
  443. align-items: center;
  444. padding: 30rpx 0;
  445. background: #fff;
  446. border-bottom: 1rpx solid #eee;
  447. .popup-title {
  448. font-size: 36rpx;
  449. font-weight: bold;
  450. color: #333;
  451. }
  452. }
  453. .popup-body {
  454. flex: 1;
  455. padding: 20rpx;
  456. height: 100%;
  457. }
  458. .section-title {
  459. font-size: 32rpx;
  460. font-weight: bold;
  461. color: #303133;
  462. margin-bottom: 20rpx;
  463. padding-left: 20rpx;
  464. border-left: 8rpx solid #1890ff;
  465. }
  466. .gauge-section {
  467. margin-bottom: 30rpx;
  468. }
  469. .gauge-grid {
  470. display: flex;
  471. flex-wrap: wrap;
  472. gap: 20rpx;
  473. }
  474. .gauge-item {
  475. display: flex;
  476. gap: 20rpx;
  477. width: 100%;
  478. }
  479. .gauge-card {
  480. flex: 1;
  481. min-width: 200rpx;
  482. background: #fff;
  483. border-radius: 20rpx;
  484. padding: 20rpx;
  485. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
  486. }
  487. .data-section {
  488. margin-bottom: 30rpx;
  489. }
  490. .data-grid {
  491. display: grid;
  492. grid-template-columns: 1fr 1fr;
  493. gap: 20rpx;
  494. }
  495. .data-item {
  496. display: flex;
  497. align-items: center;
  498. background: #fff;
  499. padding: 24rpx;
  500. border-radius: 16rpx;
  501. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
  502. }
  503. .data-label {
  504. font-size: 28rpx;
  505. color: #666;
  506. margin-right: 10rpx;
  507. }
  508. .data-value {
  509. font-size: 30rpx;
  510. color: #333;
  511. font-weight: bold;
  512. }
  513. .data-unit {
  514. font-size: 24rpx;
  515. color: #999;
  516. }
  517. .history-section {
  518. margin-bottom: 30rpx;
  519. }
  520. .chart-card {
  521. background: #fff;
  522. border-radius: 20rpx;
  523. padding: 24rpx;
  524. margin-bottom: 24rpx;
  525. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
  526. }
  527. .chart-header {
  528. margin-bottom: 20rpx;
  529. }
  530. .chart-name {
  531. font-size: 30rpx;
  532. font-weight: bold;
  533. color: #333;
  534. display: block;
  535. margin-bottom: 16rpx;
  536. }
  537. .chart-stats {
  538. display: flex;
  539. flex-wrap: wrap;
  540. gap: 20rpx;
  541. }
  542. .stat-item {
  543. font-size: 26rpx;
  544. color: #666;
  545. }
  546. .stat-value {
  547. font-weight: bold;
  548. color: #ff3e52;
  549. margin-left: 8rpx;
  550. &.avg {
  551. color: #0840d5;
  552. }
  553. }
  554. .chart-tools {
  555. display: flex;
  556. align-items: center;
  557. flex-wrap: wrap;
  558. gap: 16rpx;
  559. margin-bottom: 20rpx;
  560. }
  561. .time-type {
  562. display: flex;
  563. gap: 16rpx;
  564. text {
  565. font-size: 28rpx;
  566. color: #666;
  567. padding: 8rpx 20rpx;
  568. border-radius: 8rpx;
  569. background: #f0f0f0;
  570. &.active {
  571. background: #1890ff;
  572. color: #fff;
  573. }
  574. }
  575. }
  576. </style>