HazardOperationPage.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <div class="app-container">
  3. <el-form ref="queryRef" :inline="true" :model="queryParams" v-show="showSearch" label-width="80px">
  4. <el-form-item label="关键词" prop="keyword">
  5. <el-input
  6. v-model="queryParams.keyword"
  7. clearable
  8. placeholder="隐患编号/描述"
  9. style="width: 220px"
  10. @keyup.enter="handleSearch"
  11. />
  12. </el-form-item>
  13. <el-form-item label="隐患类型" prop="hazardType">
  14. <el-select v-model="queryParams.hazardType" clearable placeholder="全部" style="width: 150px">
  15. <el-option v-for="item in hazardTypeDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
  16. </el-select>
  17. </el-form-item>
  18. <el-form-item label="隐患等级" prop="hazardLevel">
  19. <el-select v-model="queryParams.hazardLevel" clearable placeholder="全部" style="width: 140px">
  20. <el-option v-for="item in hazardLevelDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
  21. </el-select>
  22. </el-form-item>
  23. <el-form-item label="隐患来源" prop="hazardSource">
  24. <el-select v-model="queryParams.hazardSource" clearable placeholder="全部" style="width: 160px">
  25. <el-option v-for="item in hazardSourceDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
  26. </el-select>
  27. </el-form-item>
  28. <el-form-item label="所属片区" prop="district">
  29. <el-select v-model="queryParams.district" clearable placeholder="全部" style="width: 140px">
  30. <el-option v-for="item in districtDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
  31. </el-select>
  32. </el-form-item>
  33. <el-form-item v-if="mode === 'submission'" label="治理状态" prop="hazardStatus">
  34. <el-select v-model="queryParams.hazardStatus" clearable placeholder="全部" style="width: 140px">
  35. <el-option v-for="item in hazardStatusDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
  36. </el-select>
  37. </el-form-item>
  38. <el-form-item v-if="mode === 'feedback'" label="反馈状态" prop="feedbackStatus">
  39. <el-select v-model="queryParams.feedbackStatus" clearable placeholder="全部" style="width: 140px">
  40. <el-option label="未反馈" value="0" />
  41. <el-option label="已反馈" value="1" />
  42. </el-select>
  43. </el-form-item>
  44. <el-form-item label="时间范围">
  45. <el-date-picker
  46. v-model="dateRange"
  47. type="daterange"
  48. value-format="YYYY-MM-DD"
  49. range-separator="至"
  50. start-placeholder="开始日期"
  51. end-placeholder="结束日期"
  52. style="width: 250px"
  53. />
  54. </el-form-item>
  55. <el-form-item>
  56. <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
  57. <el-button icon="Refresh" @click="handleReset">重置</el-button>
  58. </el-form-item>
  59. </el-form>
  60. <el-row :gutter="10" class="mb8">
  61. <right-toolbar v-model:showSearch="showSearch" @queryTable="loadData" />
  62. </el-row>
  63. <el-table v-loading="loading" :data="tableData">
  64. <el-table-column prop="hazardNo" label="隐患编号" width="155" show-overflow-tooltip fixed="left" />
  65. <el-table-column label="隐患类型" width="115" align="center">
  66. <template #default="{ row }">{{ getDictLabel(hazardTypeDict, row.hazardType) }}</template>
  67. </el-table-column>
  68. <el-table-column label="隐患等级" width="105" align="center">
  69. <template #default="{ row }">
  70. <el-tag :type="levelTag(row.hazardLevel)">{{ getDictLabel(hazardLevelDict, row.hazardLevel) }}</el-tag>
  71. </template>
  72. </el-table-column>
  73. <el-table-column prop="hazardDesc" label="隐患描述" min-width="220" show-overflow-tooltip />
  74. <el-table-column label="隐患来源" width="125" align="center">
  75. <template #default="{ row }">{{ getDictLabel(hazardSourceDict, row.hazardSource) || '-' }}</template>
  76. </el-table-column>
  77. <el-table-column prop="district" label="所属片区" width="100" align="center">
  78. <template #default="{ row }">{{ getDictLabel(districtDict, row.district) || '-' }}</template>
  79. </el-table-column>
  80. <el-table-column prop="reportTime" label="上报时间" width="165" align="center" />
  81. <template v-if="mode === 'feedback'">
  82. <el-table-column label="反馈状态" width="110" align="center">
  83. <template #default="{ row }">
  84. <el-tag :type="hasFeedback(row) ? 'success' : 'info'">{{ hasFeedback(row) ? '已反馈' : '未反馈' }}</el-tag>
  85. </template>
  86. </el-table-column>
  87. <el-table-column prop="rectifyFeedback" label="治理反馈" min-width="180" show-overflow-tooltip>
  88. <template #default="{ row }">{{ row.rectifyFeedback || '-' }}</template>
  89. </el-table-column>
  90. </template>
  91. <el-table-column label="操作" :width="mode === 'submission' ? 150 : 250" fixed="right" align="center" class-name="small-padding fixed-width">
  92. <template #default="{ row }">
  93. <el-button link type="primary" icon="View" @click="handleDetail(row)">详情</el-button>
  94. <el-button
  95. v-if="mode === 'submission'"
  96. link
  97. type="success"
  98. icon="Promotion"
  99. :disabled="isSubmitted(row)"
  100. @click="handleSubmit(row)"
  101. >
  102. {{ isSubmitted(row) ? '已报送' : '报送' }}
  103. </el-button>
  104. <template v-else>
  105. <el-button
  106. link
  107. :type="hasFeedback(row) ? 'success' : 'warning'"
  108. icon="Edit"
  109. :disabled="hasFeedback(row)"
  110. @click="handleFeedback(row)"
  111. >
  112. {{ hasFeedback(row) ? '已反馈' : '治理反馈' }}
  113. </el-button>
  114. <el-button v-if="row.workOrderId" link type="primary" icon="Tickets" @click="handleViewOrder(row)">查看工单</el-button>
  115. </template>
  116. </template>
  117. </el-table-column>
  118. </el-table>
  119. <pagination
  120. v-show="total > 0"
  121. :total="total"
  122. v-model:page="pageNum"
  123. v-model:limit="pageSize"
  124. :page-sizes="[10, 20, 50]"
  125. @pagination="loadData"
  126. />
  127. <el-drawer v-model="detailVisible" title="隐患详情" size="760px" direction="rtl">
  128. <el-descriptions v-if="detailData" :column="2" border>
  129. <el-descriptions-item label="隐患编号">{{ detailData.hazardNo }}</el-descriptions-item>
  130. <el-descriptions-item label="隐患类型">{{ getDictLabel(hazardTypeDict, detailData.hazardType) }}</el-descriptions-item>
  131. <el-descriptions-item label="隐患等级">
  132. <el-tag :type="levelTag(detailData.hazardLevel)">{{ getDictLabel(hazardLevelDict, detailData.hazardLevel) }}</el-tag>
  133. </el-descriptions-item>
  134. <el-descriptions-item label="隐患来源">{{ getDictLabel(hazardSourceDict, detailData.hazardSource) || '-' }}</el-descriptions-item>
  135. <el-descriptions-item label="所属片区">{{ getDictLabel(districtDict, detailData.district) || '-' }}</el-descriptions-item>
  136. <el-descriptions-item label="治理状态" v-if="mode === 'submission'">
  137. <el-tag :type="statusTag(detailData.hazardStatus)">{{ getDictLabel(hazardStatusDict, detailData.hazardStatus) }}</el-tag>
  138. </el-descriptions-item>
  139. <el-descriptions-item label="反馈状态" v-else>
  140. <el-tag :type="hasFeedback(detailData) ? 'success' : 'info'">{{ hasFeedback(detailData) ? '已反馈' : '未反馈' }}</el-tag>
  141. </el-descriptions-item>
  142. <el-descriptions-item label="报送状态">
  143. <el-tag :type="isSubmitted(detailData) ? 'success' : 'info'">{{ isSubmitted(detailData) ? '已报送' : '未报送' }}</el-tag>
  144. </el-descriptions-item>
  145. <el-descriptions-item v-if="mode === 'feedback'" label="关联工单" :span="2">
  146. <template v-if="detailOrder">
  147. <el-tag :type="orderStatusType(detailOrder.orderStatus)" class="order-tag">{{ orderStatusText(detailOrder.orderStatus) }}</el-tag>
  148. <span class="order-no">{{ detailOrder.orderNo || detailOrder.orderId }}</span>
  149. </template>
  150. <span v-else-if="detailData.workOrderId">工单ID:{{ detailData.workOrderId }}</span>
  151. <span v-else>-</span>
  152. </el-descriptions-item>
  153. <el-descriptions-item label="上报时间">{{ detailData.reportTime || '-' }}</el-descriptions-item>
  154. <el-descriptions-item label="报送时间">{{ detailData.submitTime || '-' }}</el-descriptions-item>
  155. <el-descriptions-item v-if="mode === 'submission'" label="核查人">{{ detailData.verifyPerson || '-' }}</el-descriptions-item>
  156. <el-descriptions-item v-if="mode === 'submission'" label="核查时间">{{ detailData.verifyTime || '-' }}</el-descriptions-item>
  157. <el-descriptions-item v-if="mode === 'submission'" label="整改责任人">{{ detailData.rectifyPerson || '-' }}</el-descriptions-item>
  158. <el-descriptions-item v-if="mode === 'submission'" label="整改期限">{{ detailData.rectifyDeadline || '-' }}</el-descriptions-item>
  159. <el-descriptions-item label="隐患描述" :span="2">{{ detailData.hazardDesc || '-' }}</el-descriptions-item>
  160. <el-descriptions-item label="整改方案" :span="2">{{ detailData.rectifyPlan || '-' }}</el-descriptions-item>
  161. <el-descriptions-item label="治理反馈" :span="2">{{ detailData.rectifyFeedback || '-' }}</el-descriptions-item>
  162. <el-descriptions-item label="隐患图片" :span="2">
  163. <div v-if="detailImageUrls.length" class="detail-images">
  164. <el-image
  165. v-for="(image, index) in detailImageUrls"
  166. :key="image"
  167. :src="image"
  168. :preview-src-list="detailImageUrls"
  169. :initial-index="index"
  170. preview-teleported
  171. fit="contain"
  172. lazy
  173. />
  174. </div>
  175. <span v-else>-</span>
  176. </el-descriptions-item>
  177. <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
  178. </el-descriptions>
  179. </el-drawer>
  180. <el-dialog v-model="feedbackVisible" title="隐患治理反馈" width="720px" append-to-body destroy-on-close>
  181. <el-form ref="feedbackFormRef" :model="feedbackForm" :rules="feedbackRules" label-width="105px">
  182. <el-alert
  183. :title="`${feedbackForm.hazardNo || ''} ${feedbackForm.hazardDesc || ''}`"
  184. type="info"
  185. :closable="false"
  186. class="feedback-alert"
  187. />
  188. <!-- 关联工单信息:辅助填写治理反馈内容 -->
  189. <div v-if="feedbackOrder || feedbackOrderLoading" v-loading="feedbackOrderLoading" class="feedback-order">
  190. <div class="feedback-order-title">关联工单信息</div>
  191. <el-descriptions v-if="feedbackOrder" :column="2" border size="small">
  192. <el-descriptions-item label="工单编号">{{ feedbackOrder.orderNo || feedbackOrder.orderId || '-' }}</el-descriptions-item>
  193. <el-descriptions-item label="工单状态">
  194. <el-tag :type="orderStatusType(feedbackOrder.orderStatus)" size="small">{{ orderStatusText(feedbackOrder.orderStatus) }}</el-tag>
  195. </el-descriptions-item>
  196. <el-descriptions-item label="设备名称">{{ feedbackOrder.deviceName || '-' }}</el-descriptions-item>
  197. <el-descriptions-item label="处置人">{{ feedbackOrder.receiveUserName || '-' }}</el-descriptions-item>
  198. <el-descriptions-item label="故障发生时间">{{ feedbackOrder.faultTime || '-' }}</el-descriptions-item>
  199. <el-descriptions-item label="计划完成时间">{{ feedbackOrder.planFinishTime || '-' }}</el-descriptions-item>
  200. <el-descriptions-item label="实际完成时间">{{ feedbackOrder.realFinishTime || '-' }}</el-descriptions-item>
  201. <el-descriptions-item label="核验人">{{ feedbackOrder.verifyUserName || '-' }}</el-descriptions-item>
  202. <el-descriptions-item label="问题描述" :span="2">{{ feedbackOrder.orderDesc || '-' }}</el-descriptions-item>
  203. <el-descriptions-item label="维修处置结果" :span="2">{{ feedbackOrder.repairResult || '-' }}</el-descriptions-item>
  204. </el-descriptions>
  205. </div>
  206. <el-form-item label="治理反馈" prop="rectifyFeedback">
  207. <el-input v-model="feedbackForm.rectifyFeedback" type="textarea" :rows="5" placeholder="请输入治理过程、处理结果及现场复核情况" />
  208. </el-form-item>
  209. </el-form>
  210. <template #footer>
  211. <div class="dialog-footer">
  212. <el-button type="primary" :loading="saving" @click="saveFeedback">提交反馈</el-button>
  213. <el-button @click="feedbackVisible = false">取 消</el-button>
  214. </div>
  215. </template>
  216. </el-dialog>
  217. <el-dialog v-model="orderVisible" title="关联工单信息" width="720px" append-to-body destroy-on-close>
  218. <el-descriptions v-if="orderData" :column="2" border v-loading="orderLoading">
  219. <el-descriptions-item label="工单编号">{{ orderData.orderNo || '-' }}</el-descriptions-item>
  220. <el-descriptions-item label="工单状态">
  221. <el-tag :type="orderStatusType(orderData.orderStatus)">{{ orderStatusText(orderData.orderStatus) }}</el-tag>
  222. </el-descriptions-item>
  223. <el-descriptions-item label="设备名称">{{ orderData.deviceName || '-' }}</el-descriptions-item>
  224. <el-descriptions-item label="设备编码">{{ orderData.deviceCode || '-' }}</el-descriptions-item>
  225. <el-descriptions-item label="处置人">{{ orderData.receiveUserName || '-' }}</el-descriptions-item>
  226. <el-descriptions-item label="核验人">{{ orderData.verifyUserName || '-' }}</el-descriptions-item>
  227. <el-descriptions-item label="故障发生时间">{{ orderData.faultTime || '-' }}</el-descriptions-item>
  228. <el-descriptions-item label="计划完成时间">{{ orderData.planFinishTime || '-' }}</el-descriptions-item>
  229. <el-descriptions-item label="实际完成时间">{{ orderData.realFinishTime || '-' }}</el-descriptions-item>
  230. <el-descriptions-item label="问题描述" :span="2">{{ orderData.orderDesc || '-' }}</el-descriptions-item>
  231. <el-descriptions-item label="维修处置结果" :span="2">{{ orderData.repairResult || '-' }}</el-descriptions-item>
  232. </el-descriptions>
  233. <el-empty v-else-if="!orderLoading" description="工单信息加载失败" />
  234. </el-dialog>
  235. </div>
  236. </template>
  237. <script setup>
  238. import { computed, onMounted, reactive, ref } from 'vue'
  239. import { ElMessage, ElMessageBox } from 'element-plus'
  240. import { getDicts } from '@/api/system/dict/data'
  241. import { getHazardById, getHazardPage, submitHazard, feedbackHazard } from '@/api/pipeNetwork/hazard'
  242. import { getWorkOrder } from '@/api/pipeNetwork/ops/workOrder'
  243. import { orderStatusText, orderStatusType } from '@/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
  244. import { buildMinioAttachmentUrl } from '@/utils/minioAttachment'
  245. const props = defineProps({
  246. mode: {
  247. type: String,
  248. required: true,
  249. validator: value => ['submission', 'feedback'].includes(value)
  250. }
  251. })
  252. const loading = ref(false)
  253. const saving = ref(false)
  254. const showSearch = ref(true)
  255. const queryRef = ref(null)
  256. const tableData = ref([])
  257. const pageNum = ref(1)
  258. const pageSize = ref(10)
  259. const total = ref(0)
  260. const dateRange = ref([])
  261. const queryParams = reactive({
  262. keyword: '',
  263. hazardType: '',
  264. hazardLevel: '',
  265. hazardSource: '',
  266. district: '',
  267. hazardStatus: '',
  268. // 报送页固定只展示未报送隐患;治理反馈页固定只看已报送数据。
  269. isSubmitted: props.mode === 'feedback' ? 1 : 0,
  270. // 反馈状态筛选(仅治理反馈页):0-未反馈 1-已反馈
  271. feedbackStatus: '',
  272. timeType: 'report'
  273. })
  274. const hazardTypeDict = ref([])
  275. const hazardLevelDict = ref([])
  276. const hazardStatusDict = ref([])
  277. const hazardSourceDict = ref([])
  278. // 所属片区,与字典 drainage_district、DistrictEnum、equipment_base.district 同口径(0城南/1城北/2太常)
  279. const districtDict = ref([])
  280. const detailVisible = ref(false)
  281. const detailData = ref(null)
  282. const detailImageUrls = computed(() => parseImageUrls(detailData.value?.hazardImages))
  283. const feedbackVisible = ref(false)
  284. const feedbackFormRef = ref(null)
  285. const feedbackRecord = ref(null)
  286. // 反馈弹窗内展示的关联工单信息(辅助填写治理反馈)
  287. const feedbackOrder = ref(null)
  288. const feedbackOrderLoading = ref(false)
  289. const orderVisible = ref(false)
  290. const orderLoading = ref(false)
  291. const orderData = ref(null)
  292. const detailOrder = ref(null)
  293. const feedbackForm = reactive({
  294. hazardNo: '',
  295. hazardDesc: '',
  296. rectifyFeedback: ''
  297. })
  298. const feedbackRules = {
  299. rectifyFeedback: [{ required: true, message: '请输入治理反馈', trigger: 'blur' }]
  300. }
  301. function getDictLabel(dict, value) {
  302. const item = dict.find(entry => String(entry.dictValue) === String(value))
  303. return item ? item.dictLabel : value
  304. }
  305. function isSubmitted(row) {
  306. return Number(row?.isSubmitted) === 1
  307. }
  308. // 反馈状态判定:治理反馈内容已填写即视为已反馈(与隐患一张图口径一致)
  309. function hasFeedback(row) {
  310. return !!(row?.rectifyFeedback && String(row.rectifyFeedback).trim())
  311. }
  312. function levelTag(value) {
  313. // 与报警等级口径一致:1 一级(红 danger) / 2 二级(橙 warning) / 3 三级(蓝 默认色) / 4 四级(绿 success)
  314. const map = { '1': 'danger', '2': 'warning', '3': '', '4': 'success' }
  315. return map[String(value)] || 'info'
  316. }
  317. function statusTag(value) {
  318. const label = String(getDictLabel(hazardStatusDict.value, value) || '')
  319. if (label.includes('已整改') || label.includes('闭环')) return 'success'
  320. if (label.includes('整改中') || label.includes('治理中')) return 'warning'
  321. return 'info'
  322. }
  323. function parseImageUrls(value) {
  324. if (!value) return []
  325. let values = value
  326. if (typeof values === 'string') {
  327. const text = values.trim()
  328. if (text.startsWith('[') || text.startsWith('{')) {
  329. try {
  330. values = JSON.parse(text)
  331. } catch (error) {
  332. values = text.split(/[,;\n]/)
  333. }
  334. } else {
  335. values = text.split(/[,;\n]/)
  336. }
  337. }
  338. if (!Array.isArray(values)) values = [values]
  339. return values.map(item => {
  340. const source = typeof item === 'object' ? (item.url || item.fileName || item.name) : item
  341. return normalizeImageUrl(source)
  342. }).filter(Boolean)
  343. }
  344. function normalizeImageUrl(value) {
  345. const url = String(value || '').trim()
  346. if (!url || /^(blob:|data:)/i.test(url)) return url
  347. const minioBase = String(import.meta.env.VITE_APP_MINIO_BASE_URL || '').trim().replace(/\/+$/, '')
  348. if (/^https?:\/\//i.test(url)) return buildMinioAttachmentUrl(url)
  349. if (minioBase) {
  350. if (url.startsWith('/')) return buildMinioAttachmentUrl(url)
  351. if (url.startsWith('upload/')) return `${minioBase}/${url}`
  352. if (!url.includes('/')) return `${minioBase}/upload/${url}`
  353. }
  354. const baseApi = String(import.meta.env.VITE_APP_BASE_API || '').trim().replace(/\/+$/, '')
  355. return url.startsWith('/') ? `${baseApi}${url}` : `${baseApi}/${url}`
  356. }
  357. function buildQuery() {
  358. const params = {}
  359. Object.entries(queryParams).forEach(([key, value]) => {
  360. if (value !== '' && value !== null && value !== undefined) params[key] = value
  361. })
  362. if (dateRange.value?.length === 2) {
  363. params.startDate = dateRange.value[0]
  364. params.endDate = dateRange.value[1]
  365. }
  366. if (props.mode === 'submission') {
  367. // 报送页固定只展示未报送数据
  368. params.timeType = 'report'
  369. params.isSubmitted = 0
  370. } else {
  371. // 治理反馈页固定只展示已报送数据
  372. params.timeType = 'report'
  373. params.isSubmitted = 1
  374. }
  375. return params
  376. }
  377. async function loadDicts() {
  378. try {
  379. const [typeRes, levelRes, statusRes, sourceRes, districtRes] = await Promise.all([
  380. getDicts('hazard_type'),
  381. getDicts('hazard_level'),
  382. getDicts('hazard_status'),
  383. getDicts('hazard_source'),
  384. getDicts('drainage_district')
  385. ])
  386. hazardTypeDict.value = typeRes.data || []
  387. hazardLevelDict.value = levelRes.data || []
  388. hazardStatusDict.value = statusRes.data || []
  389. hazardSourceDict.value = sourceRes.data || []
  390. districtDict.value = districtRes.data || []
  391. } catch (error) {
  392. ElMessage.error(error?.message || '隐患字典加载失败')
  393. }
  394. }
  395. async function loadData() {
  396. loading.value = true
  397. try {
  398. const response = await getHazardPage(pageNum.value, pageSize.value, buildQuery())
  399. tableData.value = response.data?.records || []
  400. total.value = Number(response.data?.total || 0)
  401. } catch (error) {
  402. ElMessage.error(error?.response?.data?.msg || error?.message || '隐患数据加载失败')
  403. } finally {
  404. loading.value = false
  405. }
  406. }
  407. function handleSearch() {
  408. pageNum.value = 1
  409. loadData()
  410. }
  411. function handleReset() {
  412. queryRef.value?.resetFields()
  413. Object.assign(queryParams, {
  414. keyword: '',
  415. hazardType: '',
  416. hazardLevel: '',
  417. hazardSource: '',
  418. district: '',
  419. hazardStatus: '',
  420. isSubmitted: props.mode === 'feedback' ? 1 : 0,
  421. feedbackStatus: '',
  422. timeType: 'report'
  423. })
  424. dateRange.value = []
  425. handleSearch()
  426. }
  427. async function fetchDetail(row) {
  428. try {
  429. const response = await getHazardById(row.hazardId)
  430. return response.data || row
  431. } catch (error) {
  432. return row
  433. }
  434. }
  435. async function handleDetail(row) {
  436. detailData.value = await fetchDetail(row)
  437. detailOrder.value = null
  438. detailVisible.value = true
  439. if (props.mode === 'feedback' && detailData.value?.workOrderId) {
  440. try {
  441. const response = await getWorkOrder(detailData.value.workOrderId)
  442. // 详情接口返回 { order, logs },工单字段在 order 里
  443. detailOrder.value = response.data?.order || null
  444. } catch (error) {
  445. detailOrder.value = null
  446. }
  447. }
  448. }
  449. async function handleSubmit(row) {
  450. try {
  451. await ElMessageBox.confirm(
  452. `确认将隐患“${row.hazardNo}”报送到上级主管单位?报送成功后将自动生成工单并流转至工单系统。`,
  453. '隐患报送确认',
  454. { type: 'warning', confirmButtonText: '确认报送', cancelButtonText: '取消' }
  455. )
  456. const response = await submitHazard(row.hazardId)
  457. ElMessage.success(response.data ? '报送成功,已自动创建工单' : '报送成功')
  458. loadData()
  459. } catch (error) {
  460. if (error === 'cancel' || error === 'close') return
  461. ElMessage.error(error?.response?.data?.msg || error?.message || '报送失败')
  462. }
  463. }
  464. function handleViewOrder(row) {
  465. if (!row?.workOrderId) return
  466. orderVisible.value = true
  467. orderLoading.value = true
  468. orderData.value = null
  469. getWorkOrder(row.workOrderId)
  470. .then(response => {
  471. // 详情接口返回 { order, logs },工单字段在 order 里
  472. orderData.value = response.data?.order || null
  473. })
  474. .catch(error => {
  475. ElMessage.error(error?.response?.data?.msg || error?.message || '工单信息加载失败')
  476. })
  477. .finally(() => {
  478. orderLoading.value = false
  479. })
  480. }
  481. async function handleFeedback(row) {
  482. const record = await fetchDetail(row)
  483. if (hasFeedback(record)) {
  484. ElMessage.warning('该隐患已反馈,不允许二次修改')
  485. return
  486. }
  487. feedbackRecord.value = record
  488. Object.assign(feedbackForm, {
  489. hazardNo: record.hazardNo || '',
  490. hazardDesc: record.hazardDesc || '',
  491. rectifyFeedback: ''
  492. })
  493. feedbackVisible.value = true
  494. // 拉取关联工单,展示在反馈表单内辅助填写
  495. feedbackOrder.value = null
  496. if (record.workOrderId) {
  497. feedbackOrderLoading.value = true
  498. getWorkOrder(record.workOrderId)
  499. .then(response => {
  500. // 详情接口返回 { order, logs },工单字段在 order 里
  501. feedbackOrder.value = response.data?.order || null
  502. })
  503. .catch(() => {
  504. feedbackOrder.value = null
  505. })
  506. .finally(() => {
  507. feedbackOrderLoading.value = false
  508. })
  509. }
  510. }
  511. async function saveFeedback() {
  512. if (!feedbackFormRef.value || !feedbackRecord.value) return
  513. try {
  514. await feedbackFormRef.value.validate()
  515. saving.value = true
  516. // 仅提交反馈内容,反馈后不允许二次修改
  517. await feedbackHazard(feedbackRecord.value.hazardId, feedbackForm.rectifyFeedback)
  518. ElMessage.success('反馈提交成功')
  519. feedbackVisible.value = false
  520. loadData()
  521. } catch (error) {
  522. if (error?.fields) return
  523. ElMessage.error(error?.response?.data?.msg || error?.message || '治理反馈提交失败')
  524. } finally {
  525. saving.value = false
  526. }
  527. }
  528. onMounted(async () => {
  529. await loadDicts()
  530. loadData()
  531. })
  532. </script>
  533. <style scoped>
  534. .detail-images {
  535. display: flex;
  536. flex-wrap: wrap;
  537. gap: 10px;
  538. }
  539. .detail-images .el-image {
  540. width: 110px;
  541. height: 110px;
  542. border-radius: 6px;
  543. }
  544. .feedback-alert {
  545. margin-bottom: 20px;
  546. }
  547. .feedback-order {
  548. margin-bottom: 20px;
  549. }
  550. .feedback-order-title {
  551. font-size: 14px;
  552. font-weight: 600;
  553. color: var(--el-text-color-primary);
  554. margin-bottom: 8px;
  555. }
  556. .order-tag {
  557. margin-right: 8px;
  558. }
  559. .order-no {
  560. color: var(--el-text-color-primary);
  561. }
  562. .dialog-footer {
  563. display: flex;
  564. align-items: center;
  565. justify-content: flex-end;
  566. gap: 10px;
  567. }
  568. </style>