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;