DrainageWorkOrder.vue 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  1. <!-- 排水管网工单管理 -->
  2. <template>
  3. <div class="drainage-work-order">
  4. <!-- 顶部搜索区 -->
  5. <el-card class="search-card" shadow="never">
  6. <div class="search-form">
  7. <el-form :model="queryParams" ref="queryFormRef" :inline>
  8. <el-form-item label="类型">
  9. <el-select v-model="queryParams.orderType" placeholder="全部类型" clearable style="width: 150px">
  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.orderLevel" placeholder="全部优先级" clearable style="width: 120px">
  17. <el-option label="紧急" :value="1" />
  18. <el-option label="一般" :value="2" />
  19. <el-option label="低" :value="3" />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item label="状态">
  23. <el-select v-model="queryParams.orderStatus" placeholder="全部状态" clearable style="width: 120px">
  24. <el-option label="待派单" :value="1" />
  25. <el-option label="待接单" :value="2" />
  26. <el-option label="已接单" :value="3" />
  27. <el-option label="处理中" :value="4" />
  28. <el-option label="已提交验收" :value="5" />
  29. <el-option label="已结案" :value="6" />
  30. <el-option label="已驳回" :value="7" />
  31. </el-select>
  32. </el-form-item>
  33. <el-form-item label="关键词">
  34. <el-input
  35. v-model="queryParams.keyword"
  36. placeholder="工单编号/设备编码"
  37. clearable
  38. style="width: 200px"
  39. />
  40. </el-form-item>
  41. <el-form-item>
  42. <el-button type="primary" @click="handleSearch">查询</el-button>
  43. <el-button @click="handleReset">重置</el-button>
  44. </el-form-item>
  45. </el-form>
  46. </div>
  47. </el-card>
  48. <!-- 主容器 -->
  49. <el-row :gutter="16" style="height: calc(100vh - 200px)">
  50. <!-- 左侧:工单列表 -->
  51. <el-col :span="8" style="height: 100%">
  52. <el-card class="box-card" shadow="always">
  53. <template #header>
  54. <div class="card-header">
  55. <span>工单列表</span>
  56. </div>
  57. </template>
  58. <!-- 工单列表 -->
  59. <el-scrollbar style="flex: 1">
  60. <div class="order-list">
  61. <div
  62. v-for="order in tableData"
  63. :key="order.orderId"
  64. class="order-item"
  65. :class="{ 'active': selectedOrderId === order.orderId }"
  66. @click="handleSelectOrder(order)"
  67. >
  68. <div class="order-header">
  69. <span class="order-no">{{ order.orderNo }}</span>
  70. <el-tag
  71. v-if="order.orderStatus === 1"
  72. type="warning"
  73. size="small"
  74. effect="light"
  75. class="status-badge"
  76. >待派单</el-tag>
  77. <el-tag
  78. v-else-if="order.orderStatus === 2"
  79. type="info"
  80. size="small"
  81. effect="light"
  82. class="status-badge"
  83. >待接单</el-tag>
  84. <el-tag
  85. v-else-if="order.orderStatus === 3"
  86. type="success"
  87. size="small"
  88. effect="light"
  89. class="status-badge"
  90. >已接单</el-tag>
  91. <el-tag
  92. v-else-if="order.orderStatus === 4"
  93. type="danger"
  94. size="small"
  95. effect="light"
  96. class="status-badge"
  97. >处理中</el-tag>
  98. <el-tag
  99. v-else-if="order.orderStatus === 5"
  100. type="success"
  101. size="small"
  102. effect="light"
  103. class="status-badge"
  104. >已提交验收</el-tag>
  105. <el-tag
  106. v-else-if="order.orderStatus === 6"
  107. type="info"
  108. size="small"
  109. effect="light"
  110. class="status-badge"
  111. >已结案</el-tag>
  112. <el-tag
  113. v-else-if="order.orderStatus === 7"
  114. type="danger"
  115. size="small"
  116. effect="light"
  117. class="status-badge"
  118. >已驳回</el-tag>
  119. </div>
  120. <div class="order-body">
  121. <div class="order-tags">
  122. <el-tag :type="getLevelTag(order.orderLevel)" size="small" effect="plain">{{ getLevelLabel(order.orderLevel) }}</el-tag>
  123. <el-tag :type="getTypeTag(order.orderType)" size="small" effect="plain">{{ getTypeLabel(order.orderType) }}</el-tag>
  124. </div>
  125. <div class="order-desc" v-if="order.orderDesc">{{ order.orderDesc }}</div>
  126. <div class="device-code">设备编号:{{ order.deviceCode || '-' }}</div>
  127. <div class="create-time">创建:{{ formatDate(order.createTime) }}</div>
  128. </div>
  129. </div>
  130. <el-empty v-if="tableData.length === 0" description="暂无数据" :image-size="100"></el-empty>
  131. </div>
  132. </el-scrollbar>
  133. <!-- 分页 -->
  134. <div class="pagination-wrapper">
  135. <el-pagination
  136. v-model:current-page="pageNum"
  137. v-model:page-size="pageSize"
  138. :total="total"
  139. layout="total, prev, pager, next"
  140. :page-sizes="[10, 20, 50, 100]"
  141. @size-change="handleSizeChange"
  142. @current-change="handlePageChange"
  143. class="pagination"
  144. />
  145. </div>
  146. </el-card>
  147. </el-col>
  148. <!-- 右侧:工单详情 -->
  149. <el-col :span="16" style="height: 100%">
  150. <el-card v-loading="detailLoading" class="box-card" shadow="always">
  151. <template #header>
  152. <div class="card-header">
  153. <span v-if="selectedOrder && selectedOrder.orderId">工单详情 - {{ selectedOrder.orderNo }}</span>
  154. <span v-else>请选择工单查看详情</span>
  155. </div>
  156. </template>
  157. <div v-if="selectedOrder && selectedOrder.orderId">
  158. <!-- 标签页 -->
  159. <el-tabs v-model="activeTab" class="detail-tabs">
  160. <!-- 工单信息 -->
  161. <el-tab-pane label="工单信息" name="info">
  162. <el-descriptions :column="2" border size="small">
  163. <el-descriptions-item label="工单编号">{{ selectedOrder.orderNo }}</el-descriptions-item>
  164. <el-descriptions-item label="工单状态">
  165. <el-tag
  166. v-if="selectedOrder.orderStatus === 1"
  167. type="warning"
  168. size="small"
  169. >待派单</el-tag>
  170. <el-tag
  171. v-else-if="selectedOrder.orderStatus === 2"
  172. type="info"
  173. size="small"
  174. >待接单</el-tag>
  175. <el-tag
  176. v-else-if="selectedOrder.orderStatus === 3"
  177. type="success"
  178. size="small"
  179. >已接单</el-tag>
  180. <el-tag
  181. v-else-if="selectedOrder.orderStatus === 4"
  182. type="danger"
  183. size="small"
  184. >处理中</el-tag>
  185. <el-tag
  186. v-else-if="selectedOrder.orderStatus === 5"
  187. type="success"
  188. size="small"
  189. >已提交验收</el-tag>
  190. <el-tag
  191. v-else-if="selectedOrder.orderStatus === 6"
  192. type="info"
  193. size="small"
  194. >已结案</el-tag>
  195. <el-tag
  196. v-else-if="selectedOrder.orderStatus === 7"
  197. type="danger"
  198. size="small"
  199. >已驳回</el-tag>
  200. </el-descriptions-item>
  201. <el-descriptions-item label="关联案件编号">{{ selectedOrder.caseNo || '-' }}</el-descriptions-item>
  202. <el-descriptions-item label="设备编号">{{ selectedOrder.deviceCode || '-' }}</el-descriptions-item>
  203. <el-descriptions-item label="优先级">
  204. <el-tag
  205. v-if="selectedOrder.orderLevel === 1"
  206. type="danger"
  207. size="small"
  208. >紧急</el-tag>
  209. <el-tag
  210. v-else-if="selectedOrder.orderLevel === 2"
  211. type="warning"
  212. size="small"
  213. >一般</el-tag>
  214. <el-tag
  215. v-else
  216. type="info"
  217. size="small"
  218. >低</el-tag>
  219. </el-descriptions-item>
  220. <el-descriptions-item label="工单类型">{{ getOrderTypeText(selectedOrder.orderType) }}</el-descriptions-item>
  221. <el-descriptions-item label="问题描述" :span="2">{{ selectedOrder.orderDesc }}</el-descriptions-item>
  222. <el-descriptions-item label="创建时间">{{ formatDate(selectedOrder.createTime) }}</el-descriptions-item>
  223. <el-descriptions-item label="更新时间">{{ formatDate(selectedOrder.updateTime) }}</el-descriptions-item>
  224. <el-descriptions-item label="运维状态">在用</el-descriptions-item>
  225. <el-descriptions-item label="在线状态">在线</el-descriptions-item>
  226. <el-descriptions-item label="报警状态">正常</el-descriptions-item>
  227. <el-descriptions-item label="处置结果">{{ selectedOrder.repairResult || '-' }}</el-descriptions-item>
  228. </el-descriptions>
  229. <RepairImagePanel :before-images="selectedOrder.maintenanceBeforeImg" :after-images="selectedOrder.maintenanceAfterImg" />
  230. <!-- 工单操作 -->
  231. <div class="action-section">
  232. <h4 class="section-title">工单操作</h4>
  233. <div class="action-buttons">
  234. <el-button
  235. v-if="canDispatch()"
  236. type="primary"
  237. icon="Download"
  238. @click="handleDispatch"
  239. >下派任务</el-button>
  240. <el-button
  241. v-if="canVerify()"
  242. type="warning"
  243. icon="Check"
  244. @click="handleVerify"
  245. >核查</el-button>
  246. <el-button
  247. v-if="canProcess()"
  248. type="success"
  249. icon="Edit"
  250. @click="handleProcess"
  251. >处理</el-button>
  252. <el-button
  253. v-if="canComplete()"
  254. type="danger"
  255. icon="Checked"
  256. @click="handleComplete"
  257. >提交验收</el-button>
  258. <el-button
  259. v-if="canClose()"
  260. type="success"
  261. icon="CircleCheck"
  262. @click="handleClose"
  263. >验收通过</el-button>
  264. <el-button
  265. v-if="canReject()"
  266. type="info"
  267. icon="Close"
  268. @click="handleReject"
  269. >驳回</el-button>
  270. </div>
  271. </div>
  272. </el-tab-pane>
  273. <!-- 处理历史 -->
  274. <el-tab-pane label="处理历史" name="history">
  275. <div class="timeline-scroll">
  276. <el-timeline v-if="currentOrderLogs && currentOrderLogs.length > 0">
  277. <el-timeline-item
  278. v-for="log in currentOrderLogs"
  279. :key="log.logId"
  280. :timestamp="formatDate(log.operTime)"
  281. placement="top"
  282. :type="getLogType(log.operType)"
  283. :size="'large'"
  284. >
  285. <el-card shadow="hover">
  286. <div class="timeline-content">
  287. <div class="timestamp">{{ formatDate(log.operTime) }}</div>
  288. <div class="operator-info">
  289. <span class="operator-name">{{ log.operUserName }}</span>
  290. <span class="oper-type">· {{ getOperTypeLabel(log.operType) }}</span>
  291. </div>
  292. <div class="oper-desc">{{ log.operDesc }}</div>
  293. </div>
  294. </el-card>
  295. </el-timeline-item>
  296. </el-timeline>
  297. <el-empty v-else description="暂无处理记录" :image-size="100"></el-empty>
  298. </div>
  299. </el-tab-pane>
  300. </el-tabs>
  301. </div>
  302. <el-empty v-else description="请从左侧选择工单" :image-size="150"></el-empty>
  303. </el-card>
  304. </el-col>
  305. </el-row>
  306. <!-- 派单弹窗 -->
  307. <el-dialog v-model="dispatchDialogVisible" title="下派任务 - 选择接单人" width="750px">
  308. <div style="display: flex; gap: 16px; min-height: 350px;">
  309. <!-- 左侧:部门树 -->
  310. <div style="width: 280px; border-right: 1px solid #e4e7ed; padding-right: 12px;">
  311. <el-input
  312. v-model="deptFilterText"
  313. placeholder="筛选部门"
  314. clearable
  315. size="small"
  316. style="margin-bottom: 8px"
  317. />
  318. <el-tree
  319. ref="deptTreeRef"
  320. :data="deptTreeData"
  321. :props="{ label: 'deptName', children: 'children' }"
  322. :filter-node-method="filterDeptNode"
  323. node-key="deptId"
  324. highlight-current
  325. @node-click="handleDeptClick"
  326. style="font-size: 14px"
  327. />
  328. </div>
  329. <!-- 右侧:用户列表 -->
  330. <div style="flex: 1; min-width: 0;">
  331. <el-table
  332. :data="userList"
  333. border
  334. size="small"
  335. max-height="300"
  336. ref="userTableRef"
  337. row-key="userId"
  338. @selection-change="handleSelectionChange"
  339. @row-click="handleRowClick"
  340. >
  341. <el-table-column type="selection" width="40" align="center" :reserve-selection="true" />
  342. <el-table-column prop="userName" label="用户名" width="100" />
  343. <el-table-column prop="nickName" label="昵称" width="100" />
  344. <el-table-column prop="dept.deptName" label="部门" show-overflow-tooltip />
  345. </el-table>
  346. <div v-if="userList.length === 0" style="text-align: center; color: #909399; padding: 40px 0;">请先在左侧选择部门</div>
  347. </div>
  348. </div>
  349. <el-form :model="dispatchForm" label-width="80px" style="margin-top: 16px">
  350. <el-form-item label="备注"><el-input v-model="dispatchForm.remarks" placeholder="请输入派单备注" /></el-form-item>
  351. </el-form>
  352. <template #footer>
  353. <span style="float: left; line-height: 32px; color: #606266;">
  354. 已选人员:<span v-if="selectedUserNames.length > 0" style="color: #409eff; font-weight: bold;">{{ selectedUserNames.join(', ') }}</span>
  355. <span v-else style="color: #909399">未选择</span>
  356. (<span v-if="dispatchForm.handlerIds && dispatchForm.handlerIds.length > 0">{{ dispatchForm.handlerIds.length }}</span><span v-else>0</span>)
  357. </span>
  358. <el-button @click="dispatchDialogVisible = false">取消</el-button>
  359. <el-button type="primary" @click="submitDispatch">确认</el-button>
  360. </template>
  361. </el-dialog>
  362. <!-- 核查/处理弹窗 -->
  363. <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px">
  364. <ImageUpload v-if="actionTitle === '处理工单'" v-model="processBeforeImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
  365. <el-input v-model="actionFormData.solution" type="textarea" :rows="6" placeholder="请输入说明文字" />
  366. <template #footer>
  367. <el-button @click="actionDialogVisible = false">取消</el-button>
  368. <el-button type="primary" @click="submitAction">确定</el-button>
  369. </template>
  370. </el-dialog>
  371. <!-- 完成/验收弹窗 -->
  372. <el-dialog v-model="completeDialogVisible" title="提交验收" width="600px">
  373. <ImageUpload v-model="maintenanceAfterImages" :limit="9" :isShowTip="true" :upload-url="imageUploadUrl" />
  374. <el-form label-width="80px">
  375. <el-form-item label="验收意见">
  376. <el-input v-model="completeForm.result" type="textarea" :rows="4" />
  377. </el-form-item>
  378. </el-form>
  379. <template #footer>
  380. <el-button @click="completeDialogVisible = false">取消</el-button>
  381. <el-button type="primary" @click="submitComplete">提交</el-button>
  382. </template>
  383. </el-dialog>
  384. <!-- 验收通过/结案弹窗 -->
  385. <el-dialog v-model="closeDialogVisible" title="验收通过" width="500px">
  386. <el-form label-width="80px">
  387. <el-form-item label="验收结果">
  388. <el-input v-model="closeForm.result" type="textarea" :rows="4" placeholder="请输入验收结果/整改反馈" />
  389. </el-form-item>
  390. </el-form>
  391. <template #footer>
  392. <el-button @click="closeDialogVisible = false">取消</el-button>
  393. <el-button type="primary" @click="submitClose">确认验收</el-button>
  394. </template>
  395. </el-dialog>
  396. <!-- 驳回工单弹窗 -->
  397. <el-dialog v-model="rejectDialogVisible" title="驳回工单" width="500px">
  398. <el-form label-width="80px">
  399. <el-form-item label="驳回原因" required>
  400. <el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="请输入驳回原因(驳回后工单状态为已驳回)" maxlength="300" show-word-limit />
  401. </el-form-item>
  402. </el-form>
  403. <template #footer>
  404. <el-button @click="rejectDialogVisible = false">取消</el-button>
  405. <el-button type="danger" :loading="rejecting" @click="submitReject">确认驳回</el-button>
  406. </template>
  407. </el-dialog>
  408. </div>
  409. </template>
  410. <script setup name="DrainageWorkOrder">
  411. import { ElMessage } from 'element-plus'
  412. import ImageUpload from '@/components/ImageUpload/index.vue'
  413. import RepairImagePanel from '@/components/RepairImagePanel/index.vue'
  414. import { listWorkOrder, getWorkOrderDetail, dispatchWorkOrder, verifyWorkOrder, processWorkOrder, completeWorkOrder, closeWorkOrder, rejectWorkOrder } from '@/api/work/order.js'
  415. import { listDept } from '@/api/system/dept.js'
  416. import { listUser } from '@/api/system/user.js'
  417. import { handleTree } from '@/utils/ruoyi.js'
  418. const listLoading = ref(false)
  419. const tableData = ref([])
  420. const pageNum = ref(1)
  421. const pageSize = ref(10)
  422. const total = ref(0)
  423. const queryFormRef = ref(null)
  424. const queryParams = reactive({
  425. orderType: null,
  426. orderLevel: null,
  427. keyword: '',
  428. // 排水管网工单:只查询排水类型设备工单
  429. equipmentType: 'drainage'
  430. })
  431. // 选中的工单
  432. const selectedOrderId = ref(null)
  433. const selectedOrder = ref({})
  434. const activeTab = ref('info')
  435. const currentOrderLogs = ref([])
  436. // 派单相关
  437. const dispatchDialogVisible = ref(false)
  438. const dispatchForm = reactive({ handlerIds: [], remarks: '' })
  439. const deptTreeRef = ref(null)
  440. const userTableRef = ref(null)
  441. const deptTreeData = ref([])
  442. const deptFilterText = ref('')
  443. const userList = ref([])
  444. const selectedUserList = ref([]) // 存储选中的用户行数据
  445. // 部门筛选
  446. watch(deptFilterText, (val) => { deptTreeRef.value && deptTreeRef.value.filter(val) })
  447. function filterDeptNode(value, data) {
  448. if (!value) return true
  449. return data.deptName.includes(value)
  450. }
  451. // 加载部门树
  452. async function loadDeptTree() {
  453. if (deptTreeData.value.length > 0) return
  454. try {
  455. const res = await listDept()
  456. deptTreeData.value = handleTree(res.data || [], 'deptId')
  457. } catch (e) { console.error('加载部门失败', e) }
  458. }
  459. // 点击部门加载用户
  460. async function handleDeptClick(node) {
  461. try {
  462. // 清除当前选择
  463. userTableRef.value && userTableRef.value.clearSelection()
  464. selectedUserList.value = []
  465. dispatchForm.handlerIds = []
  466. const res = await listUser({ deptId: node.deptId, pageNum: 1, pageSize: 100 })
  467. userList.value = res.rows || []
  468. } catch (e) { console.error('加载用户失败', e) }
  469. }
  470. // 选择变化时触发
  471. function handleSelectionChange(selection) {
  472. selectedUserList.value = selection
  473. dispatchForm.handlerIds = selection.map(item => item.userId)
  474. }
  475. // 点击行切换选中状态
  476. function handleRowClick(row) {
  477. userTableRef.value && userTableRef.value.toggleRowSelection(row)
  478. }
  479. // 获取已选人员昵称(用于显示)
  480. const selectedUserNames = computed(() => {
  481. return selectedUserList.value.map(item => item.nickName || item.userName)
  482. })
  483. /** 查询工单列表 */
  484. async function handleSearch() {
  485. listLoading.value = true
  486. try {
  487. const res = await listWorkOrder(queryParams, pageNum.value, pageSize.value)
  488. if (res.code === 200) {
  489. tableData.value = res.data.records || []
  490. total.value = res.data.total || 0
  491. }
  492. } catch (e) {
  493. ElMessage.error('查询失败')
  494. } finally {
  495. listLoading.value = false
  496. }
  497. }
  498. /** 重置搜索 */
  499. function handleReset() {
  500. queryFormRef.value && queryFormRef.value.resetFields()
  501. queryParams.orderType = null
  502. queryParams.orderLevel = null
  503. queryParams.keyword = ''
  504. pageNum.value = 1
  505. selectedOrderId.value = null
  506. selectedOrder.value = {}
  507. handleSearch()
  508. }
  509. /** 选择工单 */
  510. async function handleSelectOrder(order) {
  511. selectedOrderId.value = order.orderId
  512. selectedOrder.value = { ...order } // 克隆对象确保更新
  513. activeTab.value = 'info'
  514. currentOrderLogs.value = []
  515. // 加载工单详情和处理历史
  516. await loadOrderDetail(order.orderId)
  517. }
  518. /** 分页变化 */
  519. function handleSizeChange() {
  520. pageNum.value = 1
  521. selectedOrderId.value = null
  522. selectedOrder.value = {}
  523. handleSearch()
  524. }
  525. function handlePageChange() {
  526. selectedOrderId.value = null
  527. selectedOrder.value = {}
  528. handleSearch()
  529. }
  530. /** 派单、核查、处理、完成、驳回的判断函数(示例) */
  531. function canDispatch() { return selectedOrder.value.orderStatus === 1 }
  532. function canVerify() { return [2, 3].includes(selectedOrder.value.orderStatus) }
  533. function canProcess() { return [3, 4].includes(selectedOrder.value.orderStatus) }
  534. function canComplete() { return selectedOrder.value.orderStatus === 4 }
  535. function canClose() { return selectedOrder.value.orderStatus === 5 }
  536. function canReject() { return [1, 2].includes(selectedOrder.value.orderStatus) }
  537. function handleDispatch() {
  538. if (!canDispatch()) {
  539. ElMessage.warning('仅可对待派单工单进行派单操作')
  540. return
  541. }
  542. Object.assign(dispatchForm, { handlerIds: [], remarks: '' })
  543. selectedUserList.value = []
  544. userList.value = []
  545. loadDeptTree()
  546. dispatchDialogVisible.value = true
  547. }
  548. async function submitDispatch() {
  549. if (!dispatchForm.handlerIds || dispatchForm.handlerIds.length === 0) {
  550. ElMessage.warning('请选择至少一个接单人')
  551. return
  552. }
  553. try {
  554. await dispatchWorkOrder({
  555. orderId: selectedOrder.value.orderId,
  556. handlerIds: dispatchForm.handlerIds, // 传递数组
  557. remarks: dispatchForm.remarks
  558. })
  559. ElMessage.success('派单成功!已向 ' + dispatchForm.handlerIds.length + ' 人下发任务')
  560. dispatchDialogVisible.value = false
  561. handleSearch()
  562. } catch (e) {
  563. ElMessage.error('派单失败')
  564. }
  565. }
  566. /** 核查、处理相关 */
  567. const actionDialogVisible = ref(false)
  568. const actionTitle = ref('')
  569. const actionFormData = reactive({ solution: '' })
  570. const processBeforeImages = ref('')
  571. const imageUploadUrl = import.meta.env.VITE_APP_BASE_API + '/minio/upload'
  572. function handleVerify() {
  573. if (![2, 3].includes(selectedOrder.value.orderStatus)) {
  574. ElMessage.warning('当前工单不允许核查')
  575. return
  576. }
  577. actionTitle.value = '核查工单'
  578. actionFormData.solution = ''
  579. processBeforeImages.value = ''
  580. actionDialogVisible.value = true
  581. }
  582. function handleProcess() {
  583. if (![3, 4].includes(selectedOrder.value.orderStatus)) {
  584. ElMessage.warning('当前工单不允许处理')
  585. return
  586. }
  587. actionTitle.value = '处理工单'
  588. actionFormData.solution = ''
  589. processBeforeImages.value = ''
  590. actionDialogVisible.value = true
  591. }
  592. async function submitAction() {
  593. if (!actionFormData.solution.trim()) {
  594. ElMessage.warning('请填写说明')
  595. return
  596. }
  597. try {
  598. const data = {
  599. orderId: selectedOrder.value.orderId,
  600. handleResult: 'solved',
  601. solution: actionFormData.solution
  602. }
  603. if (actionTitle.value === '处理工单' && processBeforeImages.value) {
  604. data.maintenanceBeforeImg = processBeforeImages.value
  605. }
  606. if (actionTitle.value === '核查工单') {
  607. await verifyWorkOrder(data)
  608. } else {
  609. await processWorkOrder(data)
  610. }
  611. ElMessage.success(actionTitle.value + '成功')
  612. actionDialogVisible.value = false
  613. handleSearch()
  614. } catch (e) {
  615. ElMessage.error('操作失败')
  616. }
  617. }
  618. /** 完成/验收相关 */
  619. const completeDialogVisible = ref(false)
  620. const completeForm = reactive({ result: '' })
  621. const maintenanceAfterImages = ref('')
  622. function handleComplete() {
  623. if (![4, 5].includes(selectedOrder.value.orderStatus)) {
  624. ElMessage.warning('当前工单不允许提交验收')
  625. return
  626. }
  627. completeForm.result = ''
  628. maintenanceAfterImages.value = ''
  629. completeDialogVisible.value = true
  630. }
  631. async function submitComplete() {
  632. if (!completeForm.result.trim()) {
  633. ElMessage.warning('请填写验收意见')
  634. return
  635. }
  636. try {
  637. await completeWorkOrder({
  638. orderId: selectedOrder.value.orderId,
  639. result: completeForm.result,
  640. maintenanceAfterImg: maintenanceAfterImages.value
  641. })
  642. ElMessage.success('提交验收成功')
  643. completeDialogVisible.value = false
  644. handleSearch()
  645. } catch (e) {
  646. ElMessage.error('提交失败')
  647. }
  648. }
  649. /** 驳回相关 */
  650. const rejectDialogVisible = ref(false)
  651. const rejectReason = ref('')
  652. const rejecting = ref(false)
  653. function handleReject() {
  654. if (![1, 2].includes(selectedOrder.value.orderStatus)) {
  655. ElMessage.warning('当前工单不允许驳回')
  656. return
  657. }
  658. rejectReason.value = ''
  659. rejectDialogVisible.value = true
  660. }
  661. async function submitReject() {
  662. if (!rejectReason.value.trim()) {
  663. ElMessage.warning('请填写驳回原因')
  664. return
  665. }
  666. rejecting.value = true
  667. try {
  668. await rejectWorkOrder({
  669. orderId: selectedOrder.value.orderId,
  670. reason: rejectReason.value.trim()
  671. })
  672. ElMessage.success('驳回成功,工单状态已更新为已驳回')
  673. rejectDialogVisible.value = false
  674. handleSearch()
  675. } catch (e) {
  676. ElMessage.error('驳回失败:' + (e.msg || e.message || '请重试'))
  677. } finally {
  678. rejecting.value = false
  679. }
  680. }
  681. /** 验收通过/结案相关 */
  682. const closeDialogVisible = ref(false)
  683. const closeForm = reactive({ result: '' })
  684. function handleClose() {
  685. if (selectedOrder.value.orderStatus !== 5) {
  686. ElMessage.warning('当前工单不允许验收通过')
  687. return
  688. }
  689. closeForm.result = ''
  690. closeDialogVisible.value = true
  691. }
  692. async function submitClose() {
  693. if (!closeForm.result.trim()) {
  694. ElMessage.warning('请填写验收结果')
  695. return
  696. }
  697. try {
  698. await closeWorkOrder({
  699. orderId: selectedOrder.value.orderId,
  700. result: closeForm.result
  701. })
  702. ElMessage.success('验收通过,关联隐患已同步为已整改')
  703. closeDialogVisible.value = false
  704. handleSearch()
  705. } catch (e) {
  706. ElMessage.error('验收失败')
  707. }
  708. }
  709. /** 辅助函数 */
  710. function getOrderTypeText(type) {
  711. const map = { 1: '故障维修', 2: '日常巡检', 3: '设备保养' }
  712. return map[type] || '-'
  713. }
  714. function getOperTypeLabel(type) {
  715. const map = {
  716. 1: '创建工单', 2: '派单', 3: '接单', 4: '开始维修',
  717. 5: '提交验收', 6: '验收通过', 7: '驳回工单', 8: '申请延期'
  718. }
  719. return map[type] || '-'
  720. }
  721. function getLogType(type) {
  722. const map = {
  723. 1: 'primary', 2: 'success', 3: 'warning', 4: 'danger',
  724. 5: 'success', 6: 'success', 7: 'danger', 8: 'warning'
  725. }
  726. return map[type] || ''
  727. }
  728. function formatDate(dateStr) {
  729. if (!dateStr) return '-'
  730. return new Date(dateStr).toLocaleString('zh-CN')
  731. }
  732. /** 优先级标签 */
  733. function getLevelLabel(level) {
  734. const map = { 1: '紧急', 2: '一般', 3: '低' }
  735. return map[level] || '未知'
  736. }
  737. function getLevelTag(level) {
  738. const map = { 1: 'danger', 2: 'warning', 3: 'info' }
  739. return map[level] || 'info'
  740. }
  741. /** 工单类型标签 */
  742. function getTypeLabel(type) {
  743. const map = { 1: '故障维修', 2: '日常巡检', 3: '设备保养' }
  744. return map[type] || '未知'
  745. }
  746. function getTypeTag(type) {
  747. const map = { 1: 'danger', 2: 'primary', 3: 'success' }
  748. return map[type] || 'info'
  749. }
  750. /** 加载工单详情和处理历史 */
  751. async function loadOrderDetail(orderId) {
  752. try {
  753. const res = await getWorkOrderDetail(orderId)
  754. if (res.code === 200) {
  755. selectedOrder.value = { ...selectedOrder.value, ...res.data.order }
  756. currentOrderLogs.value = res.data.logs || []
  757. }
  758. } catch (e) {
  759. ElMessage.error('加载工单详情失败')
  760. }
  761. }
  762. onMounted(() => {
  763. handleSearch()
  764. })
  765. </script>
  766. <style scoped lang="scss">
  767. .drainage-work-order {
  768. padding: 0 16px 16px;
  769. .search-card {
  770. margin-bottom: 16px;
  771. :deep(.el-card__body) {
  772. padding: 16px;
  773. }
  774. }
  775. .search-form {
  776. .el-form {
  777. display: flex;
  778. flex-wrap: wrap;
  779. align-items: flex-start;
  780. gap: 12px;
  781. .el-form-item {
  782. margin-bottom: 0;
  783. min-width: 150px;
  784. &:last-child {
  785. margin-left: auto;
  786. }
  787. }
  788. }
  789. }
  790. .order-list {
  791. overflow-y: auto;
  792. }
  793. .order-item {
  794. padding: 16px;
  795. margin-bottom: 8px;
  796. border-radius: 8px;
  797. cursor: pointer;
  798. transition: all 0.2s;
  799. border: 1px solid #e4e7ed;
  800. &:hover {
  801. background-color: #f5f7fa;
  802. }
  803. &.active {
  804. background-color: #ecf5ff;
  805. border-color: #409eff;
  806. }
  807. .order-header {
  808. display: flex;
  809. justify-content: space-between;
  810. align-items: center;
  811. margin-bottom: 8px;
  812. .order-no {
  813. font-weight: bold;
  814. font-size: 14px;
  815. }
  816. .status-badge {
  817. min-width: 60px;
  818. }
  819. }
  820. .order-body {
  821. font-size: 12px;
  822. color: #606266;
  823. .order-tags {
  824. display: flex;
  825. gap: 6px;
  826. margin-bottom: 6px;
  827. }
  828. .order-desc {
  829. margin-top: 4px;
  830. color: #303133;
  831. font-size: 13px;
  832. overflow: hidden;
  833. text-overflow: ellipsis;
  834. white-space: nowrap;
  835. }
  836. .device-code, .create-time {
  837. margin-top: 4px;
  838. }
  839. }
  840. }
  841. .pagination-wrapper {
  842. padding: 16px 0;
  843. border-top: 1px solid #e4e7ed;
  844. margin-top: 8px;
  845. .pagination {
  846. display: flex;
  847. justify-content: flex-end;
  848. }
  849. }
  850. .box-card {
  851. height: 100%;
  852. display: flex;
  853. flex-direction: column;
  854. :deep(.el-card__body) {
  855. flex: 1;
  856. display: flex;
  857. flex-direction: column;
  858. overflow: hidden;
  859. }
  860. }
  861. .detail-tabs {
  862. margin-top: 16px;
  863. }
  864. .timeline-scroll {
  865. max-height: calc(100vh - 340px);
  866. overflow-y: auto;
  867. padding-right: 8px;
  868. }
  869. .image-section {
  870. margin-top: 24px;
  871. padding-top: 24px;
  872. border-top: 1px solid #e4e7ed;
  873. .image-group {
  874. margin-bottom: 16px;
  875. }
  876. .image-group-title {
  877. margin-bottom: 8px;
  878. color: #606266;
  879. font-weight: 600;
  880. }
  881. .image-list {
  882. display: flex;
  883. flex-wrap: wrap;
  884. gap: 12px;
  885. }
  886. }
  887. .action-section {
  888. margin-top: 24px;
  889. padding-top: 24px;
  890. border-top: 1px solid #e4e7ed;
  891. .section-title {
  892. font-size: 16px;
  893. font-weight: bold;
  894. margin-bottom: 16px;
  895. }
  896. .action-buttons {
  897. display: flex;
  898. gap: 12px;
  899. flex-wrap: wrap;
  900. .el-button {
  901. min-width: 100px;
  902. }
  903. }
  904. }
  905. .timeline-content {
  906. .timestamp {
  907. font-size: 12px;
  908. color: #909399;
  909. margin-bottom: 6px;
  910. }
  911. .operator-info {
  912. font-size: 14px;
  913. margin-bottom: 8px;
  914. .operator-name {
  915. font-weight: bold;
  916. color: #303133;
  917. }
  918. .oper-type {
  919. color: #909399;
  920. }
  921. }
  922. .oper-desc {
  923. font-size: 14px;
  924. color: #606266;
  925. }
  926. }
  927. }
  928. </style>