diff --git a/.DS_Store b/.DS_Store index d19af9d..3ead5c2 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/src/pages/report-view/reportView.vue b/src/pages/report-view/reportView.vue index 0839322..2f51639 100644 --- a/src/pages/report-view/reportView.vue +++ b/src/pages/report-view/reportView.vue @@ -142,6 +142,53 @@ + + + + + + 服务过程中 + + + + + 照片 {{ duringPhotoItems.length }} 张 + + + + + + + + + + 视频 {{ duringVideoItems.length }} 个 + + + + + + + + @@ -235,12 +282,24 @@ const isStaff = computed(() => isLoggedIn()) const beforePhotos = computed(() => { if (!reportData.value?.beforePhotos) return [] - return reportData.value.beforePhotos + // 支持新旧两种格式:旧版是 string[],新版是 {url, mediaType}[] + const raw = reportData.value.beforePhotos + if (raw.length === 0) return [] + if (typeof raw[0] === 'string') return raw + return raw.map(item => item.url) }) const afterPhotos = computed(() => { if (!reportData.value?.afterPhotos) return [] - return reportData.value.afterPhotos + const raw = reportData.value.afterPhotos + if (raw.length === 0) return [] + if (typeof raw[0] === 'string') return raw + return raw.map(item => item.url) }) +const duringMedia = computed(() => { + return reportData.value?.duringMedia || [] +}) +const duringPhotoItems = computed(() => duringMedia.value.filter(i => i.mediaType !== 'video').map(i => i.url)) +const duringVideoItems = computed(() => duringMedia.value.filter(i => i.mediaType === 'video').map(i => i.url)) const beforePhotoUrls = computed(() => beforePhotos.value.map((item) => imgUrl(item))) const afterPhotoUrls = computed(() => afterPhotos.value.map((item) => imgUrl(item))) const beforeMorePhotos = computed(() => beforePhotos.value.slice(1)) @@ -282,6 +341,13 @@ const loadReport = async () => { else notFound.value = true } +const playVideo = (url) => { + if (!url) return + uni.navigateTo({ + url: `/pages/video-player/videoPlayer?url=${encodeURIComponent(url)}`} + }) +} + const goHome = () => uni.reLaunch({ url: '/pages/home/Home' }) const callStore = () => { @@ -304,8 +370,12 @@ const navToStore = () => { } const previewPhotos = (group, index = 0) => { - const urls = group === 'before' ? beforePhotoUrls.value : afterPhotoUrls.value - if (!urls.length) return + let urls + if (group === 'before') urls = beforePhotoUrls.value + else if (group === 'after') urls = afterPhotoUrls.value + else if (group === 'during-photo') urls = duringPhotoItems.value.map(imgUrl) + else if (group === 'during-video') urls = duringVideoItems.value.map(imgUrl) + if (!urls || !urls.length) return const current = urls[Math.max(0, Math.min(index, urls.length - 1))] uni.previewImage({ urls, @@ -993,4 +1063,55 @@ onMounted(() => loadReport()) margin-top: 4px; letter-spacing: 0.5px; } + +/* 过程中区块 */ +.section-gap { + padding: 0 16px; +} +.during-section { + background: #fff; + border-radius: 16px; + padding: 18px 18px 4px; +} +.during-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 14px; +} +.during-title-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #7c5ce9; +} +.during-group { + margin-bottom: 14px; +} +.during-group-label { + font-size: 12px; + font-weight: 600; + color: #999993; + margin-bottom: 8px; +} +.during-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} +.during-thumb { + position: relative; + aspect-ratio: 1; + border-radius: 10px; + overflow: hidden; + background: #f0f0ee; +} +.during-thumb-img { + width: 100%; + height: 100%; + display: block; +} diff --git a/src/pages/report/Report.vue b/src/pages/report/Report.vue index 4bff6ed..862c633 100644 --- a/src/pages/report/Report.vue +++ b/src/pages/report/Report.vue @@ -62,11 +62,11 @@ 服务前 - - - + + + - + @@ -80,11 +80,11 @@ 服务后 - - - + + + - + @@ -92,6 +92,48 @@ + + + + + 照片或视频,记录服务细节 + + + + + + 上传照片 + + + + 拍摄视频 + + + + + + + + + + + 暂无过程中记录 + + + @@ -145,7 +187,17 @@ const navSafeStyle = (() => { return `padding-top:${statusBarHeight}px;height:${navHeight}px;` })() -const report = ref({ petName: '', serviceType: '', appointmentTime: '', before: [], after: [], remark: '' }) +// beforeMedia / afterMedia: [{url, mediaType}] mediaType 固定为 "photo" +// duringMedia: [{url, mediaType}] mediaType 可以是 "photo" 或 "video" +const report = ref({ + petName: '', + serviceType: '', + appointmentTime: '', + beforeMedia: [], + afterMedia: [], + duringMedia: [], + remark: '' +}) const serviceTypes = ref([]) const currentAppointmentId = ref(null) const submitting = ref(false) @@ -160,43 +212,103 @@ const loadServiceTypes = async () => { if (res.code === 200) serviceTypes.value = res.data } -const uploadPhoto = (field) => { - const currentCount = report.value[field].length - if (currentCount >= 4) return uni.showToast({ title: '最多上传4张', icon: 'none' }) - uni.chooseImage({ - count: 4 - currentCount, - success: (res) => { - const tempPaths = res.tempFilePaths - uni.showLoading({ title: '上传中...' }) - let failCount = 0 - const tasks = tempPaths.map(tempPath => { - return new Promise((resolve) => { - uni.uploadFile({ - url: `${BASE_URL}/upload/image`, - filePath: tempPath, - name: 'file', - success: (uploadRes) => { - try { - const data = JSON.parse(uploadRes.data) - if (data.code === 200) { - report.value[field].push(imgUrl(data.data.url)) - } else { failCount++ } - } catch (e) { failCount++ } +/** + * 上传媒体文件 + * @param {string} field - beforeMedia | afterMedia | duringMedia + * @param {string} mediaType - photo | video + */ +const uploadMedia = (field, mediaType) => { + const maxTotal = 12 // 全局最多12条 + if (report.value[field].length >= maxTotal) { + return uni.showToast({ title: `最多上传${maxTotal}个`, icon: 'none' }) + } + const remain = maxTotal - report.value[field].length + + if (mediaType === 'video') { + // 拍摄/选择视频 + uni.chooseMedia({ + count: Math.min(remain, 4), + mediaType: ['video'], + success: (res) => { + const tempFiles = res.tempFiles + uni.showLoading({ title: '上传中...' }) + let failCount = 0 + const tasks = tempFiles.map(f => { + return new Promise((resolve) => { + const sizeMB = (f.size || 0) / 1024 / 1024 + if (sizeMB > 100) { + failCount++ + uni.showToast({ title: '视频不能超过100MB', icon: 'none' }) resolve() - }, - fail: () => { failCount++; resolve() } + return + } + uni.uploadFile({ + url: `${BASE_URL}/upload/image`, + filePath: f.tempFilePath, + name: 'file', + success: (uploadRes) => { + try { + const data = JSON.parse(uploadRes.data) + if (data.code === 200) { + report.value[field].push({ url: imgUrl(data.data.url), mediaType: 'video' }) + } else { + failCount++ + } + } catch (e) { failCount++ } + resolve() + }, + fail: () => { failCount++; resolve() } + }) }) }) - }) - Promise.all(tasks).then(() => { - uni.hideLoading() - if (failCount > 0) uni.showToast({ title: `${failCount}张上传失败`, icon: 'none' }) - }) + Promise.all(tasks).then(() => { + uni.hideLoading() + if (failCount > 0) uni.showToast({ title: `${failCount}个上传失败`, icon: 'none' }) + }) + } + }) + } else { + // 上传照片 + const fieldMax = 4 + if (report.value[field].length >= fieldMax) { + return uni.showToast({ title: '最多上传4张', icon: 'none' }) } - }) + const count = Math.min(fieldMax - report.value[field].length, remain, 4) + uni.chooseImage({ + count, + success: (res) => { + const tempPaths = res.tempFilePaths + uni.showLoading({ title: '上传中...' }) + let failCount = 0 + const tasks = tempPaths.map(tempPath => { + return new Promise((resolve) => { + uni.uploadFile({ + url: `${BASE_URL}/upload/image`, + filePath: tempPath, + name: 'file', + success: (uploadRes) => { + try { + const data = JSON.parse(uploadRes.data) + if (data.code === 200) { + report.value[field].push({ url: imgUrl(data.data.url), mediaType: 'photo' }) + } else { failCount++ } + } catch (e) { failCount++ } + resolve() + }, + fail: () => { failCount++; resolve() } + }) + }) + }) + Promise.all(tasks).then(() => { + uni.hideLoading() + if (failCount > 0) uni.showToast({ title: `${failCount}张上传失败`, icon: 'none' }) + }) + } + }) + } } -const removePhoto = (field, idx) => { report.value[field].splice(idx, 1) } +const removeMedia = (field, idx) => { report.value[field].splice(idx, 1) } const onServiceConfirm = (e) => { report.value.serviceType = serviceColumns.value[e.detail.value].value @@ -238,14 +350,19 @@ const submitReport = async () => { if (!report.value.serviceType) return uni.showToast({ title: '请选择服务类型', icon: 'none' }) if (!report.value.appointmentTime) return uni.showToast({ title: '请选择服务时间', icon: 'none' }) submitting.value = true + // 构建图片列表 const images = [] - report.value.before.forEach((url, i) => { - images.push({ photoUrl: url, photoType: 'before', sortOrder: i }) + report.value.beforeMedia.forEach((item, i) => { + images.push({ photoUrl: item.url, photoType: 'before', mediaType: item.mediaType || 'photo', sortOrder: i }) }) - report.value.after.forEach((url, i) => { - images.push({ photoUrl: url, photoType: 'after', sortOrder: i }) + report.value.afterMedia.forEach((item, i) => { + images.push({ photoUrl: item.url, photoType: 'after', mediaType: item.mediaType || 'photo', sortOrder: i }) }) + report.value.duringMedia.forEach((item, i) => { + images.push({ photoUrl: item.url, photoType: 'during', mediaType: item.mediaType || 'photo', sortOrder: i }) + }) + const payload = { appointmentId: currentAppointmentId.value || null, userId: userInfo.id, @@ -255,13 +372,14 @@ const submitReport = async () => { remark: report.value.remark, images } + const res = await createReport(payload) submitting.value = false if (res.code === 200) { const token = res.data.reportToken uni.showToast({ title: '已生成报告', icon: 'success' }) setTimeout(() => { - report.value = { petName: '', serviceType: '', appointmentTime: '', before: [], after: [], remark: '' } + report.value = { petName: '', serviceType: '', appointmentTime: '', beforeMedia: [], afterMedia: [], duringMedia: [], remark: '' } currentAppointmentId.value = null uni.navigateTo({ url: `/pages/report-view/reportView?token=${token}` }) }, 800) @@ -423,6 +541,7 @@ onMounted(async () => { aspect-ratio: 1; border-radius: 10px; overflow: hidden; + background: #f0f0ee; } .thumb-img { width: 100%; @@ -453,6 +572,69 @@ onMounted(async () => { background: #fafaf8; } +/* 过程中区块 */ +.during-actions { + display: flex; + gap: 10px; + margin-bottom: 14px; +} +.during-action-btn { + flex: 1; + height: 42px; + border-radius: 12px; + border: 1.5px solid #ebebea; + background: #fafaf8; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 14px; + font-weight: 600; + color: #666660; +} +.during-action-btn:active { + background: #f0f0ee; +} +.during-action-btn--video { + border-color: #e0d8f5; + background: #f8f5ff; +} +.during-action-btn--video:active { + background: #f0ecff; +} + +.during-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.video-badge { + position: absolute; + bottom: 5px; + left: 5px; + height: 18px; + padding: 0 6px; + border-radius: 4px; + background: rgba(0,0,0,0.55); + display: flex; + align-items: center; + justify-content: center; +} +.video-badge text { + font-size: 10px; + font-weight: 700; + color: #fff; + letter-spacing: 0.3px; +} + +.during-empty { + padding: 16px 0 4px; + text-align: center; + font-size: 13px; + color: #bbbbb5; +} + /* 备注 */ .form-textarea { background: #f5f5f2;