PkeyPoints.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. <!-- 关键监控点位管理 -->
  2. <template>
  3. <div class="app-container point-page">
  4. <div class="summary-grid">
  5. <el-card shadow="hover" class="summary-card primary">
  6. <div class="summary-label">点位总数</div>
  7. <div class="summary-value">{{ summary.totalPoints }}</div>
  8. <div class="summary-desc">平台已接入关键监控点位</div>
  9. </el-card>
  10. <el-card shadow="hover" class="summary-card success">
  11. <div class="summary-label">正常点位</div>
  12. <div class="summary-value">{{ summary.normalPoints }}</div>
  13. <div class="summary-desc">状态为正常或启用的点位</div>
  14. </el-card>
  15. <el-card shadow="hover" class="summary-card warning">
  16. <div class="summary-label">异常点位</div>
  17. <div class="summary-value">{{ summary.warningPoints }}</div>
  18. <div class="summary-desc">状态需关注的关键监控点</div>
  19. </el-card>
  20. <el-card shadow="hover" class="summary-card info">
  21. <div class="summary-label">已绑设备</div>
  22. <div class="summary-value">{{ summary.boundEquipments }}</div>
  23. <div class="summary-desc">监控点位已关联感知设备数量</div>
  24. </el-card>
  25. </div>
  26. <el-card shadow="never" class="query-card">
  27. <el-form ref="queryRef" :model="queryParams" :inline="true" v-show="showSearch">
  28. <el-form-item label="监测点编码" prop="pointCode">
  29. <el-input
  30. v-model="queryParams.pointCode"
  31. placeholder="请输入监测点编码"
  32. clearable
  33. style="width: 220px"
  34. @keyup.enter="handleQuery"
  35. />
  36. </el-form-item>
  37. <el-form-item label="监测点名称" prop="pointName">
  38. <el-input
  39. v-model="queryParams.pointName"
  40. placeholder="请输入监测点名称"
  41. clearable
  42. style="width: 220px"
  43. @keyup.enter="handleQuery"
  44. />
  45. </el-form-item>
  46. <el-form-item label="监测类型" prop="pointType">
  47. <el-select
  48. v-model="queryParams.pointType"
  49. placeholder="请选择监测类型"
  50. clearable
  51. filterable
  52. allow-create
  53. default-first-option
  54. style="width: 220px"
  55. >
  56. <el-option v-for="item in pointTypeOptions" :key="item" :label="item" :value="item" />
  57. </el-select>
  58. </el-form-item>
  59. <el-form-item label="状态" prop="status">
  60. <el-select
  61. v-model="queryParams.status"
  62. placeholder="请选择状态"
  63. clearable
  64. filterable
  65. allow-create
  66. default-first-option
  67. style="width: 180px"
  68. >
  69. <el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
  70. </el-select>
  71. </el-form-item>
  72. <el-form-item>
  73. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  74. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  75. </el-form-item>
  76. </el-form>
  77. </el-card>
  78. <el-row :gutter="16" class="main-row">
  79. <el-col :xs="24" :lg="10">
  80. <el-card shadow="never" class="list-card">
  81. <template #header>
  82. <div class="card-header-row">
  83. <div>
  84. <div class="card-title">点位清单</div>
  85. <div class="card-subtitle">关键监控点位的查询、维护与定位管理</div>
  86. </div>
  87. <div class="card-actions">
  88. <el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
  89. <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
  90. <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">删除</el-button>
  91. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
  92. </div>
  93. </div>
  94. </template>
  95. <el-table
  96. v-loading="loading"
  97. :data="pointList"
  98. highlight-current-row
  99. @selection-change="handleSelectionChange"
  100. @current-change="handleCurrentChange"
  101. @row-click="handleRowClick"
  102. >
  103. <el-table-column type="selection" width="55" align="center" />
  104. <el-table-column label="监测点编码" prop="pointCode" min-width="120" show-overflow-tooltip />
  105. <el-table-column label="监测点名称" prop="pointName" min-width="170" show-overflow-tooltip />
  106. <el-table-column label="监测类型" prop="pointType" min-width="110" />
  107. <el-table-column label="状态" width="90">
  108. <template #default="scope">
  109. <el-tag :type="getStatusTagType(scope.row.status)" size="small">
  110. {{ scope.row.status || "-" }}
  111. </el-tag>
  112. </template>
  113. </el-table-column>
  114. <el-table-column label="设施" min-width="140" show-overflow-tooltip>
  115. <template #default="scope">
  116. {{ networkNameMap[scope.row.networkId] || "-" }}
  117. </template>
  118. </el-table-column>
  119. <el-table-column label="操作" align="center" width="150">
  120. <template #default="scope">
  121. <el-button link type="primary" icon="Edit" @click.stop="handleUpdate(scope.row)">修改</el-button>
  122. </template>
  123. </el-table-column>
  124. </el-table>
  125. <pagination
  126. v-show="total > 0"
  127. :total="total"
  128. v-model:page="queryParams.pageNum"
  129. v-model:limit="queryParams.pageSize"
  130. @pagination="getList"
  131. />
  132. </el-card>
  133. </el-col>
  134. <el-col :xs="24" :lg="14">
  135. <el-card shadow="never" class="detail-card">
  136. <template #header>
  137. <div class="card-header-row">
  138. <div>
  139. <div class="card-title">点位详情与关联设备</div>
  140. <div class="card-subtitle">突出点位基础信息、所属设施和已绑定感知设备</div>
  141. </div>
  142. <div class="detail-top-actions" v-if="selectedPoint.pointInfo">
  143. <el-button link type="primary" icon="Connection" @click="openBindDialog(selectedPoint.pointInfo)">
  144. 设备关联
  145. </el-button>
  146. <el-button link type="primary" icon="Edit" @click="handleUpdate(selectedPoint.pointInfo)">编辑</el-button>
  147. </div>
  148. </div>
  149. </template>
  150. <el-empty v-if="!selectedPoint.pointInfo" description="请从左侧选择一个关键监控点位" />
  151. <template v-else>
  152. <div class="point-hero">
  153. <div class="hero-main">
  154. <div class="hero-title">{{ selectedPoint.pointInfo.pointName }}</div>
  155. <div class="hero-code">{{ selectedPoint.pointInfo.pointCode }}</div>
  156. </div>
  157. <div class="hero-tags">
  158. <el-tag>{{ selectedPoint.pointInfo.pointType || "未分类" }}</el-tag>
  159. <el-tag :type="getStatusTagType(selectedPoint.pointInfo.status)">
  160. {{ selectedPoint.pointInfo.status || "未设置状态" }}
  161. </el-tag>
  162. </div>
  163. </div>
  164. <div class="detail-panels">
  165. <el-card shadow="never" class="mini-panel">
  166. <template #header>
  167. <div class="mini-panel-title">点位基础信息</div>
  168. </template>
  169. <el-descriptions :column="1" border size="small">
  170. <el-descriptions-item label="关联设施">
  171. {{ selectedPoint.networkInfo?.networkName || "-" }}
  172. </el-descriptions-item>
  173. <el-descriptions-item label="位置描述">
  174. {{ selectedPoint.pointInfo.location || "-" }}
  175. </el-descriptions-item>
  176. <el-descriptions-item label="安装日期">
  177. {{ selectedPoint.pointInfo.installationDate || "-" }}
  178. </el-descriptions-item>
  179. <el-descriptions-item label="经纬度">
  180. {{ selectedPoint.pointInfo.longitude ?? "-" }} / {{ selectedPoint.pointInfo.latitude ?? "-" }}
  181. </el-descriptions-item>
  182. <el-descriptions-item label="备注">
  183. {{ selectedPoint.pointInfo.remark || "-" }}
  184. </el-descriptions-item>
  185. </el-descriptions>
  186. </el-card>
  187. <el-card shadow="never" class="mini-panel">
  188. <template #header>
  189. <div class="mini-panel-title">所属设施概况</div>
  190. </template>
  191. <el-descriptions :column="1" border size="small">
  192. <el-descriptions-item label="设施名称">
  193. {{ selectedPoint.networkInfo?.networkName || "-" }}
  194. </el-descriptions-item>
  195. <el-descriptions-item label="设施编码">
  196. {{ selectedPoint.networkInfo?.networkCode || "-" }}
  197. </el-descriptions-item>
  198. <el-descriptions-item label="设施类型">
  199. {{ selectedPoint.networkInfo?.networkType || "-" }}
  200. </el-descriptions-item>
  201. <el-descriptions-item label="管养单位">
  202. {{ selectedPoint.networkInfo?.managementUnit || "-" }}
  203. </el-descriptions-item>
  204. <el-descriptions-item label="设施状态">
  205. {{ selectedPoint.networkInfo?.status || "-" }}
  206. </el-descriptions-item>
  207. </el-descriptions>
  208. </el-card>
  209. </div>
  210. <el-card shadow="never" class="equipment-panel">
  211. <template #header>
  212. <div class="card-header-row">
  213. <div>
  214. <div class="mini-panel-title">关联监测设备</div>
  215. <div class="panel-tip">
  216. 当前共 {{ selectedEquipmentList.length }} 台设备,支持在右上角进行绑定和解绑
  217. </div>
  218. </div>
  219. <el-button type="primary" plain icon="Connection" @click="openBindDialog(selectedPoint.pointInfo)">
  220. 关联设备
  221. </el-button>
  222. </div>
  223. </template>
  224. <el-table :data="selectedEquipmentList" border size="small" max-height="320">
  225. <el-table-column label="设备编码" prop="equipmentCode" min-width="140" />
  226. <el-table-column label="设备名称" prop="equipmentName" min-width="160" show-overflow-tooltip />
  227. <el-table-column label="设备型号" prop="equipmentModel" min-width="120" />
  228. <el-table-column label="制造商" prop="manufacturer" min-width="120" show-overflow-tooltip />
  229. <el-table-column label="设备位置" prop="equipmentLocation" min-width="160" show-overflow-tooltip />
  230. <el-table-column label="操作" width="90" align="center">
  231. <template #default="scope">
  232. <el-button link type="danger" icon="Close" @click="handleUnbind(scope.row)">解绑</el-button>
  233. </template>
  234. </el-table-column>
  235. </el-table>
  236. </el-card>
  237. </template>
  238. </el-card>
  239. </el-col>
  240. </el-row>
  241. <el-dialog :title="title" v-model="open" width="780px" append-to-body>
  242. <el-form ref="pointRef" :model="form" :rules="rules" label-width="110px">
  243. <el-row :gutter="16">
  244. <el-col :span="12">
  245. <el-form-item label="监测点编码" prop="pointCode">
  246. <el-input v-model="form.pointCode" placeholder="请输入监测点编码" />
  247. </el-form-item>
  248. </el-col>
  249. <el-col :span="12">
  250. <el-form-item label="监测点名称" prop="pointName">
  251. <el-input v-model="form.pointName" placeholder="请输入监测点名称" />
  252. </el-form-item>
  253. </el-col>
  254. <el-col :span="12">
  255. <el-form-item label="关联设施" prop="networkId">
  256. <el-select v-model="form.networkId" clearable placeholder="请选择管网设施" style="width: 100%">
  257. <el-option
  258. v-for="item in networkOptions"
  259. :key="item.networkId"
  260. :label="`${item.networkName}(${item.networkCode})`"
  261. :value="item.networkId"
  262. />
  263. </el-select>
  264. </el-form-item>
  265. </el-col>
  266. <el-col :span="12">
  267. <el-form-item label="监测类型" prop="pointType">
  268. <el-select
  269. v-model="form.pointType"
  270. placeholder="请选择或输入监测类型"
  271. filterable
  272. allow-create
  273. default-first-option
  274. style="width: 100%"
  275. >
  276. <el-option v-for="item in pointTypeOptions" :key="item" :label="item" :value="item" />
  277. </el-select>
  278. </el-form-item>
  279. </el-col>
  280. <el-col :span="12">
  281. <el-form-item label="状态" prop="status">
  282. <el-select
  283. v-model="form.status"
  284. placeholder="请选择或输入状态"
  285. filterable
  286. allow-create
  287. default-first-option
  288. style="width: 100%"
  289. >
  290. <el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
  291. </el-select>
  292. </el-form-item>
  293. </el-col>
  294. <el-col :span="12">
  295. <el-form-item label="安装日期" prop="installationDate">
  296. <el-date-picker
  297. v-model="form.installationDate"
  298. type="date"
  299. value-format="YYYY-MM-DD"
  300. placeholder="请选择安装日期"
  301. style="width: 100%"
  302. />
  303. </el-form-item>
  304. </el-col>
  305. <el-col :span="24">
  306. <el-form-item label="位置描述" prop="location">
  307. <el-input v-model="form.location" placeholder="请输入位置描述" />
  308. </el-form-item>
  309. </el-col>
  310. <el-col :span="12">
  311. <el-form-item label="经度" prop="longitude">
  312. <el-input-number v-model="form.longitude" :precision="6" :step="0.000001" style="width: 100%" />
  313. </el-form-item>
  314. </el-col>
  315. <el-col :span="12">
  316. <el-form-item label="纬度" prop="latitude">
  317. <el-input-number v-model="form.latitude" :precision="6" :step="0.000001" style="width: 100%" />
  318. </el-form-item>
  319. </el-col>
  320. <el-col :span="24">
  321. <el-form-item label="备注" prop="remark">
  322. <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
  323. </el-form-item>
  324. </el-col>
  325. </el-row>
  326. </el-form>
  327. <template #footer>
  328. <div class="dialog-footer">
  329. <el-button type="primary" @click="submitForm">确 定</el-button>
  330. <el-button @click="cancel">取 消</el-button>
  331. </div>
  332. </template>
  333. </el-dialog>
  334. <el-dialog title="关联监测设备" v-model="bindOpen" width="860px" append-to-body>
  335. <el-form label-width="90px">
  336. <el-form-item label="当前点位">
  337. <span>{{ currentRow.pointName || "-" }}</span>
  338. </el-form-item>
  339. <el-form-item label="选择设备">
  340. <el-select
  341. v-model="bindForm.equipmentId"
  342. filterable
  343. clearable
  344. placeholder="请选择设备"
  345. style="width: 100%"
  346. >
  347. <el-option
  348. v-for="item in unboundEquipmentOptions"
  349. :key="item.equipmentId"
  350. :label="`${item.equipmentName}(${item.equipmentCode})`"
  351. :value="item.equipmentId"
  352. />
  353. </el-select>
  354. </el-form-item>
  355. </el-form>
  356. <div class="bind-toolbar">
  357. <el-button type="primary" @click="submitBind">添加关联</el-button>
  358. </div>
  359. <el-table :data="bindDetail.equipmentList || []" border>
  360. <el-table-column label="设备编码" prop="equipmentCode" min-width="140" />
  361. <el-table-column label="设备名称" prop="equipmentName" min-width="180" show-overflow-tooltip />
  362. <el-table-column label="设备型号" prop="equipmentModel" min-width="120" />
  363. <el-table-column label="制造商" prop="manufacturer" min-width="140" show-overflow-tooltip />
  364. <el-table-column label="设备位置" prop="equipmentLocation" min-width="180" show-overflow-tooltip />
  365. <el-table-column label="操作" align="center" width="100">
  366. <template #default="scope">
  367. <el-button link type="danger" icon="Close" @click="handleUnbind(scope.row)">解绑</el-button>
  368. </template>
  369. </el-table-column>
  370. </el-table>
  371. </el-dialog>
  372. </div>
  373. </template>
  374. <script setup>
  375. import { computed, getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
  376. import {
  377. addMonitorPoint,
  378. bindPointEquipment,
  379. delMonitorPoint,
  380. getMonitorPoint,
  381. getMonitorPointDetail,
  382. listEquipment,
  383. listMonitorPoint,
  384. listMonitorPointAll,
  385. listPipeNetworkAll,
  386. unbindPointEquipment,
  387. updateMonitorPoint
  388. } from "@/api/pipeNetwork/pdeviceInform";
  389. const { proxy } = getCurrentInstance();
  390. const pointList = ref([]);
  391. const networkOptions = ref([]);
  392. const equipmentOptions = ref([]);
  393. const loading = ref(false);
  394. const open = ref(false);
  395. const bindOpen = ref(false);
  396. const showSearch = ref(true);
  397. const ids = ref([]);
  398. const single = ref(true);
  399. const multiple = ref(true);
  400. const total = ref(0);
  401. const title = ref("");
  402. const currentRow = ref({});
  403. const pointTypeOptions = ["水质监测", "空气质量", "压力监测", "流量监测", "温度监测", "泄漏检测"];
  404. const statusOptions = ["正常", "启用", "停用", "维修中", "预警"];
  405. const summary = reactive({
  406. totalPoints: 0,
  407. normalPoints: 0,
  408. warningPoints: 0,
  409. boundEquipments: 0
  410. });
  411. const networkNameMap = computed(() => {
  412. return networkOptions.value.reduce((acc, item) => {
  413. acc[item.networkId] = item.networkName;
  414. return acc;
  415. }, {});
  416. });
  417. const data = reactive({
  418. queryParams: {
  419. pageNum: 1,
  420. pageSize: 10,
  421. pointCode: undefined,
  422. pointName: undefined,
  423. pointType: undefined,
  424. status: undefined
  425. },
  426. form: {},
  427. selectedPoint: {},
  428. bindDetail: {},
  429. bindForm: {
  430. equipmentId: undefined
  431. },
  432. rules: {
  433. pointCode: [{ required: true, message: "监测点编码不能为空", trigger: "blur" }],
  434. pointName: [{ required: true, message: "监测点名称不能为空", trigger: "blur" }],
  435. pointType: [{ required: true, message: "监测类型不能为空", trigger: "change" }]
  436. }
  437. });
  438. const { queryParams, form, selectedPoint, bindDetail, bindForm, rules } = toRefs(data);
  439. const selectedEquipmentList = computed(() => {
  440. return selectedPoint.value.equipmentList || [];
  441. });
  442. const unboundEquipmentOptions = computed(() => {
  443. const boundIds = new Set((bindDetail.value.equipmentList || []).map((item) => item.equipmentId));
  444. return equipmentOptions.value.filter((item) => !boundIds.has(item.equipmentId));
  445. });
  446. function createEmptyForm() {
  447. return {
  448. pointId: undefined,
  449. pointCode: undefined,
  450. pointName: undefined,
  451. networkId: undefined,
  452. pointType: undefined,
  453. location: undefined,
  454. longitude: undefined,
  455. latitude: undefined,
  456. installationDate: undefined,
  457. status: "正常",
  458. remark: undefined
  459. };
  460. }
  461. function getStatusTagType(status) {
  462. if (["正常", "启用"].includes(status)) {
  463. return "success";
  464. }
  465. if (status === "预警") {
  466. return "warning";
  467. }
  468. if (["停用", "禁用"].includes(status)) {
  469. return "info";
  470. }
  471. if (["故障", "异常"].includes(status)) {
  472. return "danger";
  473. }
  474. return "";
  475. }
  476. function getList() {
  477. loading.value = true;
  478. return listMonitorPoint(queryParams.value)
  479. .then((response) => {
  480. pointList.value = response.records || [];
  481. total.value = response.total || 0;
  482. if (!selectedPoint.value.pointInfo && pointList.value.length) {
  483. return selectPoint(pointList.value[0]);
  484. }
  485. return undefined;
  486. })
  487. .finally(() => {
  488. loading.value = false;
  489. });
  490. }
  491. function loadAssistData() {
  492. return Promise.all([
  493. listPipeNetworkAll(),
  494. listEquipment({
  495. pageNum: 1,
  496. pageSize: 1000
  497. }),
  498. listMonitorPointAll()
  499. ]).then(([networkRes, equipmentRes, pointRes]) => {
  500. networkOptions.value = networkRes.data || [];
  501. equipmentOptions.value = equipmentRes.records || [];
  502. const allPoints = pointRes.data || [];
  503. summary.totalPoints = allPoints.length;
  504. summary.normalPoints = allPoints.filter((item) => ["正常", "启用"].includes(item.status)).length;
  505. summary.warningPoints = allPoints.filter((item) => !["正常", "启用"].includes(item.status)).length;
  506. });
  507. }
  508. function loadPointDetail(pointId, targetRef) {
  509. return getMonitorPointDetail(pointId).then((response) => {
  510. targetRef.value = response.data || {};
  511. });
  512. }
  513. function refreshBoundCount() {
  514. return listMonitorPoint({
  515. pageNum: 1,
  516. pageSize: 1000
  517. }).then((response) => {
  518. const records = response.records || [];
  519. const detailRequests = records.map((item) => getMonitorPointDetail(item.pointId));
  520. return Promise.all(detailRequests).then((detailResponses) => {
  521. summary.boundEquipments = detailResponses.reduce((count, item) => {
  522. return count + ((item.data?.equipmentList || []).length || 0);
  523. }, 0);
  524. });
  525. });
  526. }
  527. function selectPoint(row) {
  528. if (!row?.pointId) {
  529. return;
  530. }
  531. return loadPointDetail(row.pointId, selectedPoint);
  532. }
  533. function reset() {
  534. form.value = createEmptyForm();
  535. proxy.resetForm("pointRef");
  536. }
  537. function handleQuery() {
  538. queryParams.value.pageNum = 1;
  539. getList();
  540. }
  541. function resetQuery() {
  542. proxy.resetForm("queryRef");
  543. handleQuery();
  544. }
  545. function handleSelectionChange(selection) {
  546. ids.value = selection.map((item) => item.pointId);
  547. single.value = selection.length !== 1;
  548. multiple.value = !selection.length;
  549. }
  550. function handleCurrentChange(currentRow) {
  551. if (currentRow?.pointId) {
  552. selectPoint(currentRow);
  553. }
  554. }
  555. function handleRowClick(row) {
  556. selectPoint(row);
  557. }
  558. function handleAdd() {
  559. reset();
  560. open.value = true;
  561. title.value = "新增关键监控点位";
  562. }
  563. function handleUpdate(row) {
  564. reset();
  565. const pointId = row?.pointId || row?.pointInfo?.pointId || ids.value[0];
  566. if (!pointId) {
  567. return;
  568. }
  569. getMonitorPoint(pointId).then((response) => {
  570. form.value = { ...createEmptyForm(), ...(response.data || {}) };
  571. open.value = true;
  572. title.value = "修改关键监控点位";
  573. });
  574. }
  575. function cancel() {
  576. open.value = false;
  577. reset();
  578. }
  579. function submitForm() {
  580. proxy.$refs.pointRef.validate((valid) => {
  581. if (!valid) {
  582. return;
  583. }
  584. const request = form.value.pointId ? updateMonitorPoint(form.value) : addMonitorPoint(form.value);
  585. request.then(() => {
  586. proxy.$modal.msgSuccess(form.value.pointId ? "修改成功" : "新增成功");
  587. open.value = false;
  588. Promise.all([getList(), loadAssistData(), refreshBoundCount()]).then(() => {
  589. if (form.value.pointId) {
  590. selectPoint({ pointId: form.value.pointId });
  591. }
  592. });
  593. });
  594. });
  595. }
  596. function handleDelete(row) {
  597. const deleteIds = row?.pointId ? [row.pointId] : ids.value;
  598. if (!deleteIds.length) {
  599. return;
  600. }
  601. proxy.$modal
  602. .confirm(`是否确认删除选中的 ${deleteIds.length} 个监测点?`)
  603. .then(() => Promise.all(deleteIds.map((id) => delMonitorPoint(id))))
  604. .then(() => {
  605. proxy.$modal.msgSuccess("删除成功");
  606. if (deleteIds.includes(selectedPoint.value?.pointInfo?.pointId)) {
  607. selectedPoint.value = {};
  608. }
  609. return Promise.all([getList(), loadAssistData(), refreshBoundCount()]);
  610. });
  611. }
  612. function openBindDialog(row) {
  613. const pointId = row?.pointId || row?.pointInfo?.pointId;
  614. if (!pointId) {
  615. return;
  616. }
  617. currentRow.value = row.pointInfo ? row.pointInfo : row;
  618. bindForm.value.equipmentId = undefined;
  619. loadPointDetail(pointId, bindDetail).then(() => {
  620. bindOpen.value = true;
  621. });
  622. }
  623. function submitBind() {
  624. if (!bindForm.value.equipmentId) {
  625. proxy.$modal.msgError("请选择要关联的设备");
  626. return;
  627. }
  628. bindPointEquipment(currentRow.value.pointId, [bindForm.value.equipmentId]).then(() => {
  629. proxy.$modal.msgSuccess("关联成功");
  630. bindForm.value.equipmentId = undefined;
  631. Promise.all([
  632. loadPointDetail(currentRow.value.pointId, bindDetail),
  633. loadPointDetail(currentRow.value.pointId, selectedPoint),
  634. refreshBoundCount()
  635. ]);
  636. });
  637. }
  638. function handleUnbind(row) {
  639. const pointId = currentRow.value.pointId || selectedPoint.value?.pointInfo?.pointId;
  640. if (!pointId) {
  641. return;
  642. }
  643. proxy.$modal
  644. .confirm(`是否确认解绑设备“${row.equipmentName}”?`)
  645. .then(() => unbindPointEquipment(pointId, [row.equipmentId]))
  646. .then(() => {
  647. proxy.$modal.msgSuccess("解绑成功");
  648. return Promise.all([
  649. loadPointDetail(pointId, bindDetail),
  650. loadPointDetail(pointId, selectedPoint),
  651. refreshBoundCount()
  652. ]);
  653. });
  654. }
  655. onMounted(() => {
  656. reset();
  657. Promise.all([getList(), loadAssistData(), refreshBoundCount()]);
  658. });
  659. </script>
  660. <style scoped>
  661. .point-page {
  662. background: #f5f8fc;
  663. }
  664. .summary-grid {
  665. display: grid;
  666. grid-template-columns: repeat(4, minmax(0, 1fr));
  667. gap: 16px;
  668. margin-bottom: 16px;
  669. }
  670. .summary-card {
  671. border: none;
  672. color: #fff;
  673. }
  674. .summary-card :deep(.el-card__body) {
  675. padding: 20px;
  676. }
  677. .summary-card.primary {
  678. background: linear-gradient(135deg, #2563eb, #5b93ff);
  679. }
  680. .summary-card.success {
  681. background: linear-gradient(135deg, #059669, #34d399);
  682. }
  683. .summary-card.warning {
  684. background: linear-gradient(135deg, #d97706, #f7b955);
  685. }
  686. .summary-card.info {
  687. background: linear-gradient(135deg, #7c3aed, #9d72ff);
  688. }
  689. .summary-label {
  690. font-size: 14px;
  691. opacity: 0.9;
  692. }
  693. .summary-value {
  694. margin: 8px 0 10px;
  695. font-size: 30px;
  696. font-weight: 700;
  697. line-height: 1;
  698. }
  699. .summary-desc {
  700. font-size: 12px;
  701. opacity: 0.9;
  702. }
  703. .query-card,
  704. .list-card,
  705. .detail-card {
  706. border-radius: 16px;
  707. }
  708. .query-card {
  709. margin-bottom: 16px;
  710. }
  711. .card-header-row {
  712. display: flex;
  713. justify-content: space-between;
  714. align-items: center;
  715. gap: 12px;
  716. }
  717. .card-title {
  718. font-size: 16px;
  719. font-weight: 600;
  720. color: #1f2937;
  721. }
  722. .card-subtitle,
  723. .panel-tip {
  724. margin-top: 4px;
  725. font-size: 12px;
  726. color: #6b7280;
  727. }
  728. .card-actions,
  729. .detail-top-actions {
  730. display: flex;
  731. align-items: center;
  732. gap: 8px;
  733. flex-wrap: wrap;
  734. }
  735. .point-hero {
  736. display: flex;
  737. justify-content: space-between;
  738. gap: 12px;
  739. padding: 16px;
  740. border-radius: 14px;
  741. background: linear-gradient(135deg, #eff6ff, #f8fbff);
  742. border: 1px solid #dbeafe;
  743. margin-bottom: 16px;
  744. }
  745. .hero-title {
  746. font-size: 18px;
  747. font-weight: 700;
  748. color: #1d4ed8;
  749. }
  750. .hero-code {
  751. margin-top: 8px;
  752. font-size: 13px;
  753. color: #4b5563;
  754. }
  755. .hero-tags {
  756. display: flex;
  757. gap: 8px;
  758. flex-wrap: wrap;
  759. align-items: flex-start;
  760. }
  761. .detail-panels {
  762. display: grid;
  763. grid-template-columns: repeat(2, minmax(0, 1fr));
  764. gap: 16px;
  765. margin-bottom: 16px;
  766. }
  767. .mini-panel,
  768. .equipment-panel {
  769. border-radius: 14px;
  770. }
  771. .mini-panel-title {
  772. font-size: 15px;
  773. font-weight: 600;
  774. color: #1f2937;
  775. }
  776. .bind-toolbar {
  777. margin-bottom: 12px;
  778. }
  779. @media (max-width: 1200px) {
  780. .summary-grid {
  781. grid-template-columns: repeat(2, minmax(0, 1fr));
  782. }
  783. }
  784. @media (max-width: 768px) {
  785. .summary-grid,
  786. .detail-panels {
  787. grid-template-columns: 1fr;
  788. }
  789. .card-header-row,
  790. .point-hero {
  791. flex-direction: column;
  792. align-items: flex-start;
  793. }
  794. }
  795. </style>