bjqd.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. <template>
  2. <div class="app-container">
  3. <!-- 搜索区 -->
  4. <el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
  5. <el-form-item label="设备名称">
  6. <el-input v-model="queryParams.equipmentName" placeholder="请输入设备名称" clearable style="width: 180px" />
  7. </el-form-item>
  8. <el-form-item label="报警等级">
  9. <el-select v-model="queryParams.warningLevel" placeholder="请选择" clearable style="width: 160px">
  10. <el-option label="严重" :value="1" />
  11. <el-option label="重要" :value="2" />
  12. <el-option label="一般" :value="3" />
  13. </el-select>
  14. </el-form-item>
  15. <el-form-item label="处理状态">
  16. <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 140px">
  17. <el-option label="未下派" :value="-1" />
  18. <el-option label="待处理" :value="0" />
  19. <el-option label="已处理" :value="1" />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item label="时间范围">
  23. <el-date-picker
  24. v-model="queryParams.dateRange"
  25. type="daterange"
  26. range-separator="至"
  27. start-placeholder="开始日期"
  28. end-placeholder="结束日期"
  29. style="width: 240px"
  30. value-format="YYYY-MM-DD"
  31. />
  32. </el-form-item>
  33. <el-form-item>
  34. <el-button type="primary" @click="handleQuery">搜索</el-button>
  35. <el-button @click="resetQuery">重置</el-button>
  36. </el-form-item>
  37. </el-form>
  38. <!-- 数据表格 -->
  39. <el-table :data="tableData" v-loading="loading" border style="width: 100%" size="default">
  40. <el-table-column label="报警编号" prop="id" min-width="80" align="center" />
  41. <el-table-column label="设备名称" prop="equipmentName" min-width="140" show-overflow-tooltip />
  42. <el-table-column label="报警时间" prop="warningTime" min-width="160" />
  43. <el-table-column label="报警等级" min-width="90" align="center">
  44. <template #default="{ row }">
  45. <span :class="levelClass(row.warningLevel)">{{ levelText(row.warningLevel) }}</span>
  46. </template>
  47. </el-table-column>
  48. <el-table-column label="报警内容" prop="content" min-width="180" show-overflow-tooltip />
  49. <el-table-column label="位置" prop="location" min-width="180" show-overflow-tooltip />
  50. <el-table-column label="处理状态" min-width="90" align="center">
  51. <template #default="{ row }">
  52. <el-tag :type="statusTagType(row.status)" size="small" effect="plain">{{ statusText(row.status) }}</el-tag>
  53. </template>
  54. </el-table-column>
  55. <el-table-column label="操作" width="80" align="center">
  56. <template #default="{ row }">
  57. <el-button link type="primary" @click="handleView(row)">查看</el-button>
  58. </template>
  59. </el-table-column>
  60. </el-table>
  61. <!-- 分页 -->
  62. <div class="pagination-wrap">
  63. <el-pagination
  64. v-model:current-page="pageNum"
  65. v-model:page-size="pageSize"
  66. :page-sizes="[10, 20, 50, 100]"
  67. :total="total"
  68. layout="total, sizes, prev, pager, next, jumper"
  69. @size-change="handleSizeChange"
  70. @current-change="handlePageChange"
  71. />
  72. </div>
  73. <!-- 查看详情弹窗 -->
  74. <el-dialog v-model="dialogVisible" title="报警详情" width="680px" :before-close="handleClose">
  75. <div class="detail-panel">
  76. <div class="detail-top">
  77. <div class="detail-image">
  78. <el-icon :size="48" color="#b0c4de"><Monitor /></el-icon>
  79. </div>
  80. <div class="detail-info">
  81. <div class="detail-number">编号: {{ currentRow.id }}</div>
  82. <el-row :gutter="16">
  83. <el-col :span="12">
  84. <div class="info-item">
  85. <span class="info-label">设备名称</span>
  86. <span class="info-value">{{ currentRow.equipmentName }}</span>
  87. </div>
  88. </el-col>
  89. <el-col :span="12">
  90. <div class="info-item">
  91. <span class="info-label">报警时间</span>
  92. <span class="info-value">{{ currentRow.warningTime }}</span>
  93. </div>
  94. </el-col>
  95. </el-row>
  96. <el-row :gutter="16">
  97. <el-col :span="12">
  98. <div class="info-item">
  99. <span class="info-label">报警等级</span>
  100. <span class="info-value">
  101. <span :class="levelClass(currentRow.warningLevel)">{{ levelText(currentRow.warningLevel) }}</span>
  102. </span>
  103. </div>
  104. </el-col>
  105. <el-col :span="12">
  106. <div class="info-item">
  107. <span class="info-label">处理状态</span>
  108. <span class="info-value">
  109. <el-tag :type="statusTagType(currentRow.status)" size="small" effect="plain">{{ statusText(currentRow.status) }}</el-tag>
  110. </span>
  111. </div>
  112. </el-col>
  113. </el-row>
  114. <el-row :gutter="16">
  115. <el-col :span="12">
  116. <div class="info-item">
  117. <span class="info-label">当前值</span>
  118. <span class="info-value">{{ currentRow.currentValue || '-' }}</span>
  119. </div>
  120. </el-col>
  121. <el-col :span="12">
  122. <div class="info-item">
  123. <span class="info-label">阈值</span>
  124. <span class="info-value">{{ currentRow.thresholdValue || '-' }}</span>
  125. </div>
  126. </el-col>
  127. </el-row>
  128. <el-row :gutter="16">
  129. <el-col :span="12">
  130. <div class="info-item">
  131. <span class="info-label">详细地址</span>
  132. <span class="info-value">{{ currentRow.location }}</span>
  133. </div>
  134. </el-col>
  135. <el-col :span="12">
  136. <div class="info-item">
  137. <span class="info-label">报警类型</span>
  138. <span class="info-value">{{ currentRow.warningType || '-' }}</span>
  139. </div>
  140. </el-col>
  141. </el-row>
  142. <div class="info-item desc-item">
  143. <span class="info-label">描述</span>
  144. <span class="info-value desc-text">{{ currentRow.content }}</span>
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. </el-dialog>
  150. </div>
  151. </template>
  152. <script setup name="Bjqd">
  153. import { ref, computed, onMounted } from 'vue'
  154. import { ElMessage, ElMessageBox } from 'element-plus'
  155. import { Monitor } from '@element-plus/icons-vue'
  156. import { listUser, deptTreeSelect } from '@/api/system/user'
  157. import { getEquipmentPage, getWarningCurrentPage, updateWarning, addWarning, firstApproveWarning } from '@/api/drainage'
  158. import useUserStore from '@/store/modules/user'
  159. // ==================== 用户信息 ====================
  160. const userStore = useUserStore()
  161. const isAdmin = computed(() => userStore.roles && userStore.roles.includes('admin'))
  162. // ==================== 搜索参数 ====================
  163. const queryParams = ref({
  164. equipmentName: '',
  165. warningLevel: '',
  166. status: '',
  167. dateRange: []
  168. })
  169. const queryRef = ref(null)
  170. function handleQuery() {
  171. pageNum.value = 1
  172. loadData()
  173. }
  174. function resetQuery() {
  175. queryParams.value = { equipmentName: '', warningLevel: '', status: '', dateRange: [] }
  176. pageNum.value = 1
  177. loadData()
  178. }
  179. // ==================== 等级/状态映射 ====================
  180. const levelMap = { 1: '严重', 2: '重要', 3: '一般' }
  181. const statusMap = { '-1': '未下派', 0: '待处理', 1: '已处理' }
  182. function levelText(level) {
  183. return levelMap[level] || '-'
  184. }
  185. function levelClass(level) {
  186. const map = { 1: 'level-critical', 2: 'level-major', 3: 'level-minor' }
  187. return map[level] || ''
  188. }
  189. function statusText(status) {
  190. return statusMap[status] ?? '-'
  191. }
  192. function statusTagType(status) {
  193. const map = { '-1': 'info', 0: 'warning', 1: 'success' }
  194. return map[status] || 'info'
  195. }
  196. // ==================== 设备报警数据转换 ====================
  197. function convertEquipToWarning(row) {
  198. return {
  199. id: `EQ_${row.equipmentId || row.id}`,
  200. equipmentId: row.equipmentId || row.id,
  201. equipmentName: row.equipmentName || '-',
  202. warningTime: row.updateTime || row.createTime || '-',
  203. warningLevel: 1,
  204. content: `设备${row.equipmentName}报警`,
  205. location: row.equipmentLocation || row.installLocation || '-',
  206. longitude: row.longitude,
  207. latitude: row.latitude,
  208. status: -1,
  209. currentValue: '',
  210. thresholdValue: '',
  211. warningType: '设备报警',
  212. source: 'equipment'
  213. }
  214. }
  215. // ==================== 表格数据 ====================
  216. const allMergedData = ref([])
  217. const tableData = ref([])
  218. const total = ref(0)
  219. const loading = ref(false)
  220. const pageNum = ref(1)
  221. const pageSize = ref(10)
  222. async function loadData() {
  223. loading.value = true
  224. try {
  225. const equipParams = { equipmentTypeId: 'drainage' }
  226. if (queryParams.value.equipmentName) equipParams.equipmentName = queryParams.value.equipmentName
  227. // 并行加载设备报警和已下派的预警记录
  228. const [equipRes, warningRes] = await Promise.allSettled([
  229. getEquipmentPage(1, 1000, equipParams),
  230. getWarningCurrentPage(1, 1000, { warningType: '设备报警', isAdmin: true })
  231. ])
  232. // 解析设备报警
  233. let equipRecords = []
  234. if (equipRes.status === 'fulfilled') {
  235. const res = equipRes.value
  236. const pageData = res.code !== undefined ? res.data : res
  237. const records = pageData.records || pageData.rows || []
  238. records.forEach(row => {
  239. if (row.alarmStatus === 1) {
  240. equipRecords.push(convertEquipToWarning(row))
  241. }
  242. })
  243. }
  244. // 解析已下派的预警记录,用于匹配状态
  245. let dispatchedMap = {}
  246. if (warningRes.status === 'fulfilled') {
  247. const res = warningRes.value
  248. const pageData = res.code !== undefined ? res.data : res
  249. const records = pageData.records || pageData.rows || []
  250. records.forEach(w => {
  251. if (w.equipmentName) {
  252. dispatchedMap[w.equipmentName] = w
  253. }
  254. })
  255. }
  256. // 合并状态:匹配到预警记录的设备报警更新状态
  257. equipRecords.forEach(item => {
  258. const matched = dispatchedMap[item.equipmentName]
  259. if (matched) {
  260. item.status = matched.status >= 1 ? 1 : 0 // 0=待处理, 1=已处理
  261. item.firstApprover = matched.firstApprover
  262. item.firstApproverName = matched.firstApproverName
  263. item.warningId = matched.id // 保存关联的预警记录ID
  264. }
  265. })
  266. // 筛选状态
  267. let filtered = equipRecords
  268. if (queryParams.value.status !== '' && queryParams.value.status !== null) {
  269. filtered = equipRecords.filter(item => item.status === queryParams.value.status)
  270. }
  271. allMergedData.value = filtered
  272. total.value = allMergedData.value.length
  273. const start = (pageNum.value - 1) * pageSize.value
  274. tableData.value = allMergedData.value.slice(start, start + pageSize.value)
  275. } catch (error) {
  276. console.error('加载报警清单失败', error)
  277. ElMessage.error('加载报警清单失败')
  278. } finally {
  279. loading.value = false
  280. }
  281. }
  282. function handlePageChange() {
  283. const start = (pageNum.value - 1) * pageSize.value
  284. tableData.value = allMergedData.value.slice(start, start + pageSize.value)
  285. }
  286. function handleSizeChange() {
  287. pageNum.value = 1
  288. tableData.value = allMergedData.value.slice(0, pageSize.value)
  289. }
  290. // ==================== 查看详情 ====================
  291. const dialogVisible = ref(false)
  292. const currentRow = ref({})
  293. const dispatchUserId = ref('')
  294. const personOptions = ref([])
  295. async function loadDrainageUsers() {
  296. try {
  297. // 先查找排水部门
  298. const treeRes = await deptTreeSelect()
  299. const treeData = treeRes.data || treeRes
  300. const findDept = (nodes) => {
  301. for (const node of nodes) {
  302. if (node.label && node.label.includes('排水')) return node
  303. if (node.children) {
  304. const found = findDept(node.children)
  305. if (found) return found
  306. }
  307. }
  308. return null
  309. }
  310. const drainageDept = findDept(treeData)
  311. if (drainageDept) {
  312. const userRes = await listUser({ deptId: drainageDept.id, status: '0', pageSize: 1000 })
  313. personOptions.value = (userRes.rows || userRes.data?.rows || []).map(u => ({
  314. userId: u.userId,
  315. nickName: u.nickName || u.userName
  316. }))
  317. }
  318. } catch (e) {
  319. console.error('加载排水单位人员失败', e)
  320. }
  321. }
  322. async function handleView(row) {
  323. currentRow.value = { ...row }
  324. dispatchUserId.value = ''
  325. dialogVisible.value = true
  326. }
  327. function handleClose() {
  328. dialogVisible.value = false
  329. }
  330. async function handleDispatch() {
  331. if (!dispatchUserId.value) {
  332. ElMessage.warning('请选择下派人员')
  333. return
  334. }
  335. try {
  336. const selectedUser = personOptions.value.find(p => p.userId === dispatchUserId.value)
  337. if (currentRow.value.source === 'equipment' || String(currentRow.value.id).startsWith('EQ_')) {
  338. // 设备报警:创建一条预警记录到后端,持久化下派信息
  339. await addWarning({
  340. equipmentName: currentRow.value.equipmentName,
  341. warningLevel: currentRow.value.warningLevel || 1,
  342. warningType: currentRow.value.warningType || '设备报警',
  343. content: currentRow.value.content,
  344. location: currentRow.value.location,
  345. warningTime: currentRow.value.warningTime,
  346. longitude: currentRow.value.longitude,
  347. latitude: currentRow.value.latitude,
  348. firstApprover: dispatchUserId.value,
  349. firstApproverName: selectedUser?.nickName || '',
  350. publishScope: 3,
  351. status: 0
  352. })
  353. } else {
  354. // 预警类型:直接更新firstApprover字段
  355. await updateWarning({
  356. id: currentRow.value.id,
  357. firstApprover: dispatchUserId.value,
  358. firstApproverName: selectedUser?.nickName || '',
  359. publishScope: 3
  360. })
  361. }
  362. ElMessage.success(`已下派给 ${selectedUser?.nickName || ''}`)
  363. dialogVisible.value = false
  364. loadData()
  365. } catch (e) {
  366. console.error('下派失败', e)
  367. ElMessage.error('下派失败,请重试')
  368. }
  369. }
  370. async function handleProcess(row) {
  371. try {
  372. await ElMessageBox.confirm('确认处理此报警?', '提示', { type: 'warning' })
  373. // 用关联的预警记录ID调用处理接口
  374. const warningId = row.warningId || row.id
  375. await firstApproveWarning(warningId, {
  376. userId: userStore.id,
  377. userName: userStore.name
  378. })
  379. ElMessage.success('处理成功')
  380. loadData()
  381. } catch (e) {
  382. if (e !== 'cancel') {
  383. console.error('处理失败', e)
  384. ElMessage.error('处理失败,请重试')
  385. }
  386. }
  387. }
  388. // ==================== 生命周期 ====================
  389. onMounted(() => {
  390. loadDrainageUsers()
  391. loadData()
  392. })
  393. </script>
  394. <style scoped>
  395. .app-container {
  396. padding: 20px;
  397. background: #fff;
  398. min-height: calc(100vh - 84px);
  399. }
  400. .search-form {
  401. margin-bottom: 16px;
  402. padding: 16px 16px 0;
  403. background: #f5f7fa;
  404. border-radius: 6px;
  405. }
  406. .pagination-wrap {
  407. margin-top: 16px;
  408. display: flex;
  409. justify-content: flex-end;
  410. }
  411. /* 等级彩色文字 */
  412. .level-critical {
  413. color: #f56c6c;
  414. font-weight: 700;
  415. }
  416. .level-major {
  417. color: #e6a23c;
  418. font-weight: 700;
  419. }
  420. .level-minor {
  421. color: #409eff;
  422. font-weight: 600;
  423. }
  424. /* 详情弹窗 */
  425. .detail-panel {
  426. padding: 0 4px;
  427. }
  428. .detail-top {
  429. display: flex;
  430. gap: 24px;
  431. }
  432. .detail-image {
  433. width: 150px;
  434. height: 150px;
  435. flex-shrink: 0;
  436. background: #dce6f0;
  437. border-radius: 8px;
  438. display: flex;
  439. align-items: center;
  440. justify-content: center;
  441. border: 1px solid #c0cfe0;
  442. }
  443. .detail-info {
  444. flex: 1;
  445. }
  446. .detail-number {
  447. font-size: 20px;
  448. font-weight: 700;
  449. color: #303133;
  450. margin-bottom: 14px;
  451. }
  452. .info-item {
  453. display: flex;
  454. align-items: flex-start;
  455. margin-bottom: 10px;
  456. line-height: 22px;
  457. }
  458. .info-label {
  459. color: #909399;
  460. font-size: 14px;
  461. white-space: nowrap;
  462. min-width: 65px;
  463. flex-shrink: 0;
  464. }
  465. .info-value {
  466. color: #303133;
  467. font-size: 14px;
  468. flex: 1;
  469. }
  470. .desc-item {
  471. align-items: flex-start;
  472. }
  473. .desc-text {
  474. line-height: 22px;
  475. color: #606266;
  476. }
  477. /* 底部操作区 */
  478. .detail-footer {
  479. margin-top: 20px;
  480. padding-top: 16px;
  481. border-top: 1px solid #ebeef5;
  482. }
  483. .dispatch-row {
  484. display: flex;
  485. align-items: center;
  486. }
  487. .dispatch-label {
  488. font-size: 14px;
  489. color: #303133;
  490. font-weight: 600;
  491. white-space: nowrap;
  492. }
  493. </style>