jbgl.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <div class="app-container jbgl-page">
  3. <!-- 搜索区(若依风格:条件带标题 + 日期区间 + 图标按钮) -->
  4. <div class="filter-panel">
  5. <el-form :inline="true" :model="searchForm" class="filter-form" label-width="80px">
  6. <el-form-item label="事件来源">
  7. <el-select v-model="searchForm.eventSource" placeholder="请选择事件来源" clearable filterable style="width:160px">
  8. <el-option v-for="d in dis_event_source" :key="d.value" :label="d.label" :value="d.value" />
  9. </el-select>
  10. </el-form-item>
  11. <el-form-item label="事件状态">
  12. <el-select v-model="searchForm.status" placeholder="请选择事件状态" clearable style="width:140px">
  13. <el-option v-for="d in eventStatusOptions" :key="d.value" :label="d.label" :value="d.value" />
  14. </el-select>
  15. </el-form-item>
  16. <el-form-item label="接报时间">
  17. <el-date-picker
  18. v-model="searchForm.dateRange"
  19. type="daterange"
  20. range-separator="至"
  21. start-placeholder="开始日期"
  22. end-placeholder="结束日期"
  23. style="width:240px"
  24. value-format="YYYY-MM-DD"
  25. />
  26. </el-form-item>
  27. <el-form-item>
  28. <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
  29. <el-button :icon="Refresh" @click="handleReset">重置</el-button>
  30. </el-form-item>
  31. </el-form>
  32. <div class="filter-actions">
  33. <el-button type="primary" @click="handleAdd" v-hasPermi="['dispatch:event:add']">新增接报</el-button>
  34. </div>
  35. </div>
  36. <!-- 数据表格 -->
  37. <div class="table-panel">
  38. <el-table :data="tableData" border stripe style="width:100%" v-loading="loading">
  39. <el-table-column prop="eventNo" label="事件编号" width="140" />
  40. <el-table-column prop="eventSource" label="事件来源" width="110">
  41. <template #default="{ row }">{{ dictLabel(dis_event_source, row.eventSource) }}</template>
  42. </el-table-column>
  43. <el-table-column prop="eventType" label="管网类型" width="110">
  44. <template #default="{ row }">{{ dictLabel(dis_event_type, row.eventType) }}</template>
  45. </el-table-column>
  46. <el-table-column prop="location" label="发生地点" show-overflow-tooltip />
  47. <el-table-column prop="happenTime" label="发生时间" width="150" />
  48. <el-table-column prop="createTime" label="创建时间" width="150" />
  49. <el-table-column prop="description" label="事件描述" show-overflow-tooltip />
  50. <el-table-column prop="reporter" label="接报人" width="100" />
  51. <el-table-column prop="statusName" label="状态" width="100">
  52. <template #default="{ row }">
  53. <el-tag :type="statusTagType(row.status)" size="small">{{ row.statusName }}</el-tag>
  54. </template>
  55. </el-table-column>
  56. <el-table-column label="操作" width="220" fixed="right">
  57. <template #default="{ row }">
  58. <el-button v-if="row.editable" type="primary" plain size="small" @click="handleEdit(row)" v-hasPermi="['dispatch:event:edit']">编辑</el-button>
  59. <el-button type="info" plain size="small" @click="handleDetail(row)">详情</el-button>
  60. <el-button v-if="row.editable" type="danger" plain size="small" @click="handleDelete(row)" v-hasPermi="['dispatch:event:remove']">删除</el-button>
  61. </template>
  62. </el-table-column>
  63. </el-table>
  64. <div class="pagination-wrap">
  65. <el-pagination
  66. v-model:current-page="pagination.page"
  67. v-model:page-size="pagination.pageSize"
  68. :page-sizes="[10, 20, 50]"
  69. layout="total, sizes, prev, pager, next, jumper"
  70. :total="total"
  71. @size-change="handleSizeChange"
  72. @current-change="handleCurrentChange"
  73. />
  74. </div>
  75. </div>
  76. <!-- 详情弹窗 -->
  77. <el-drawer v-model="detailVisible" title="事件详情" size="600px" :close-on-click-modal="false" class="detail-drawer" direction="rtl">
  78. <div v-if="currentRow" class="detail-content">
  79. <el-form label-width="100px" size="default">
  80. <el-row :gutter="20">
  81. <el-col :span="12">
  82. <el-form-item label="事件编号">{{ currentRow.eventNo }}</el-form-item>
  83. </el-col>
  84. <el-col :span="12">
  85. <el-form-item label="事件来源">{{ dictLabel(dis_event_source, currentRow.eventSource) }}</el-form-item>
  86. </el-col>
  87. </el-row>
  88. <el-row :gutter="20">
  89. <el-col :span="12">
  90. <el-form-item label="发生地点">{{ currentRow.location }}</el-form-item>
  91. </el-col>
  92. <el-col :span="12">
  93. <el-form-item label="发生时间">{{ currentRow.happenTime }}</el-form-item>
  94. </el-col>
  95. </el-row>
  96. <el-form-item label="事件描述">
  97. <div class="desc-text">{{ currentRow.description }}</div>
  98. </el-form-item>
  99. <el-row :gutter="20">
  100. <el-col :span="12">
  101. <el-form-item label="接报人">{{ currentRow.reporter }}</el-form-item>
  102. </el-col>
  103. <el-col :span="12">
  104. <el-form-item label="接报时间">{{ currentRow.reportTime }}</el-form-item>
  105. </el-col>
  106. </el-row>
  107. <el-row :gutter="20">
  108. <el-col :span="12">
  109. <el-form-item label="管网类型">{{ dictLabel(dis_event_type, currentRow.eventType) }}</el-form-item>
  110. </el-col>
  111. <el-col :span="12">
  112. <el-form-item label="来源渠道">{{ dictLabel(dis_event_channel, currentRow.channel) }}</el-form-item>
  113. </el-col>
  114. </el-row>
  115. <el-row :gutter="20">
  116. <el-col :span="12">
  117. <el-form-item label="事件坐标">{{ formatGeo(currentRow.longitude, currentRow.latitude) }}</el-form-item>
  118. </el-col>
  119. <el-col :span="12">
  120. <el-form-item label="联系电话">{{ currentRow.reporterPhone || '-' }}</el-form-item>
  121. </el-col>
  122. </el-row>
  123. <el-form-item label="状态">
  124. <el-tag :type="statusTagType(currentRow.status)" size="small">{{ currentRow.statusName }}</el-tag>
  125. </el-form-item>
  126. <div class="section-title">事件记录(操作日志)</div>
  127. <!-- 参考工单系统样式:按日分组 + 时间列 + 虚线轴 + 操作人加粗 + 附件缩略图 -->
  128. <EventLogTimeline v-if="currentRow.rawLogs && currentRow.rawLogs.length" :logs="currentRow.rawLogs" />
  129. <div v-else class="desc-text">暂无记录</div>
  130. </el-form>
  131. </div>
  132. <template #footer>
  133. <el-button @click="detailVisible = false">关闭</el-button>
  134. </template>
  135. </el-drawer>
  136. <!-- 新增/编辑接报弹窗 -->
  137. <el-dialog v-model="addVisible" :title="editingId ? '编辑接报' : '新增接报'" width="600px" :close-on-click-modal="false">
  138. <el-form :model="addForm" :rules="addRules" ref="addFormRef" label-width="100px">
  139. <el-form-item label="事件来源" prop="eventSource">
  140. <el-select v-model="addForm.eventSource" placeholder="请选择" style="width:100%">
  141. <el-option v-for="d in dis_event_source" :key="d.value" :label="d.label" :value="d.value" />
  142. </el-select>
  143. </el-form-item>
  144. <el-form-item label="管网类型" prop="eventType">
  145. <el-select v-model="addForm.eventType" placeholder="请选择管网类型" style="width:100%">
  146. <el-option v-for="d in dis_event_type" :key="d.value" :label="d.label" :value="d.value" />
  147. </el-select>
  148. </el-form-item>
  149. <el-form-item label="发生地点" prop="location">
  150. <el-input v-model="addForm.location" placeholder="请输入发生地点" />
  151. </el-form-item>
  152. <el-form-item label="事件坐标" prop="longitude">
  153. <div style="display:flex;gap:8px;width:100%">
  154. <el-input :model-value="geoText" readonly placeholder="填写发生地点后点击「解析坐标」" />
  155. <el-button :loading="geoLoading" @click="handleGeocode">解析坐标</el-button>
  156. </div>
  157. </el-form-item>
  158. <el-form-item label="发生时间" prop="happenTime">
  159. <el-date-picker v-model="addForm.happenTime" type="datetime" placeholder="选择日期时间" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" />
  160. </el-form-item>
  161. <el-form-item label="事件描述" prop="description">
  162. <el-input v-model="addForm.description" type="textarea" :rows="3" placeholder="请输入事件描述" />
  163. </el-form-item>
  164. <el-form-item label="接报人" prop="reporter">
  165. <el-input v-model="addForm.reporter" readonly placeholder="自动取当前录入人员" />
  166. </el-form-item>
  167. <el-form-item label="接报时间" prop="reportTime">
  168. <!-- 默认当前时间,可选填:清空后由后端兜底为保存时刻 -->
  169. <el-date-picker v-model="addForm.reportTime" type="datetime" placeholder="默认当前时间,可修改" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" />
  170. </el-form-item>
  171. <el-form-item label="联系电话" prop="reporterPhone">
  172. <el-input v-model="addForm.reporterPhone" placeholder="请输入上报人联系电话" />
  173. </el-form-item>
  174. <el-form-item label="来源渠道" prop="channel">
  175. <el-select v-model="addForm.channel" placeholder="请选择来源渠道" style="width:100%">
  176. <el-option v-for="d in dis_event_channel" :key="d.value" :label="d.label" :value="d.value" />
  177. </el-select>
  178. </el-form-item>
  179. </el-form>
  180. <template #footer>
  181. <el-button @click="addVisible = false">取消</el-button>
  182. <el-button type="primary" @click="handleSubmit">确认</el-button>
  183. </template>
  184. </el-dialog>
  185. </div>
  186. </template>
  187. <script setup name="Jbgl">
  188. import { ref, computed, nextTick, onMounted } from 'vue'
  189. import { ElMessage, ElMessageBox } from 'element-plus'
  190. import { useDict } from '@/utils/dict'
  191. import useUserStore from '@/store/modules/user'
  192. import { bd09ToWgs84 } from '@/utils/coordTransform'
  193. import { getEventPage, saveEventReport, updateEventInfo, deleteEventInfo, getEventStatusOptions, getEventLogs } from '@/api/dispatch'
  194. import { Search, Refresh } from '@element-plus/icons-vue'
  195. import EventLogTimeline from './EventLogTimeline.vue'
  196. // ==================== 字典 ====================
  197. const { dis_event_source, dis_event_channel, dis_event_type } = useDict('dis_event_source', 'dis_event_channel', 'dis_event_type')
  198. const eventStatusOptions = ref([])
  199. // ==================== 当前录入人员 ====================
  200. // 接报人固定取当前录入人员,不手填(后端落库时同样以当前登录用户为准)
  201. const userStore = useUserStore()
  202. function currentOperatorName() {
  203. return userStore.nickName || userStore.name || ''
  204. }
  205. function dictLabel(list, value) {
  206. if (value === null || value === undefined || value === '') return '-'
  207. const hit = (list || []).find(item => String(item.value) === String(value))
  208. return hit ? hit.label : value
  209. }
  210. // 状态码 -> 标签样式(0 待核实 1 已核实 2 处置中 3 已解除 4 不实事件)
  211. const STATUS_TAG = { 0: 'warning', 1: 'success', 2: 'primary', 3: 'warning', 4: 'info' }
  212. function statusTagType(status) {
  213. return STATUS_TAG[status] || 'info'
  214. }
  215. // ==================== 数据状态 ====================
  216. const tableData = ref([])
  217. const total = ref(0)
  218. const loading = ref(false)
  219. const searchForm = ref({ eventSource: '', dateRange: null, status: '' })
  220. const pagination = ref({ page: 1, pageSize: 10 })
  221. // ==================== 加载数据 ====================
  222. async function loadData() {
  223. loading.value = true
  224. try {
  225. const params = {}
  226. if (searchForm.value.eventSource) params.eventSource = searchForm.value.eventSource
  227. if (searchForm.value.status !== '' && searchForm.value.status !== null) params.status = searchForm.value.status
  228. if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
  229. params.startDate = searchForm.value.dateRange[0]
  230. params.endDate = searchForm.value.dateRange[1]
  231. }
  232. const res = await getEventPage(pagination.value.page, pagination.value.pageSize, params)
  233. tableData.value = res.data?.rows || res.data?.records || res.data || []
  234. total.value = res.data?.total || 0
  235. } catch (error) {
  236. ElMessage.error('加载数据失败:' + (error.message || '未知错误'))
  237. } finally {
  238. loading.value = false
  239. }
  240. }
  241. async function loadStatusOptions() {
  242. try {
  243. const res = await getEventStatusOptions()
  244. eventStatusOptions.value = res.data || []
  245. } catch {
  246. eventStatusOptions.value = []
  247. }
  248. }
  249. onMounted(() => {
  250. loadStatusOptions()
  251. loadData()
  252. })
  253. // ==================== 搜索筛选 ====================
  254. function handleSearch() {
  255. pagination.value.page = 1
  256. loadData()
  257. }
  258. function handleReset() {
  259. searchForm.value = { eventSource: '', dateRange: null, status: '' }
  260. pagination.value.page = 1
  261. loadData()
  262. }
  263. // ==================== 分页 ====================
  264. function handleSizeChange(val) {
  265. pagination.value.pageSize = val
  266. pagination.value.page = 1
  267. loadData()
  268. }
  269. function handleCurrentChange(val) {
  270. pagination.value.page = val
  271. loadData()
  272. }
  273. // ==================== 详情 ====================
  274. const detailVisible = ref(false)
  275. const currentRow = ref(null)
  276. async function handleDetail(row) {
  277. currentRow.value = { ...row, rawLogs: [] }
  278. detailVisible.value = true
  279. try {
  280. const res = await getEventLogs(row.id)
  281. currentRow.value = { ...currentRow.value, rawLogs: res.data || [] }
  282. } catch {
  283. // 加载失败时保留列表行数据
  284. }
  285. }
  286. // ==================== 新增/编辑接报 ====================
  287. const addVisible = ref(false)
  288. const addFormRef = ref(null)
  289. const editingId = ref(null)
  290. // 接报表单初值(含原型要求的「事件坐标」与「联系电话」;接报人自动取当前录入人员)
  291. function blankForm() {
  292. return {
  293. eventSource: '', eventType: '', location: '', longitude: null, latitude: null,
  294. happenTime: '', description: '', reporter: currentOperatorName(), reportTime: nowStr(),
  295. reporterPhone: '', channel: ''
  296. }
  297. }
  298. // 当前时间(yyyy-MM-dd HH:mm:ss),接报时间默认值
  299. function nowStr() {
  300. const d = new Date()
  301. const p = n => String(n).padStart(2, '0')
  302. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
  303. }
  304. const addForm = ref(blankForm())
  305. const addRules = {
  306. eventSource: [{ required: true, message: '请选择事件来源', trigger: 'change' }],
  307. location: [{ required: true, message: '请输入发生地点', trigger: 'blur' }],
  308. happenTime: [{ required: true, message: '请选择发生时间', trigger: 'change' }],
  309. description: [{ required: true, message: '请输入事件描述', trigger: 'blur' }],
  310. reporter: [{ required: true, message: '请输入接报人', trigger: 'blur' }],
  311. reportTime: [],
  312. channel: [{ required: true, message: '请选择来源渠道', trigger: 'change' }]
  313. }
  314. // ==================== 事件坐标 ====================
  315. const geoLoading = ref(false)
  316. const hasCoord = (val) => val !== null && val !== undefined && val !== ''
  317. // 展示口径:经度, 纬度(不带 °N / °E 后缀;库内统一存 WGS84,与管网 / 设备模块同口径)
  318. function formatGeo(lng, lat) {
  319. if (!hasCoord(lng) || !hasCoord(lat)) return '-'
  320. return `${Number(lng).toFixed(6)}, ${Number(lat).toFixed(6)}`
  321. }
  322. const geoText = computed(() => {
  323. const text = formatGeo(addForm.value.longitude, addForm.value.latitude)
  324. return text === '-' ? '' : text
  325. })
  326. // 发生地点 → 坐标:百度 Geocoder 返回 BD-09,落库前转 WGS84
  327. function handleGeocode() {
  328. const address = (addForm.value.location || '').trim()
  329. if (!address) {
  330. ElMessage.warning('请先填写发生地点,再解析坐标')
  331. return
  332. }
  333. const BMapGL = window.BMapGL
  334. if (!BMapGL || typeof BMapGL.Geocoder !== 'function') {
  335. ElMessage.warning('地图服务未就绪,请稍后重试')
  336. return
  337. }
  338. geoLoading.value = true
  339. try {
  340. const geocoder = new BMapGL.Geocoder()
  341. geocoder.getPoint(address, (point) => {
  342. geoLoading.value = false
  343. if (!point) {
  344. ElMessage.warning('地址解析失败,请把发生地点写得更具体后重试')
  345. return
  346. }
  347. const [lng, lat] = bd09ToWgs84(point.lng, point.lat)
  348. addForm.value.longitude = Number(lng.toFixed(6))
  349. addForm.value.latitude = Number(lat.toFixed(6))
  350. ElMessage.success('坐标解析成功:' + formatGeo(addForm.value.longitude, addForm.value.latitude))
  351. })
  352. } catch (error) {
  353. geoLoading.value = false
  354. ElMessage.warning('地址解析失败,请稍后重试')
  355. }
  356. }
  357. function handleAdd() {
  358. editingId.value = null
  359. addForm.value = blankForm()
  360. addVisible.value = true
  361. nextTick(() => addFormRef.value?.clearValidate())
  362. }
  363. function handleEdit(row) {
  364. editingId.value = row.id
  365. addForm.value = {
  366. eventSource: row.eventSource,
  367. eventType: row.eventType,
  368. location: row.location,
  369. longitude: row.longitude,
  370. latitude: row.latitude,
  371. happenTime: row.happenTime,
  372. description: row.description,
  373. // 接报人在接报登记时已按「当前录入人员」确定,编辑环节只读回显、不可改
  374. reporter: row.reporter,
  375. reportTime: row.reportTime,
  376. reporterPhone: row.reporterPhone,
  377. channel: row.channel
  378. }
  379. addVisible.value = true
  380. nextTick(() => addFormRef.value?.clearValidate())
  381. }
  382. function handleSubmit() {
  383. addFormRef.value.validate(async (valid) => {
  384. if (!valid) return
  385. // 事件坐标要落库:保存前若还没解析,明确确认一次,避免静默存进空坐标
  386. if (!hasCoord(addForm.value.longitude) || !hasCoord(addForm.value.latitude)) {
  387. try {
  388. await ElMessageBox.confirm('尚未解析事件坐标,保存后该事件将没有坐标信息,是否继续?', '提示', { type: 'warning' })
  389. } catch {
  390. return
  391. }
  392. }
  393. try {
  394. if (editingId.value) {
  395. await updateEventInfo({ id: editingId.value, ...addForm.value })
  396. ElMessage.success('编辑成功')
  397. } else {
  398. await saveEventReport(addForm.value)
  399. ElMessage.success('新增成功')
  400. }
  401. addVisible.value = false
  402. loadData()
  403. } catch (error) {
  404. ElMessage.error('操作失败:' + (error.message || '未知错误'))
  405. }
  406. })
  407. }
  408. // ==================== 删除 ====================
  409. async function handleDelete(row) {
  410. try {
  411. await ElMessageBox.confirm('确定要删除该接报记录吗?', '删除确认', { type: 'warning' })
  412. await deleteEventInfo(row.id)
  413. ElMessage.success('删除成功')
  414. loadData()
  415. } catch (error) {
  416. if (error !== 'cancel' && error !== 'close') {
  417. ElMessage.error('删除失败:' + (error.message || '未知错误'))
  418. }
  419. }
  420. }
  421. </script>
  422. <style scoped>
  423. .jbgl-page {
  424. padding: 16px;
  425. background: #f5f5f5;
  426. min-height: calc(100vh - 84px);
  427. }
  428. .filter-panel {
  429. background: #fff;
  430. border: 1px solid #dcdfe6;
  431. border-radius: 4px;
  432. padding: 12px 16px;
  433. margin-bottom: 12px;
  434. display: flex;
  435. justify-content: space-between;
  436. align-items: center;
  437. flex-wrap: wrap;
  438. gap: 12px;
  439. }
  440. .filter-form {
  441. display: flex;
  442. align-items: center;
  443. gap: 0 12px;
  444. flex-wrap: wrap;
  445. }
  446. /* 若依风格查询栏:内联表单项去默认外边距,间距统一交给 gap */
  447. .filter-form :deep(.el-form-item) {
  448. margin-right: 0;
  449. margin-bottom: 0;
  450. }
  451. .table-panel {
  452. background: #fff;
  453. border: 1px solid #dcdfe6;
  454. border-radius: 4px;
  455. padding: 16px;
  456. }
  457. .pagination-wrap {
  458. margin-top: 16px;
  459. display: flex;
  460. justify-content: flex-end;
  461. }
  462. .detail-content {
  463. padding: 0 8px;
  464. }
  465. .desc-text {
  466. background: #f5f7fa;
  467. border-radius: 4px;
  468. padding: 10px;
  469. font-size: 13px;
  470. color: #606266;
  471. line-height: 1.6;
  472. max-height: 120px;
  473. overflow-y: auto;
  474. }
  475. :deep(.el-form-item__label) {
  476. color: #909399;
  477. }
  478. :deep(.el-form-item__content) {
  479. color: #303133;
  480. }
  481. </style>