BusinessWorkOrder.vue 33 KB

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