detail-logic.js 34 KB

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