import { getOrderInfo, clockIn, submitNursingSummary } from '@/api/order/subOrder' import { getOrderLogs } from '@/api/order/subOrderLog' import { uploadFile } from '@/api/fulfiller/app' import { getAnomalyList } from '@/api/fulfiller/anamaly' import { listAllService } from '@/api/service/list' import { reportGps } from '@/utils/gps' import { getDictDataByType } from '@/api/system/dict' import { getPetDetail, submitPetRemark as apiSubmitPetRemark } from '@/api/archieves/pet' import { listChangeLog } from '@/api/archieves/changeLog' export default { data() { return { orderId: null, pageLoading: true, // 页面数据加载中 orderType: 1, orderStatus: 2, serviceId: null, // 当前订单的服务类型ID serviceMode: null, // 当前订单的服务模式 (0: 喂遛/洗护, 1: 接送) petId: null, // 当前订单关联的宠物ID petDetail: null, // 宠物档案详情 // 从后端 clockInRemark 解析出的打卡步骤列表 // 格式: [{step:1, title:'到达打卡', remark:'照片视频二选一即可'}, ...] clockInSteps: [], // 当前应执行的打卡信息(从 clockInSteps 中取出) currentClockIn: null, currentStep: 0, orderDetail: { type: 1, price: '0.00', timeLabel: '服务时间', time: '', petAvatar: '/static/dog.png', petName: '', petBreed: '', serviceTag: '', startLocation: '', startAddress: '', endAddress: '', customerPhone: '', serviceContent: '', remark: '', orderNo: '', createTime: '', serviceName: '', // 服务类型名称 progressLogs: [], nursingSummary: '' // 宠护小结 }, serviceList: [], showPetModal: false, currentPetInfo: {}, showNavModal: false, navTargetPointType: '', showUploadModal: false, modalMediaList: [], modalRemark: '', showSumModal: false, sumContent: '', sumDate: '', sumSigner: '未知', showPetRemarkInput: false, petRemarkText: '', showAnomalyModal: false, anomalyList: [], anomalyTypeDict: [], // 媒体预览相关 videoPlayerShow: false, videoPlayerUrl: '' } }, computed: { // 从 clockInSteps 中提取 title 数组作为打卡步骤名(内部逻辑用) steps() { if (this.clockInSteps.length > 0) { return this.clockInSteps.map(s => s.title) } // 兜底:如果 clockInSteps 未加载则使用默认 return this.orderType === 1 ? ['到达打卡', '确认出发', '送达打卡'] : ['到达打卡', '开始服务', '服务结束'] }, // 顶部进度条展示用:已接单 -> 各打卡步骤 -> 订单完成 progressSteps() { return ['已接单', ...this.steps, '订单完成'] }, // 进度条当前激活索引(= currentStep + 1,因为首位是"已接单") progressIndex() { // 已接单是第0步,始终已完成;打卡步骤从索引1开始 return this.currentStep + 1 }, displayStatusText() { if (this.currentStep >= this.steps.length) return '已完成'; // 判断是否在服务中 if (this.currentStep > 0) { return this.orderType === 1 ? '配送中' : '服务中'; } return this.orderType === 1 ? '待接送' : '待服务'; }, currentStatusText() { return this.currentStep >= this.steps.length ? '已完成' : this.steps[this.currentStep]; }, // 按钮文本:使用 clockInSteps 中对应步骤的 title currentTaskTitle() { if (this.currentStep >= this.steps.length) return '订单已完成'; if (this.currentClockIn) { return this.currentClockIn.title; } return this.steps[this.currentStep] || '打卡'; }, // 任务描述小字:使用 clockInSteps 中对应步骤的 remark currentTaskDesc() { if (this.currentStep >= this.steps.length) return '感谢您的服务,请注意休息'; if (this.currentClockIn && this.currentClockIn.remark) { return this.currentClockIn.remark; } return '请按要求提交照片或视频及备注'; } }, async onLoad(options) { if (options.id) { this.orderId = options.id } this.pageLoading = true // 显式请求一次定位授权 reportGps(true).catch(e => console.log('Init GPS check skipped', e)); try { // 先加载字典 await this.loadAnomalyTypeDict() // 先加载所有服务列表 await this.loadServiceList() // 获取订单详情 await this.loadOrderDetail() } finally { this.pageLoading = false } }, methods: { async loadServiceList() { try { const res = await listAllService() this.serviceList = res.data || [] } catch (err) { console.error('获取服务类型失败:', err) } }, /** * 根据服务类型ID获取服务详情,解析 clockInRemark 为打卡步骤 */ /** * 基于已加载的 serviceList 进行前端匹配,解析 clockInRemark 为打卡步骤 */ loadServiceDetail(serviceId) { console.log('前端匹配服务详情, ID:', serviceId) const serviceInfo = (this.serviceList || []).find(s => s.id === serviceId) console.log('匹配到的服务信息:', serviceInfo) if (serviceInfo) { this.serviceMode = serviceInfo.mode this.orderDetail.serviceName = serviceInfo.name console.log('当前服务模式(mode):', this.serviceMode) if (serviceInfo.clockInRemark) { try { const parsed = JSON.parse(serviceInfo.clockInRemark) if (Array.isArray(parsed) && parsed.length > 0) { this.clockInSteps = parsed console.log('解析打卡步骤:', this.clockInSteps) } } catch (parseErr) { console.error('解析 clockInRemark 失败:', parseErr) } } } }, async loadOrderDetail() { if (!this.orderId) { console.log('订单ID缺失') uni.showToast({ title: '订单ID缺失', icon: 'none' }) return } try { console.log('请求订单详情,ID:', this.orderId) const res = await getOrderInfo(this.orderId) console.log('订单详情响应:', res) const order = res.data if (!order) { console.log('订单数据为空') uni.showToast({ title: '订单不存在', icon: 'none' }) return } console.log('订单数据:', order) this.serviceId = order.service this.petId = order.usrPet || null this.transformOrderData(order) console.log('解析出的 serviceId:', this.serviceId) // 根据订单的服务类型ID获取服务详情(含 clockInRemark) if (this.serviceId) { this.loadServiceDetail(this.serviceId) } else { console.warn('订单中未找到 service 字段,无法加载服务步骤') } // 加载宠物档案详情 if (this.petId) { await this.loadPetDetail(this.petId) } // 加载订单日志并根据 step 确定当前进度 await this.loadOrderLogs() } catch (err) { console.error('获取订单详情失败:', err) uni.showToast({ title: '加载失败', icon: 'none' }) } }, async loadOrderLogs() { try { const res = await getOrderLogs(this.orderId) const logs = res.data || [] console.log('订单日志:', logs) // 渲染进度日志列表 const progressLogs = logs.filter(log => log.logType === 1) this.orderDetail.progressLogs = progressLogs.map(log => ({ status: log.title || '', time: log.createTime || '', medias: log.photoUrls || [], remark: log.content || '' })) // 根据打卡日志的 step 确定下一步骤 // 查找最新的一条打卡日志(logType=1),取其 step,下一步为 step+1 const validLogs = logs.filter(log => log.logType === 1 && log.step !== undefined && log.step !== null) .sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()) if (validLogs.length > 0) { const latestLog = validLogs[0] const latestStep = latestLog.step console.log('最新打卡日志 step:', latestStep) // 在 clockInSteps 中找到该 step 对应的索引,然后 +1 得到下一步 const stepIndex = this.clockInSteps.findIndex(s => s.step === latestStep) if (stepIndex >= 0) { this.currentStep = stepIndex + 1 } else { // 兜底:直接按 step 值推算 this.currentStep = Number(latestStep) } } else { this.currentStep = 0 } // 更新当前打卡信息 this.updateCurrentClockIn() console.log('根据最新日志推算的当前步骤:', this.currentStep, '当前打卡信息:', this.currentClockIn) } catch (err) { console.error('获取订单日志失败:', err) } }, /** * 根据 currentStep 更新当前打卡信息 */ updateCurrentClockIn() { if (this.currentStep < this.clockInSteps.length) { this.currentClockIn = this.clockInSteps[this.currentStep] } else { this.currentClockIn = null } }, transformOrderData(order) { const mode = order.mode || 0 const isRoundTrip = mode === 1 this.orderType = isRoundTrip ? 1 : 2 this.orderStatus = order.status || 2 this.orderDetail = { type: this.orderType, price: (order.price / 100).toFixed(2), timeLabel: isRoundTrip ? '取货时间' : '服务时间', time: order.serviceTime || '', petAvatar: '/static/dog.png', petName: order.petName || order.contact || '', petBreed: order.breed || '', serviceTag: order.groupPurchasePackageName || '', startLocation: order.fromAddress || '暂无起点', startAddress: order.fromAddress || '', fromAddress: order.fromAddress || '', fromLat: order.fromLat, fromLng: order.fromLng, endLocation: (order.contact || '') + ' ' + (order.contactPhoneNumber || ''), endAddress: order.toAddress || '', toAddress: order.toAddress || '', toLat: order.toLat, toLng: order.toLng, customerPhone: order.contactPhoneNumber || '', ownerName: order.contact || '', // 宠主姓名(默认使用客户姓名) serviceContent: '', remark: '', orderNo: order.code || 'T' + order.id, createTime: order.serviceTime || '', nursingSummary: order.nursingSummary || '', fulfillerName: order.fulfillerName || '', // 履约者/护宠师姓名 progressLogs: [ { status: '您已接单', time: order.serviceTime || '' } ] } // 更新签名 if (this.orderDetail.fulfillerName) { this.sumSigner = this.orderDetail.fulfillerName } }, /** * 根据宠物ID获取宠物档案详情 */ async loadPetDetail(petId) { try { const res = await getPetDetail(petId) const pet = res.data if (pet) { this.petDetail = pet // 同步更新订单详情中的宠物信息 this.orderDetail.petAvatar = pet.avatarUrl || '/static/dog.png' this.orderDetail.petName = pet.name || this.orderDetail.petName this.orderDetail.petBreed = pet.breed || this.orderDetail.petBreed this.orderDetail.ownerName = pet.ownerName || this.orderDetail.ownerName // 如果宠物档案有宠主姓名,则覆盖 console.log('宠物档案:', pet) } } catch (err) { console.error('获取宠物档案失败:', err) } }, /** * 加载异常记录列表 */ async loadAnomalyList() { if (!this.orderId) return try { const res = await getAnomalyList(this.orderId) const list = res.data || [] // 过滤和转换 this.anomalyList = list.map(item => { // 映射类型 const dict = this.anomalyTypeDict.find(d => d.value === item.type) return { ...item, typeLabel: dict ? dict.label : item.type, // 确保有图片数组供展示,如果后端没返 photoUrls,尝试兼容 photoUrls: item.photoUrls || [] } }) } catch (err) { console.error('获取异常列表失败:', err) } }, async loadAnomalyTypeDict() { try { const res = await getDictDataByType('flf_anamaly_type') this.anomalyTypeDict = res.data.map(item => ({ label: item.dictLabel, value: item.dictValue })) } catch (err) { console.error('获取异常字典失败:', err) } }, openAnomalyModal() { this.showAnomalyModal = true this.loadAnomalyList() }, closeAnomalyModal() { this.showAnomalyModal = false }, getAnomalyStatusLabel(status) { const map = { 0: '待审核', 1: '已通过', 2: '已驳回' } return map[status] || '未知' }, updateStepByStatus() { if (this.orderStatus === 2) { this.currentStep = 0 } else if (this.orderStatus === 3) { this.currentStep = 1 } else if (this.orderStatus === 4) { this.currentStep = this.steps.length - 1 } else { this.currentStep = 0 } }, showPetProfile() { const pet = this.petDetail if (pet) { // 使用后端返回的真实宠物数据 this.currentPetInfo = { petAvatar: pet.avatarUrl || '/static/dog.png', petName: pet.name || '', petBreed: pet.breed || '', petGender: pet.gender === 1 ? 'M' : (pet.gender === 2 ? 'F' : ''), petAge: pet.age ? pet.age + '岁' : '未知', petWeight: pet.weight ? pet.weight + 'kg' : '未知', petPersonality: pet.personality || pet.cutePersonality || '无', petHobby: '', petRemark: pet.remark || '无', petTags: (pet.tags || []).map(t => t.name), petLogs: [], // 额外信息 petSize: pet.size || '', petIsSterilized: pet.isSterilized, petHealthStatus: pet.healthStatus || '', petAllergies: pet.allergies || '', petMedicalHistory: pet.medicalHistory || '', petVaccineStatus: pet.vaccineStatus || '', ownerName: pet.ownerName || '', ownerPhone: pet.ownerPhone || '' } // 同步加载备注日志 this.loadPetChangeLogs(pet.id) } else { // 兜底:如果宠物档案未加载成功,使用订单中的基本信息 this.currentPetInfo = { ...this.orderDetail, petGender: '', petAge: '未知', petWeight: '未知', petPersonality: '无', petHobby: '', petRemark: '无', petTags: [], petLogs: [] } } this.showPetModal = true }, async loadPetChangeLogs(petId) { if (!petId) return try { const res = await listChangeLog({ targetId: petId, targetType: 'pet' }) const logs = res.data || [] this.currentPetInfo.petLogs = logs.map(item => ({ date: item.createTime || '', content: item.content || '', recorder: item.operatorName || '未知' })) } catch (err) { console.error('获取宠物备注列表失败:', err) } }, closePetProfile() { this.showPetModal = false; }, openPetRemarkInput() { this.petRemarkText = ''; this.showPetRemarkInput = true; }, closePetRemarkInput() { this.showPetRemarkInput = false; }, async submitPetRemark() { if (!this.petRemarkText.trim()) { uni.showToast({ title: '备注内容不能为空', icon: 'none' }); return; } if (!this.petId) { uni.showToast({ title: '宠物信息缺失', icon: 'none' }); return; } uni.showLoading({ title: '提交中...', mask: true }); try { await apiSubmitPetRemark({ petId: this.petId, content: this.petRemarkText }); uni.hideLoading(); uni.showToast({ title: '备注已添加', icon: 'success' }); this.closePetRemarkInput(); // 提交成功后,重新加载最新的备注日志列表 this.loadPetChangeLogs(this.petId); } catch (err) { uni.hideLoading(); console.error('提交宠物备注失败:', err); // 具体的错误提示已由 request.js 处理 } }, goToAnomaly() { uni.navigateTo({ url: '/pages/orders/anomaly?orderId=' + (this.orderId || '') }); }, /** * 拨打电话 (带授权引导) */ callPhone() { const phoneNum = this.orderDetail.customerPhone || '18900008451' if (!phoneNum) { uni.showToast({ title: '手机号不存在', icon: 'none' }); return; } // 引导用户主动点击授权确认 uni.showModal({ title: '拨号提示', content: `系统将为您拨打手机号: ${phoneNum},请授予拨号权限以正常通话。`, confirmText: '呼叫', cancelText: '取消', success: (res) => { if (res.confirm) { uni.makePhoneCall({ phoneNumber: phoneNum, fail: (err) => { console.error('拨号失败:', err); // 如果是由于权限拒绝,提示用户 uni.showToast({ title: '无法唤起拨号盘,请检查权限设置', icon: 'none' }); } }); } } }); }, openNavigation(type) { this.navTargetPointType = type; this.showNavModal = true; }, closeNavModal() { this.showNavModal = false; }, chooseMap(mapType) { let pointType = this.navTargetPointType; // 起 -> fromAddress ; 终 -> toAddress let name = pointType === 'start' ? (this.orderDetail.fromAddress || '起点') : (this.orderDetail.toAddress || '终点'); let address = pointType === 'start' ? (this.orderDetail.fromAddress || '起点地址') : (this.orderDetail.toAddress || '终点地址'); let latitude = pointType === 'start' ? Number(this.orderDetail.fromLat) : Number(this.orderDetail.toLat); let longitude = pointType === 'start' ? Number(this.orderDetail.fromLng) : Number(this.orderDetail.toLng); this.showNavModal = false; // 统一定义打开地图的函数 const navigateTo = (lat, lng, addrName, addrDesc) => { uni.openLocation({ latitude: lat, longitude: lng, name: addrName, address: addrDesc || '无法获取详细地址', success: function () { console.log('打开导航成功: ' + mapType); }, fail: function (err) { console.error('打开导航失败:', err); uni.showToast({ title: '打开地图失败', icon: 'none' }); } }); }; // 如果有目标经纬度,直接打开 if (latitude && longitude && !isNaN(latitude) && !isNaN(longitude)) { navigateTo(latitude, longitude, name, address); } else { // 如果没有经纬度,按照需求:使用自己当前的经纬度,然后搜索 fromAddress 或者 toAddress uni.showLoading({ title: '获取当前位置...', mask: true }); reportGps(true).then(res => { uni.hideLoading(); // 使用用户当前经纬度作为锚点打开地图,展示目标地址信息 navigateTo(res.latitude, res.longitude, name, address); }).catch(err => { uni.hideLoading(); console.error('获取地理位置失败:', err); // 具体的授权引导已在 reportGps 内部处理 }); } }, openUploadModal() { this.modalMediaList = []; this.modalRemark = ''; this.showUploadModal = true; }, closeUploadModal() { this.showUploadModal = false; }, handleConfirmUpload() { console.log('handleConfirmUpload被调用'); this.confirmUploadModal(); }, async chooseModalMedia() { console.log('chooseModalMedia被调用'); // 使用 uni.chooseMedia 支持图片和视频 uni.chooseMedia({ count: 5 - this.modalMediaList.length, mediaType: ['image', 'video'], sourceType: ['album', 'camera'], success: async (res) => { console.log('选择媒体文件成功:', res.tempFiles); uni.showLoading({ title: '上传中...', mask: true }); try { for (const file of res.tempFiles) { const filePath = file.tempFilePath; const fileType = file.fileType; // 'image' or 'video' console.log('开始上传文件:', filePath, '类型:', fileType); const uploadRes = await uploadFile(filePath); console.log('服务器响应:', uploadRes); if (uploadRes.code === 200) { this.modalMediaList.push({ url: uploadRes.data.url, ossId: uploadRes.data.ossId, localPath: filePath, mediaType: fileType, thumb: file.thumbTempFilePath // 视频缩略图(如果有) }); console.log('媒体文件添加成功'); } } uni.hideLoading(); uni.showToast({ title: '上传成功', icon: 'success' }); } catch (err) { uni.hideLoading(); console.error('上传失败详情:', err); uni.showToast({ title: '上传失败', icon: 'none' }); } }, fail: (err) => { console.error('选择媒体文件失败:', err); // 某些平台如果不兼容 chooseMedia,由开发者决定是否回退到 chooseImage/chooseVideo } }); }, removeModalMedia(index) { this.modalMediaList.splice(index, 1); }, getCurrentTime() { const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const h = String(now.getHours()).padStart(2, '0'); const min = String(now.getMinutes()).padStart(2, '0'); return `${y}/${m}/${d} ${h}:${min}`; }, async confirmUploadModal() { console.log('confirmUploadModal被调用,文件数量:', this.modalMediaList.length); if (this.modalMediaList.length === 0) { uni.showToast({ title: '请上传至少一张图片或视频', icon: 'none' }); return; } try { uni.showLoading({ title: '提交中...' }); const uploadedMedias = this.modalMediaList.map(item => item.url); const ossIds = this.modalMediaList.map(item => item.ossId); console.log('准备打卡,ossIds:', ossIds); // 使用 clockInSteps 中对应步骤的 step 值作为打卡 type const clockInType = this.currentClockIn ? this.currentClockIn.step : (this.currentStep + 1); const clockInData = { orderId: this.orderId, photos: ossIds, content: this.modalRemark || '', step: clockInType, title: this.currentTaskTitle, startFlag: Number(clockInType) === 1, endFlag: Number(this.currentStep) === this.steps.length - 1 }; console.log('打卡数据:', clockInData); await clockIn(clockInData); uni.hideLoading(); this.closeUploadModal(); uni.showToast({ title: '打卡成功', icon: 'success' }); await this.loadOrderDetail(); } catch (err) { uni.hideLoading(); console.error('打卡失败:', err); uni.showToast({ title: '打卡失败,请重试', icon: 'none' }); } }, copyOrderNo() { uni.setClipboardData({ data: this.orderDetail.orderNo, success: () => { uni.showToast({ title: '复制成功', icon: 'none' }); } }); }, openSumModal() { // 初始化日期:优先使用订单中的服务时间,如果没有则使用当前时间 let displayDate = ''; if (this.orderDetail.time) { // 如果是带时间的字符串,只取日期部分 displayDate = this.orderDetail.time.split(' ')[0].replace(/-/g, '/'); } else { const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); displayDate = `${y}/${m}/${d}`; } this.sumDate = displayDate; // 优先使用后端返回的小结,如果没有则判断本地是否有输入,都没有则使用预设服务内容模板 if (this.orderDetail.nursingSummary) { this.sumContent = this.orderDetail.nursingSummary; } else if (!this.sumContent) { this.sumContent = '1. 精神/身体状态:\n' + '2. 进食/饮水:\n' + '3. 排泤情况:\n' + '4. 卫生情况:\n' + '5. 互动情况:\n' + '6. 特殊情况/备注:'; } this.showSumModal = true; }, closeSumModal() { this.showSumModal = false; }, async submitSumModal() { if (!this.sumContent.trim()) { uni.showToast({ title: '请填写服务内容', icon: 'none' }); return; } uni.showLoading({ title: '提交中...', mask: true }); try { const res = await submitNursingSummary({ orderId: this.orderId, content: this.sumContent }); uni.hideLoading(); if (res.code === 200) { uni.showToast({ title: '小结已提交', icon: 'success' }); this.closeSumModal(); // 重新加载订单详情以获取最新的已保存数据 await this.loadOrderDetail(); } else { uni.showToast({ title: res.msg || '提交失败', icon: 'none' }); } } catch (err) { uni.hideLoading(); console.error('提交宠护小结失败:', err); uni.showToast({ title: '提交失败,请重试', icon: 'none' }); } }, /** * 检查是否为视频 */ isVideo(url) { if (!url) return false; const videoExts = ['.mp4', '.mov', '.m4v', '.3gp', '.avi', '.wmv']; const lowerUrl = url.toLowerCase(); return videoExts.some(ext => lowerUrl.includes(ext)); }, /** * 获取视频封面图 (第一帧) * 兼容阿里云、腾讯云等主流 OSS */ getVideoPoster(url) { if (!this.isVideo(url)) return url; // 已经带了处理逻辑的直接返回 if (url.includes('?x-oss-process') || url.includes('?ci-process') || url.includes('?vframe')) { return url; } // 兼容性尝试: // 1. 阿里云 OSS 截图: ?x-oss-process=video/snapshot,t_1,f_jpg,w_300,m_fast // 2. 腾讯云 COS 截图: ?ci-process=snapshot&time=1 // 3. 七牛云 截图: ?vframe/jpg/offset/1 // 默认拼接多个参数(实际使用中通常只会生效一种,根据具体后端使用的服务决定) // 这里为了通用,先猜测阿里的 const aliyun = `?x-oss-process=video/snapshot,t_1,f_jpg,w_300,m_fast`; const tencent = `?ci-process=snapshot&time=1`; // 如果后端没有特殊说明,我们根据域名简单判断或直接拼接(部分OSS支持第一个 ? 后的参数) if (url.includes('myqcloud.com')) { return url + tencent; } return url + aliyun; }, /** * 统一预览媒体 */ previewMedia(medias, currentIdx) { const url = medias[currentIdx]; if (this.isVideo(url)) { // 如果是视频,播放视频 this.videoPlayerUrl = url; this.videoPlayerShow = true; } else { // 如果是图片,预览图片(过滤掉数组中的视频) const imageUrls = medias.filter(m => !this.isVideo(m)); // 调整当前图片的索引 const currentImgUrl = url; const newIdx = imageUrls.indexOf(currentImgUrl); uni.previewImage({ current: newIdx >= 0 ? newIdx : 0, urls: imageUrls }); } }, closeVideoPlayer() { this.videoPlayerShow = false; this.videoPlayerUrl = ''; } } }