detail-logic.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. import { getOrderInfo, getOrderLogs, uploadFile, clockIn, getAnomalyList } from '@/api/fulfiller'
  2. import { getServiceList, getServiceDetail } from '@/api/service'
  3. import { getDictDataByType } from '@/api/system/dict/index'
  4. import { getPetDetail } from '@/api/archieves/pet/index'
  5. export default {
  6. data() {
  7. return {
  8. orderId: null,
  9. pageLoading: true, // 页面数据加载中
  10. orderType: 1,
  11. orderStatus: 2,
  12. serviceId: null, // 当前订单的服务类型ID
  13. petId: null, // 当前订单关联的宠物ID
  14. petDetail: null, // 宠物档案详情
  15. // 从后端 clockInRemark 解析出的打卡步骤列表
  16. // 格式: [{step:1, title:'到达打卡', remark:'照片视频二选一即可'}, ...]
  17. clockInSteps: [],
  18. // 当前应执行的打卡信息(从 clockInSteps 中取出)
  19. currentClockIn: null,
  20. currentStep: 0,
  21. orderDetail: {
  22. type: 1,
  23. price: '0.00',
  24. timeLabel: '服务时间',
  25. time: '',
  26. petAvatar: '/static/dog.png',
  27. petName: '',
  28. petBreed: '',
  29. serviceTag: '',
  30. startLocation: '',
  31. startAddress: '',
  32. endLocation: '',
  33. endAddress: '',
  34. serviceContent: '',
  35. remark: '',
  36. orderNo: '',
  37. createTime: '',
  38. progressLogs: []
  39. },
  40. serviceList: [],
  41. showPetModal: false,
  42. currentPetInfo: {},
  43. showNavModal: false,
  44. navTargetPointType: '',
  45. showUploadModal: false,
  46. modalMediaList: [],
  47. modalRemark: '',
  48. showSumModal: false,
  49. sumContent: '',
  50. sumDate: '',
  51. sumSigner: '张*哥',
  52. showPetRemarkInput: false,
  53. petRemarkText: '',
  54. showAnomalyModal: false,
  55. anomalyList: [],
  56. anomalyTypeDict: []
  57. }
  58. },
  59. computed: {
  60. // 从 clockInSteps 中提取 title 数组作为打卡步骤名(内部逻辑用)
  61. steps() {
  62. if (this.clockInSteps.length > 0) {
  63. return this.clockInSteps.map(s => s.title)
  64. }
  65. // 兜底:如果 clockInSteps 未加载则使用默认
  66. return this.orderType === 1
  67. ? ['到达打卡', '确认出发', '送达打卡']
  68. : ['到达打卡', '开始服务', '服务结束']
  69. },
  70. // 顶部进度条展示用:已接单 -> 各打卡步骤 -> 订单完成
  71. progressSteps() {
  72. return ['已接单', ...this.steps, '订单完成']
  73. },
  74. // 进度条当前激活索引(= currentStep + 1,因为首位是"已接单")
  75. progressIndex() {
  76. // 已接单是第0步,始终已完成;打卡步骤从索引1开始
  77. return this.currentStep + 1
  78. },
  79. displayStatusText() {
  80. if (this.currentStep >= this.steps.length) return '已完成';
  81. // 判断是否在服务中
  82. if (this.currentStep > 0) {
  83. return this.orderType === 1 ? '配送中' : '服务中';
  84. }
  85. return this.orderType === 1 ? '待接送' : '待服务';
  86. },
  87. currentStatusText() {
  88. return this.currentStep >= this.steps.length ? '已完成' : this.steps[this.currentStep];
  89. },
  90. // 按钮文本:使用 clockInSteps 中对应步骤的 title
  91. currentTaskTitle() {
  92. if (this.currentStep >= this.steps.length) return '订单已完成';
  93. if (this.currentClockIn) {
  94. return this.currentClockIn.title;
  95. }
  96. return this.steps[this.currentStep] || '打卡';
  97. },
  98. // 任务描述小字:使用 clockInSteps 中对应步骤的 remark
  99. currentTaskDesc() {
  100. if (this.currentStep >= this.steps.length) return '感谢您的服务,请注意休息';
  101. if (this.currentClockIn && this.currentClockIn.remark) {
  102. return this.currentClockIn.remark;
  103. }
  104. return '请按要求提交照片或视频及备注';
  105. }
  106. },
  107. async onLoad(options) {
  108. if (options.id) {
  109. this.orderId = options.id
  110. }
  111. this.pageLoading = true
  112. try {
  113. // 先加载字典
  114. await this.loadAnomalyTypeDict()
  115. // 先获取服务列表(用于匹配服务类型名称等)
  116. await this.loadServiceList()
  117. // 获取订单详情(内部会拿到 serviceId,然后加载服务详情获取 clockInRemark)
  118. await this.loadOrderDetail()
  119. } finally {
  120. this.pageLoading = false
  121. }
  122. },
  123. methods: {
  124. async loadServiceList() {
  125. try {
  126. const res = await getServiceList()
  127. this.serviceList = res.data || []
  128. } catch (err) {
  129. console.error('获取服务类型失败:', err)
  130. }
  131. },
  132. /**
  133. * 根据服务类型ID获取服务详情,解析 clockInRemark 为打卡步骤
  134. */
  135. async loadServiceDetail(serviceId) {
  136. try {
  137. const res = await getServiceDetail(serviceId)
  138. const serviceInfo = res.data
  139. if (serviceInfo && serviceInfo.clockInRemark) {
  140. try {
  141. const parsed = JSON.parse(serviceInfo.clockInRemark)
  142. if (Array.isArray(parsed) && parsed.length > 0) {
  143. this.clockInSteps = parsed
  144. console.log('解析打卡步骤:', this.clockInSteps)
  145. }
  146. } catch (parseErr) {
  147. console.error('解析 clockInRemark 失败:', parseErr)
  148. }
  149. }
  150. } catch (err) {
  151. console.error('获取服务类型详情失败:', err)
  152. }
  153. },
  154. async loadOrderDetail() {
  155. if (!this.orderId) {
  156. console.log('订单ID缺失')
  157. uni.showToast({ title: '订单ID缺失', icon: 'none' })
  158. return
  159. }
  160. try {
  161. console.log('请求订单详情,ID:', this.orderId)
  162. const res = await getOrderInfo(this.orderId)
  163. console.log('订单详情响应:', res)
  164. const order = res.data
  165. if (!order) {
  166. console.log('订单数据为空')
  167. uni.showToast({ title: '订单不存在', icon: 'none' })
  168. return
  169. }
  170. console.log('订单数据:', order)
  171. this.serviceId = order.service
  172. this.petId = order.usrPet || null
  173. this.transformOrderData(order)
  174. // 根据订单的服务类型ID获取服务详情(含 clockInRemark)
  175. if (this.serviceId) {
  176. await this.loadServiceDetail(this.serviceId)
  177. }
  178. // 加载宠物档案详情
  179. if (this.petId) {
  180. await this.loadPetDetail(this.petId)
  181. }
  182. // 加载订单日志并根据 step 确定当前进度
  183. await this.loadOrderLogs()
  184. } catch (err) {
  185. console.error('获取订单详情失败:', err)
  186. uni.showToast({ title: '加载失败', icon: 'none' })
  187. }
  188. },
  189. async loadOrderLogs() {
  190. try {
  191. const res = await getOrderLogs(this.orderId)
  192. const logs = res.data || []
  193. console.log('订单日志:', logs)
  194. // 渲染进度日志列表
  195. const progressLogs = logs.filter(log => log.logType === 1)
  196. this.orderDetail.progressLogs = progressLogs.map(log => ({
  197. status: log.title || '',
  198. time: log.createTime || '',
  199. medias: log.photoUrls || [],
  200. remark: log.content || ''
  201. }))
  202. // 根据打卡日志的 step 确定下一步骤
  203. // 查找最新的一条打卡日志(logType=1),取其 step,下一步为 step+1
  204. const validLogs = logs.filter(log => log.logType === 1 && log.step !== undefined && log.step !== null)
  205. .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
  206. if (validLogs.length > 0) {
  207. const latestLog = validLogs[0]
  208. const latestStep = latestLog.step
  209. console.log('最新打卡日志 step:', latestStep)
  210. // 在 clockInSteps 中找到该 step 对应的索引,然后 +1 得到下一步
  211. const stepIndex = this.clockInSteps.findIndex(s => s.step === latestStep)
  212. if (stepIndex >= 0) {
  213. this.currentStep = stepIndex + 1
  214. } else {
  215. // 兜底:直接按 step 值推算
  216. this.currentStep = latestStep
  217. }
  218. } else {
  219. this.currentStep = 0
  220. }
  221. // 更新当前打卡信息
  222. this.updateCurrentClockIn()
  223. console.log('根据最新日志推算的当前步骤:', this.currentStep, '当前打卡信息:', this.currentClockIn)
  224. } catch (err) {
  225. console.error('获取订单日志失败:', err)
  226. }
  227. },
  228. /**
  229. * 根据 currentStep 更新当前打卡信息
  230. */
  231. updateCurrentClockIn() {
  232. if (this.currentStep < this.clockInSteps.length) {
  233. this.currentClockIn = this.clockInSteps[this.currentStep]
  234. } else {
  235. this.currentClockIn = null
  236. }
  237. },
  238. transformOrderData(order) {
  239. const mode = order.mode || 0
  240. const isRoundTrip = mode === 1
  241. this.orderType = isRoundTrip ? 1 : 2
  242. this.orderStatus = order.status || 2
  243. this.orderDetail = {
  244. type: this.orderType,
  245. price: (order.price / 100).toFixed(2),
  246. timeLabel: isRoundTrip ? '取货时间' : '服务时间',
  247. time: order.serviceTime || '',
  248. petAvatar: '/static/dog.png',
  249. petName: order.petName || order.contact || '',
  250. petBreed: order.breed || '',
  251. serviceTag: order.groupPurchasePackageName || '',
  252. startLocation: order.fromAddress || '',
  253. startAddress: order.fromAddress || '',
  254. endLocation: (order.contact || '') + ' ' + (order.contactPhoneNumber || ''),
  255. endAddress: order.toAddress || '',
  256. serviceContent: '',
  257. remark: '',
  258. orderNo: order.code || 'T' + order.id,
  259. createTime: order.serviceTime || '',
  260. progressLogs: [
  261. { status: '您已接单', time: order.serviceTime || '' }
  262. ]
  263. }
  264. },
  265. /**
  266. * 根据宠物ID获取宠物档案详情
  267. */
  268. async loadPetDetail(petId) {
  269. try {
  270. const res = await getPetDetail(petId)
  271. const pet = res.data
  272. if (pet) {
  273. this.petDetail = pet
  274. // 同步更新订单详情中的宠物信息
  275. this.orderDetail.petAvatar = pet.avatarUrl || '/static/dog.png'
  276. this.orderDetail.petName = pet.name || this.orderDetail.petName
  277. this.orderDetail.petBreed = pet.breed || this.orderDetail.petBreed
  278. console.log('宠物档案:', pet)
  279. }
  280. } catch (err) {
  281. console.error('获取宠物档案失败:', err)
  282. }
  283. },
  284. /**
  285. * 加载异常记录列表
  286. */
  287. async loadAnomalyList() {
  288. if (!this.orderId) return
  289. try {
  290. const res = await getAnomalyList(this.orderId)
  291. const list = res.data || []
  292. // 过滤和转换
  293. this.anomalyList = list.map(item => {
  294. // 映射类型
  295. const dict = this.anomalyTypeDict.find(d => d.value === item.type)
  296. return {
  297. ...item,
  298. typeLabel: dict ? dict.label : item.type,
  299. // 确保有图片数组供展示,如果后端没返 photoUrls,尝试兼容
  300. photoUrls: item.photoUrls || []
  301. }
  302. })
  303. } catch (err) {
  304. console.error('获取异常列表失败:', err)
  305. }
  306. },
  307. async loadAnomalyTypeDict() {
  308. try {
  309. const res = await getDictDataByType('flf_anamaly_type')
  310. this.anomalyTypeDict = res.data.map(item => ({
  311. label: item.dictLabel,
  312. value: item.dictValue
  313. }))
  314. } catch (err) {
  315. console.error('获取异常字典失败:', err)
  316. }
  317. },
  318. openAnomalyModal() {
  319. this.showAnomalyModal = true
  320. this.loadAnomalyList()
  321. },
  322. closeAnomalyModal() {
  323. this.showAnomalyModal = false
  324. },
  325. getAnomalyStatusLabel(status) {
  326. const map = {
  327. 0: '待审核',
  328. 1: '已通过',
  329. 2: '已驳回'
  330. }
  331. return map[status] || '未知'
  332. },
  333. updateStepByStatus() {
  334. if (this.orderStatus === 2) {
  335. this.currentStep = 0
  336. } else if (this.orderStatus === 3) {
  337. this.currentStep = 1
  338. } else if (this.orderStatus === 5) {
  339. this.currentStep = this.steps.length - 1
  340. } else {
  341. this.currentStep = 0
  342. }
  343. },
  344. showPetProfile() {
  345. const pet = this.petDetail
  346. if (pet) {
  347. // 使用后端返回的真实宠物数据
  348. this.currentPetInfo = {
  349. petAvatar: pet.avatarUrl || '/static/dog.png',
  350. petName: pet.name || '',
  351. petBreed: pet.breed || '',
  352. petGender: pet.gender === 1 ? 'M' : (pet.gender === 2 ? 'F' : ''),
  353. petAge: pet.age ? pet.age + '岁' : '未知',
  354. petWeight: pet.weight ? pet.weight + 'kg' : '未知',
  355. petPersonality: pet.personality || pet.cutePersonality || '无',
  356. petHobby: '',
  357. petRemark: pet.remark || '无',
  358. petTags: (pet.tags || []).map(t => t.name),
  359. petLogs: [],
  360. // 额外信息
  361. petSize: pet.size || '',
  362. petIsSterilized: pet.isSterilized,
  363. petHealthStatus: pet.healthStatus || '',
  364. petAllergies: pet.allergies || '',
  365. petMedicalHistory: pet.medicalHistory || '',
  366. petVaccineStatus: pet.vaccineStatus || '',
  367. ownerName: pet.ownerName || '',
  368. ownerPhone: pet.ownerPhone || ''
  369. }
  370. } else {
  371. // 兜底:如果宠物档案未加载成功,使用订单中的基本信息
  372. this.currentPetInfo = {
  373. ...this.orderDetail,
  374. petGender: '',
  375. petAge: '未知',
  376. petWeight: '未知',
  377. petPersonality: '无',
  378. petHobby: '',
  379. petRemark: '无',
  380. petTags: [],
  381. petLogs: []
  382. }
  383. }
  384. this.showPetModal = true
  385. },
  386. closePetProfile() {
  387. this.showPetModal = false;
  388. },
  389. openPetRemarkInput() {
  390. this.petRemarkText = '';
  391. this.showPetRemarkInput = true;
  392. },
  393. closePetRemarkInput() {
  394. this.showPetRemarkInput = false;
  395. },
  396. submitPetRemark() {
  397. if (!this.petRemarkText.trim()) {
  398. uni.showToast({ title: '备注内容不能为空', icon: 'none' });
  399. return;
  400. }
  401. const now = new Date();
  402. const date = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
  403. if (!this.currentPetInfo.petLogs) {
  404. this.currentPetInfo.petLogs = [];
  405. }
  406. this.currentPetInfo.petLogs.unshift({
  407. date: date,
  408. content: this.petRemarkText,
  409. recorder: '张*哥'
  410. });
  411. this.closePetRemarkInput();
  412. uni.showToast({ title: '备注已添加', icon: 'success' });
  413. },
  414. goToAnomaly() {
  415. uni.navigateTo({
  416. url: '/pages/orders/anomaly?orderId=' + (this.orderDetail.orderNo || '')
  417. });
  418. },
  419. callPhone() {
  420. uni.makePhoneCall({ phoneNumber: '18900008451' });
  421. },
  422. openNavigation(type) {
  423. this.navTargetPointType = type;
  424. this.showNavModal = true;
  425. },
  426. closeNavModal() {
  427. this.showNavModal = false;
  428. },
  429. chooseMap(mapType) {
  430. let pointType = this.navTargetPointType;
  431. let name = pointType === 'start' ? this.orderDetail.startLocation : this.orderDetail.endLocation;
  432. let address = pointType === 'start' ? this.orderDetail.startAddress : this.orderDetail.endAddress;
  433. this.showNavModal = false;
  434. uni.openLocation({
  435. latitude: 30.52, // Mock lat
  436. longitude: 114.31, // Mock lng
  437. name: name || '目的地',
  438. address: address || '默认地址',
  439. success: function () {
  440. console.log('打开导航成功: ' + mapType);
  441. }
  442. });
  443. },
  444. openUploadModal() {
  445. this.modalMediaList = [];
  446. this.modalRemark = '';
  447. this.showUploadModal = true;
  448. },
  449. closeUploadModal() {
  450. this.showUploadModal = false;
  451. },
  452. handleConfirmUpload() {
  453. console.log('handleConfirmUpload被调用');
  454. this.confirmUploadModal();
  455. },
  456. async chooseModalMedia() {
  457. console.log('chooseModalMedia被调用');
  458. uni.chooseImage({
  459. count: 5 - this.modalMediaList.length,
  460. success: async (res) => {
  461. console.log('选择图片成功,文件路径:', res.tempFilePaths);
  462. uni.showLoading({ title: '上传中...' });
  463. try {
  464. for (const filePath of res.tempFilePaths) {
  465. console.log('上传文件:', filePath);
  466. const uploadRes = await uploadFile(filePath);
  467. console.log('上传响应:', uploadRes);
  468. if (uploadRes.code === 200) {
  469. this.modalMediaList.push({
  470. url: uploadRes.data.url,
  471. ossId: uploadRes.data.ossId,
  472. localPath: filePath
  473. });
  474. console.log('上传成功,url:', uploadRes.data.url);
  475. }
  476. }
  477. uni.hideLoading();
  478. console.log('当前modalMediaList:', this.modalMediaList);
  479. uni.showToast({ title: '上传成功', icon: 'success' });
  480. } catch (err) {
  481. uni.hideLoading();
  482. console.error('上传失败:', err);
  483. uni.showToast({ title: '上传失败', icon: 'none' });
  484. }
  485. },
  486. fail: (err) => {
  487. console.error('选择图片失败:', err);
  488. }
  489. });
  490. },
  491. removeModalMedia(index) {
  492. this.modalMediaList.splice(index, 1);
  493. },
  494. getCurrentTime() {
  495. const now = new Date();
  496. const y = now.getFullYear();
  497. const m = String(now.getMonth() + 1).padStart(2, '0');
  498. const d = String(now.getDate()).padStart(2, '0');
  499. const h = String(now.getHours()).padStart(2, '0');
  500. const min = String(now.getMinutes()).padStart(2, '0');
  501. return `${y}/${m}/${d} ${h}:${min}`;
  502. },
  503. async confirmUploadModal() {
  504. console.log('confirmUploadModal被调用,文件数量:', this.modalMediaList.length);
  505. if (this.modalMediaList.length === 0) {
  506. uni.showToast({ title: '请上传至少一张图片或视频', icon: 'none' });
  507. return;
  508. }
  509. try {
  510. uni.showLoading({ title: '提交中...' });
  511. const uploadedMedias = this.modalMediaList.map(item => item.url);
  512. const ossIds = this.modalMediaList.map(item => item.ossId);
  513. console.log('准备打卡,ossIds:', ossIds);
  514. // 使用 clockInSteps 中对应步骤的 step 值作为打卡 type
  515. const clockInType = this.currentClockIn ? this.currentClockIn.step : (this.currentStep + 1);
  516. const clockInData = {
  517. orderId: this.orderId,
  518. photos: ossIds,
  519. content: this.modalRemark || '',
  520. type: clockInType,
  521. title: this.currentTaskTitle
  522. };
  523. console.log('打卡数据:', clockInData);
  524. await clockIn(clockInData);
  525. uni.hideLoading();
  526. this.closeUploadModal();
  527. uni.showToast({ title: '打卡成功', icon: 'success' });
  528. await this.loadOrderDetail();
  529. } catch (err) {
  530. uni.hideLoading();
  531. console.error('打卡失败:', err);
  532. uni.showToast({ title: '打卡失败,请重试', icon: 'none' });
  533. }
  534. },
  535. copyOrderNo() {
  536. uni.setClipboardData({
  537. data: this.orderDetail.orderNo,
  538. success: () => {
  539. uni.showToast({ title: '复制成功', icon: 'none' });
  540. }
  541. });
  542. },
  543. openSumModal() {
  544. // 初始化日期
  545. const now = new Date();
  546. const y = now.getFullYear();
  547. const m = String(now.getMonth() + 1).padStart(2, '0');
  548. const d = String(now.getDate()).padStart(2, '0');
  549. this.sumDate = `${y}/${m}/${d}`;
  550. // 预设服务内容模板
  551. if (!this.sumContent) {
  552. this.sumContent =
  553. '1. 精神/身体状态:\n' +
  554. '2. 进食/饮水:\n' +
  555. '3. 排泤情况:\n' +
  556. '4. 卫生情况:\n' +
  557. '5. 互动情况:\n' +
  558. '6. 特殊情况/备注:';
  559. }
  560. this.showSumModal = true;
  561. },
  562. closeSumModal() {
  563. this.showSumModal = false;
  564. },
  565. submitSumModal() {
  566. if (!this.sumContent.trim()) {
  567. uni.showToast({ title: '请填写服务内容', icon: 'none' });
  568. return;
  569. }
  570. this.closeSumModal();
  571. uni.showToast({ title: '小结已提交', icon: 'success' });
  572. }
  573. }
  574. }