finalize.vue 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. <template>
  2. <el-dialog title="结单" v-model="visible" width="800px" append-to-body @close="handleClose">
  3. <el-alert type="info" :closable="false" style="border-color: #14b8a6; background-color: #f0fdfa; color: #0d9488; height: 35px;">
  4. <template #default>
  5. <i class="fa fa-file-text-o mr-2" style="color: #0d9488;"> 请输入相关作业信息,请上传附件完成结单。</i>
  6. </template>
  7. </el-alert>
  8. <el-form ref="finishRef" :model="formData" :rules="finishRules" label-width="120px" label-position="top">
  9. <!-- 工单信息 -->
  10. <h4 class="text-sm font-medium text-gray-800 mb-3"></h4>
  11. <el-row :gutter="20">
  12. <el-col :span="12">
  13. <el-form-item label="工单编码">
  14. <el-input v-model="formData.workOrderProjectNo" disabled />
  15. </el-form-item>
  16. </el-col>
  17. <el-col :span="12">
  18. <el-form-item label="工单状态" prop="workOrderStatus">
  19. <el-select v-model="formData.workOrderStatus" disabled>
  20. <el-option
  21. v-for="dict in workOrderStatusOptions"
  22. :key="dict.value"
  23. :label="dict.label"
  24. :value="dict.value"
  25. />
  26. </el-select>
  27. </el-form-item>
  28. </el-col>
  29. </el-row>
  30. <el-row :gutter="20">
  31. <el-col :span="12">
  32. <el-form-item label="风机编号">
  33. <el-input v-model="formData.pcsDeviceName" disabled />
  34. </el-form-item>
  35. </el-col>
  36. <el-col :span="12">
  37. <el-form-item label="维保中心">
  38. <el-input v-model="formData.gxtCenter" disabled />
  39. </el-form-item>
  40. </el-col>
  41. </el-row>
  42. <el-row :gutter="20">
  43. <el-col :span="12">
  44. <el-form-item label="场站">
  45. <el-input v-model="formData.pcsStationName" disabled />
  46. </el-form-item>
  47. </el-col>
  48. <el-col :span="12">
  49. <el-form-item label="品牌">
  50. <el-input v-model="formData.brand" disabled />
  51. </el-form-item>
  52. </el-col>
  53. </el-row>
  54. <el-row :gutter="20">
  55. <el-col :span="12">
  56. <el-form-item label="机型">
  57. <el-input v-model="formData.model" disabled />
  58. </el-form-item>
  59. </el-col>
  60. <!-- <el-col :span="12" v-if="formData.infoEntry == '1'">-->
  61. <!-- <el-form-item label="MIS工单编码">-->
  62. <!-- <el-input v-model="formData.misNo" disabled />-->
  63. <!-- </el-form-item>-->
  64. <!-- </el-col>-->
  65. <!-- <el-col :span="12" v-if="formData.infoEntry == '2'">-->
  66. <!-- <el-form-item label="工作票编号">-->
  67. <!-- <el-input v-model="formData.workPermitNum" disabled />-->
  68. <!-- </el-form-item>-->
  69. <!-- </el-col>-->
  70. </el-row>
  71. <el-row>
  72. <el-col :span="12">
  73. <el-form-item label="接单人">
  74. <el-input v-model="formData.acceptUserName" disabled />
  75. </el-form-item>
  76. </el-col>
  77. <el-col :span="12">
  78. <el-form-item label="接单时间">
  79. <el-input v-model="formData.acceptTime" disabled />
  80. </el-form-item>
  81. </el-col>
  82. </el-row>
  83. <!-- 维保详情 -->
  84. <el-row :gutter="20">
  85. <el-col :span="24">
  86. <el-form-item label="维保内容">
  87. <el-input v-model="formData.content" type="textarea" :rows="3" maxlength="500" disabled show-word-limit />
  88. </el-form-item>
  89. </el-col>
  90. </el-row>
  91. <el-row>
  92. <el-col :span="12">
  93. <el-form-item label="信息录入" prop="infoEntry">
  94. <el-radio-group v-model="formData.infoEntry" @change="handleInfoEntryChange">
  95. <el-radio
  96. v-for="dict in infoEntryOptions"
  97. :key="dict.value"
  98. :label="dict.value"
  99. >
  100. {{ dict.label }}
  101. </el-radio>
  102. </el-radio-group>
  103. </el-form-item>
  104. </el-col>
  105. </el-row>
  106. <el-row>
  107. <el-col :span="12" v-if="formData.infoEntry == 1">
  108. <el-form-item label="MIS工单编码" prop="misNo" >
  109. <el-input
  110. v-model="formData.misNo"
  111. placeholder="请输入MIS工单编码或点击搜索选择"
  112. clearable
  113. @focus="handleMisNoInputFocus"
  114. @blur="handleMisNoInputBlur"
  115. @input="handleMisNoInput"
  116. @clear="handleMisNoClear"
  117. >
  118. <template #append>
  119. <el-button @click="handleSelectMisInfo" icon="Search"></el-button>
  120. </template>
  121. </el-input>
  122. <!-- 快速检索下拉框 -->
  123. <div class="quick-select-dropdown" v-show="showMisNoQuickSelect && quickMisNoList.length > 0">
  124. <div
  125. v-for="item in quickMisNoList"
  126. :key="item.misNo"
  127. class="quick-select-item"
  128. @click="handleMisNoQuickSelect(item)">
  129. <span class="mis-no">{{ item.misNo }}</span>
  130. </div>
  131. </div>
  132. <div class="quick-select-dropdown no-data" v-show="showMisNoQuickSelect && quickMisNoList.length === 0 && formData.misNo">
  133. <div>未找到匹配的MIS工单</div>
  134. </div>
  135. </el-form-item>
  136. </el-col>
  137. <!-- MIS选择组件 -->
  138. <slot name="mis-info-select"></slot>
  139. <el-col :span="12">
  140. <el-form-item label="工作票编号" prop="workPermitNum">
  141. <el-input v-model="workPermitNumProxy" maxlength="20" show-word-limit :readonly="formData.infoEntry == '1'" />
  142. </el-form-item>
  143. </el-col>
  144. <el-col :span="12">
  145. <el-form-item label="开始时间" prop="realStartTime">
  146. <el-date-picker
  147. v-model="formData.realStartTime"
  148. type="datetime"
  149. format="YYYY-MM-DD HH:mm"
  150. value-format="YYYY-MM-DD HH:mm"
  151. placeholder="请选择开始时间"
  152. style="width: 100%"
  153. :disabled-date="disabledStartDate"
  154. @change="handleStartTimeChange"
  155. :readonly="formData.infoEntry == '1'"
  156. />
  157. </el-form-item>
  158. </el-col>
  159. <el-col :span="12">
  160. <el-form-item label="结束时间" prop="realEndTime">
  161. <el-date-picker
  162. v-model="formData.realEndTime"
  163. type="datetime"
  164. format="YYYY-MM-DD HH:mm"
  165. value-format="YYYY-MM-DD HH:mm"
  166. placeholder="请选择结束时间"
  167. style="width: 100%"
  168. :disabled-date="disabledEndDate"
  169. @change="handleEndTimeChange"
  170. :readonly="formData.infoEntry == '1'"
  171. />
  172. </el-form-item>
  173. </el-col>
  174. <el-col :span="12" v-if="resumeInfo && resumeShow">
  175. <el-form-item label="挂起结束时间" prop="resumeTime">
  176. <el-date-picker
  177. v-model="formData.resumeTime"
  178. type="datetime"
  179. format="YYYY-MM-DD HH:mm"
  180. value-format="YYYY-MM-DD HH:mm"
  181. placeholder="请选择挂起结束时间"
  182. style="width: 100%"
  183. />
  184. </el-form-item>
  185. </el-col>
  186. </el-row>
  187. <el-row>
  188. <el-col :span="12">
  189. <el-form-item label="工作负责人" prop="teamLeaderName">
  190. <el-input
  191. v-model="formData.teamLeaderName"
  192. placeholder="请输入工作负责人姓名或点击选择"
  193. clearable
  194. @focus="handleTeamLeaderInputFocus"
  195. @blur="handleTeamLeaderInputBlur"
  196. @input="handleTeamLeaderInput"
  197. @clear="handleTeamLeaderClear"
  198. :readonly="formData.infoEntry == '1'"
  199. >
  200. </el-input>
  201. <!-- 快速检索下拉框 -->
  202. <div class="quick-select-dropdown" v-show="showTeamLeaderQuickSelect && quickTeamLeaderList.length > 0">
  203. <div
  204. v-for="item in quickTeamLeaderList"
  205. :key="item.userId"
  206. class="quick-select-item"
  207. @click="handleTeamLeaderQuickSelect(item)">
  208. <span class="user-name">{{ item.nickName }}</span>
  209. <span class="user-name">{{ item.dept.deptName }}</span>
  210. </div>
  211. </div>
  212. <div class="quick-select-dropdown no-data" v-show="showTeamLeaderQuickSelect && quickTeamLeaderList.length === 0 && formData.teamLeaderName && !teamLeaderLoading">
  213. <div>未找到匹配的人员</div>
  214. </div>
  215. <div class="quick-select-dropdown no-data" v-show="showTeamLeaderQuickSelect && teamLeaderLoading">
  216. <div>
  217. <i class="el-icon-loading"></i>
  218. 搜索中...
  219. </div>
  220. </div>
  221. </el-form-item>
  222. </el-col>
  223. <!-- </el-row>-->
  224. <!-- <el-row>-->
  225. <el-col :span="12">
  226. <el-form-item label="工作班成员" prop="workGroupMemberName">
  227. <el-input
  228. v-model="formData.workGroupMemberName"
  229. placeholder="请选择工作班成员"
  230. :readonly="formData.infoEntry == '1'"
  231. >
  232. <template #append v-if="formData.infoEntry == '2'">
  233. <el-button @click="userSelectVisible = true" icon="User"></el-button>
  234. </template>
  235. </el-input>
  236. </el-form-item>
  237. </el-col>
  238. </el-row>
  239. <el-row>
  240. <el-col :span="12">
  241. <el-form-item label="外委人员数(人)" prop="wwryNum">
  242. <el-input-number
  243. v-model="formData.wwryNum"
  244. placeholder="请输入外委人员数"
  245. controls-position="right"
  246. style="width: 100%"
  247. class="input-number-left"
  248. :min="0"
  249. :step="1"
  250. :precision="0"
  251. />
  252. </el-form-item>
  253. </el-col>
  254. <el-col :span="12">
  255. <el-form-item label="外来人员数(人)" prop="wlryNum">
  256. <el-input-number
  257. v-model="formData.wlryNum"
  258. placeholder="请输入外来人员数"
  259. controls-position="right"
  260. style="width: 100%"
  261. class="input-number-left"
  262. :min="0"
  263. :step="1"
  264. :precision="0"
  265. />
  266. </el-form-item>
  267. </el-col>
  268. </el-row>
  269. <el-row>
  270. <el-col :span="24">
  271. <el-form-item label="附件(可选)">
  272. <preview :limit="8" v-model="formData.attachmentUrls" :filesize="5"></preview>
  273. </el-form-item>
  274. </el-col>
  275. </el-row>
  276. <MisInfoSelectSingle :key="commonKey" v-model="misInfoSelectVisible" @onSelected="onMisInfoSelected"
  277. :pcsStationName="formData.pcsStationName" :pcsDeviceName="formData.pcsDeviceName"
  278. :workOrderStatus="'结束'" >
  279. </MisInfoSelectSingle>
  280. </el-form>
  281. <template #footer>
  282. <div class="dialog-footer">
  283. <el-button @click="handleCancel">取 消</el-button>
  284. <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确认结单</el-button>
  285. </div>
  286. </template>
  287. </el-dialog>
  288. <!-- 人员选择组件 -->
  289. <UserSelectMulti
  290. v-model="userSelectVisible"
  291. :pre-selected-users="selectedUsers"
  292. @onSelected="onUserSelected"
  293. />
  294. </template>
  295. <script setup>
  296. import { ref, defineProps, defineEmits, getCurrentInstance, watch } from 'vue'
  297. import preview from '@/components/FileUpload/preview.vue'
  298. import UserSelectMulti from "@/components/userSelect/multi.vue";
  299. import {ElMessageBox} from "element-plus";
  300. import {listGxtOrder} from "@/api/gxt/gxtOrder.js";
  301. import {listUser, listUserData, listUserNoPermi, listLeader} from "@/api/system/user";
  302. import MisInfoSelectSingle from "@/components/misInfoSelect/single.vue";
  303. import {listWorkPerson} from "@/api/gxt/misInfo.js";
  304. // 获取当前实例
  305. const { proxy } = getCurrentInstance()
  306. // 定义属性
  307. const props = defineProps({
  308. modelValue: {
  309. type: Boolean,
  310. default: false
  311. },
  312. data: {
  313. type: Object,
  314. default: () => ({})
  315. },
  316. workOrderStatusOptions: {
  317. type: Array,
  318. default: () => ([])
  319. },
  320. listUserData: {
  321. type: Function,
  322. default: null
  323. },
  324. onSubmit: {
  325. type: Function,
  326. default: null
  327. },
  328. infoEntryOptions: {
  329. type: Array,
  330. default: () => ([])
  331. },
  332. listMisInfo: {
  333. type: Function,
  334. default: null
  335. },
  336. listWorkPerson: {
  337. type: Function,
  338. default: null
  339. },
  340. infoEntryDisabled: {
  341. type: Boolean,
  342. default: false
  343. },
  344. commonKey: {
  345. type: Number,
  346. default: 0
  347. }
  348. })
  349. // 定义事件
  350. const emit = defineEmits(['update:modelValue', 'success', 'select-mis-info'])
  351. // 响应式数据
  352. const visible = ref(false)
  353. const formData = ref({})
  354. const finishRef = ref()
  355. const submitLoading = ref(false)
  356. const userSelectVisible = ref(false) // 添加人员选择组件可见性
  357. const selectedUsers = ref([]) // 存储选中的用户
  358. const inputUsers = ref([]) // 存储选中的用户
  359. const flowList = ref([])
  360. const suspendInfo = ref(null)
  361. const resumeInfo = ref(null)
  362. const resumeShow = ref(false)
  363. const showMisNoQuickSelect = ref(false)
  364. const quickMisNoList = ref([])
  365. const misInfoSelectVisible = ref(false)
  366. // 工作负责人快速检索相关响应式数据
  367. const showTeamLeaderQuickSelect = ref(false)
  368. const quickTeamLeaderList = ref([])
  369. const teamLeaderLoading = ref(false)
  370. const teamLeaderSearchTimer = ref(null)
  371. const allUserList = ref([]) // 存储所有设备数据用于快速检索
  372. const lastLoadedCenterId = ref(null)
  373. // 计算属性
  374. const workPermitNumProxy = computed({
  375. get() {
  376. return formData.value.workPermitNum
  377. },
  378. set(val) {
  379. formData.value.workPermitNum = val
  380. }
  381. })
  382. // 表单验证规则
  383. const finishRules = ref({
  384. realStartTime: [
  385. { required: true, message: "开始时间不能为空", trigger: "change" },
  386. {
  387. validator: (rule, value, callback) => {
  388. if (value && new Date(value) > new Date()) {
  389. callback(new Error('开始时间不能大于当前时间'));
  390. // } else if(value && new Date(value) < new Date(formData.value.acceptTime) && formData.value.infoEntry == '2') {
  391. // callback(new Error('开始时间不能小于接单时间'));
  392. } else {
  393. callback();
  394. }
  395. },
  396. trigger: 'change'
  397. }
  398. ],
  399. realEndTime: [
  400. { required: true, message: "结束时间不能为空", trigger: "change" },
  401. {
  402. validator: (rule, value, callback) => {
  403. if (value && new Date(value) > new Date() && formData.value.infoEntry == '2') {
  404. callback(new Error('结束时间不能大于当前时间'));
  405. } else if(value && new Date(value) < new Date(formData.value.realStartTime) && formData.value.infoEntry == '2') {
  406. callback(new Error('结束时间不能小于开始时间'));
  407. } else {
  408. callback();
  409. }
  410. },
  411. trigger: 'change'
  412. }
  413. ],
  414. workGroupMemberName: [
  415. { required: true, message: "请输入工作班成员", trigger: "change" },
  416. {
  417. validator: async (rule, value, callback) => {
  418. // 如果值为空、关联MIS,直接通过验证
  419. if (!value || formData.value.infoEntry == '1') {
  420. selectedUsers.value = [];
  421. return callback();
  422. }
  423. try {
  424. inputUsers.value = []
  425. // 将输入的工作班成员姓名按逗号分割
  426. const names = value.split(',').map(name => name.trim());
  427. // 验证每个工作班成员是否存在于组织架构中
  428. for (const name of names) {
  429. if (name.length > 0) {
  430. // 检查输入中重复
  431. if (inputUsers.value.some(u => u.nickName === name)) {
  432. return callback(new Error(`工作班成员"${name}"重复,请重新输入`));
  433. }
  434. // 使用从属性传入的listUserData方法(如果有的话)
  435. if (props.listUserData) {
  436. const response = await props.listUserData({nickName: name});
  437. if (!response.rows || response.rows.length === 0) {
  438. return callback(new Error(`工作班成员"${name}"非系统内人员,请重新输入`));
  439. }else{
  440. inputUsers.value.push(response.rows[0]);
  441. }
  442. }
  443. } else {
  444. return callback(new Error(`请正确输入工作班成员名单`));
  445. }
  446. }
  447. selectedUsers.value = inputUsers.value;
  448. callback();
  449. } catch (error) {
  450. callback(new Error('验证工作班成员时发生错误'));
  451. }
  452. },
  453. trigger: 'change'
  454. }
  455. ],
  456. content: [
  457. { required: true, message: "请输入维保内容", trigger: "change" }
  458. ],
  459. resumeTime: [
  460. { required: true, message: "挂起结束时间不能为空", trigger: "change" },
  461. {
  462. validator: (rule, value, callback) => {
  463. const realStartTime = formData.value.realStartTime
  464. const realEndTime = formData.value.realEndTime
  465. const suspendTime = suspendInfo.value.actionTime
  466. debugger
  467. if (suspendTime && realStartTime && new Date(suspendTime) < new Date(realStartTime)) { // 开工前挂起
  468. if (value && new Date(value) > new Date(realStartTime)) {
  469. callback(new Error('开工前挂起结束时间晚于实际开始时间,请调整'));
  470. } else if(realEndTime && value && new Date(value) > new Date(realEndTime)) {
  471. callback(new Error('开工前挂起结束时间晚于实际结束时间,请调整'));
  472. } else {
  473. callback();
  474. }
  475. } else if(suspendTime && realStartTime && new Date(suspendTime) >= new Date(realStartTime)) { // 作业中挂起
  476. if (value && new Date(value) < new Date(realStartTime)) {
  477. callback(new Error('作业中挂起结束时间早于实际开始时间,请调整'));
  478. } else if(realEndTime && value && new Date(value) > new Date(realEndTime)) {
  479. callback(new Error('作业中挂起结束时间晚于实际结束时间,请调整'));
  480. } else {
  481. callback();
  482. }
  483. } else {
  484. callback();
  485. }
  486. },
  487. trigger: 'change'
  488. }
  489. ],
  490. misNo: [
  491. { required: true, message: "MIS工单编码不能为空", trigger: "change" }
  492. ],
  493. workPermitNum: [
  494. { required: true, message: "工作号编号不能为空", trigger: "change" },
  495. {
  496. validator: (rule, value, callback) => {
  497. if (formData.value.infoEntry == '2') {
  498. if (!value) {
  499. callback(new Error('工作票编号不能为空'))
  500. } else if (!/^[a-zA-Z0-9_\-\.]*$/.test(value)) {
  501. callback(new Error('仅支持英文、数字、下划线'))
  502. } else if (value.length > 20) {
  503. callback(new Error('不能超过20个字符'))
  504. } else {
  505. // 验证唯一性
  506. if (value) {
  507. listGxtOrder({pageNum: 1, pageSize: 10, workPermitNum: value}).then(response => {
  508. const gxtOrders = response.rows
  509. if (gxtOrders.length > 0) {
  510. if (gxtOrders[0].id != formData.value.id) {
  511. callback(new Error('工作票编号已存在!'))
  512. } else {
  513. callback()
  514. }
  515. } else {
  516. callback()
  517. }
  518. }).catch(() => {
  519. callback()
  520. })
  521. } else {
  522. callback()
  523. }
  524. }
  525. } else {
  526. callback()
  527. }
  528. },
  529. trigger: 'change'
  530. }
  531. ],
  532. teamLeaderName: [
  533. { required: true, message: "工作负责人不能为空", trigger: "change" }
  534. ],
  535. })
  536. // 时间禁用函数
  537. const disabledStartDate = (time) => {
  538. const getYYYYMMDD = (date) => {
  539. const y = date.getFullYear();
  540. const m = String(date.getMonth() + 1).padStart(2, '0');
  541. const d = String(date.getDate()).padStart(2, '0');
  542. return `${y}-${m}-${d}`;
  543. };
  544. const today = getYYYYMMDD(new Date());
  545. let acceptDateStr = null;
  546. const acceptStr = formData.value.acceptTime;
  547. if (acceptStr) {
  548. acceptDateStr = acceptStr.split(' ')[0];
  549. }
  550. const selectedDateStr = getYYYYMMDD(time);
  551. // 如果没有 acceptTime,只禁用未来日期
  552. if (!acceptDateStr) {
  553. return selectedDateStr > today;
  554. }
  555. // 要用:早于 acceptDate 或 晚于今天
  556. // return selectedDateStr < acceptDateStr || selectedDateStr > today;
  557. return selectedDateStr > today;
  558. };
  559. const disabledEndDate = (time) => {
  560. const getYYYYMMDD = (date) => {
  561. const y = date.getFullYear();
  562. const m = String(date.getMonth() + 1).padStart(2, '0');
  563. const d = String(date.getDate()).padStart(2, '0');
  564. return `${y}-${m}-${d}`;
  565. };
  566. const today = getYYYYMMDD(new Date());
  567. let acceptDateStr = null;
  568. const acceptStr = formData.value.acceptTime;
  569. const startStr = formData.value.realStartTime;
  570. if (startStr) {
  571. acceptDateStr = startStr.split(' ')[0];
  572. } else if(acceptStr) {
  573. acceptDateStr = acceptStr.split(' ')[0];
  574. }
  575. const selectedDateStr = getYYYYMMDD(time);
  576. // 如果没有 acceptTime,只禁用未来日期
  577. if (!acceptDateStr) {
  578. return selectedDateStr > today;
  579. }
  580. // 要用:早于 acceptDate 或 晚于今天
  581. return selectedDateStr < acceptDateStr || selectedDateStr > today;
  582. };
  583. // 监听modelValue变化
  584. watch(() => props.modelValue, (val) => {
  585. visible.value = val
  586. if (val) {
  587. // 初始化表单数据
  588. formData.value = { ...props.data }
  589. flowList.value = formData.value.workOrderFlowList || []
  590. debugger
  591. if (formData.value.suspendReason && flowList.value.length > 0) {
  592. // 获取最后一个 actionType 等于 'resume' 的项
  593. const lastResumeItem = flowList.value.findLast(item => item.actionType === 'resume')
  594. if (lastResumeItem) {
  595. // 做你想做的事,比如记录时间、设置状态等
  596. console.log('最后一个 resume 项:', lastResumeItem)
  597. resumeInfo.value = lastResumeItem
  598. formData.value.resumeTime = lastResumeItem.actionTime
  599. }
  600. const lastSuspendItem = flowList.value.findLast(item => item.actionType === 'to_approve')
  601. if (lastSuspendItem) {
  602. // 做你想做的事,比如记录时间、设置状态等
  603. console.log('最后一个 to_approve 项:', lastSuspendItem)
  604. suspendInfo.value = lastSuspendItem
  605. }
  606. handleStartTimeChange(formData.value.realStartTime)
  607. }
  608. }
  609. })
  610. // 监听props.data变化
  611. watch(() => props.data, (newData) => {
  612. if (visible.value) {
  613. // 只有在对话框打开时才更新数据
  614. formData.value = { ...newData }
  615. flowList.value = formData.value.workOrderFlowList || []
  616. debugger
  617. if (formData.value.suspendReason && flowList.value.length > 0) {
  618. // 获取最后一个 actionType 等于 'resume' 的项
  619. const lastResumeItem = flowList.value.findLast(item => item.actionType === 'resume')
  620. if (lastResumeItem) {
  621. // 做你想做的事,比如记录时间、设置状态等
  622. console.log('最后一个 resume 项:', lastResumeItem)
  623. resumeInfo.value = lastResumeItem
  624. formData.value.resumeTime = lastResumeItem.actionTime
  625. }
  626. const lastSuspendItem = flowList.value.findLast(item => item.actionType === 'to_approve')
  627. if (lastSuspendItem) {
  628. // 做你想做的事,比如记录时间、设置状态等
  629. console.log('最后一个 to_approve 项:', lastSuspendItem)
  630. suspendInfo.value = lastSuspendItem
  631. }
  632. handleStartTimeChange(formData.value.realStartTime)
  633. }
  634. }
  635. }, { deep: true })
  636. // 监听visible变化
  637. watch(visible, (val) => {
  638. emit('update:modelValue', val)
  639. if (val) {
  640. // 打开对话框后重置表单验证错误
  641. proxy.$nextTick(() => {
  642. if (finishRef.value) {
  643. finishRef.value.clearValidate()
  644. }
  645. })
  646. }
  647. })
  648. watch(
  649. () => props.data?.realStartTime, // 只监听 realStartTime
  650. (newStartTime, oldStartTime) => {
  651. // 判断是否真的变化了(避免 undefined === undefined 误判)
  652. debugger
  653. if (resumeInfo.value && newStartTime && newStartTime !== oldStartTime) {
  654. // 注意:此时对话框可能未打开,根据你的需求决定是否加 visible 判断
  655. if (visible.value) {
  656. handleStartTimeChange(newStartTime);
  657. }
  658. }
  659. },
  660. { immediate: true } // 如果需要初始化时也触发一次,可加;否则去掉
  661. );
  662. watch(
  663. () => props.data?.realEndTime, // 只监听 realEndTime
  664. (newEndTime, oldEndTime) => {
  665. // 判断是否真的变化了(避免 undefined === undefined 误判)
  666. debugger
  667. if (resumeInfo.value && newEndTime && oldEndTime !== oldEndTime) {
  668. // 注意:此时对话框可能未打开,根据你的需求决定是否加 visible 判断
  669. if (visible.value) {
  670. handleEndTimeChange(newEndTime);
  671. }
  672. }
  673. },
  674. { immediate: true } // 如果需要初始化时也触发一次,可加;否则去掉
  675. );
  676. // 关闭对话框
  677. const handleClose = () => {
  678. visible.value = false
  679. selectedUsers.value = []
  680. resumeInfo.value = null
  681. suspendInfo.value = null
  682. resumeShow.value = false
  683. }
  684. // 取消操作
  685. const handleCancel = () => {
  686. visible.value = false
  687. selectedUsers.value = []
  688. resumeInfo.value = null
  689. suspendInfo.value = null
  690. resumeShow.value = false
  691. }
  692. // 用户选择回调函数
  693. const onUserSelected = (users) => {
  694. if (users && users.length > 0) {
  695. // 合并用户,避免重复
  696. const existingNames = selectedUsers.value.map(u => u.nickName);
  697. const newUsers = users.filter(u => !existingNames.includes(u.nickName));
  698. selectedUsers.value = [...selectedUsers.value, ...newUsers];
  699. // 构建用户姓名列表
  700. const userNames = selectedUsers.value.map(user => user.nickName).join(',');
  701. formData.value.workGroupMemberName = userNames;
  702. } else {
  703. selectedUsers.value = [];
  704. formData.value.workGroupMemberName = '';
  705. }
  706. formData.value.workOrderPersonList = selectedUsers.value.map(user => ({
  707. userId: user.userId,
  708. nickName: user.nickName,
  709. orderId: formData.value.id,
  710. orderCode: formData.value.workOrderProjectNo,
  711. status: 1
  712. }));
  713. };
  714. // 提交操作
  715. const handleSubmit = async () => {
  716. if (!finishRef.value) return
  717. await finishRef.value.validate(async (valid) => {
  718. if (valid) {
  719. const { realStartTime, acceptTime } = formData.value;
  720. debugger
  721. // if (realStartTime && acceptTime && (new Date(realStartTime) < new Date(acceptTime))) { //开始时间早于接单时间,为补录工单
  722. // formData.value.orderEntryType = '2'
  723. // try {
  724. // debugger
  725. // await ElMessageBox.confirm(
  726. // '检测到开始时间早于接单时间,系统将按“补录工单”处理,准备工时为0。是否继续?',
  727. // '提示',
  728. // {
  729. // confirmButtonText: '是',
  730. // cancelButtonText: '否',
  731. // type: 'warning',
  732. // distinguishCancelAndClose: true
  733. // }
  734. // );
  735. // formData.value.orderEntryType = '2'
  736. // // 用户点击“是”,继续提交
  737. // } catch (error) {
  738. // // 用户点击“否”或关闭弹窗
  739. // finalizeFormRef.value?.validateField('realStartTime');
  740. // return;
  741. // }
  742. // }
  743. try {
  744. if (!formData.value.realEndTime) {
  745. proxy.$modal.msgError("该工单未结束,无法结单!")
  746. return
  747. }
  748. submitLoading.value = true
  749. // 调用父组件传入的提交函数
  750. if (props.onSubmit && typeof props.onSubmit === 'function') {
  751. flowList.value = []
  752. if (formData.value.resumeTime && formData.value.resumeTime != resumeInfo.value.actionTime) { //存入新的挂起结束时间
  753. resumeInfo.value.actionTime = formData.value.resumeTime
  754. flowList.value.push(resumeInfo.value)
  755. }
  756. formData.value.workOrderFlowList = flowList.value
  757. formData.value.finalizeMethod = '2'
  758. formData.value.createTime = null
  759. formData.value.updateTime = null
  760. await props.onSubmit(formData.value)
  761. } else {
  762. throw new Error("未提供提交方法")
  763. }
  764. proxy.$modal.msgSuccess("结单成功")
  765. visible.value = false
  766. emit('success')
  767. } catch (error) {
  768. proxy.$modal.msgError("操作失败: " + (error.message || "未知错误"))
  769. } finally {
  770. submitLoading.value = false
  771. }
  772. }
  773. })
  774. }
  775. // 开始时间变化处理
  776. const handleStartTimeChange = (value) => {
  777. debugger
  778. if (resumeInfo.value && value) {
  779. const realStartTime = formData.value.realStartTime
  780. const realEndTime = formData.value.realEndTime
  781. const suspendTime = suspendInfo.value.actionTime
  782. const resumeTime = formData.value.resumeTime
  783. if (suspendTime && value && new Date(suspendTime) < new Date(value)) { // 开工前挂起
  784. if (value && resumeTime > new Date(value)) {
  785. resumeShow.value = true
  786. } else if(realEndTime && resumeTime && new Date(resumeTime) > new Date(realEndTime)) {
  787. resumeShow.value = true
  788. } else {
  789. resumeShow.value = false
  790. }
  791. } else if(suspendTime && value && new Date(suspendTime) >= new Date(value)) { // 作业中挂起
  792. if (value && new Date(resumeTime) < new Date(value)) {
  793. resumeShow.value = true
  794. } else if(realEndTime && resumeTime && new Date(resumeTime) > new Date(realEndTime)) {
  795. resumeShow.value = true
  796. } else {
  797. resumeShow.value = false
  798. }
  799. } else {
  800. resumeShow.value = false
  801. }
  802. }
  803. }
  804. // 结束时间变化处理
  805. const handleEndTimeChange = (value) => {
  806. debugger
  807. if (resumeInfo.value && value) {
  808. const realStartTime = formData.value.realStartTime
  809. const realEndTime = formData.value.realEndTime
  810. const suspendTime = suspendInfo.value.actionTime
  811. const resumeTime = formData.value.resumeTime
  812. if (suspendTime && realStartTime && new Date(suspendTime) < new Date(realStartTime)) { // 开工前挂起
  813. if (value && resumeTime > new Date(realStartTime)) {
  814. resumeShow.value = true
  815. } else if(value && resumeTime && new Date(resumeTime) > new Date(value)) {
  816. resumeShow.value = true
  817. } else {
  818. resumeShow.value = false
  819. }
  820. } else if(suspendTime && realStartTime && new Date(suspendTime) >= new Date(realStartTime)) { // 作业中挂起
  821. if (realStartTime && new Date(resumeTime) < new Date(realStartTime)) {
  822. resumeShow.value = true
  823. } else if(value && resumeTime && new Date(resumeTime) > new Date(value)) {
  824. resumeShow.value = true
  825. } else {
  826. resumeShow.value = false
  827. }
  828. } else {
  829. resumeShow.value = false
  830. }
  831. }
  832. }
  833. // MIS工单相关处理函数
  834. const handleMisNoInputFocus = () => {
  835. // showMisNoQuickSelect.value = true
  836. showMisNoQuickSelect.value = true
  837. // 如果已有输入内容,立即搜索
  838. if (formData.value.misNo && formData.value.misNo.trim()) {
  839. handleMisNoInput(formData.value.misNo)
  840. }
  841. }
  842. const handleMisNoInputBlur = () => {
  843. // 延迟隐藏下拉框,确保点击选项能触发
  844. setTimeout(() => {
  845. showMisNoQuickSelect.value = false
  846. }, 200)
  847. }
  848. const handleMisNoInput = (value) => {
  849. const searchText = value.trim()
  850. // 这里应该调用搜索函数
  851. if (!searchText) {
  852. quickMisNoList.value = []
  853. showMisNoQuickSelect.value = false
  854. return
  855. }
  856. showMisNoQuickSelect.value = true
  857. // 清除之前的定时器
  858. if (misNoSearchTimer.value) {
  859. clearTimeout(misNoSearchTimer.value)
  860. }
  861. // 设置新的定时器,防抖处理(500ms)
  862. misNoSearchTimer.value = setTimeout(() => {
  863. searchMisNoList(searchText)
  864. }, 500)
  865. }
  866. const handleMisNoClear = () => {
  867. formData.value.misNo = undefined
  868. }
  869. const handleSelectMisInfo = () => {
  870. // emit('select-mis-info')
  871. misInfoSelectVisible.value = true
  872. }
  873. const handleMisNoQuickSelect = (item) => {
  874. formData.value.misNo = item.misNo
  875. showMisNoQuickSelect.value = false
  876. }
  877. /** 搜索MIS工单列表 */
  878. const searchMisNoList = async (keyword) => {
  879. if (!keyword) {
  880. quickMisNoList.value = []
  881. return
  882. }
  883. debugger
  884. // 检查是否提供了listMisInfo函数
  885. if (!props.listMisInfo || typeof props.listMisInfo !== 'function') {
  886. console.error('未提供listMisInfo函数')
  887. quickMisNoList.value = []
  888. return
  889. }
  890. try {
  891. const response = await props.listMisInfo({
  892. pageNum: 1,
  893. misNo: keyword
  894. })
  895. quickMisNoList.value = response.rows || []
  896. } catch (error) {
  897. console.error('搜索MIS工单失败:', error)
  898. proxy.$modal.msgError('搜索失败,请重试')
  899. quickMisNoList.value = []
  900. }
  901. }
  902. // 信息录入方式变化处理
  903. const handleInfoEntryChange = (val) => {
  904. // 选中2工作票编号时修改其他值
  905. if (val === '2') {
  906. formData.value.misNo = undefined;
  907. formData.value.realStartTime = undefined;
  908. formData.value.realEndTime = undefined;
  909. formData.value.workGroupMemberName = undefined;
  910. formData.value.workPermitNum = undefined
  911. formData.value.teamLeaderName = undefined
  912. formData.value.teamLeaderId = undefined
  913. formData.value.repairOrderPersonList = [];
  914. selectedUsers.value = [];
  915. } else {
  916. formData.value.workPermitNum = undefined;
  917. formData.value.misOrderNo = undefined;
  918. formData.value.realStartTime = undefined;
  919. formData.value.realEndTime = undefined;
  920. formData.value.workGroupMemberName = undefined;
  921. formData.value.teamLeaderName = undefined
  922. formData.value.teamLeaderId = undefined
  923. formData.value.repairOrderPersonList = [];
  924. }
  925. // if (finishRef.value) {
  926. // finishRef.value.clearValidate()
  927. // }
  928. }
  929. // 工作负责人快速检索方法
  930. /** 过滤快速检索用户列表 */
  931. const filterQuickUserList = (keyword) => {
  932. debugger
  933. if (!allUserList.value.length) {
  934. loadQuickTeamLeaderList();
  935. return;
  936. }
  937. const lowerKeyword = keyword.toLowerCase();
  938. quickTeamLeaderList.value = allUserList.value.filter(item =>
  939. (item.nickName && item.nickName.toLowerCase().includes(lowerKeyword))
  940. );
  941. }
  942. /** 工作负责人输入框获取焦点 */
  943. const handleTeamLeaderInputFocus = () => {
  944. showTeamLeaderQuickSelect.value = true
  945. // 如果已有输入内容,立即搜索
  946. if (formData.value.teamLeaderName && formData.value.teamLeaderName.trim()) {
  947. handleTeamLeaderInput(formData.value.teamLeaderName)
  948. } else {
  949. // 如果没有输入内容,加载默认列表
  950. loadQuickTeamLeaderList()
  951. }
  952. }
  953. /** 工作负责人输入框失去焦点 */
  954. const handleTeamLeaderInputBlur = () => {
  955. // 延迟隐藏下拉框,确保点击选项能触发
  956. setTimeout(() => {
  957. showTeamLeaderQuickSelect.value = false
  958. }, 200)
  959. }
  960. /** 工作负责人输入事件 - 实时搜索 */
  961. const handleTeamLeaderInput = (value) => {
  962. const searchText = value.trim()
  963. if (!searchText) {
  964. quickTeamLeaderList.value = []
  965. showTeamLeaderQuickSelect.value = false
  966. return
  967. }
  968. showTeamLeaderQuickSelect.value = true
  969. // 清除之前的定时器
  970. if (teamLeaderSearchTimer.value) {
  971. clearTimeout(teamLeaderSearchTimer.value)
  972. }
  973. // 设置新的定时器,防抖处理(300ms)
  974. // teamLeaderSearchTimer.value = setTimeout(() => {
  975. // searchTeamLeaderList(searchText)
  976. filterQuickUserList(searchText)
  977. // }, 300)
  978. }
  979. /** 搜索工作负责人列表 */
  980. const searchTeamLeaderList = async (keyword) => {
  981. if (!keyword) {
  982. quickTeamLeaderList.value = []
  983. return
  984. }
  985. teamLeaderLoading.value = true
  986. try {
  987. const response = await listLeader({
  988. nickName: keyword,
  989. // 可以根据需要添加其他搜索条件
  990. status: '0' // 只搜索启用状态的用户
  991. })
  992. quickTeamLeaderList.value = response.data || []
  993. } catch (error) {
  994. console.error('搜索工作负责人失败:', error)
  995. proxy.$modal.msgError('搜索失败,请重试')
  996. quickTeamLeaderList.value = []
  997. } finally {
  998. teamLeaderLoading.value = false
  999. }
  1000. }
  1001. /** 加载快速检索工作负责人列表 */
  1002. const loadQuickTeamLeaderList = async () => {
  1003. // 如果已有所有用户数据,且是同一个维保中心,直接使用
  1004. if (allUserList.value.length > 0 && lastLoadedCenterId.value === formData.value.gxtCenterId) {
  1005. quickTeamLeaderList.value = allUserList.value;
  1006. return;
  1007. }
  1008. // 记录当前维保中心ID
  1009. lastLoadedCenterId.value = formData.value.gxtCenterId;
  1010. teamLeaderLoading.value = true
  1011. try {
  1012. // 加载当前部门下的用户列表
  1013. const response = await listLeader({
  1014. deptId: -1, // 通过后台配置部门
  1015. status: '0'
  1016. })
  1017. allUserList.value = response.data || []
  1018. quickTeamLeaderList.value = allUserList.value;
  1019. } catch (error) {
  1020. console.error('加载工作负责人列表失败:', error)
  1021. allUserList.value = []
  1022. } finally {
  1023. teamLeaderLoading.value = false
  1024. }
  1025. }
  1026. /** 快速选择工作负责人 */
  1027. const handleTeamLeaderQuickSelect = (item) => {
  1028. formData.value.teamLeaderId = item.userId
  1029. formData.value.teamLeaderName = item.nickName
  1030. showTeamLeaderQuickSelect.value = false
  1031. }
  1032. /** 清空工作负责人 */
  1033. const handleTeamLeaderClear = () => {
  1034. formData.value.teamLeaderId = undefined
  1035. formData.value.teamLeaderName = ''
  1036. quickTeamLeaderList.value = []
  1037. showTeamLeaderQuickSelect.value = false
  1038. }
  1039. const onMisInfoSelected = (row) => {
  1040. if (row) {
  1041. listGxtOrder({pageNum: 1, pageSize: 10, misNo: row.misNo }).then(response => {
  1042. const gxtOrders= response.rows
  1043. debugger
  1044. if (gxtOrders.length > 0) {
  1045. if (formData.value.id == null) {
  1046. proxy.$modal.msgWarning('选择工单已存在!请重新选择!')
  1047. return
  1048. } else {
  1049. if (gxtOrders[0].id != formData.value.id) {
  1050. proxy.$modal.msgWarning('选择工单已存在!请重新选择!')
  1051. return
  1052. }
  1053. }
  1054. }
  1055. // 使用展开运算符创建新对象以确保响应式更新
  1056. formData.value = {
  1057. ...formData.value,
  1058. misNo: row.misNo,
  1059. realStartTime: row.realStartTime,
  1060. realEndTime: row.realEndTime,
  1061. content: row.content,
  1062. workPermitNum: row.workPermitNum
  1063. };
  1064. // 使用从属性传入的listWorkPerson方法
  1065. if (typeof listWorkPerson === 'function') {
  1066. listWorkPerson({ misNo: row.misNo }).then(response => {
  1067. debugger
  1068. const updatedData = {
  1069. ...formData.value,
  1070. workOrderPersonList: response.rows
  1071. };
  1072. if (response.rows) {
  1073. // 查找 isLeader 等于 1 的负责人(优先获取第一个符合条件的,贴合常规单负责人场景)
  1074. const leaderPerson = response.rows.find(person => person.isLeader === 1);
  1075. // 给 teamLeaderName 赋值
  1076. updatedData.teamLeaderName = leaderPerson?.nickName || '';
  1077. const nickNames = response.rows
  1078. .filter(person => person.isLeader != 1)
  1079. .map(person => person.nickName)
  1080. .join(',');
  1081. updatedData.workGroupMemberName = nickNames
  1082. }
  1083. formData.value = updatedData;
  1084. })
  1085. }
  1086. misInfoSelectVisible.value = false
  1087. })
  1088. }
  1089. }
  1090. </script>
  1091. <style scoped>
  1092. /* 表单中的列间距调整 */
  1093. :deep(.el-col) {
  1094. padding-left: 5px;
  1095. padding-right: 5px;
  1096. }
  1097. /* 穿透修改组件内部输入框的对齐方式(关键) */
  1098. :deep(.input-number-left .el-input__inner) {
  1099. text-align: left !important; /* !important 覆盖组件默认的居中 */
  1100. }
  1101. /* MIS工单快速检索样式 */
  1102. .quick-select-dropdown {
  1103. position: absolute;
  1104. top: 100%;
  1105. left: 0;
  1106. right: 0;
  1107. background: white;
  1108. border: 1px solid #e4e7ed;
  1109. border-top: none;
  1110. border-radius: 0 0 4px 4px;
  1111. max-height: 200px;
  1112. overflow-y: auto;
  1113. z-index: 2000;
  1114. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  1115. }
  1116. .quick-select-item {
  1117. padding: 8px 12px;
  1118. cursor: pointer;
  1119. border-bottom: 1px solid #f0f0f0;
  1120. display: flex;
  1121. justify-content: space-between;
  1122. align-items: center;
  1123. }
  1124. .quick-select-item:hover {
  1125. background-color: #f5f7fa;
  1126. }
  1127. .quick-select-item:last-child {
  1128. border-bottom: none;
  1129. }
  1130. </style>