detail-logic.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. import { getOrderInfo, getOrderLogs, uploadFile, clockIn, getAnomalyList, submitNursingSummary } from '@/api/fulfiller'
  2. import { getServiceList, getServiceDetail } from '@/api/service'
  3. import { getDictDataByType } from '@/api/system/dict/index'
  4. import { getPetDetail, submitPetRemark as apiSubmitPetRemark } from '@/api/archieves/pet/index'
  5. import { listChangeLog } from '@/api/archieves/changeLog/index'
  6. export default {
  7. data() {
  8. return {
  9. orderId: null,
  10. pageLoading: true, // 页面数据加载中
  11. orderType: 1,
  12. orderStatus: 2,
  13. serviceId: null, // 当前订单的服务类型ID
  14. serviceMode: null, // 当前订单的服务模式 (0: 喂遛/洗护, 1: 接送)
  15. petId: null, // 当前订单关联的宠物ID
  16. petDetail: null, // 宠物档案详情
  17. // 从后端 clockInRemark 解析出的打卡步骤列表
  18. // 格式: [{step:1, title:'到达打卡', remark:'照片视频二选一即可'}, ...]
  19. clockInSteps: [],
  20. // 当前应执行的打卡信息(从 clockInSteps 中取出)
  21. currentClockIn: null,
  22. currentStep: 0,
  23. orderDetail: {
  24. type: 1,
  25. price: '0.00',
  26. timeLabel: '服务时间',
  27. time: '',
  28. petAvatar: '/static/dog.png',
  29. petName: '',
  30. petBreed: '',
  31. serviceTag: '',
  32. startLocation: '',
  33. startAddress: '',
  34. endAddress: '',
  35. customerPhone: '',
  36. serviceContent: '',
  37. remark: '',
  38. orderNo: '',
  39. createTime: '',
  40. progressLogs: [],
  41. nursingSummary: '' // 宠护小结
  42. },
  43. serviceList: [],
  44. showPetModal: false,
  45. currentPetInfo: {},
  46. showNavModal: false,
  47. navTargetPointType: '',
  48. showUploadModal: false,
  49. modalMediaList: [],
  50. modalRemark: '',
  51. showSumModal: false,
  52. sumContent: '',
  53. sumDate: '',
  54. sumSigner: '未知',
  55. showPetRemarkInput: false,
  56. petRemarkText: '',
  57. showAnomalyModal: false,
  58. anomalyList: [],
  59. anomalyTypeDict: [],
  60. // 媒体预览相关
  61. videoPlayerShow: false,
  62. videoPlayerUrl: ''
  63. }
  64. },
  65. computed: {
  66. // 从 clockInSteps 中提取 title 数组作为打卡步骤名(内部逻辑用)
  67. steps() {
  68. if (this.clockInSteps.length > 0) {
  69. return this.clockInSteps.map(s => s.title)
  70. }
  71. // 兜底:如果 clockInSteps 未加载则使用默认
  72. return this.orderType === 1
  73. ? ['到达打卡', '确认出发', '送达打卡']
  74. : ['到达打卡', '开始服务', '服务结束']
  75. },
  76. // 顶部进度条展示用:已接单 -> 各打卡步骤 -> 订单完成
  77. progressSteps() {
  78. return ['已接单', ...this.steps, '订单完成']
  79. },
  80. // 进度条当前激活索引(= currentStep + 1,因为首位是"已接单")
  81. progressIndex() {
  82. // 已接单是第0步,始终已完成;打卡步骤从索引1开始
  83. return this.currentStep + 1
  84. },
  85. displayStatusText() {
  86. if (this.currentStep >= this.steps.length) return '已完成';
  87. // 判断是否在服务中
  88. if (this.currentStep > 0) {
  89. return this.orderType === 1 ? '配送中' : '服务中';
  90. }
  91. return this.orderType === 1 ? '待接送' : '待服务';
  92. },
  93. currentStatusText() {
  94. return this.currentStep >= this.steps.length ? '已完成' : this.steps[this.currentStep];
  95. },
  96. // 按钮文本:使用 clockInSteps 中对应步骤的 title
  97. currentTaskTitle() {
  98. if (this.currentStep >= this.steps.length) return '订单已完成';
  99. if (this.currentClockIn) {
  100. return this.currentClockIn.title;
  101. }
  102. return this.steps[this.currentStep] || '打卡';
  103. },
  104. // 任务描述小字:使用 clockInSteps 中对应步骤的 remark
  105. currentTaskDesc() {
  106. if (this.currentStep >= this.steps.length) return '感谢您的服务,请注意休息';
  107. if (this.currentClockIn && this.currentClockIn.remark) {
  108. return this.currentClockIn.remark;
  109. }
  110. return '请按要求提交照片或视频及备注';
  111. }
  112. },
  113. async onLoad(options) {
  114. if (options.id) {
  115. this.orderId = options.id
  116. }
  117. this.pageLoading = true
  118. try {
  119. // 先加载字典
  120. await this.loadAnomalyTypeDict()
  121. // 获取订单详情(内部会拿到 serviceId,然后加载服务详情获取 clockInRemark)
  122. await this.loadOrderDetail()
  123. } finally {
  124. this.pageLoading = false
  125. }
  126. },
  127. methods: {
  128. async loadServiceList() {
  129. try {
  130. const res = await getServiceList()
  131. this.serviceList = res.data || []
  132. } catch (err) {
  133. console.error('获取服务类型失败:', err)
  134. }
  135. },
  136. /**
  137. * 根据服务类型ID获取服务详情,解析 clockInRemark 为打卡步骤
  138. */
  139. async loadServiceDetail(serviceId) {
  140. console.log('开始加载服务详情, ID:', serviceId)
  141. try {
  142. const res = await getServiceDetail(serviceId)
  143. const serviceInfo = res.data
  144. console.log('服务详情响应结果:', serviceInfo)
  145. if (serviceInfo) {
  146. this.serviceMode = serviceInfo.mode
  147. console.log('当前服务模式(mode):', this.serviceMode)
  148. if (serviceInfo.clockInRemark) {
  149. try {
  150. const parsed = JSON.parse(serviceInfo.clockInRemark)
  151. if (Array.isArray(parsed) && parsed.length > 0) {
  152. this.clockInSteps = parsed
  153. console.log('解析打卡步骤:', this.clockInSteps)
  154. }
  155. } catch (parseErr) {
  156. console.error('解析 clockInRemark 失败:', parseErr)
  157. }
  158. }
  159. }
  160. } catch (err) {
  161. console.error('获取服务类型详情失败:', err)
  162. }
  163. },
  164. async loadOrderDetail() {
  165. if (!this.orderId) {
  166. console.log('订单ID缺失')
  167. uni.showToast({ title: '订单ID缺失', icon: 'none' })
  168. return
  169. }
  170. try {
  171. console.log('请求订单详情,ID:', this.orderId)
  172. const res = await getOrderInfo(this.orderId)
  173. console.log('订单详情响应:', res)
  174. const order = res.data
  175. if (!order) {
  176. console.log('订单数据为空')
  177. uni.showToast({ title: '订单不存在', icon: 'none' })
  178. return
  179. }
  180. console.log('订单数据:', order)
  181. this.serviceId = order.service
  182. this.petId = order.usrPet || null
  183. this.transformOrderData(order)
  184. console.log('解析出的 serviceId:', this.serviceId)
  185. // 根据订单的服务类型ID获取服务详情(含 clockInRemark)
  186. if (this.serviceId) {
  187. await this.loadServiceDetail(this.serviceId)
  188. } else {
  189. console.warn('订单中未找到 service 字段,无法加载服务步骤')
  190. }
  191. // 加载宠物档案详情
  192. if (this.petId) {
  193. await this.loadPetDetail(this.petId)
  194. }
  195. // 加载订单日志并根据 step 确定当前进度
  196. await this.loadOrderLogs()
  197. } catch (err) {
  198. console.error('获取订单详情失败:', err)
  199. uni.showToast({ title: '加载失败', icon: 'none' })
  200. }
  201. },
  202. async loadOrderLogs() {
  203. try {
  204. const res = await getOrderLogs(this.orderId)
  205. const logs = res.data || []
  206. console.log('订单日志:', logs)
  207. // 渲染进度日志列表
  208. const progressLogs = logs.filter(log => log.logType === 1)
  209. this.orderDetail.progressLogs = progressLogs.map(log => ({
  210. status: log.title || '',
  211. time: log.createTime || '',
  212. medias: log.photoUrls || [],
  213. remark: log.content || ''
  214. }))
  215. // 根据打卡日志的 step 确定下一步骤
  216. // 查找最新的一条打卡日志(logType=1),取其 step,下一步为 step+1
  217. const validLogs = logs.filter(log => log.logType === 1 && log.step !== undefined && log.step !== null)
  218. .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
  219. if (validLogs.length > 0) {
  220. const latestLog = validLogs[0]
  221. const latestStep = latestLog.step
  222. console.log('最新打卡日志 step:', latestStep)
  223. // 在 clockInSteps 中找到该 step 对应的索引,然后 +1 得到下一步
  224. const stepIndex = this.clockInSteps.findIndex(s => s.step === latestStep)
  225. if (stepIndex >= 0) {
  226. this.currentStep = stepIndex + 1
  227. } else {
  228. // 兜底:直接按 step 值推算
  229. this.currentStep = Number(latestStep)
  230. }
  231. } else {
  232. this.currentStep = 0
  233. }
  234. // 更新当前打卡信息
  235. this.updateCurrentClockIn()
  236. console.log('根据最新日志推算的当前步骤:', this.currentStep, '当前打卡信息:', this.currentClockIn)
  237. } catch (err) {
  238. console.error('获取订单日志失败:', err)
  239. }
  240. },
  241. /**
  242. * 根据 currentStep 更新当前打卡信息
  243. */
  244. updateCurrentClockIn() {
  245. if (this.currentStep < this.clockInSteps.length) {
  246. this.currentClockIn = this.clockInSteps[this.currentStep]
  247. } else {
  248. this.currentClockIn = null
  249. }
  250. },
  251. transformOrderData(order) {
  252. const mode = order.mode || 0
  253. const isRoundTrip = mode === 1
  254. this.orderType = isRoundTrip ? 1 : 2
  255. this.orderStatus = order.status || 2
  256. this.orderDetail = {
  257. type: this.orderType,
  258. price: (order.price / 100).toFixed(2),
  259. timeLabel: isRoundTrip ? '取货时间' : '服务时间',
  260. time: order.serviceTime || '',
  261. petAvatar: '/static/dog.png',
  262. petName: order.petName || order.contact || '',
  263. petBreed: order.breed || '',
  264. serviceTag: order.groupPurchasePackageName || '',
  265. startLocation: order.fromAddress || '',
  266. startAddress: order.fromAddress || '',
  267. endLocation: (order.contact || '') + ' ' + (order.contactPhoneNumber || ''),
  268. endAddress: order.toAddress || '',
  269. customerPhone: order.contactPhoneNumber || '',
  270. ownerName: order.contact || '', // 宠主姓名(默认使用客户姓名)
  271. serviceContent: '',
  272. remark: '',
  273. orderNo: order.code || 'T' + order.id,
  274. createTime: order.serviceTime || '',
  275. nursingSummary: order.nursingSummary || '',
  276. fulfillerName: order.fulfillerName || '', // 履约者/护宠师姓名
  277. progressLogs: [
  278. { status: '您已接单', time: order.serviceTime || '' }
  279. ]
  280. }
  281. // 更新签名
  282. if (this.orderDetail.fulfillerName) {
  283. this.sumSigner = this.orderDetail.fulfillerName
  284. }
  285. },
  286. /**
  287. * 根据宠物ID获取宠物档案详情
  288. */
  289. async loadPetDetail(petId) {
  290. try {
  291. const res = await getPetDetail(petId)
  292. const pet = res.data
  293. if (pet) {
  294. this.petDetail = pet
  295. // 同步更新订单详情中的宠物信息
  296. this.orderDetail.petAvatar = pet.avatarUrl || '/static/dog.png'
  297. this.orderDetail.petName = pet.name || this.orderDetail.petName
  298. this.orderDetail.petBreed = pet.breed || this.orderDetail.petBreed
  299. this.orderDetail.ownerName = pet.ownerName || this.orderDetail.ownerName // 如果宠物档案有宠主姓名,则覆盖
  300. console.log('宠物档案:', pet)
  301. }
  302. } catch (err) {
  303. console.error('获取宠物档案失败:', err)
  304. }
  305. },
  306. /**
  307. * 加载异常记录列表
  308. */
  309. async loadAnomalyList() {
  310. if (!this.orderId) return
  311. try {
  312. const res = await getAnomalyList(this.orderId)
  313. const list = res.data || []
  314. // 过滤和转换
  315. this.anomalyList = list.map(item => {
  316. // 映射类型
  317. const dict = this.anomalyTypeDict.find(d => d.value === item.type)
  318. return {
  319. ...item,
  320. typeLabel: dict ? dict.label : item.type,
  321. // 确保有图片数组供展示,如果后端没返 photoUrls,尝试兼容
  322. photoUrls: item.photoUrls || []
  323. }
  324. })
  325. } catch (err) {
  326. console.error('获取异常列表失败:', err)
  327. }
  328. },
  329. async loadAnomalyTypeDict() {
  330. try {
  331. const res = await getDictDataByType('flf_anamaly_type')
  332. this.anomalyTypeDict = res.data.map(item => ({
  333. label: item.dictLabel,
  334. value: item.dictValue
  335. }))
  336. } catch (err) {
  337. console.error('获取异常字典失败:', err)
  338. }
  339. },
  340. openAnomalyModal() {
  341. this.showAnomalyModal = true
  342. this.loadAnomalyList()
  343. },
  344. closeAnomalyModal() {
  345. this.showAnomalyModal = false
  346. },
  347. getAnomalyStatusLabel(status) {
  348. const map = {
  349. 0: '待审核',
  350. 1: '已通过',
  351. 2: '已驳回'
  352. }
  353. return map[status] || '未知'
  354. },
  355. updateStepByStatus() {
  356. if (this.orderStatus === 2) {
  357. this.currentStep = 0
  358. } else if (this.orderStatus === 3) {
  359. this.currentStep = 1
  360. } else if (this.orderStatus === 4) {
  361. this.currentStep = this.steps.length - 1
  362. } else {
  363. this.currentStep = 0
  364. }
  365. },
  366. showPetProfile() {
  367. const pet = this.petDetail
  368. if (pet) {
  369. // 使用后端返回的真实宠物数据
  370. this.currentPetInfo = {
  371. petAvatar: pet.avatarUrl || '/static/dog.png',
  372. petName: pet.name || '',
  373. petBreed: pet.breed || '',
  374. petGender: pet.gender === 1 ? 'M' : (pet.gender === 2 ? 'F' : ''),
  375. petAge: pet.age ? pet.age + '岁' : '未知',
  376. petWeight: pet.weight ? pet.weight + 'kg' : '未知',
  377. petPersonality: pet.personality || pet.cutePersonality || '无',
  378. petHobby: '',
  379. petRemark: pet.remark || '无',
  380. petTags: (pet.tags || []).map(t => t.name),
  381. petLogs: [],
  382. // 额外信息
  383. petSize: pet.size || '',
  384. petIsSterilized: pet.isSterilized,
  385. petHealthStatus: pet.healthStatus || '',
  386. petAllergies: pet.allergies || '',
  387. petMedicalHistory: pet.medicalHistory || '',
  388. petVaccineStatus: pet.vaccineStatus || '',
  389. ownerName: pet.ownerName || '',
  390. ownerPhone: pet.ownerPhone || ''
  391. }
  392. // 同步加载备注日志
  393. this.loadPetChangeLogs(pet.id)
  394. } else {
  395. // 兜底:如果宠物档案未加载成功,使用订单中的基本信息
  396. this.currentPetInfo = {
  397. ...this.orderDetail,
  398. petGender: '',
  399. petAge: '未知',
  400. petWeight: '未知',
  401. petPersonality: '无',
  402. petHobby: '',
  403. petRemark: '无',
  404. petTags: [],
  405. petLogs: []
  406. }
  407. }
  408. this.showPetModal = true
  409. },
  410. async loadPetChangeLogs(petId) {
  411. if (!petId) return
  412. try {
  413. const res = await listChangeLog({
  414. targetId: petId,
  415. targetType: 'pet'
  416. })
  417. const logs = res.data || []
  418. this.currentPetInfo.petLogs = logs.map(item => ({
  419. date: item.createTime || '',
  420. content: item.content || '',
  421. recorder: item.operatorName || '未知'
  422. }))
  423. } catch (err) {
  424. console.error('获取宠物备注列表失败:', err)
  425. }
  426. },
  427. closePetProfile() {
  428. this.showPetModal = false;
  429. },
  430. openPetRemarkInput() {
  431. this.petRemarkText = '';
  432. this.showPetRemarkInput = true;
  433. },
  434. closePetRemarkInput() {
  435. this.showPetRemarkInput = false;
  436. },
  437. async submitPetRemark() {
  438. if (!this.petRemarkText.trim()) {
  439. uni.showToast({ title: '备注内容不能为空', icon: 'none' });
  440. return;
  441. }
  442. if (!this.petId) {
  443. uni.showToast({ title: '宠物信息缺失', icon: 'none' });
  444. return;
  445. }
  446. uni.showLoading({ title: '提交中...', mask: true });
  447. try {
  448. await apiSubmitPetRemark({
  449. petId: this.petId,
  450. content: this.petRemarkText
  451. });
  452. uni.hideLoading();
  453. uni.showToast({ title: '备注已添加', icon: 'success' });
  454. this.closePetRemarkInput();
  455. // 提交成功后,重新加载最新的备注日志列表
  456. this.loadPetChangeLogs(this.petId);
  457. } catch (err) {
  458. uni.hideLoading();
  459. console.error('提交宠物备注失败:', err);
  460. // 具体的错误提示已由 request.js 处理
  461. }
  462. },
  463. goToAnomaly() {
  464. uni.navigateTo({
  465. url: '/pages/orders/anomaly?orderId=' + (this.orderDetail.orderNo || '')
  466. });
  467. },
  468. callPhone() {
  469. const phoneNum = this.orderDetail.customerPhone || '18900008451'
  470. uni.makePhoneCall({ phoneNumber: phoneNum });
  471. },
  472. openNavigation(type) {
  473. this.navTargetPointType = type;
  474. this.showNavModal = true;
  475. },
  476. closeNavModal() {
  477. this.showNavModal = false;
  478. },
  479. chooseMap(mapType) {
  480. let pointType = this.navTargetPointType;
  481. let name = pointType === 'start' ? this.orderDetail.startLocation : this.orderDetail.endLocation;
  482. let address = pointType === 'start' ? this.orderDetail.startAddress : this.orderDetail.endAddress;
  483. this.showNavModal = false;
  484. uni.openLocation({
  485. latitude: 30.52, // Mock lat
  486. longitude: 114.31, // Mock lng
  487. name: name || '目的地',
  488. address: address || '默认地址',
  489. success: function () {
  490. console.log('打开导航成功: ' + mapType);
  491. }
  492. });
  493. },
  494. openUploadModal() {
  495. this.modalMediaList = [];
  496. this.modalRemark = '';
  497. this.showUploadModal = true;
  498. },
  499. closeUploadModal() {
  500. this.showUploadModal = false;
  501. },
  502. handleConfirmUpload() {
  503. console.log('handleConfirmUpload被调用');
  504. this.confirmUploadModal();
  505. },
  506. async chooseModalMedia() {
  507. console.log('chooseModalMedia被调用');
  508. // 使用 uni.chooseMedia 支持图片和视频
  509. uni.chooseMedia({
  510. count: 5 - this.modalMediaList.length,
  511. mediaType: ['image', 'video'],
  512. sourceType: ['album', 'camera'],
  513. success: async (res) => {
  514. console.log('选择媒体文件成功:', res.tempFiles);
  515. uni.showLoading({ title: '上传中...', mask: true });
  516. try {
  517. for (const file of res.tempFiles) {
  518. const filePath = file.tempFilePath;
  519. const fileType = file.fileType; // 'image' or 'video'
  520. console.log('开始上传文件:', filePath, '类型:', fileType);
  521. const uploadRes = await uploadFile(filePath);
  522. console.log('服务器响应:', uploadRes);
  523. if (uploadRes.code === 200) {
  524. this.modalMediaList.push({
  525. url: uploadRes.data.url,
  526. ossId: uploadRes.data.ossId,
  527. localPath: filePath,
  528. mediaType: fileType,
  529. thumb: file.thumbTempFilePath // 视频缩略图(如果有)
  530. });
  531. console.log('媒体文件添加成功');
  532. }
  533. }
  534. uni.hideLoading();
  535. uni.showToast({ title: '上传成功', icon: 'success' });
  536. } catch (err) {
  537. uni.hideLoading();
  538. console.error('上传失败详情:', err);
  539. uni.showToast({ title: '上传失败', icon: 'none' });
  540. }
  541. },
  542. fail: (err) => {
  543. console.error('选择媒体文件失败:', err);
  544. // 某些平台如果不兼容 chooseMedia,由开发者决定是否回退到 chooseImage/chooseVideo
  545. }
  546. });
  547. },
  548. removeModalMedia(index) {
  549. this.modalMediaList.splice(index, 1);
  550. },
  551. getCurrentTime() {
  552. const now = new Date();
  553. const y = now.getFullYear();
  554. const m = String(now.getMonth() + 1).padStart(2, '0');
  555. const d = String(now.getDate()).padStart(2, '0');
  556. const h = String(now.getHours()).padStart(2, '0');
  557. const min = String(now.getMinutes()).padStart(2, '0');
  558. return `${y}/${m}/${d} ${h}:${min}`;
  559. },
  560. async confirmUploadModal() {
  561. console.log('confirmUploadModal被调用,文件数量:', this.modalMediaList.length);
  562. if (this.modalMediaList.length === 0) {
  563. uni.showToast({ title: '请上传至少一张图片或视频', icon: 'none' });
  564. return;
  565. }
  566. try {
  567. uni.showLoading({ title: '提交中...' });
  568. const uploadedMedias = this.modalMediaList.map(item => item.url);
  569. const ossIds = this.modalMediaList.map(item => item.ossId);
  570. console.log('准备打卡,ossIds:', ossIds);
  571. // 使用 clockInSteps 中对应步骤的 step 值作为打卡 type
  572. const clockInType = this.currentClockIn ? this.currentClockIn.step : (this.currentStep + 1);
  573. const clockInData = {
  574. orderId: this.orderId,
  575. photos: ossIds,
  576. content: this.modalRemark || '',
  577. step: clockInType,
  578. title: this.currentTaskTitle,
  579. startFlag: Number(clockInType) === 1,
  580. endFlag: Number(this.currentStep) === this.steps.length - 1
  581. };
  582. console.log('打卡数据:', clockInData);
  583. await clockIn(clockInData);
  584. uni.hideLoading();
  585. this.closeUploadModal();
  586. uni.showToast({ title: '打卡成功', icon: 'success' });
  587. await this.loadOrderDetail();
  588. } catch (err) {
  589. uni.hideLoading();
  590. console.error('打卡失败:', err);
  591. uni.showToast({ title: '打卡失败,请重试', icon: 'none' });
  592. }
  593. },
  594. copyOrderNo() {
  595. uni.setClipboardData({
  596. data: this.orderDetail.orderNo,
  597. success: () => {
  598. uni.showToast({ title: '复制成功', icon: 'none' });
  599. }
  600. });
  601. },
  602. openSumModal() {
  603. // 初始化日期:优先使用订单中的服务时间,如果没有则使用当前时间
  604. let displayDate = '';
  605. if (this.orderDetail.time) {
  606. // 如果是带时间的字符串,只取日期部分
  607. displayDate = this.orderDetail.time.split(' ')[0].replace(/-/g, '/');
  608. } else {
  609. const now = new Date();
  610. const y = now.getFullYear();
  611. const m = String(now.getMonth() + 1).padStart(2, '0');
  612. const d = String(now.getDate()).padStart(2, '0');
  613. displayDate = `${y}/${m}/${d}`;
  614. }
  615. this.sumDate = displayDate;
  616. // 优先使用后端返回的小结,如果没有则判断本地是否有输入,都没有则使用预设服务内容模板
  617. if (this.orderDetail.nursingSummary) {
  618. this.sumContent = this.orderDetail.nursingSummary;
  619. } else if (!this.sumContent) {
  620. this.sumContent =
  621. '1. 精神/身体状态:\n' +
  622. '2. 进食/饮水:\n' +
  623. '3. 排泤情况:\n' +
  624. '4. 卫生情况:\n' +
  625. '5. 互动情况:\n' +
  626. '6. 特殊情况/备注:';
  627. }
  628. this.showSumModal = true;
  629. },
  630. closeSumModal() {
  631. this.showSumModal = false;
  632. },
  633. async submitSumModal() {
  634. if (!this.sumContent.trim()) {
  635. uni.showToast({ title: '请填写服务内容', icon: 'none' });
  636. return;
  637. }
  638. uni.showLoading({ title: '提交中...', mask: true });
  639. try {
  640. const res = await submitNursingSummary({
  641. orderId: this.orderId,
  642. content: this.sumContent
  643. });
  644. uni.hideLoading();
  645. if (res.code === 200) {
  646. uni.showToast({ title: '小结已提交', icon: 'success' });
  647. this.closeSumModal();
  648. // 重新加载订单详情以获取最新的已保存数据
  649. await this.loadOrderDetail();
  650. } else {
  651. uni.showToast({ title: res.msg || '提交失败', icon: 'none' });
  652. }
  653. } catch (err) {
  654. uni.hideLoading();
  655. console.error('提交宠护小结失败:', err);
  656. uni.showToast({ title: '提交失败,请重试', icon: 'none' });
  657. }
  658. },
  659. /**
  660. * 检查是否为视频
  661. */
  662. isVideo(url) {
  663. if (!url) return false;
  664. const videoExts = ['.mp4', '.mov', '.m4v', '.3gp', '.avi', '.wmv'];
  665. const lowerUrl = url.toLowerCase();
  666. return videoExts.some(ext => lowerUrl.includes(ext));
  667. },
  668. /**
  669. * 获取视频封面图 (第一帧)
  670. * 兼容阿里云、腾讯云等主流 OSS
  671. */
  672. getVideoPoster(url) {
  673. if (!this.isVideo(url)) return url;
  674. // 已经带了处理逻辑的直接返回
  675. if (url.includes('?x-oss-process') || url.includes('?ci-process') || url.includes('?vframe')) {
  676. return url;
  677. }
  678. // 兼容性尝试:
  679. // 1. 阿里云 OSS 截图: ?x-oss-process=video/snapshot,t_1,f_jpg,w_300,m_fast
  680. // 2. 腾讯云 COS 截图: ?ci-process=snapshot&time=1
  681. // 3. 七牛云 截图: ?vframe/jpg/offset/1
  682. // 默认拼接多个参数(实际使用中通常只会生效一种,根据具体后端使用的服务决定)
  683. // 这里为了通用,先猜测阿里的
  684. const aliyun = `?x-oss-process=video/snapshot,t_1,f_jpg,w_300,m_fast`;
  685. const tencent = `?ci-process=snapshot&time=1`;
  686. // 如果后端没有特殊说明,我们根据域名简单判断或直接拼接(部分OSS支持第一个 ? 后的参数)
  687. if (url.includes('myqcloud.com')) {
  688. return url + tencent;
  689. }
  690. return url + aliyun;
  691. },
  692. /**
  693. * 统一预览媒体
  694. */
  695. previewMedia(medias, currentIdx) {
  696. const url = medias[currentIdx];
  697. if (this.isVideo(url)) {
  698. // 如果是视频,播放视频
  699. this.videoPlayerUrl = url;
  700. this.videoPlayerShow = true;
  701. } else {
  702. // 如果是图片,预览图片(过滤掉数组中的视频)
  703. const imageUrls = medias.filter(m => !this.isVideo(m));
  704. // 调整当前图片的索引
  705. const currentImgUrl = url;
  706. const newIdx = imageUrls.indexOf(currentImgUrl);
  707. uni.previewImage({
  708. current: newIdx >= 0 ? newIdx : 0,
  709. urls: imageUrls
  710. });
  711. }
  712. },
  713. closeVideoPlayer() {
  714. this.videoPlayerShow = false;
  715. this.videoPlayerUrl = '';
  716. }
  717. }
  718. }