device-check.uvue 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. <template>
  2. <uni-navbar-lite :showLeft="true" title="设备检查"></uni-navbar-lite>
  3. <scroll-view class="container" scroll-y="true">
  4. <view class="info-card">
  5. <view class="info-item">
  6. <text class="info-label">设备名称</text>
  7. <text class="info-value">{{ deviceInfo.machineryName ?? '' }}</text>
  8. </view>
  9. <view class="info-item">
  10. <text class="info-label">设备编号</text>
  11. <text class="info-value">{{ deviceInfo.machinerySpec ?? '' }}</text>
  12. </view>
  13. <view class="info-item">
  14. <text class="info-label">位置</text>
  15. <text class="info-value">{{ deviceInfo.machineryLocation ?? '' }}</text>
  16. </view>
  17. </view>
  18. <!-- 检查项目列表 -->
  19. <!-- <view class="section">
  20. <view class="section-header">
  21. <text class="section-title">检查项目</text>
  22. </view> -->
  23. <view class="check-items-list">
  24. <view class="check-item" v-for="(item, index) in checkItems" :key="index">
  25. <view class="item-container">
  26. <view class="item-header">
  27. <text class="item-title">{{ item.subjectName ?? '' }}</text>
  28. <view class="check-item-controls">
  29. <view class="form-picker" @click="showStatusPicker(item)">
  30. <view class="picker-display">
  31. <text v-if="item.status != null" class="selected-value">{{ getStatusText(item.status) }}</text>
  32. <text v-else class="placeholder">请选择状态</text>
  33. <text class="arrow">▼</text>
  34. </view>
  35. </view>
  36. <view class="expand-toggle" @click="toggleExpand(item)">
  37. <text class="expand-icon" :class="{'rotated': item.expanded}">▼</text>
  38. </view>
  39. </view>
  40. </view>
  41. <!-- 展开区域 -->
  42. <view class="expanded-content" v-if="!item.expanded">
  43. <!-- 图片和视频上传区域 -->
  44. <view class="check-item-media">
  45. <view class="media-section">
  46. <text class="media-title">上传图片</text>
  47. <view class="upload-wrapper" v-if="currentOpType != 'view'">
  48. <UploadImage
  49. :limit="6"
  50. :modelValue="item.photos ?? []"
  51. businessType="deviceCheck"
  52. @update:modelValue="(value: UTSArray<UploadResponse>) => updatePhotos(item, value)"
  53. />
  54. </view>
  55. <view class="image-container" v-else>
  56. <image
  57. v-for="(attachment, index) in getAttachmentList(item.photoUrl)"
  58. :key="index"
  59. :src="attachment"
  60. :alt="'附件图片' + (index + 1)"
  61. class="attachment-image"
  62. @click="previewImage(attachment)"
  63. />
  64. </view>
  65. </view>
  66. <!-- 视频上传区域 -->
  67. <view class="media-section">
  68. <text class="media-title">上传视频</text>
  69. <view class="upload-wrapper" v-if="currentOpType != 'view'">
  70. <UploadVideo
  71. :limit="3"
  72. :modelValue="item.videos ?? []"
  73. businessType="deviceCheck"
  74. @update:modelValue="(value: UTSArray<VideoItem>) => updateVideos(item, value)"
  75. />
  76. </view>
  77. <view class="image-container" v-else>
  78. <video
  79. v-for="(attachment, index) in getVideoList(item.videoUrl)"
  80. :key="index"
  81. :src="attachment"
  82. :alt="'附件视频' + (index + 1)"
  83. class="attachment-image"
  84. :controls="true"
  85. />
  86. </view>
  87. </view>
  88. </view>
  89. </view>
  90. </view>
  91. </view>
  92. </view>
  93. <!-- </view> -->
  94. <!-- 全局备注 -->
  95. <view class="section">
  96. <view class="section-header">
  97. <text class="section-title">备注</text>
  98. </view>
  99. <view class="remark-container">
  100. <textarea class="remark-textarea" v-model="deviceInfo.remark" placeholder="请输入备注信息" maxlength="500"></textarea>
  101. </view>
  102. </view>
  103. </scroll-view>
  104. <!-- 底部操作栏 -->
  105. <view class="bottom-actions" v-if="currentOpType != 'view'" >
  106. <!-- <button class="action-btn draft-btn" @click="saveDraft">保存草稿</button>
  107. <button
  108. v-if="showMaintenanceBtn"
  109. class="action-btn maintenance-btn"
  110. @click="submitMaintenance"
  111. >
  112. 提交维修
  113. </button> -->
  114. <button class="action-btn submit-btn" @click="submitCheck">提交检查</button>
  115. </view>
  116. <!-- 状态选择器弹窗 -->
  117. <view v-if="currentItem != null" class="picker-modal">
  118. <view class="modal-mask" @click="hideStatusPicker"></view>
  119. <view class="modal-content">
  120. <view class="modal-header">
  121. <text class="modal-title">选择状态</text>
  122. <text class="modal-close" @click="hideStatusPicker">取消</text>
  123. </view>
  124. <scroll-view class="modal-body" scroll-y="true">
  125. <view
  126. v-for="(option, index) in statusOptions"
  127. :key="index"
  128. class="picker-option"
  129. :class="{ 'selected': index == selectedStatusIndex }"
  130. @click="selectStatusManually(index)"
  131. >
  132. <text class="option-text">{{ option.label }}</text>
  133. <text v-if="index == selectedStatusIndex" class="option-check">✓</text>
  134. </view>
  135. </scroll-view>
  136. </view>
  137. </view>
  138. </template>
  139. <script setup lang="uts">
  140. import { ref, reactive, onMounted, computed} from 'vue'
  141. import UploadImage from '../../components/upload-image/upload-image.uvue'
  142. import UploadVideo from '../../components/upload-video/upload-video.uvue'
  143. import type { UploadResponse } from '../../types/workbench'
  144. import type { VideoItem } from '../../types/workbench'
  145. import type { SysDictData } from '../../types/dict'
  146. import { getDetailInfo, batchEditTaskDetail } from '../../api/task/detail.uts'
  147. import { getDictDataByType } from '../../api/dict/index'
  148. import { getBaseUrl } from '../../utils/request'
  149. // TaskDetailSubject类型定义(与后端保持一致,并添加前端需要的字段)
  150. type TaskDetailSubject = {
  151. id: Number | null
  152. taskDetailId: Number | null
  153. subjectName: string | null
  154. subjectValue: string | null
  155. photoUrl: string | null
  156. videoUrl: string | null
  157. remarks: string | null
  158. sortOrder: Number | null
  159. status: string | null
  160. // 前端扩展字段
  161. expanded: boolean // 控制展开收起
  162. photos?: UploadResponse[] // 前端临时存储图片上传的文件对象
  163. videos?: VideoItem[] // 前端临时存储视频上传的文件对象
  164. }
  165. // DvCheckTaskDetail类型定义
  166. type DvCheckTaskDetail = {
  167. detailId: Number | null
  168. taskId: Number | null
  169. machineryRecordId: Number | null
  170. machineryId: Number | null
  171. machineryTypeName: string | null
  172. machinerySpec: string | null
  173. machineryLocation: string | null
  174. machineryName: string | null
  175. subjectId: Number | null
  176. subjectCode: string | null
  177. subjectName: string | null
  178. locationRecordId: Number | null
  179. locationId: Number | null
  180. locationName: string | null
  181. locationCode: string | null
  182. sourceType: string | null
  183. orderNum: Number | null
  184. status: string | null
  185. taskStatus: string | null
  186. imagesUrl: string | null
  187. videosUrl: string | null
  188. resultValue: string | null
  189. resultDesc: string | null
  190. remark: string | null
  191. attr1: string | null
  192. attr2: string | null
  193. attr3: Number | null
  194. attr4: Number | null
  195. taskDetailSubjectList: TaskDetailSubject[] | null
  196. }
  197. // 设备信息
  198. const deviceInfo = ref<DvCheckTaskDetail>({
  199. detailId: null,
  200. taskId: null,
  201. machineryRecordId: null,
  202. machineryId: null,
  203. machineryTypeName: null,
  204. machinerySpec: null,
  205. machineryLocation: null,
  206. machineryName: null,
  207. subjectId: null,
  208. subjectCode: null,
  209. subjectName: null,
  210. locationRecordId: null,
  211. locationId: null,
  212. locationName: null,
  213. locationCode: null,
  214. sourceType: null,
  215. orderNum: null,
  216. status: null,
  217. taskStatus: null,
  218. imagesUrl: null,
  219. videosUrl: null,
  220. resultValue: null,
  221. resultDesc: null,
  222. remark: null,
  223. attr1: null,
  224. attr2: null,
  225. attr3: null,
  226. attr4: null,
  227. taskDetailSubjectList: null
  228. })
  229. // 当前任务明细ID
  230. const currentDetailId = ref<string>('')
  231. const currentOpType = ref<string>('')
  232. // 检查项目列表(直接使用TaskDetailSubject类型)
  233. const checkItems = ref<TaskDetailSubject[]>([])
  234. // 字典数据
  235. const subjectStatusDictList = ref<SysDictData[]>([]) // 检查项状态字典列表
  236. const dictLoaded = ref<boolean>(false) // 字典加载状态
  237. // 选择器选项类型
  238. type PickerOption = {
  239. label: string
  240. value: string
  241. }
  242. // 状态选项
  243. const statusOptions = ref<PickerOption[]>([])
  244. // 获取检查项状态字典列表
  245. const loadSubjectStatusDictList = async (): Promise<void> => {
  246. try {
  247. const result = await getDictDataByType('mes_subject_status')
  248. const resultObj = result as UTSJSONObject
  249. if (resultObj['code'] == 200) {
  250. const data = resultObj['data'] as any[]
  251. const dictData: SysDictData[] = []
  252. if (data.length > 0) {
  253. for (let i = 0; i < data.length; i++) {
  254. const item = data[i] as UTSJSONObject
  255. const dictItem: SysDictData = {
  256. dictValue: item['dictValue'] as string | null,
  257. dictLabel: item['dictLabel'] as string | null,
  258. dictCode: null,
  259. dictSort: null,
  260. dictType: null,
  261. cssClass: null,
  262. listClass: null,
  263. isDefault: null,
  264. status: null,
  265. default: null,
  266. createTime: null,
  267. remark: null
  268. }
  269. dictData.push(dictItem)
  270. }
  271. }
  272. subjectStatusDictList.value = dictData
  273. dictLoaded.value = true
  274. // 更新状态选项
  275. const newOptions: PickerOption[] = []
  276. for (let i = 0; i < dictData.length; i++) {
  277. const dict = dictData[i]
  278. if (dict.dictValue != null && dict.dictLabel != null) {
  279. newOptions.push({
  280. label: dict.dictLabel,
  281. value: dict.dictValue
  282. })
  283. }
  284. }
  285. statusOptions.value = newOptions
  286. }
  287. } catch (e: any) {
  288. console.error('获取检查项状态字典失败:', e.message)
  289. dictLoaded.value = true
  290. }
  291. }
  292. // 获取状态文本
  293. const getStatusText = (status: string | null): string => {
  294. if (status == null || status.length == 0) return ''
  295. if (dictLoaded.value != true) {
  296. return status
  297. }
  298. const dictItem = subjectStatusDictList.value.find((dict: SysDictData) => dict.dictValue == status)
  299. return dictItem != null && dictItem.dictLabel != null ? dictItem.dictLabel : status
  300. }
  301. // 当前选中的检查项
  302. const currentItem = ref<TaskDetailSubject | null>(null)
  303. // 选中的状态索引
  304. const selectedStatusIndex = ref<number>(-1)
  305. // 显示状态选择器
  306. const showStatusPicker = (item: TaskDetailSubject): void => {
  307. currentItem.value = item
  308. // 根据字典数据动态设置当前选中索引
  309. if (item.status != null && dictLoaded.value) {
  310. // 在字典选项中查找匹配的索引
  311. for (let i = 0; i < statusOptions.value.length; i++) {
  312. if (statusOptions.value[i].value == item.status) {
  313. selectedStatusIndex.value = i
  314. return
  315. }
  316. }
  317. }
  318. // 如果没找到匹配项或字典未加载,设置为-1
  319. selectedStatusIndex.value = -1
  320. }
  321. // 隐藏状态选择器
  322. const hideStatusPicker = (): void => {
  323. currentItem.value = null
  324. selectedStatusIndex.value = -1
  325. }
  326. // 手动选择状态
  327. const selectStatusManually = (index: number): void => {
  328. selectedStatusIndex.value = index
  329. if (currentItem.value != null && index >= 0 && index < statusOptions.value.length) {
  330. const selectedOption = statusOptions.value[index]
  331. currentItem.value.status = selectedOption.value
  332. }
  333. hideStatusPicker()
  334. }
  335. /* // 从弹窗处理状态更改
  336. const handleStatusChangeFromPopup = (item: TaskDetailSubject, value: string): void => {
  337. item.status = value
  338. // 隐藏弹窗
  339. hideStatusPicker();
  340. } */
  341. // 切换展开/收起
  342. const toggleExpand = (item: TaskDetailSubject): void => {
  343. item.expanded = !item.expanded
  344. }
  345. // 加载设备检查任务明细数据
  346. const loadDeviceDetail = async (): Promise<void> => {
  347. if (currentDetailId.value == null || currentDetailId.value.length == 0) {
  348. uni.showToast({
  349. title: '缺少任务明细ID参数',
  350. icon: 'none'
  351. })
  352. return
  353. }
  354. try {
  355. uni.showLoading({
  356. title: '加载中...'
  357. })
  358. const result = await getDetailInfo(currentDetailId.value)
  359. const resultObj = result as UTSJSONObject
  360. const code = resultObj['code'] as number
  361. if (code == 200) {
  362. const data = resultObj['data'] as UTSJSONObject
  363. // 转换设备信息
  364. const newDeviceInfo: DvCheckTaskDetail = {
  365. detailId: data['detailId'] != null ? (data['detailId'] as Number) : null,
  366. taskId: data['taskId'] != null ? (data['taskId'] as Number) : null,
  367. machineryRecordId: data['machineryRecordId'] != null ? (data['machineryRecordId'] as Number) : null,
  368. machineryId: data['machineryId'] != null ? (data['machineryId'] as Number) : null,
  369. machineryTypeName: data['machineryTypeName'] as string | null,
  370. machinerySpec: data['machinerySpec'] as string | null,
  371. machineryLocation: data['machineryLocation'] as string | null,
  372. machineryName: data['machineryName'] as string | null,
  373. subjectId: data['subjectId'] != null ? (data['subjectId'] as Number) : null,
  374. subjectCode: data['subjectCode'] as string | null,
  375. subjectName: data['subjectName'] as string | null,
  376. locationRecordId: data['locationRecordId'] != null ? (data['locationRecordId'] as Number) : null,
  377. locationId: data['locationId'] != null ? (data['locationId'] as Number) : null,
  378. locationName: data['locationName'] as string | null,
  379. locationCode: data['locationCode'] as string | null,
  380. sourceType: data['sourceType'] as string | null,
  381. orderNum: data['orderNum'] != null ? (data['orderNum'] as Number) : null,
  382. status: data['status'] as string | null,
  383. taskStatus: data['taskStatus'] as string | null,
  384. imagesUrl: data['imagesUrl'] as string | null,
  385. videosUrl: data['videosUrl'] as string | null,
  386. resultValue: data['resultValue'] as string | null,
  387. resultDesc: data['resultDesc'] as string | null,
  388. remark: data['remark'] as string | null,
  389. attr1: data['attr1'] as string | null,
  390. attr2: data['attr2'] as string | null,
  391. attr3: data['attr3'] != null ? (data['attr3'] as Number) : null,
  392. attr4: data['attr4'] != null ? (data['attr4'] as Number) : null,
  393. taskDetailSubjectList: null
  394. }
  395. deviceInfo.value = newDeviceInfo
  396. // 处理检查项目列表
  397. const subjectList = data['taskDetailSubjectList'] as any[] | null
  398. if (subjectList != null && subjectList.length > 0) {
  399. const newCheckItems: TaskDetailSubject[] = []
  400. for (let i = 0; i < subjectList.length; i++) {
  401. const subject = subjectList[i] as UTSJSONObject
  402. const checkItem: TaskDetailSubject = {
  403. id: subject['id'] != null ? (subject['id'] as Number) : null,
  404. taskDetailId: subject['taskDetailId'] != null ? (subject['taskDetailId'] as Number) : null,
  405. subjectName: subject['subjectName'] as string | null,
  406. subjectValue: subject['subjectValue'] as string | null,
  407. photoUrl: subject['photoUrl'] as string | null,
  408. videoUrl: subject['videoUrl'] as string | null,
  409. remarks: subject['remarks'] as string | null,
  410. sortOrder: subject['sortOrder'] != null ? (subject['sortOrder'] as Number) : null,
  411. //status: '0', // 默认状态为正常
  412. status: subject['status'] != null ? (subject['status'] as string) : null,
  413. // 前端扩展字段
  414. expanded: false,
  415. photos: [] as UploadResponse[],
  416. videos: [] as VideoItem[]
  417. }
  418. newCheckItems.push(checkItem)
  419. }
  420. checkItems.value = newCheckItems
  421. }
  422. uni.hideLoading()
  423. } else {
  424. const msg = resultObj['msg'] as string | null
  425. uni.hideLoading()
  426. uni.showToast({
  427. title: msg ?? '加载失败',
  428. icon: 'none'
  429. })
  430. }
  431. } catch (e: any) {
  432. uni.hideLoading()
  433. uni.showToast({
  434. title: e.message ?? '加载失败',
  435. icon: 'none'
  436. })
  437. console.error('加载设备检查任务明细失败:', e)
  438. }
  439. }
  440. // 更新图片
  441. const updatePhotos = (item: TaskDetailSubject, value: UTSArray<UploadResponse>): void => {
  442. item.photos = value;
  443. // 同步更新photoUrl字段
  444. if (value.length > 0) {
  445. /* const urls: string[] = [];
  446. for (let i = 0; i < value.length; i++) {
  447. urls.push(value[i].url);
  448. }
  449. item.photoUrl = urls.join(','); */
  450. const fileNames: string[] = [];
  451. for (let i = 0; i < value.length; i++) {
  452. fileNames.push(value[i].fileName);
  453. }
  454. item.photoUrl = fileNames.join(',');
  455. } else {
  456. item.photoUrl = null;
  457. }
  458. };
  459. // 更新视频
  460. const updateVideos = (item: TaskDetailSubject, value: UTSArray<VideoItem>): void => {
  461. item.videos = value;
  462. // 同步更新videoUrl字段
  463. if (value.length > 0) {
  464. /* const urls: string[] = [];
  465. for (let i = 0; i < value.length; i++) {
  466. urls.push(value[i].url);
  467. }
  468. item.videoUrl = urls.join(','); */
  469. const fileNames: string[] = [];
  470. for (let i = 0; i < value.length; i++) {
  471. fileNames.push(value[i].fileName);
  472. }
  473. item.videoUrl = fileNames.join(',');
  474. } else {
  475. item.videoUrl = null;
  476. }
  477. };
  478. // 异常项目列表
  479. const abnormalItems = computed((): TaskDetailSubject[] => {
  480. return checkItems.value.filter(item => item.status != null && item.status == 'ABNORMAL')
  481. })
  482. // 是否显示维修按钮
  483. const showMaintenanceBtn = computed((): boolean => {
  484. return abnormalItems.value.length > 0
  485. })
  486. // 保存草稿
  487. const saveDraft = (): void => {
  488. uni.showModal({
  489. title: '提示',
  490. content: '检查信息已保存为草稿',
  491. showCancel: false,
  492. success: (res) => {
  493. if (res.confirm) {
  494. // 这里可以将数据存储到本地缓存
  495. const draftData = {
  496. deviceInfo: deviceInfo.value,
  497. checkItems: checkItems.value,
  498. savedAt: new Date().toISOString()
  499. }
  500. uni.setStorageSync('deviceCheckDraft', JSON.stringify(draftData))
  501. uni.showToast({
  502. title: '已保存草稿',
  503. icon: 'success'
  504. })
  505. }
  506. }
  507. })
  508. }
  509. // 提交检查
  510. const submitCheck = (): void => {
  511. // 提交检查结果
  512. // 验证每个检查项的状态不能为空
  513. for (let i = 0; i < checkItems.value.length; i++) {
  514. const item = checkItems.value[i];
  515. if (item.status == null || item.status.length === 0) {
  516. uni.showToast({
  517. title: `检查项 "${item.subjectName}" 的状态不能为空`,
  518. icon: 'none'
  519. });
  520. return;
  521. }
  522. // 当状态为 ABNORMAL 时,验证 photoUrl 或 videoUrl 至少一个不能为空
  523. // 使用 includes 方法来判断是否包含 ABNORMAL,解决字符串比较问题
  524. if (item.status.includes('ABNORMAL')) {
  525. // 检查是否至少有一个媒体文件
  526. const hasPhotos = item.photoUrl != null && item.photoUrl.length > 0;
  527. const hasVideos = item.videoUrl != null && item.videoUrl.length > 0;
  528. if (!hasPhotos && !hasVideos) {
  529. uni.showToast({
  530. title: `检查项 "${item.subjectName}" 状态为异常时,必须上传图片或视频`,
  531. icon: 'none'
  532. });
  533. return;
  534. }
  535. }
  536. }
  537. const deviceNameStr = (deviceInfo.value.machineryName != null && deviceInfo.value.machineryName.length > 0) ? deviceInfo.value.machineryName : '';
  538. uni.showModal({
  539. title: '确认提交',
  540. //content: `确定提交设备检查结果?\n设备:${deviceNameStr}`,
  541. success: (res) => {
  542. if (res.confirm) {
  543. // 构造提交数据
  544. const submitData: UTSJSONObject = {
  545. 'detailId': deviceInfo.value.detailId,
  546. 'taskId': deviceInfo.value.taskId,
  547. 'machineryRecordId': deviceInfo.value.machineryRecordId,
  548. 'machineryId': deviceInfo.value.machineryId,
  549. 'machineryTypeName': deviceInfo.value.machineryTypeName,
  550. 'machinerySpec': deviceInfo.value.machinerySpec,
  551. 'machineryLocation': deviceInfo.value.machineryLocation,
  552. 'machineryName': deviceInfo.value.machineryName,
  553. 'subjectId': deviceInfo.value.subjectId,
  554. 'subjectCode': deviceInfo.value.subjectCode,
  555. 'subjectName': deviceInfo.value.subjectName,
  556. 'locationRecordId': deviceInfo.value.locationRecordId,
  557. 'locationId': deviceInfo.value.locationId,
  558. 'locationName': deviceInfo.value.locationName,
  559. 'locationCode': deviceInfo.value.locationCode,
  560. 'sourceType': deviceInfo.value.sourceType,
  561. 'orderNum': deviceInfo.value.orderNum,
  562. 'status': deviceInfo.value.status,
  563. 'taskStatus': deviceInfo.value.taskStatus,
  564. 'imagesUrl': deviceInfo.value.imagesUrl,
  565. 'videosUrl': deviceInfo.value.videosUrl,
  566. 'resultValue': deviceInfo.value.resultValue,
  567. 'resultDesc': deviceInfo.value.resultDesc,
  568. 'remark': deviceInfo.value.remark,
  569. 'attr1': deviceInfo.value.attr1,
  570. 'attr2': deviceInfo.value.attr2,
  571. 'attr3': deviceInfo.value.attr3,
  572. 'attr4': deviceInfo.value.attr4,
  573. 'taskDetailSubjectList': checkItems.value.map((item): UTSJSONObject => ({
  574. 'id': item.id,
  575. 'taskDetailId': item.taskDetailId,
  576. 'subjectName': item.subjectName,
  577. 'subjectValue': item.subjectValue,
  578. 'photoUrl': item.photoUrl,
  579. 'videoUrl': item.videoUrl,
  580. 'remarks': item.remarks,
  581. 'sortOrder': item.sortOrder,
  582. 'status': item.status
  583. }))
  584. } as UTSJSONObject;
  585. // 调用批量编辑接口
  586. uni.showLoading({
  587. title: '提交中...'
  588. });
  589. batchEditTaskDetail(submitData)
  590. .then((result: any) => {
  591. const resultObj = result as UTSJSONObject;
  592. const code = resultObj['code'] as number;
  593. uni.hideLoading();
  594. if (code == 200) {
  595. uni.showToast({
  596. title: '检查提交成功!',
  597. icon: 'success'
  598. });
  599. // 设置标记,通知上一个页面需要刷新数据
  600. uni.setStorageSync('needRefresh', true);
  601. // 返回上一页
  602. setTimeout(() => {
  603. uni.navigateBack();
  604. }, 1500);
  605. } else {
  606. uni.hideLoading(); // 确保在失败时也隐藏加载图标
  607. const msg = resultObj['msg'] as string | null;
  608. uni.showToast({
  609. title: msg ?? '提交失败',
  610. icon: 'none'
  611. });
  612. }
  613. })
  614. .catch((error) => {
  615. uni.hideLoading();
  616. uni.showToast({
  617. title: '提交失败',
  618. icon: 'none'
  619. });
  620. console.error('提交检查结果失败:', error);
  621. });
  622. }
  623. }
  624. });
  625. }
  626. // 提交维修申请
  627. const submitMaintenance = (): void => {
  628. uni.showModal({
  629. title: '提交维修申请',
  630. //content: `检测到设备存在异常,是否提交维修申请?\n设备:${deviceInfo.value.machineryName}\n异常项目数:${abnormalItems.value != null ? abnormalItems.value.length : 0}`,
  631. confirmText: '提交申请',
  632. cancelText: '暂不申请',
  633. success: (res) => {
  634. if (res.confirm) {
  635. // 模拟提交维修申请
  636. uni.showLoading({
  637. title: '提交中...'
  638. })
  639. setTimeout(() => {
  640. uni.hideLoading()
  641. uni.showToast({
  642. title: '维修申请已提交!',
  643. icon: 'success'
  644. })
  645. // 可以跳转到维修申请详情页或返回上一页
  646. /* setTimeout(() => {
  647. uni.navigateBack()
  648. }, 1500) */
  649. }, 1500)
  650. }
  651. }
  652. })
  653. }
  654. const getAttachmentList = (photoUrl: string | null): string[] => {
  655. if (photoUrl != null && photoUrl != '') {
  656. // 按逗号分割附件URL字符串,并加上基础URL
  657. const arr = photoUrl.split(',')
  658. .map((url: string) => {
  659. const trimmedUrl = url.trim();
  660. // 如果是相对路径,加上基础URL
  661. if (trimmedUrl.startsWith('/')) {
  662. return getBaseUrl() + trimmedUrl;
  663. }
  664. return trimmedUrl;
  665. })
  666. .filter((url: string) => url.length > 0);
  667. return arr;
  668. }
  669. return []
  670. }
  671. // 计算附件列表
  672. const getVideoList = (videoUrl: string | null): string[] => {
  673. if (videoUrl != null && videoUrl != '') {
  674. // 按逗号分割附件URL字符串,并加上基础URL
  675. const arr = videoUrl.split(',')
  676. .map((url: string) => {
  677. const trimmedUrl = url.trim();
  678. // 如果是相对路径,加上基础URL
  679. if (trimmedUrl.startsWith('/')) {
  680. return getBaseUrl() + trimmedUrl;
  681. }
  682. return trimmedUrl;
  683. })
  684. .filter((url: string) => url.length > 0);
  685. return arr;
  686. }
  687. return []
  688. }
  689. // 预览图片
  690. const previewImage = (url: string) => {
  691. if (url == '') return
  692. // 检查是否为图片文件
  693. const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
  694. const isImage = imageExtensions.some(ext => url.toLowerCase().endsWith(ext))
  695. if (isImage) {
  696. // 如果是图片,使用uni.previewImage预览
  697. uni.previewImage({
  698. urls: [url],
  699. current: 0
  700. })
  701. } else {
  702. // 对于非图片文件,显示提示
  703. uni.showToast({
  704. title: '不支持预览此文件类型',
  705. icon: 'none',
  706. duration: 2000
  707. })
  708. }
  709. }
  710. onLoad((options: any) => {
  711. const params = options as UTSJSONObject
  712. const detailId = params['detailId'] as string | null
  713. const opType = params.get('opType') as string | null
  714. if(opType != null){
  715. currentOpType.value = opType;
  716. }
  717. // 获取传递的任务明细ID
  718. if (detailId != null && detailId.length > 0) {
  719. currentDetailId.value = detailId
  720. // 并行加载字典数据和设备检查任务明细数据
  721. Promise.all([
  722. loadSubjectStatusDictList(),
  723. loadDeviceDetail()
  724. ]).catch(error => {
  725. console.error('加载数据失败:', error)
  726. })
  727. } else {
  728. uni.showToast({
  729. title: '缺少任务明细ID参数',
  730. icon: 'none'
  731. })
  732. }
  733. })
  734. </script>
  735. <style lang="scss">
  736. .container {
  737. flex: 1;
  738. background-color: #e8f0f9;
  739. padding: 20rpx;
  740. padding-bottom: 120rpx; // 为底部操作栏留出空间
  741. }
  742. .info-card {
  743. background-color: #fff;
  744. padding: 30rpx;
  745. border-radius: 16rpx;
  746. margin: 12rpx 10rpx;
  747. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  748. }
  749. .info-item {
  750. flex-direction: row;
  751. padding: 20rpx 0;
  752. border-bottom: 1rpx solid #f0f0f0;
  753. &:last-child {
  754. border-bottom: none;
  755. }
  756. .info-label {
  757. width: 240rpx;
  758. font-size: 28rpx;
  759. color: #666666;
  760. white-space: nowrap;
  761. }
  762. .info-value {
  763. flex: 1;
  764. font-size: 28rpx;
  765. color: #333333;
  766. text-align: right;
  767. }
  768. }
  769. .section {
  770. background-color: #ffffff;
  771. padding: 30rpx;
  772. border-radius: 16rpx;
  773. margin: 12rpx 10rpx;
  774. }
  775. .remark-container {
  776. margin-top: 20rpx;
  777. }
  778. .remark-textarea {
  779. width: 100%;
  780. min-height: 120rpx;
  781. padding: 20rpx;
  782. border: 1rpx solid #e0e0e0;
  783. border-radius: 8rpx;
  784. background-color: #fff;
  785. font-size: 28rpx;
  786. color: #333;
  787. box-sizing: border-box;
  788. }
  789. .section-header {
  790. margin-bottom: 20rpx;
  791. padding-bottom: 15rpx;
  792. border-bottom: 1rpx solid #eee;
  793. }
  794. .section-title {
  795. font-size: 32rpx;
  796. font-weight: bold;
  797. color: #333;
  798. }
  799. .check-items-list {
  800. .check-item {
  801. margin: 12rpx 10rpx;
  802. background-color: #ffffff;
  803. border-radius: 16rpx;
  804. &:last-child {
  805. border-bottom: none;
  806. }
  807. }
  808. .item-container {
  809. padding: 30rpx;
  810. }
  811. .item-header {
  812. flex-direction: row;
  813. align-items: flex-start;
  814. margin-bottom: 16rpx;
  815. justify-content: space-between; /* 主轴两端对齐 */
  816. min-height: 55rpx;
  817. .item-title {
  818. font-size: 30rpx;
  819. color: #333333;
  820. font-weight: bold;
  821. flex-wrap: wrap;
  822. flex: 0 1 75%;
  823. min-width: 0;
  824. line-height: 40rpx;
  825. height: 40rpx;
  826. }
  827. }
  828. .check-item-controls {
  829. display: flex;
  830. flex-direction: row;
  831. align-items: center;
  832. }
  833. .expand-toggle {
  834. margin-left: 30rpx;
  835. padding: 8rpx;
  836. }
  837. .expand-icon {
  838. font-size: 24rpx;
  839. transition: transform 0.3s;
  840. }
  841. .expand-icon.rotated {
  842. transform: rotate(180deg);
  843. }
  844. .form-picker {
  845. flex: 1;
  846. }
  847. .picker-display {
  848. flex-direction: row;
  849. justify-content: space-between;
  850. align-items: center;
  851. min-height: 40rpx;
  852. padding: 12rpx 16rpx;
  853. border: 1rpx solid #e0e0e0;
  854. border-radius: 8rpx;
  855. background-color: #f8f9fa;
  856. }
  857. .selected-value {
  858. font-size: 28rpx;
  859. color: #333333;
  860. }
  861. .placeholder {
  862. font-size: 28rpx;
  863. color: #999999;
  864. }
  865. .arrow {
  866. font-size: 24rpx;
  867. color: #999999;
  868. margin-left: 12rpx;
  869. }
  870. .check-item-media {
  871. display: flex;
  872. flex-direction: column;
  873. gap: 20rpx;
  874. margin: 15rpx 0;
  875. }
  876. .media-section {
  877. margin-bottom: 15rpx;
  878. .image-container {
  879. display: flex;
  880. flex-direction: row;
  881. flex-wrap: wrap;
  882. justify-content: flex-start; // 或 space-around
  883. // justify-content: space-around; /* 自动分配间距,避免挤压 */
  884. align-items: center;
  885. padding: 0 3rpx;
  886. .attachment-image {
  887. width: 280rpx; /* 一行显示两个图片,减去间距的一半 */
  888. height: 280rpx;
  889. // padding: 3rpx;
  890. margin: 3px;
  891. }
  892. }
  893. }
  894. .media-title {
  895. display: block;
  896. font-size: 26rpx;
  897. color: #666;
  898. margin-bottom: 10rpx;
  899. font-weight: 500;
  900. }
  901. .upload-wrapper {
  902. margin-top: 10rpx;
  903. }
  904. .image {
  905. &-list {
  906. flex-direction: row;
  907. flex-wrap: wrap;
  908. }
  909. &-item {
  910. background-color: #f5f7fa;
  911. border-radius: 8rpx;
  912. position: relative;
  913. width: 160rpx;
  914. height: 160rpx;
  915. margin-right: 20rpx;
  916. margin-bottom: 20rpx;
  917. }
  918. width: 160rpx;
  919. height: 160rpx;
  920. border-radius: 8rpx;
  921. &-delete {
  922. position: absolute;
  923. top: 2rpx;
  924. right: 2rpx;
  925. width: 44rpx;
  926. height: 44rpx;
  927. justify-content: center;
  928. align-items: center;
  929. }
  930. }
  931. .delete-icon {
  932. width: 32rpx;
  933. height: 32rpx;
  934. }
  935. .upload {
  936. &-box {
  937. background-color: #f5f7fa;
  938. border-radius: 8rpx;
  939. width: 160rpx;
  940. height: 160rpx;
  941. margin-right: 20rpx;
  942. margin-bottom: 20rpx;
  943. border: 2rpx dashed #d0d0d0;
  944. border-radius: 8rpx;
  945. justify-content: center;
  946. align-items: center;
  947. }
  948. &-icon {
  949. font-size: 60rpx;
  950. color: #999999;
  951. line-height: 60rpx;
  952. margin-bottom: 10rpx;
  953. }
  954. &-text {
  955. font-size: 24rpx;
  956. color: #999999;
  957. }
  958. }
  959. .video {
  960. &-list {
  961. flex-direction: row;
  962. flex-wrap: wrap;
  963. }
  964. &-item {
  965. background-color: #f5f7fa;
  966. border-radius: 8rpx;
  967. position: relative;
  968. width: 220rpx;
  969. height: 220rpx;
  970. margin-right: 20rpx;
  971. margin-bottom: 20rpx;
  972. }
  973. width: 220rpx;
  974. height: 220rpx;
  975. border-radius: 8rpx;
  976. &-delete {
  977. position: absolute;
  978. top: 2rpx;
  979. right: 2rpx;
  980. width: 44rpx;
  981. height: 44rpx;
  982. justify-content: center;
  983. align-items: center;
  984. }
  985. }
  986. }
  987. .expanded-content {
  988. margin-top: 15rpx;
  989. padding-top: 15rpx;
  990. border-top: 1rpx solid #eee;
  991. }
  992. .bottom-actions {
  993. position: fixed;
  994. bottom: 0;
  995. left: 0;
  996. right: 0;
  997. display: flex;
  998. flex-direction: row;
  999. padding: 20rpx 30rpx;
  1000. background-color: #fff;
  1001. border-top: 1rpx solid #eee;
  1002. box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
  1003. }
  1004. .action-btn {
  1005. flex: 1;
  1006. padding: 12rpx;
  1007. border: none;
  1008. border-radius: 6rpx;
  1009. font-size: 26rpx;
  1010. font-weight: bold;
  1011. margin: 0 10rpx;
  1012. min-width: 120rpx;
  1013. }
  1014. .draft-btn {
  1015. background-color: #f0f0f0;
  1016. color: #666;
  1017. }
  1018. .submit-btn {
  1019. background-color: #165dff;
  1020. color: #fff;
  1021. }
  1022. .maintenance-btn {
  1023. background-color: #ff4d4f;
  1024. color: #fff;
  1025. }
  1026. .picker-modal {
  1027. position: fixed;
  1028. top: 0;
  1029. left: 0;
  1030. right: 0;
  1031. bottom: 0;
  1032. z-index: 1000;
  1033. }
  1034. .modal-mask {
  1035. position: absolute;
  1036. top: 0;
  1037. left: 0;
  1038. right: 0;
  1039. bottom: 0;
  1040. background-color: rgba(0, 0, 0, 0.5);
  1041. }
  1042. .modal-content {
  1043. position: absolute;
  1044. bottom: 0;
  1045. left: 0;
  1046. right: 0;
  1047. background-color: #ffffff;
  1048. border-top-left-radius: 16rpx;
  1049. border-top-right-radius: 16rpx;
  1050. min-height: 700rpx;
  1051. }
  1052. .modal-header {
  1053. flex-direction: row;
  1054. justify-content: space-between;
  1055. align-items: center;
  1056. padding: 30rpx;
  1057. border-bottom: 1rpx solid #f0f0f0;
  1058. }
  1059. .modal-title {
  1060. font-size: 32rpx;
  1061. font-weight: bold;
  1062. color: #333333;
  1063. }
  1064. .modal-close {
  1065. font-size: 28rpx;
  1066. color: #007aff;
  1067. }
  1068. .modal-body {
  1069. max-height: 600rpx;
  1070. }
  1071. .picker-option {
  1072. flex-direction: row;
  1073. justify-content: space-between;
  1074. align-items: center;
  1075. padding: 24rpx 30rpx;
  1076. border-bottom: 1rpx solid #f0f0f0;
  1077. }
  1078. .picker-option.selected {
  1079. background-color: #f8f9fa;
  1080. }
  1081. .option-text {
  1082. font-size: 28rpx;
  1083. color: #333333;
  1084. }
  1085. .option-check {
  1086. font-size: 28rpx;
  1087. color: #007aff;
  1088. }
  1089. </style>