index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <template>
  2. <div class="pwork-list-page">
  3. <!-- 搜索栏 -->
  4. <el-card shadow="never" class="search-card">
  5. <el-form :model="queryParams" inline class="search-form" @submit.prevent>
  6. <el-form-item label="关键字">
  7. <el-input
  8. v-model="queryParams.keyword"
  9. placeholder="工单编号 / 设备编码 / 故障描述"
  10. clearable
  11. style="width: 240px"
  12. @keyup.enter="handleQuery"
  13. />
  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="WO_LEVEL.EMERGENCY" />
  18. <el-option label="一般" :value="WO_LEVEL.NORMAL" />
  19. <el-option label="低" :value="WO_LEVEL.LOW" />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item label="工单状态">
  23. <el-select v-model="queryParams.orderStatus" placeholder="全部" clearable style="width: 150px">
  24. <el-option
  25. v-for="(item, key) in ORDER_STATUS_MAP"
  26. :key="key"
  27. :label="item.text"
  28. :value="Number(key)"
  29. />
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item label="故障时间">
  33. <el-date-picker
  34. v-model="faultTimeRange"
  35. type="datetimerange"
  36. value-format="YYYY-MM-DD HH:mm:ss"
  37. start-placeholder="开始时间"
  38. end-placeholder="结束时间"
  39. style="width: 360px"
  40. />
  41. </el-form-item>
  42. <el-form-item>
  43. <el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
  44. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  45. <el-button
  46. v-hasPermi="['ops:workOrder:add']"
  47. type="success"
  48. icon="Plus"
  49. @click="openAdd"
  50. >新增工单</el-button>
  51. </el-form-item>
  52. </el-form>
  53. </el-card>
  54. <el-card shadow="never" class="table-card">
  55. <el-table v-loading="loading" :data="list" border stripe>
  56. <el-table-column label="工单编号" prop="orderNo" min-width="180" show-overflow-tooltip />
  57. <el-table-column label="故障描述" min-width="240">
  58. <template #default="{ row }">
  59. <el-tooltip
  60. :content="row.orderDesc"
  61. placement="top"
  62. popper-class="order-desc-tooltip"
  63. :show-after="200"
  64. :disabled="!row.orderDesc"
  65. >
  66. <span class="desc-cell">{{ row.orderDesc || '-' }}</span>
  67. </el-tooltip>
  68. </template>
  69. </el-table-column>
  70. <el-table-column label="优先级" width="100" align="center">
  71. <template #default="{ row }">
  72. <el-tag :type="orderLevelType(row.orderLevel)">{{ orderLevelText(row.orderLevel) }}</el-tag>
  73. </template>
  74. </el-table-column>
  75. <el-table-column label="设备编码" prop="deviceCode" min-width="150" show-overflow-tooltip>
  76. <template #default="{ row }">{{ row.deviceCode || '-' }}</template>
  77. </el-table-column>
  78. <el-table-column label="责任人" prop="receiveUserName" min-width="100" show-overflow-tooltip>
  79. <template #default="{ row }">{{ row.receiveUserName || '-' }}</template>
  80. </el-table-column>
  81. <el-table-column label="故障时间" prop="faultTime" width="180" align="center">
  82. <template #default="{ row }">{{ timeText(row.faultTime) }}</template>
  83. </el-table-column>
  84. <el-table-column label="计划完成时间" prop="planFinishTime" width="180" align="center">
  85. <template #default="{ row }">{{ timeText(row.planFinishTime) }}</template>
  86. </el-table-column>
  87. <el-table-column label="状态" width="100" align="center">
  88. <template #default="{ row }">
  89. <el-tag :type="orderStatusType(row.orderStatus)">{{ orderStatusText(row.orderStatus) }}</el-tag>
  90. </template>
  91. </el-table-column>
  92. </el-table>
  93. <pagination
  94. v-show="total > 0"
  95. v-model:page="queryParams.pageNum"
  96. v-model:limit="queryParams.pageSize"
  97. :total="total"
  98. @pagination="getList"
  99. />
  100. </el-card>
  101. <!-- 新增工单弹窗 -->
  102. <el-dialog v-model="addVisible" title="新增工单" width="680px" append-to-body destroy-on-close>
  103. <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
  104. <el-form-item label="关联设备">
  105. <el-select
  106. v-model="addForm.deviceId"
  107. filterable
  108. remote
  109. clearable
  110. placeholder="输入设备编码/名称搜索"
  111. :remote-method="searchDevice"
  112. :loading="deviceLoading"
  113. style="width: 100%"
  114. @change="onDeviceChange"
  115. >
  116. <el-option
  117. v-for="dev in deviceOptions"
  118. :key="dev.equipmentId"
  119. :label="`${dev.equipmentName || dev.equipmentCode} (${dev.equipmentCode})`"
  120. :value="dev.equipmentId"
  121. />
  122. </el-select>
  123. </el-form-item>
  124. <el-form-item v-if="deviceInfo" label="设备信息">
  125. <el-descriptions :column="2" border size="small">
  126. <el-descriptions-item label="设备类型">{{ deviceInfo.equipmentTypeName || '-' }}</el-descriptions-item>
  127. <el-descriptions-item label="设备编码">{{ deviceInfo.equipmentCode || '-' }}</el-descriptions-item>
  128. <el-descriptions-item label="设备位置">{{ deviceInfo.equipmentLocation || deviceInfo.pointAddress || '-' }}</el-descriptions-item>
  129. <el-descriptions-item label="设备厂家">{{ deviceInfo.manufacturer || '-' }}</el-descriptions-item>
  130. <el-descriptions-item label="维护人员">{{ deviceInfo.maintainer || '-' }}</el-descriptions-item>
  131. <el-descriptions-item label="联系方式">{{ deviceInfo.maintainerPhone || '-' }}</el-descriptions-item>
  132. </el-descriptions>
  133. </el-form-item>
  134. <el-form-item label="优先级" prop="orderLevel">
  135. <el-radio-group v-model="addForm.orderLevel">
  136. <el-radio-button :value="WO_LEVEL.EMERGENCY">紧急</el-radio-button>
  137. <el-radio-button :value="WO_LEVEL.NORMAL">一般</el-radio-button>
  138. <el-radio-button :value="WO_LEVEL.LOW">低</el-radio-button>
  139. </el-radio-group>
  140. </el-form-item>
  141. <el-form-item label="故障时间" prop="faultTime">
  142. <el-date-picker
  143. v-model="addForm.faultTime"
  144. type="datetime"
  145. placeholder="故障时间"
  146. value-format="YYYY-MM-DD HH:mm:ss"
  147. style="width: 100%"
  148. />
  149. </el-form-item>
  150. <el-form-item label="计划完成时间" prop="planFinishTime">
  151. <el-date-picker
  152. v-model="addForm.planFinishTime"
  153. type="datetime"
  154. placeholder="请选择计划完成时间"
  155. value-format="YYYY-MM-DD HH:mm:ss"
  156. style="width: 100%"
  157. />
  158. </el-form-item>
  159. <el-form-item label="故障描述" prop="orderDesc">
  160. <el-input
  161. v-model="addForm.orderDesc"
  162. type="textarea"
  163. :rows="4"
  164. placeholder="请描述故障信息"
  165. maxlength="500"
  166. show-word-limit
  167. />
  168. </el-form-item>
  169. </el-form>
  170. <template #footer>
  171. <el-button @click="addVisible = false">取消</el-button>
  172. <el-button type="primary" :loading="submitting" @click="submitAdd">提交工单</el-button>
  173. </template>
  174. </el-dialog>
  175. </div>
  176. </template>
  177. <script setup>
  178. import { ref, reactive, computed, onMounted, watch } from 'vue'
  179. import { useRoute } from 'vue-router'
  180. import { addWorkOrder, listWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
  181. import { listEquipment } from '@/api/pipeNetwork/pdeviceInform'
  182. import {
  183. ORDER_STATUS_MAP,
  184. WO_LEVEL,
  185. orderLevelText,
  186. orderLevelType,
  187. orderStatusText,
  188. orderStatusType,
  189. timeText
  190. } from './workOrderDict.js'
  191. import { ElMessage } from 'element-plus'
  192. defineOptions({ name: 'PworkList' })
  193. const route = useRoute()
  194. /**
  195. * 本组件被多个菜单复用,不同菜单按业务口径查询不同范围的工单:
  196. * - 运维工单列表菜单(默认):查询全部工单
  197. * - 设备维修记录菜单:仅查 parentTypeId=1 的设备所产生的工单
  198. *
  199. * 判定方式:同一组件由后台 sys_menu.component 指向本文件时,菜单的 path 会体现在路由 path 中,
  200. * 因此按路由 path 关键字区分来源;匹配不上时走默认(全量),保证新菜单接入不会误过滤。
  201. * 若后续调整菜单 path,同步维护 MENU_DEVICE_TYPE_SCOPE.pathKey 即可。
  202. */
  203. const MENU_DEVICE_TYPE_SCOPE = [
  204. { pathKey: 'record', parentTypeId: '1', name: '设备维修记录' }
  205. ]
  206. const menuScope = computed(() => {
  207. const curPath = String(route.path || '')
  208. return MENU_DEVICE_TYPE_SCOPE.find((m) => curPath.includes(m.pathKey))
  209. })
  210. /** 统一构造查询入参:命中业务菜单时附加设备父类别过滤,且不随查询重置丢失 */
  211. function buildQueryParams() {
  212. return {
  213. ...queryParams,
  214. ...(menuScope.value ? { deviceTypeParentId: menuScope.value.parentTypeId } : {})
  215. }
  216. }
  217. const loading = ref(false)
  218. const submitting = ref(false)
  219. const list = ref([])
  220. const total = ref(0)
  221. const faultTimeRange = ref([])
  222. const queryParams = reactive({
  223. pageNum: 1,
  224. pageSize: 10,
  225. keyword: '',
  226. orderLevel: undefined,
  227. orderStatus: undefined,
  228. faultTimeStart: '',
  229. faultTimeEnd: ''
  230. })
  231. /* 列表查询 */
  232. async function getList() {
  233. loading.value = true
  234. try {
  235. const res = await listWorkOrder(buildQueryParams())
  236. const data = res.data || {}
  237. list.value = data.records || []
  238. total.value = data.total || 0
  239. } finally {
  240. loading.value = false
  241. }
  242. }
  243. function handleQuery() {
  244. queryParams.pageNum = 1
  245. if (faultTimeRange.value && faultTimeRange.value.length === 2) {
  246. queryParams.faultTimeStart = faultTimeRange.value[0]
  247. queryParams.faultTimeEnd = faultTimeRange.value[1]
  248. } else {
  249. queryParams.faultTimeStart = ''
  250. queryParams.faultTimeEnd = ''
  251. }
  252. getList()
  253. }
  254. function resetQuery() {
  255. queryParams.keyword = ''
  256. queryParams.orderLevel = undefined
  257. queryParams.orderStatus = undefined
  258. faultTimeRange.value = []
  259. queryParams.faultTimeStart = ''
  260. queryParams.faultTimeEnd = ''
  261. handleQuery()
  262. }
  263. /* 新增工单 */
  264. const addVisible = ref(false)
  265. const addFormRef = ref(null)
  266. const deviceOptions = ref([])
  267. const deviceLoading = ref(false)
  268. const deviceInfo = ref(null)
  269. const addForm = reactive({
  270. deviceId: '',
  271. orderLevel: WO_LEVEL.NORMAL,
  272. faultTime: '',
  273. planFinishTime: '',
  274. orderDesc: ''
  275. })
  276. /** 当前时间文本,格式与 el-date-picker 的 value-format 保持一致('YYYY-MM-DD HH:mm:ss') */
  277. function nowText() {
  278. const d = new Date()
  279. const pad = (n) => String(n).padStart(2, '0')
  280. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} `
  281. + `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
  282. }
  283. /** 故障时间不能大于系统当前时间(定长字符串字典序即时间顺序) */
  284. function validateFaultTime(rule, value, callback) {
  285. if (!value) {
  286. callback()
  287. return
  288. }
  289. if (String(value) > nowText()) {
  290. callback(new Error('故障时间不能大于当前时间'))
  291. return
  292. }
  293. callback()
  294. }
  295. /**
  296. * 计划完成时间不能早于故障时间。
  297. * 两者均为 el-date-picker 的 value-format 输出('YYYY-MM-DD HH:mm:ss' 定长字符串),
  298. * 字典序与时间顺序一致,直接比较即可,无需解析为 Date 对象。
  299. */
  300. function validatePlanFinishTime(rule, value, callback) {
  301. if (!value || !addForm.faultTime) {
  302. callback()
  303. return
  304. }
  305. if (String(value) < String(addForm.faultTime)) {
  306. callback(new Error('计划完成时间不能早于故障时间'))
  307. return
  308. }
  309. callback()
  310. }
  311. const addRules = {
  312. orderDesc: [{ required: true, message: '请填写故障描述', trigger: 'blur' }],
  313. faultTime: [
  314. { required: true, message: '请选择故障发生时间', trigger: 'change' },
  315. { validator: validateFaultTime, trigger: 'change' }
  316. ],
  317. planFinishTime: [{ validator: validatePlanFinishTime, trigger: 'change' }]
  318. }
  319. function openAdd() {
  320. addForm.deviceId = ''
  321. addForm.orderLevel = WO_LEVEL.NORMAL
  322. addForm.faultTime = ''
  323. addForm.planFinishTime = ''
  324. addForm.orderDesc = ''
  325. deviceOptions.value = []
  326. deviceInfo.value = null
  327. addVisible.value = true
  328. }
  329. async function searchDevice(query) {
  330. if (!query) {
  331. deviceOptions.value = []
  332. return
  333. }
  334. deviceLoading.value = true
  335. try {
  336. const res = await listEquipment({ keyword: query, pageNum: 1, pageSize: 20 })
  337. if (res.records) deviceOptions.value = res.records || []
  338. } catch (e) {
  339. console.warn('设备搜索失败', e)
  340. } finally {
  341. deviceLoading.value = false
  342. }
  343. }
  344. function onDeviceChange(id) {
  345. deviceInfo.value = null
  346. if (!id) {
  347. return
  348. }
  349. const dev = deviceOptions.value.find((d) => String(d.equipmentId) === String(id))
  350. if (dev) {
  351. deviceInfo.value = dev
  352. }
  353. }
  354. async function submitAdd() {
  355. await addFormRef.value.validate().catch(() => {
  356. throw new Error('校验失败')
  357. })
  358. submitting.value = true
  359. try {
  360. const data = {
  361. orderLevel: addForm.orderLevel,
  362. orderDesc: addForm.orderDesc
  363. }
  364. if (addForm.deviceId) data.deviceId = addForm.deviceId
  365. if (addForm.faultTime) data.faultTime = addForm.faultTime
  366. if (addForm.planFinishTime) data.planFinishTime = addForm.planFinishTime
  367. await addWorkOrder(data)
  368. ElMessage.success('工单创建成功,状态为待派单')
  369. addVisible.value = false
  370. getList()
  371. } finally {
  372. submitting.value = false
  373. }
  374. }
  375. // 故障时间变化后需重新校验计划完成时间:先填计划完成时间、再改故障时间时,
  376. // 原有校验结论会失效(比如故障时间改晚后,原合法的计划时间可能变得非法)
  377. watch(
  378. () => addForm.faultTime,
  379. () => {
  380. if (!addVisible.value || !addForm.planFinishTime) return
  381. addFormRef.value?.validateField('planFinishTime').catch(() => {})
  382. }
  383. )
  384. // 同一组件被多个菜单复用(运维工单列表/设备维修记录),
  385. // 路由切换时组件实例被复用、onMounted 不会重新执行,
  386. // 必须监听路由变化按新菜单的业务口径重新拉取数据
  387. watch(
  388. () => route.path,
  389. (nv, ov) => {
  390. if (nv === ov) return
  391. queryParams.pageNum = 1
  392. getList()
  393. }
  394. )
  395. onMounted(() => {
  396. getList()
  397. })
  398. </script>
  399. <style scoped lang="scss">
  400. .pwork-list-page {
  401. padding: 16px;
  402. background: #f5f7fa;
  403. min-height: calc(100vh - 84px);
  404. }
  405. .search-card {
  406. margin-bottom: 12px;
  407. }
  408. /* 查询栏:改用 flex + row-gap 控制换行时的行间距。
  409. el-form-item 默认 margin-bottom: 18px 会让表单底部留白明显大于顶部,
  410. 这里清零后由卡片自身的上下 padding 决定留白,保证上下一致 */
  411. .search-form {
  412. display: flex;
  413. flex-wrap: wrap;
  414. row-gap: 4px;
  415. :deep(.el-form-item) {
  416. margin-bottom: 0;
  417. }
  418. }
  419. /* 单元格内省略:交给自定义 tooltip 展示全文 */
  420. .desc-cell {
  421. display: inline-block;
  422. max-width: 100%;
  423. overflow: hidden;
  424. text-overflow: ellipsis;
  425. white-space: nowrap;
  426. vertical-align: bottom;
  427. }
  428. </style>
  429. <style lang="scss">
  430. /* 故障描述悬浮提示:popper 挂在 body 下不受 scoped 限制,单独用全局样式块。
  431. 限制最大宽度并允许换行,替代默认"一行横穿全屏"的效果 */
  432. .el-popper.order-desc-tooltip {
  433. max-width: 420px;
  434. line-height: 1.6;
  435. white-space: normal;
  436. word-break: break-all;
  437. }
  438. </style>