From 525223be7f8904d38534dc0523d8903ebcb5a8d2 Mon Sep 17 00:00:00 2001 From: MaDaLei Date: Thu, 16 Apr 2026 21:38:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=A5=E5=91=8A=E5=A1=AB=E5=86=99?= =?UTF-8?q?=E9=A1=B5=E6=96=B0=E5=A2=9E=E6=9C=8D=E5=8A=A1=E8=BF=87=E7=A8=8B?= =?UTF-8?q?=E4=B8=AD=E5=8C=BA=E5=9D=97=EF=BC=8C=E6=94=AF=E6=8C=81=E7=85=A7?= =?UTF-8?q?=E7=89=87+=E8=A7=86=E9=A2=91=E4=B8=8A=E4=BC=A0=EF=BC=9B?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E5=B1=95=E7=A4=BA=E9=A1=B5=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E4=B8=AD=E5=AA=92=E4=BD=93=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=EF=BC=9B=E5=89=8D=E5=90=8E=E7=AB=AFmediaType=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 8196 -> 8196 bytes src/pages/report-view/reportView.vue | 129 ++++++++++++- src/pages/report/Report.vue | 272 ++++++++++++++++++++++----- 3 files changed, 352 insertions(+), 49 deletions(-) diff --git a/.DS_Store b/.DS_Store index d19af9d253de2f453e66128ab2ffaf46ef001645..3ead5c2119e183b601d2c79fbc85244d1494f04e 100644 GIT binary patch delta 82 zcmZp1XmOa}FDlBwz`)4BAi&_6lb@WFlb;0S3v4W$#y+uucQZQ&3kRdY02Hwr=94s9AAQkKkjtm7r zsE4d7KM5!U)X3lj#Cre1fPrB%kHB + + + + + + 服务过程中 + + + + + 照片 {{ 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;