detail-logic.js 33 KB

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