detail-logic.js 31 KB

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