detail-logic.js 30 KB

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