wbBackfillFinalize.uvue 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374
  1. <template>
  2. <view class="detail-page">
  3. <scroll-view class="detail-content" :scroll-y="true">
  4. <!-- 工单信息 -->
  5. <view class="info-section">
  6. <view class="section-title">
  7. <text class="section-title-text">工单信息</text>
  8. </view>
  9. <view class="info-card">
  10. <view class="info-item">
  11. <text class="info-label">工单编码</text>
  12. <text class="info-value">{{ workOrderProjectNo ?? '' }}</text>
  13. </view>
  14. <view class="info-item">
  15. <text class="info-label">工单类型</text>
  16. <text class="info-value">{{ orderType == '1' ? '维修工单' : '维保工单' }}</text>
  17. </view>
  18. <view class="info-item">
  19. <text class="info-label">风机编号</text>
  20. <text class="info-value">{{ pcsDeviceName ?? '' }}</text>
  21. </view>
  22. <view class="info-item">
  23. <text class="info-label">维保中心</text>
  24. <text class="info-value">{{ gxtCenter ?? '' }}</text>
  25. </view>
  26. <view class="info-item">
  27. <text class="info-label">场站</text>
  28. <text class="info-value">{{ pcsStationName ?? '' }}</text>
  29. </view>
  30. <view class="info-item">
  31. <text class="info-label">机型</text>
  32. <text class="info-value">{{ brand ?? '' }} {{ model ?? '' }}</text>
  33. </view>
  34. <view class="info-item">
  35. <text class="info-label">MIS工单</text>
  36. <text class="info-value">{{ misNo ?? '' }}</text>
  37. </view>
  38. <view class="info-item">
  39. <text class="info-label">工作票编号</text>
  40. <text class="info-value">{{ workPermitNum ?? '' }}</text>
  41. </view>
  42. <view class="info-item">
  43. <text class="info-label">接单时间</text>
  44. <text class="info-value">{{ acceptTime ?? '' }}</text>
  45. </view>
  46. </view>
  47. </view>
  48. <!-- 结单表单 -->
  49. <view class="info-section">
  50. <view class="section-title">
  51. <text class="section-title-text">结单信息</text>
  52. </view>
  53. <view class="info-card">
  54. <!-- 开始时间 -->
  55. <view class="info-item">
  56. <view class="info-label">
  57. <text class="form-label required">开始时间<text style="color: red;">*</text></text>
  58. </view>
  59. <view class="info-value">
  60. <view class="form-picker" @click="showStartTimePicker = true">
  61. <input
  62. class="input-field"
  63. placeholder="请选择开始时间"
  64. v-model="realStartTime"
  65. type="none"
  66. />
  67. </view>
  68. </view>
  69. </view>
  70. <!-- 结束时间 -->
  71. <view class="info-item">
  72. <view class="info-label">
  73. <text class="form-label required">结束时间<text style="color: red;">*</text></text>
  74. </view>
  75. <view class="info-value">
  76. <view class="form-picker" @click="showEndTimePicker = true">
  77. <input
  78. class="input-field"
  79. placeholder="请选择结束时间"
  80. v-model="realEndTime"
  81. type="none"
  82. />
  83. </view>
  84. </view>
  85. </view>
  86. <!-- 挂起结束时间 -->
  87. <view class="info-item" v-if="resumeInfo != null && resumeShow">
  88. <view class="info-label">
  89. <text class="form-label required">挂起结束时间<text style="color: red;">*</text></text>
  90. </view>
  91. <view class="info-value">
  92. <view class="form-picker" @click="showResumeTimePicker = true">
  93. <input
  94. class="input-field"
  95. placeholder="请选择挂起结束时间"
  96. v-model="resumeTime"
  97. type="none"
  98. />
  99. </view>
  100. </view>
  101. </view>
  102. <!-- 外委人员数 -->
  103. <view class="info-item">
  104. <view class="info-label">
  105. <text class="form-label required">外委人员数(人)</text>
  106. </view>
  107. <view class="info-value">
  108. <input
  109. type="number"
  110. class="input-field"
  111. placeholder="请输入外委人员数"
  112. v-model="wwryNum"
  113. @input="onWwryNumInput"
  114. />
  115. </view>
  116. </view>
  117. <!-- 外来人员数 -->
  118. <view class="info-item">
  119. <view class="info-label">
  120. <text class="form-label required">外来人员数(人)</text>
  121. </view>
  122. <view class="info-value">
  123. <input
  124. type="number"
  125. class="input-field"
  126. placeholder="请输入外来人员数"
  127. v-model="wlryNum"
  128. @input="onWlryNumInput"
  129. />
  130. </view>
  131. </view>
  132. <!-- 工作负责人 -->
  133. <view class="info-item">
  134. <view class="info-label">
  135. <text class="form-label required">工作负责人</text>
  136. </view>
  137. <view class="info-value">
  138. <input
  139. class="input-field"
  140. placeholder="请选择工作负责人"
  141. v-model="teamLeaderName"
  142. :disabled="true"
  143. />
  144. </view>
  145. </view>
  146. <!-- 工作班成员选择(当信息录入为2时可编辑) -->
  147. <view class="info-item">
  148. <view class="info-label">
  149. <text class="form-label required">工作班成员</text>
  150. </view>
  151. <view class="info-value">
  152. <view class="input-with-clear">
  153. <input
  154. class="input-field"
  155. placeholder="请选择工作班成员"
  156. v-model="workGroupMemberName"
  157. @click="showUserSelect = true"
  158. :disabled="infoEntry == '1'"
  159. />
  160. <text class="select-users-count" v-if="selectedUserIds.length > 0" :style="{ marginRight: selectedUserIds.length > 0 ? '60rpx' : '0' }">({{ selectedUserIds.length }}人)</text>
  161. <text class="select-clear" v-if="selectedUserIds.length > 0" @click="clearSelectedUsers">×</text>
  162. </view>
  163. </view>
  164. </view>
  165. <!-- 维保内容 -->
  166. <view class="info-item full-width">
  167. <view class="info-label">
  168. <text class="form-label required">维保内容<text style="color: red;">*</text></text>
  169. </view>
  170. <view class="info-value">
  171. <textarea
  172. class="textarea-field"
  173. placeholder="请输入维保内容"
  174. v-model="content"
  175. maxlength="500"
  176. :disabled="infoEntry == '1'"
  177. :show-confirm-bar="false"
  178. auto-height
  179. ></textarea>
  180. </view>
  181. </view>
  182. <!-- 附件上传 -->
  183. <view class="info-item full-width">
  184. <view class="info-label">
  185. <text class="form-label">附件(可选)</text>
  186. </view>
  187. <view class="info-value">
  188. <upload-image
  189. :limit="8"
  190. :modelValue="uploadedFiles"
  191. :businessType="'workOrder'"
  192. @update:modelValue="uploadedFiles = $event as UTSArray<UploadResponse>"
  193. />
  194. </view>
  195. </view>
  196. </view>
  197. </view>
  198. <!-- 时间选择器弹窗 -->
  199. <!-- Start Date Picker -->
  200. <l-popup v-model="showStartTimePicker" position="bottom">
  201. <l-date-time-picker
  202. title="选择开始时间"
  203. :mode="1 | 2 | 4 | 8 | 16"
  204. format="YYYY-MM-DD HH:mm"
  205. :modelValue="realStartTime"
  206. confirm-btn="确定"
  207. cancel-btn="取消"
  208. @confirm="onStartDateConfirm"
  209. @cancel="showStartTimePicker = false">
  210. </l-date-time-picker>
  211. </l-popup>
  212. <!-- End Date Picker -->
  213. <l-popup v-model="showEndTimePicker" position="bottom">
  214. <l-date-time-picker
  215. title="选择结束时间"
  216. :mode="31"
  217. format="YYYY-MM-DD HH:mm"
  218. :modelValue="realEndTime"
  219. confirm-btn="确定"
  220. cancel-btn="取消"
  221. @confirm="onEndDateConfirm"
  222. @cancel="showEndTimePicker = false">
  223. </l-date-time-picker>
  224. </l-popup>
  225. <l-popup v-model="showResumeTimePicker" position="bottom" :safe-area-inset-bottom="true" :z-index="10000">
  226. <l-date-time-picker
  227. title="选择挂起结束时间"
  228. :mode="31"
  229. format="YYYY-MM-DD HH:mm"
  230. :modelValue="resumeTime"
  231. confirm-btn="确定"
  232. cancel-btn="取消"
  233. @confirm="onResumeTimeConfirm"
  234. @cancel="showResumeTimePicker = false">
  235. </l-date-time-picker>
  236. </l-popup>
  237. <!-- 人员选择弹窗 -->
  238. <view v-if="showUserSelect" class="picker-modal">
  239. <view class="modal-mask" @click="showUserSelect = false"></view>
  240. <view class="modal-content">
  241. <view class="modal-header">
  242. <text class="modal-title">选择工作班成员</text>
  243. <text class="modal-close" @click="confirmSelectedUsers">确定</text>
  244. </view>
  245. <view class="search-bar">
  246. <view class="search-box">
  247. <image class="search-icon" src="/static/images/workbench/list/1.png" mode="aspectFit"></image>
  248. <input class="search-input" type="text" placeholder="搜索姓名" v-model="userKeyword" @input="handleSearch" />
  249. <text v-if="userKeyword.length > 0" class="clear-icon" @click="clearSearch">✕</text>
  250. </view>
  251. </view>
  252. <scroll-view class="modal-body" scroll-y="true">
  253. <view
  254. v-for="(user, index) in userList"
  255. :key="index"
  256. class="picker-option"
  257. @click="toggleUserSelection(user)"
  258. >
  259. <text class="option-text">{{ (user['nickName'] as string | null) ?? '' }}</text>
  260. <text class="option-text">{{ ((user['dept'] as UTSJSONObject | null)?.['deptName'] as string | null) ?? '' }}</text>
  261. <text class="option-check" v-if="isSelected(user)">✓</text>
  262. </view>
  263. </scroll-view>
  264. </view>
  265. </view>
  266. </scroll-view>
  267. <!-- 确认结单按钮 -->
  268. <view class="accept-button-container">
  269. <button class="accept-button" @click="handleSubmit" :loading="submitLoading">{{ submitLoading ? '提交中...' : '确认结单' }}</button>
  270. </view>
  271. <!-- 加载中状态 -->
  272. <view v-if="loading" class="loading-mask">
  273. <text class="loading-text">加载中...</text>
  274. </view>
  275. </view>
  276. </template>
  277. <script setup lang="uts">
  278. import { ref, watch } from 'vue'
  279. import type { acceptOrderInfo } from '../../../types/order'
  280. import type { WorkOrderFlow } from '../../../types/flow'
  281. import { getOrderInfoById, getRepairOrderInfoById, returnRepairOrder, finishOrder } from '../../../api/order/detail'
  282. import { getMisInfoList, listWorkPerson, getOrderList, listAutoMisInfo } from '../../../api/order/list'
  283. import type { SysDictData } from '../../../types/dict'
  284. import { getDictDataByType } from '../../../api/dict/index'
  285. import type { UserInfo } from '../../../types/user'
  286. import type { UploadResponse } from '../../../types/workbench'
  287. import {checkPermi} from '../../../utils/storage'
  288. import { getUserList, getLeaderList } from '../../../api/user/list'
  289. // import uploadImage from '../../../components/upload-image/upload-image.uvue'
  290. // 工单信息
  291. const orderId = ref<string>('')
  292. const workOrderProjectNo = ref<string>('')
  293. const workOrderStatus = ref<string>('')
  294. const orderType = ref<string>('')
  295. const pcsDeviceName = ref<string>('')
  296. const gxtCenter = ref<string>('')
  297. const pcsStationName = ref<string>('')
  298. const brand = ref<string>('')
  299. const model = ref<string>('')
  300. const acceptTime = ref<string>('')
  301. const returnType = ref<string>('')
  302. const returnReason = ref<string>("")
  303. const returnTypeLabel = ref<string>("")
  304. const acceptReturnType = ref<string>('')
  305. const acceptReturnReason = ref<string>("")
  306. const teamLeaderId = ref<Number | null>(null)
  307. const teamLeaderName = ref<string>('')
  308. const pauseTime = ref<string>('')
  309. const restartTime = ref<string>('')
  310. const misNo = ref<string>('') // MIS工单编码
  311. // 添加字典加载状态
  312. const dictLoaded = ref<boolean>(false)
  313. // 结单表单相关变量
  314. const infoEntry = ref<string>('') // 信息录入
  315. const workPermitNum = ref<string>('') // 工作票编号
  316. const realStartTime = ref<string>('') // 开始时间
  317. const realEndTime = ref<string>('') // 结束时间
  318. const wwryNum = ref<string>('') // 外委人员数
  319. const wlryNum = ref<string>('') // 外来人员数
  320. const workGroupMemberName = ref<string>('') // 工作班成员
  321. const content = ref<string>('') // 维保内容
  322. const attachmentUrls = ref<string>('') // 附件URLs(逗号分隔的字符串格式)
  323. const uploadedFiles = ref<UploadResponse[]>([]) // 上传的文件对象数组
  324. const workOrderPersonList = ref<UTSJSONObject[]>([]) // 工作班成员数组
  325. const selectedUserIds = ref<string[]>([]) // 选中的用户ID数组
  326. const selectedUsers = ref<UTSJSONObject[]>([]) // 选中的用户对象数组
  327. // 时间选择器相关变量
  328. const showStartTimePicker = ref<boolean>(false)
  329. const showEndTimePicker = ref<boolean>(false)
  330. const startTimeDate = ref<string>('')
  331. const startTimeTime = ref<string>('')
  332. const endTimeDate = ref<string>('')
  333. const endTimeTime = ref<string>('')
  334. // 搜索关键词
  335. const userKeyword = ref<string>('')
  336. // 人员选择相关变量
  337. const showUserSelect = ref<boolean>(false)
  338. const userList = ref<UTSJSONObject[]>([])
  339. const userAllList = ref<UTSJSONObject[]>([])
  340. // 信息录入选项
  341. const selectedMisInfoIndex = ref<number>(-1)
  342. //挂起结束时间
  343. const suspendReason = ref<string>('') // 挂起原因
  344. const flowList = ref<UTSJSONObject[]>([]) //流转过程
  345. const suspendInfo = ref<UTSJSONObject | null>(null)
  346. const resumeInfo = ref<UTSJSONObject | null>(null)
  347. const resumeShow = ref<boolean>(false)
  348. const resumeTime = ref<string>('') // 挂起结束时间
  349. const showResumeTimePicker = ref<boolean>(false)
  350. // 监听开始时间和结束时间变化
  351. watch(realStartTime, (value: string) => {
  352. console.log('开始时间变化:', value)
  353. // 在这里添加开始时间变化时的处理逻辑
  354. if (resumeInfo.value != null && value != '') {
  355. const suspendTime = (suspendInfo.value?.['actionTime'] as string | null) ?? ''
  356. if (suspendTime != '' && value != '') {
  357. const suspendDate = new Date(suspendTime)
  358. const startDate = new Date(value)
  359. if (suspendDate.getTime() < startDate.getTime()) { // 开工前挂起
  360. const resumeDate = new Date(resumeTime.value)
  361. const endDate = new Date(realEndTime.value)
  362. if (resumeDate.getTime() > startDate.getTime() || (realEndTime.value != '' && resumeDate.getTime() > endDate.getTime())) {
  363. resumeShow.value = true
  364. } else {
  365. resumeShow.value = false
  366. }
  367. } else if (suspendDate.getTime() >= startDate.getTime()) { // 作业中挂起
  368. const resumeDate = new Date(resumeTime.value)
  369. const endDate = new Date(realEndTime.value)
  370. if (resumeDate.getTime() < startDate.getTime() || (realEndTime.value != '' && resumeDate.getTime() > endDate.getTime())) {
  371. resumeShow.value = true
  372. } else {
  373. resumeShow.value = false
  374. }
  375. } else {
  376. resumeShow.value = false
  377. }
  378. } else {
  379. resumeShow.value = false
  380. }
  381. }
  382. })
  383. watch(realEndTime, (value: string) => {
  384. console.log('结束时间变化:', value)
  385. // 在这里添加结束时间变化时的处理逻辑
  386. if (resumeInfo.value != null && value != '') {
  387. const suspendTime = (suspendInfo.value?.['actionTime'] as string | null) ?? ''
  388. if (suspendTime != '' && realStartTime.value != '' && new Date(suspendTime).getTime() < new Date(realStartTime.value).getTime()) { // 开工前挂起
  389. if (value != '' && resumeTime.value != '' && new Date(resumeTime.value).getTime() > new Date(realStartTime.value).getTime()) {
  390. resumeShow.value = true
  391. } else if(value != '' && resumeTime.value != '' && new Date(resumeTime.value).getTime() > new Date(value).getTime()) {
  392. resumeShow.value = true
  393. } else {
  394. resumeShow.value = false
  395. }
  396. } else if(suspendTime != '' && realStartTime.value != '' && new Date(suspendTime).getTime() >= new Date(realStartTime.value).getTime()) { // 作业中挂起
  397. if (realStartTime.value != '' && resumeTime.value != '' && new Date(resumeTime.value).getTime() < new Date(realStartTime.value).getTime()) {
  398. resumeShow.value = true
  399. } else if(value != '' && resumeTime.value != '' && new Date(resumeTime.value).getTime() > new Date(value).getTime()) {
  400. resumeShow.value = true
  401. } else {
  402. resumeShow.value = false
  403. }
  404. } else {
  405. resumeShow.value = false
  406. }
  407. }
  408. })
  409. // 获取用户列表
  410. const getUserAllList = async (): Promise<void> => {
  411. try {
  412. // 这里应该调用获取用户列表的API
  413. const result = await getLeaderList(-1); // 空参数调用
  414. const resultObj = result as UTSJSONObject;
  415. const code = resultObj['code'] as number
  416. const users = resultObj['data'] as UTSJSONObject[] | null
  417. if (code == 200 && users != null ) {
  418. // 解析列表数据
  419. // const rows = resultObj['rows'] as UTSJSONObject[]
  420. userList.value = users;
  421. userAllList.value = users
  422. // userList.value = result.data || [];
  423. }
  424. } catch (error) {
  425. console.error('获取用户列表失败:', error);
  426. uni.showToast({
  427. title: '获取用户列表失败',
  428. icon: 'none'
  429. });
  430. }
  431. };
  432. // 外委人员数输入处理
  433. const onWwryNumInput = (): void => {
  434. let value = wwryNum.value;
  435. // 移除非数字字符,包括负号和小数点
  436. value = value.replace(/[^0-9]/g, '');
  437. wwryNum.value = value;
  438. };
  439. // 外来人员数输入处理
  440. const onWlryNumInput = (): void => {
  441. let value = wlryNum.value;
  442. // 移除非数字字符,包括负号和小数点
  443. value = value.replace(/[^0-9]/g, '');
  444. wlryNum.value = value;
  445. };
  446. // 验证和提交
  447. const submitLoading = ref<boolean>(false)
  448. // 接受用户名
  449. const acceptUserName = ref<string>('')
  450. // 选择器选项类型
  451. type PickerOption = {
  452. label: string
  453. value: string
  454. }
  455. // 搜索
  456. const handleSearch = (): void => {
  457. const keyword = userKeyword.value
  458. userList.value = userAllList.value.filter(leader => {
  459. const nickName = leader['nickName'] as string | null
  460. return nickName != null && nickName.indexOf(keyword) >= 0
  461. })
  462. }
  463. // 清除用户搜索
  464. const clearSearch = (): void => {
  465. userKeyword.value = "";
  466. userList.value = userAllList.value
  467. };
  468. function onStartDateConfirm(value: string) {
  469. // 检查结束时间是否小于新的开始时间
  470. if (realEndTime.value != '' && new Date(value) > new Date(realEndTime.value as string)) {
  471. uni.showToast({ title: '开始时间不能大于结束时间', icon: 'none' })
  472. return
  473. }
  474. realStartTime.value = value
  475. showStartTimePicker.value = false
  476. }
  477. function onEndDateConfirm(value: string) {
  478. // 检查新的结束时间是否小于开始时间
  479. if (realStartTime.value != '' && new Date(realStartTime.value as string) > new Date(value)) {
  480. uni.showToast({ title: '结束时间不能小于开始时间', icon: 'none' })
  481. return
  482. }
  483. realEndTime.value = value
  484. showEndTimePicker.value = false
  485. }
  486. function onResumeTimeConfirm(value: string) {
  487. // 检查新的结束时间是否小于开始时间
  488. if (resumeTime.value != '' && new Date(realStartTime.value as string) > new Date(value)) {
  489. uni.showToast({ title: '结束时间不能小于开始时间', icon: 'none' })
  490. return
  491. }
  492. resumeTime.value = value
  493. showResumeTimePicker.value = false
  494. }
  495. // 检查用户是否已被选中
  496. const isSelected = (user: UTSJSONObject): boolean => {
  497. const userId = user['userId'] as string | number | null;
  498. if (userId !== null) {
  499. return selectedUserIds.value.includes(userId.toString());
  500. }
  501. // 如果没有userId,则比较nickName
  502. const nickName = user['nickName'] as string | null;
  503. if (nickName !== null) {
  504. return selectedUsers.value.some(selected =>
  505. (selected['nickName'] as string | null) === nickName
  506. );
  507. }
  508. return false;
  509. };
  510. // 切换用户选择状态
  511. const toggleUserSelection = (user: UTSJSONObject): void => {
  512. const userId = user['userId'] as string | number | null;
  513. const nickName = user['nickName'] as string | null;
  514. if (userId !== null) {
  515. const userIdStr = userId.toString();
  516. const index = selectedUserIds.value.indexOf(userIdStr);
  517. if (index > -1) {
  518. // 取消选择
  519. selectedUserIds.value.splice(index, 1);
  520. selectedUsers.value = selectedUsers.value.filter(selected =>
  521. (selected['userId'] as string | number | null)?.toString() !== userIdStr
  522. );
  523. } else {
  524. // 添加选择
  525. selectedUserIds.value.push(userIdStr);
  526. selectedUsers.value.push(user);
  527. }
  528. } else if (nickName !== null) {
  529. // 如果没有userId,通过nickName来识别
  530. const index = selectedUsers.value.findIndex(selected =>
  531. (selected['nickName'] as string | null) === nickName
  532. );
  533. if (index > -1) {
  534. // 取消选择
  535. selectedUsers.value.splice(index, 1);
  536. // 同时移除对应的userId(如果有的话)
  537. const userToRemoveId = user['userId'] as string | number | null;
  538. if (userToRemoveId !== null) {
  539. const idIndex = selectedUserIds.value.indexOf(userToRemoveId.toString());
  540. if (idIndex > -1) {
  541. selectedUserIds.value.splice(idIndex, 1);
  542. }
  543. }
  544. } else {
  545. // 添加选择
  546. selectedUsers.value.push(user);
  547. const userToAddId = user['userId'] as string | number | null;
  548. if (userToAddId !== null) {
  549. selectedUserIds.value.push(userToAddId.toString());
  550. }
  551. }
  552. }
  553. };
  554. // 确认选择的用户
  555. const confirmSelectedUsers = (): void => {
  556. // 将选中的用户姓名拼接成字符串
  557. const nickNames = selectedUsers.value
  558. .map(user => (user['nickName'] as string | null) ?? '')
  559. .filter(name => name !== '')
  560. .join(',');
  561. workGroupMemberName.value = nickNames;
  562. // 更新workOrderPersonList为选中的用户
  563. workOrderPersonList.value = [...selectedUsers.value];
  564. showUserSelect.value = false;
  565. };
  566. // 清空已选择的用户
  567. const clearSelectedUsers = (): void => {
  568. // 清空选中的用户数组
  569. selectedUsers.value = [];
  570. selectedUserIds.value = [];
  571. // 清空显示的用户名
  572. workGroupMemberName.value = '';
  573. // 清空工作班成员列表
  574. workOrderPersonList.value = [];
  575. };
  576. // 表单验证
  577. const validateForm = (): boolean => {
  578. if (infoEntry.value == '') {
  579. uni.showToast({
  580. title: '请选择信息录入方式',
  581. icon: 'none'
  582. });
  583. return false;
  584. }
  585. if (realStartTime.value == '') {
  586. uni.showToast({
  587. title: '请选择开始时间',
  588. icon: 'none'
  589. });
  590. return false;
  591. }
  592. if (realEndTime.value == '') {
  593. uni.showToast({
  594. title: '请选择结束时间',
  595. icon: 'none'
  596. });
  597. return false;
  598. }
  599. if (new Date(realEndTime.value) <= new Date(realStartTime.value)) {
  600. uni.showToast({
  601. title: '结束时间必须大于开始时间',
  602. icon: 'none'
  603. });
  604. return false;
  605. }
  606. if(resumeShow.value) {
  607. if(resumeTime.value == '') {
  608. uni.showToast({
  609. title: '请选挂起结束时间',
  610. icon: 'none'
  611. });
  612. return false;
  613. } else {
  614. const suspendTime = (suspendInfo.value?.['actionTime'] as string | null) ?? ''
  615. if (suspendTime != '' && realStartTime.value != '' && new Date(suspendTime) < new Date(realStartTime.value)) { // 开工前挂起
  616. if (resumeTime.value != '' && new Date(resumeTime.value) > new Date(realStartTime.value)) {
  617. uni.showToast({
  618. title: '开工前挂起结束时间晚于实际开始时间,请调整',
  619. icon: 'none'
  620. });
  621. return false;
  622. } else if(realEndTime.value != '' && resumeTime.value != '' && new Date(resumeTime.value) > new Date(realEndTime.value)) {
  623. uni.showToast({
  624. title: '开工前挂起结束时间晚于实际结束时间,请调整',
  625. icon: 'none'
  626. });
  627. return false;
  628. }
  629. } else if(suspendTime != '' && realStartTime.value != '' && new Date(suspendTime) >= new Date(realStartTime.value)) { // 作业中挂起
  630. if (resumeTime.value != '' && new Date(resumeTime.value) < new Date(realStartTime.value)) {
  631. uni.showToast({
  632. title: '作业中挂起结束时间早于实际开始时间,请调整',
  633. icon: 'none'
  634. });
  635. return false;
  636. } else if(realEndTime.value != '' && resumeTime.value != '' && new Date(resumeTime.value) > new Date(realEndTime.value)) {
  637. uni.showToast({
  638. title: '作业中挂起结束时间晚于实际结束时间,请调整',
  639. icon: 'none'
  640. });
  641. return false;
  642. }
  643. }
  644. }
  645. }
  646. // if (infoEntry.value == '2' && (workGroupMemberName.value == '' || selectedUsers.value.length == 0)) {
  647. // uni.showToast({
  648. // title: '请选择工作班成员',
  649. // icon: 'none'
  650. // });
  651. // return false;
  652. // }
  653. return true;
  654. };
  655. const isDealing = ref(false)
  656. const hasDealed = ref(false)
  657. // 提交表单
  658. const handleSubmit = async (): Promise<void> => {
  659. if (!validateForm()) {
  660. return;
  661. }
  662. submitLoading.value = true;
  663. try {
  664. if (isDealing.value || hasDealed.value) return // 双重保险
  665. isDealing.value = true
  666. // 确保附件URLs是最新的逗号分隔格式
  667. attachmentUrls.value = uploadedFiles.value.map(file => file.fileName).join(',');
  668. flowList.value = []
  669. if (resumeTime.value != '' && resumeInfo.value != null && resumeTime.value != (resumeInfo.value['actionTime'] as string | null)) { //存入新的挂起结束时间
  670. resumeInfo.value['actionTime'] = resumeTime.value
  671. flowList.value.push(resumeInfo.value as UTSJSONObject)
  672. }
  673. const finishData = {
  674. id: orderId.value,
  675. orderType: orderType.value,
  676. workOrderProjectNo: workOrderProjectNo.value,
  677. infoEntry: infoEntry.value,
  678. misNo: misNo.value,
  679. workPermitNum: workPermitNum.value,
  680. realStartTime: realStartTime.value,
  681. realEndTime: realEndTime.value,
  682. wwryNum: wwryNum.value,
  683. wlryNum: wlryNum.value,
  684. workGroupMemberName: workGroupMemberName.value,
  685. content: content.value,
  686. attachmentUrls: attachmentUrls.value,
  687. workOrderPersonList: workOrderPersonList.value,
  688. teamLeaderId: teamLeaderId.value,
  689. teamLeaderName: teamLeaderName.value,
  690. finalizeMethod: '2',
  691. workOrderStatus: 'completed',
  692. workOrderFlowList: flowList.value
  693. } as UTSJSONObject;
  694. const result = await finishOrder(finishData);
  695. const resultObj = result as UTSJSONObject;
  696. const code = resultObj['code'] as number;
  697. if (code == 200) {
  698. uni.showToast({
  699. title: '结单成功',
  700. icon: 'success'
  701. });
  702. hasDealed.value = true
  703. // 使用事件总线通知列表页面刷新
  704. uni.$emit('refreshOrderList', {});
  705. uni.$emit('refreshAssignedCount');
  706. uni.$emit('refreshOverdueCount');
  707. setTimeout(() => {
  708. uni.navigateBack();
  709. }, 800);
  710. } else {
  711. const msg = resultObj['msg'] as string;
  712. uni.showToast({
  713. title: msg.length > 0 ? msg : '结单失败',
  714. icon: 'none'
  715. });
  716. }
  717. } catch (error: any) {
  718. console.error('结单失败:', error);
  719. uni.showToast({
  720. title: '结单失败',
  721. icon: 'none'
  722. });
  723. } finally {
  724. submitLoading.value = false;
  725. isDealing.value = false // 无论成功失败都解锁
  726. }
  727. };
  728. const loading = ref<boolean>(false)
  729. // 加载详情数据
  730. const loadDetail = async (id: string, orderType?: string): Promise<void> => {
  731. try {
  732. loading.value = true
  733. let result: any;
  734. // 根据orderType决定调用哪个API
  735. if (orderType == '1') {
  736. // 维修工单
  737. result = await getRepairOrderInfoById(id)
  738. } else {
  739. // 维保工单
  740. result = await getOrderInfoById(id)
  741. }
  742. // 提取响应数据
  743. const resultObj = result as UTSJSONObject
  744. const code = resultObj['code'] as number
  745. const data = resultObj['data'] as UTSJSONObject | null
  746. if (code == 200 && data != null) {
  747. workOrderStatus.value = (data['workOrderStatus'] as string | null) ?? ''
  748. workOrderProjectNo.value = (data['workOrderProjectNo'] as string | null) ?? ''
  749. pcsDeviceName.value = (data['pcsDeviceName'] as string | null) ?? ''
  750. gxtCenter.value = (data['gxtCenter'] as string | null) ?? ''
  751. pcsStationName.value = (data['pcsStationName'] as string | null) ?? ''
  752. brand.value = (data['brand'] as string | null) ?? ''
  753. model.value = (data['model'] as string | null) ?? ''
  754. acceptTime.value = (data['acceptTime'] as string | null) ?? ''
  755. acceptUserName.value = (data['acceptUserName'] as string | null) ?? ''
  756. // 初始化结单表单默认值
  757. infoEntry.value = '1' // 默认为手工录入
  758. teamLeaderId.value = (data['teamLeaderId'] as Number | null) ?? null
  759. teamLeaderName.value = (data['teamLeaderName'] as string | null) ?? ''
  760. returnType.value = workOrderStatus.value == 'to_finish' ? '1' : ''
  761. content.value = (data['content'] as string | null) ?? ''
  762. misNo.value = (data['misNo'] as string | null) ?? ''
  763. workPermitNum.value = (data['workPermitNum'] as string | null) ?? ''
  764. suspendReason.value = (data['suspendReason'] as string | null) ?? ''
  765. const queryParams = {
  766. misNo: misNo.value,
  767. } as UTSJSONObject;
  768. const result = await getMisInfoList(queryParams)
  769. // 提取响应数据
  770. const resultObj = result as UTSJSONObject
  771. const code = resultObj['code'] as number
  772. const misInfo = resultObj['rows'] as UTSJSONObject[] | null
  773. if (code == 200 && misInfo != null) {
  774. if(misInfo.length > 0) {
  775. realStartTime.value = (misInfo[0]['realStartTime'] as string | null) ?? ''
  776. realEndTime.value = (misInfo[0]['realEndTime'] as string | null) ?? ''
  777. // 查询相关工作班成员
  778. await listWorkPerson(misNo.value).then(response => {
  779. const responseObj = response as UTSJSONObject
  780. const rows = responseObj['rows'] as UTSJSONObject[] | null
  781. workOrderPersonList.value = rows ?? []
  782. if (rows != null && rows.length > 0) {
  783. // 查询负责人信息并回填
  784. for (const person of workOrderPersonList.value) {
  785. // 严格判断isLeader为1(兼容数字/字符串类型)
  786. if (person.isLeader == 1) {
  787. teamLeaderName.value = (person.nickName as string | null) ?? '';
  788. break; // 找到后立即停止循环
  789. }
  790. }
  791. const nickNames = rows
  792. .map((person: UTSJSONObject) => (person['nickName'] as string | null) ?? '')
  793. .join(',');
  794. workGroupMemberName.value = nickNames;
  795. }
  796. })
  797. }
  798. flowList.value = data['workOrderFlowList'] as UTSJSONObject[]
  799. if (suspendReason.value != '' && flowList.value.length > 0) {
  800. // 获取最后一个 actionType 等于 'resume' 的项
  801. const lastResumeItem = flowList.value.findLast(item => item['actionType'] === 'resume') as UTSJSONObject | null
  802. if (lastResumeItem != null) {
  803. resumeInfo.value = lastResumeItem
  804. // 直接给 resumeTime 赋值,无需 formData
  805. resumeTime.value = (lastResumeItem['actionTime'] as string | null) ?? ''
  806. }
  807. const lastSuspendItem = flowList.value.findLast(item => (item['actionType'] as string | null) === 'to_approve') as UTSJSONObject | null
  808. if (lastSuspendItem != null) {
  809. suspendInfo.value = lastSuspendItem
  810. }
  811. }
  812. }
  813. } else {
  814. const msg = resultObj['msg'] as string | null
  815. uni.showToast({
  816. title: msg ?? '加载失败',
  817. icon: 'none'
  818. })
  819. }
  820. } catch (e) {
  821. uni.showToast({
  822. title: (e as Error).message ?? '加载失败',
  823. icon: 'none'
  824. })
  825. } finally {
  826. loading.value = false
  827. }
  828. }
  829. // 页面加载
  830. onLoad((options: any) => {
  831. const params = options as UTSJSONObject
  832. const id = params['id'] as string | null
  833. const orderTypeParam = params['orderType'] as string | null
  834. if (id != null && orderTypeParam != null) {
  835. // 先尝试从参数中获取orderType
  836. // const orderTypeNumber = parseInt(orderTypeParam)
  837. orderType.value = orderTypeParam
  838. orderId.value = id
  839. loadDetail(id, orderTypeParam)
  840. }
  841. })
  842. // 初始化
  843. onMounted(() => {
  844. getUserAllList();
  845. })
  846. </script>
  847. <style lang="scss">
  848. .detail-page {
  849. flex: 1;
  850. background-color: #e8f0f9;
  851. }
  852. .detail-content {
  853. flex: 1;
  854. padding: 20rpx 0;
  855. }
  856. .info-section {
  857. margin: 0 30rpx 24rpx;
  858. .section-title {
  859. position: relative;
  860. padding-left: 20rpx;
  861. margin-bottom: 20rpx;
  862. &::before {
  863. // content: '';
  864. position: absolute;
  865. left: 0;
  866. top: 50%;
  867. transform: translateY(-50%);
  868. width: 8rpx;
  869. height: 32rpx;
  870. background-color: #007aff;
  871. border-radius: 4rpx;
  872. }
  873. &-text {
  874. font-size: 32rpx;
  875. font-weight: bold;
  876. color: #333333;
  877. }
  878. }
  879. .info-card {
  880. background-color: #ffffff;
  881. border-radius: 16rpx;
  882. padding: 30rpx;
  883. .info-item {
  884. flex-direction: row;
  885. padding: 20rpx 0;
  886. border-bottom: 1rpx solid #f0f0f0;
  887. &:last-child {
  888. border-bottom: none;
  889. }
  890. &.full-width {
  891. flex-direction: column;
  892. .info-label {
  893. margin-bottom: 12rpx;
  894. }
  895. .info-value {
  896. line-height: 44rpx;
  897. }
  898. }
  899. .info-label {
  900. width: 240rpx;
  901. font-size: 28rpx;
  902. color: #666666;
  903. white-space: nowrap;
  904. }
  905. .info-value {
  906. flex: 1;
  907. font-size: 28rpx;
  908. color: #333333;
  909. text-align: left;
  910. &.highlight {
  911. color: #007aff;
  912. font-weight: bold;
  913. }
  914. &.input {
  915. text-align: left;
  916. border: 1rpx solid #e0e0e0;
  917. border-radius: 8rpx;
  918. padding: 10rpx;
  919. }
  920. .input-with-clear {
  921. position: relative;
  922. display: flex;
  923. align-items: center;
  924. }
  925. .select-clear {
  926. position: absolute;
  927. right: 20rpx;
  928. color: #ccc;
  929. text-align: center;
  930. font-size: 40rpx;
  931. font-weight: bold;
  932. z-index: 1;
  933. }
  934. }
  935. }
  936. .flow-item {
  937. padding: 20rpx 0;
  938. border-bottom: 1rpx solid #f0f0f0;
  939. &:last-child {
  940. border-bottom: none;
  941. }
  942. .flow-header {
  943. flex-direction: row;
  944. justify-content: space-between;
  945. margin-bottom: 10rpx;
  946. .flow-operator {
  947. font-size: 28rpx;
  948. color: #333333;
  949. font-weight: bold;
  950. }
  951. .flow-time {
  952. font-size: 24rpx;
  953. color: #999999;
  954. }
  955. }
  956. .flow-content {
  957. flex-direction: column;
  958. .flow-action {
  959. font-size: 26rpx;
  960. color: #666666;
  961. margin-bottom: 8rpx;
  962. }
  963. .flow-remark {
  964. font-size: 24rpx;
  965. color: #999999;
  966. background-color: #f5f5f5;
  967. padding: 10rpx;
  968. border-radius: 8rpx;
  969. }
  970. }
  971. }
  972. .no-data {
  973. text-align: center;
  974. padding: 40rpx 0;
  975. font-size: 28rpx;
  976. color: #999999;
  977. }
  978. }
  979. }
  980. .accept-button-container {
  981. padding: 30rpx 30rpx 50rpx;
  982. background-color: #ffffff;
  983. .accept-button {
  984. width: 100%;
  985. height: 80rpx;
  986. background-color: #007aff;
  987. color: #ffffff;
  988. font-size: 32rpx;
  989. border-radius: 16rpx;
  990. border: none;
  991. &:active {
  992. background-color: #0062cc;
  993. }
  994. }
  995. }
  996. .loading-mask {
  997. position: absolute;
  998. top: 0;
  999. left: 0;
  1000. right: 0;
  1001. bottom: 0;
  1002. justify-content: center;
  1003. align-items: center;
  1004. background-color: rgba(0, 0, 0, 0.3);
  1005. .loading-text {
  1006. padding: 30rpx 60rpx;
  1007. background-color: rgba(0, 0, 0, 0.7);
  1008. color: #ffffff;
  1009. font-size: 28rpx;
  1010. border-radius: 12rpx;
  1011. }
  1012. }
  1013. .picker-modal {
  1014. position: fixed;
  1015. top: 0;
  1016. left: 0;
  1017. right: 0;
  1018. bottom: 0;
  1019. z-index: 1000;
  1020. }
  1021. .modal-mask {
  1022. position: absolute;
  1023. top: 0;
  1024. left: 0;
  1025. right: 0;
  1026. bottom: 0;
  1027. background-color: rgba(0, 0, 0, 0.5);
  1028. }
  1029. .modal-content {
  1030. position: absolute;
  1031. bottom: 0;
  1032. left: 0;
  1033. right: 0;
  1034. background-color: #ffffff;
  1035. border-top-left-radius: 16rpx;
  1036. border-top-right-radius: 16rpx;
  1037. min-height: 700rpx;
  1038. }
  1039. .modal-header {
  1040. flex-direction: row;
  1041. justify-content: space-between;
  1042. align-items: center;
  1043. padding: 30rpx;
  1044. border-bottom: 1rpx solid #f0f0f0;
  1045. }
  1046. .modal-title {
  1047. font-size: 32rpx;
  1048. font-weight: bold;
  1049. color: #333333;
  1050. }
  1051. .modal-close {
  1052. font-size: 28rpx;
  1053. color: #007aff;
  1054. }
  1055. .modal-body {
  1056. max-height: 800rpx;
  1057. min-height: 800rpx;
  1058. }
  1059. .picker-option {
  1060. flex-direction: row;
  1061. justify-content: space-between;
  1062. align-items: center;
  1063. padding: 24rpx 30rpx;
  1064. border-bottom: 1rpx solid #f0f0f0;
  1065. }
  1066. .picker-option.selected {
  1067. background-color: #f8f9fa;
  1068. }
  1069. .picker-option:last-child {
  1070. border-bottom: none;
  1071. }
  1072. .option-text {
  1073. font-size: 28rpx;
  1074. color: #333333;
  1075. margin-bottom: 10rpx;
  1076. }
  1077. .option-text:last-child {
  1078. margin-bottom: 0;
  1079. }
  1080. .option-check {
  1081. font-size: 28rpx;
  1082. color: #007aff;
  1083. }
  1084. .empty-tip {
  1085. justify-content: space-between;
  1086. padding: 24rpx 30rpx;
  1087. display: flex;
  1088. align-items: center;
  1089. justify-content: center;
  1090. color: #999;
  1091. }
  1092. .picker-option {
  1093. flex-direction: row;
  1094. justify-content: space-between;
  1095. align-items: center;
  1096. padding: 24rpx 30rpx;
  1097. border-bottom: 1rpx solid #f0f0f0;
  1098. }
  1099. .picker-option:last-child {
  1100. border-bottom: none;
  1101. }
  1102. .picker-option.selected {
  1103. background-color: #f8f9fa;
  1104. }
  1105. .option-text {
  1106. font-size: 28rpx;
  1107. color: #333333;
  1108. }
  1109. .mis-list {
  1110. flex: 1;
  1111. height: 100%;
  1112. display: flex;
  1113. flex-direction: column;
  1114. }
  1115. .empty-tip {
  1116. text-align: center;
  1117. padding: 40rpx;
  1118. color: #999;
  1119. font-size: 28rpx;
  1120. }
  1121. .form-item {
  1122. flex-direction: row;
  1123. padding: 20rpx 0;
  1124. border-bottom: 1rpx solid #f0f0f0;
  1125. }
  1126. .reject-reason-textarea {
  1127. width: 100%;
  1128. min-height: 100rpx;
  1129. line-height: 1.5;
  1130. }
  1131. .input-field {
  1132. width: 100%;
  1133. height: 60rpx;
  1134. padding: 0 20rpx;
  1135. border: 1rpx solid #e0e0e0;
  1136. border-radius: 8rpx;
  1137. font-size: 28rpx;
  1138. background-color: #f8f9fa;
  1139. }
  1140. .select-mis-btn {
  1141. width: 150rpx;
  1142. height: 60rpx;
  1143. margin-left: 10rpx;
  1144. background-color: #007aff;
  1145. color: #fff;
  1146. border: none;
  1147. border-radius: 8rpx;
  1148. font-size: 24rpx;
  1149. }
  1150. .textarea-field {
  1151. width: 100%;
  1152. min-height: 120rpx;
  1153. padding: 20rpx;
  1154. border: 1rpx solid #e0e0e0;
  1155. border-radius: 8rpx;
  1156. font-size: 28rpx;
  1157. background-color: #f8f9fa;
  1158. }
  1159. .radio-label {
  1160. display: flex;
  1161. align-items: center;
  1162. margin-right: 30rpx;
  1163. }
  1164. .quick-select-dropdown {
  1165. position: absolute;
  1166. top: 100%;
  1167. left: 0;
  1168. right: 0;
  1169. background: #fff;
  1170. border: 1rpx solid #ddd;
  1171. border-top: none;
  1172. z-index: 1000;
  1173. max-height: 300rpx;
  1174. }
  1175. .quick-select-item {
  1176. padding: 20rpx;
  1177. border-bottom: 1rpx solid #eee;
  1178. }
  1179. .mis-no {
  1180. font-size: 28rpx;
  1181. color: #333;
  1182. }
  1183. .search-bar {
  1184. padding: 20rpx 30rpx;
  1185. background-color: #d7eafe;
  1186. }
  1187. .search-box {
  1188. flex-direction: row;
  1189. align-items: center;
  1190. height: 72rpx;
  1191. padding: 0 24rpx;
  1192. background-color: #f5f5f5;
  1193. border-radius: 36rpx;
  1194. .search-icon {
  1195. width: 32rpx;
  1196. height: 32rpx;
  1197. margin-right: 12rpx;
  1198. }
  1199. .search-input {
  1200. flex: 1;
  1201. font-size: 28rpx;
  1202. color: #333333;
  1203. }
  1204. .clear-icon {
  1205. margin-left: 12rpx;
  1206. font-size: 28rpx;
  1207. color: #999999;
  1208. }
  1209. }
  1210. .mis-list {
  1211. flex: 1;
  1212. height: 100%;
  1213. display: flex;
  1214. flex-direction: column;
  1215. }
  1216. .select-users-count {
  1217. margin-left: 10rpx;
  1218. font-size: 24rpx;
  1219. color: #666;
  1220. }
  1221. </style>