feat: 报告中增过程中媒体区块,单按钮上传照片/视频;基础信息折叠优化

This commit is contained in:
MaDaLei 2026-04-16 22:20:57 +08:00
parent d6b4419225
commit 1e10ebc759
2 changed files with 252 additions and 169 deletions

View File

@ -341,13 +341,6 @@ 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 = () => {
@ -383,6 +376,13 @@ const previewPhotos = (group, index = 0) => {
})
}
const playVideo = (url) => {
if (!url) return
uni.navigateTo({
url: `/pages/video-player/videoPlayer?url=${encodeURIComponent(url)}`
})
}
onShareAppMessage(() => {
const q = routeToken.value ? `token=${encodeURIComponent(routeToken.value)}` : ''
return {

View File

@ -9,9 +9,11 @@
<view class="report-body">
<!-- 基础信息 -->
<view class="section">
<view class="section-head">
<view class="section-head section-head--clickable" @click="basicInfoCollapsed = !basicInfoCollapsed">
<text class="section-label">基础信息</text>
<text class="section-toggle">{{ basicInfoCollapsed ? '展开' : '收起' }}</text>
</view>
<view v-show="!basicInfoCollapsed">
<view class="form-row">
<text class="form-label">宠物名字</text>
<input v-model="report.petName" class="form-input" placeholder="请输入宠物名字" />
@ -47,6 +49,7 @@
</view>
</view>
</view>
</view>
<!-- 服务照片 核心区域 -->
<view class="section">
@ -66,7 +69,7 @@
<image :src="item.url" class="thumb-img" mode="aspectFill" />
<view class="thumb-del" @click="removeMedia('beforeMedia', idx)"></view>
</view>
<view v-if="report.beforeMedia.length < 4" class="photo-add" @click="uploadMedia('beforeMedia', 'photo')">
<view v-if="report.beforeMedia.length < 4" class="photo-add" @click="uploadPhoto('beforeMedia')">
<AppIcon name="camera" :size="20" color="#bbbbb5" />
</view>
</view>
@ -84,7 +87,7 @@
<image :src="item.url" class="thumb-img" mode="aspectFill" />
<view class="thumb-del" @click="removeMedia('afterMedia', idx)"></view>
</view>
<view v-if="report.afterMedia.length < 4" class="photo-add" @click="uploadMedia('afterMedia', 'photo')">
<view v-if="report.afterMedia.length < 4" class="photo-add" @click="uploadPhoto('afterMedia')">
<AppIcon name="camera" :size="20" color="#bbbbb5" />
</view>
</view>
@ -99,14 +102,10 @@
<text class="section-hint">照片或视频记录服务细节</text>
</view>
<view class="during-actions">
<view class="during-action-btn" @click="uploadMedia('duringMedia', 'photo')">
<AppIcon name="camera" :size="18" color="#666660" />
<text>上传照片</text>
</view>
<view class="during-action-btn during-action-btn--video" @click="uploadMedia('duringMedia', 'video')">
<AppIcon name="video" :size="18" color="#666660" />
<text>拍摄视频</text>
<view class="during-add-row">
<view class="during-add-btn" @click="uploadDuring()">
<AppIcon name="camera" :size="18" color="#bbbbb5" />
<text>添加照片或视频</text>
</view>
</view>
@ -121,17 +120,12 @@
:show-play-btn="false"
objectFit="cover"
/>
<!-- 视频角标 -->
<view v-if="item.mediaType === 'video'" class="video-badge">
<text>视频</text>
</view>
<view class="thumb-del" @click="removeMedia('duringMedia', idx)"></view>
</view>
</view>
<view v-if="report.duringMedia.length === 0" class="during-empty">
<text>暂无过程中记录</text>
</view>
</view>
<!-- 备注 -->
@ -139,10 +133,23 @@
<view class="section-head">
<text class="section-label">备注</text>
</view>
<textarea v-model="report.remark" class="form-textarea" placeholder="服务细节、注意事项(可选)" />
<textarea
v-model="report.remark"
class="form-textarea"
placeholder="服务细节、注意事项(可选)"
@focus="onRemarkFocus"
@blur="onRemarkBlur"
/>
</view>
<button class="submit-btn" :disabled="submitting" @click="submitReport">
</view>
<view
class="submit-fixed"
:class="{ 'submit-fixed--scrolled': hasScrolled }"
:style="submitFixedStyle"
>
<button class="submit-btn submit-btn--fixed" :disabled="submitting" @click="submitReport">
{{ submitting ? '提交中…' : '生成报告' }}
</button>
</view>
@ -154,14 +161,13 @@
</view>
</view>
<TabBar current-page="report" @change="goPage" />
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onPageScroll } from '@dcloudio/uni-app'
import { createReport, getServiceTypeList, imgUrl, BASE_URL } from '../../api/index.js'
import TabBar from '../../components/TabBar.vue'
import AppIcon from '../../components/AppIcon.vue'
import { getUserSession, getStoreSession } from '../../utils/session.js'
import { useNavigator } from '../../composables/useNavigator.js'
@ -175,7 +181,7 @@ import {
const userInfo = getUserSession()
const storeInfo = getStoreSession()
const { goPage, navigateTo } = useNavigator()
const { navigateTo } = useNavigator()
const navSafeStyle = (() => {
const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
let navHeight = statusBarHeight + 44
@ -201,6 +207,17 @@ const report = ref({
const serviceTypes = ref([])
const currentAppointmentId = ref(null)
const submitting = ref(false)
const basicInfoCollapsed = ref(false)
const hasScrolled = ref(false)
const keyboardHeight = ref(0)
const remarkFocused = ref(false)
const autoCollapsedByKeyboard = ref(false)
let keyboardChangeHandler = null
const submitFixedStyle = computed(() => {
const bottom = keyboardHeight.value > 0 ? `${keyboardHeight.value}px` : '0px'
return `bottom:${bottom};`
})
const serviceColumns = computed(() =>
serviceTypes.value.map(s => ({ label: s.name, value: s.name }))
@ -213,67 +230,15 @@ const loadServiceTypes = async () => {
}
/**
* 上传媒体文件
* @param {string} field - beforeMedia | afterMedia | duringMedia
* @param {string} mediaType - photo | video
* 上传照片用于服务前/
*/
const uploadMedia = (field, mediaType) => {
const maxTotal = 12 // 12
const uploadPhoto = (field) => {
const fieldMax = 4
const maxTotal = 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()
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' })
})
}
})
} 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)
const count = Math.min(fieldMax - report.value[field].length, maxTotal - report.value[field].length, 4)
uni.chooseImage({
count,
success: (res) => {
@ -305,7 +270,75 @@ const uploadMedia = (field, mediaType) => {
})
}
})
}
/**
* 上传视频用于过程中
*/
const uploadVideo = (field) => {
const maxTotal = 12
if (report.value[field].length >= maxTotal) {
return uni.showToast({ title: `最多上传${maxTotal}`, icon: 'none' })
}
uni.chooseMedia({
count: 1,
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()
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' })
})
}
})
}
/**
* 上传过程中媒体照片或视频弹窗选择
*/
const uploadDuring = () => {
const maxTotal = 12
if (report.value.duringMedia.length >= maxTotal) {
return uni.showToast({ title: `最多上传${maxTotal}`, icon: 'none' })
}
uni.showActionSheet({
itemList: ['照片', '视频'],
success: (res) => {
if (res.tapIndex === 0) {
uploadPhoto('duringMedia')
} else {
uploadVideo('duringMedia')
}
}
})
}
const removeMedia = (field, idx) => { report.value[field].splice(idx, 1) }
@ -381,6 +414,7 @@ const submitReport = async () => {
setTimeout(() => {
report.value = { petName: '', serviceType: '', appointmentTime: '', beforeMedia: [], afterMedia: [], duringMedia: [], remark: '' }
currentAppointmentId.value = null
basicInfoCollapsed.value = false
uni.navigateTo({ url: `/pages/report-view/reportView?token=${token}` })
}, 800)
} else {
@ -398,6 +432,25 @@ const goBack = () => {
}
onMounted(async () => {
if (typeof uni.onKeyboardHeightChange === 'function') {
keyboardChangeHandler = (res) => {
const h = Math.max(0, Number(res?.height || 0))
keyboardHeight.value = h
//
if (h > 0 && remarkFocused.value && !basicInfoCollapsed.value) {
basicInfoCollapsed.value = true
autoCollapsedByKeyboard.value = true
}
//
if (h === 0 && autoCollapsedByKeyboard.value) {
basicInfoCollapsed.value = false
autoCollapsedByKeyboard.value = false
}
}
uni.onKeyboardHeightChange(keyboardChangeHandler)
}
await loadServiceTypes()
const prefill = JSON.parse(uni.getStorageSync('petstore_report_prefill') || 'null')
if (prefill) {
@ -412,14 +465,34 @@ onMounted(async () => {
report.value.appointmentTime = `${d}T${normalizeToHalfHour(hm)}`
}
}
//
basicInfoCollapsed.value = true
uni.removeStorageSync('petstore_report_prefill')
}
})
onUnmounted(() => {
if (typeof uni.offKeyboardHeightChange === 'function' && keyboardChangeHandler) {
uni.offKeyboardHeightChange(keyboardChangeHandler)
}
})
onPageScroll((e) => {
hasScrolled.value = (e?.scrollTop || 0) > 8
})
const onRemarkFocus = () => {
remarkFocused.value = true
}
const onRemarkBlur = () => {
remarkFocused.value = false
}
</script>
<style scoped>
.report-page {
padding-bottom: calc(76px + env(safe-area-inset-bottom));
padding-bottom: calc(96px + env(safe-area-inset-bottom));
background: #fafaf8;
}
.report-nav {
@ -433,7 +506,7 @@ onMounted(async () => {
}
.report-body {
padding: 12px 16px 32px;
padding: 12px 16px 20px;
}
/* 区块 */
@ -446,6 +519,16 @@ onMounted(async () => {
.section-head {
margin-bottom: 16px;
}
.section-head--clickable {
display: flex;
align-items: center;
justify-content: space-between;
}
.section-toggle {
font-size: 13px;
font-weight: 600;
color: #2db96d;
}
.section-label {
font-size: 16px;
font-weight: 700;
@ -573,35 +656,24 @@ onMounted(async () => {
}
/* 过程中区块 */
.during-actions {
display: flex;
gap: 10px;
.during-add-row {
margin-bottom: 14px;
}
.during-action-btn {
flex: 1;
height: 42px;
.during-add-btn {
height: 44px;
border-radius: 12px;
border: 1.5px solid #ebebea;
border: 1.5px dashed #ddddd8;
background: #fafaf8;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: #666660;
color: #999993;
}
.during-action-btn:active {
.during-add-btn:active {
background: #f0f0ee;
}
.during-action-btn--video {
border-color: #e0d8f5;
background: #f8f5ff;
}
.during-action-btn--video:active {
background: #f0ecff;
}
.during-grid {
display: flex;
@ -628,13 +700,6 @@ onMounted(async () => {
letter-spacing: 0.3px;
}
.during-empty {
padding: 16px 0 4px;
text-align: center;
font-size: 13px;
color: #bbbbb5;
}
/* 备注 */
.form-textarea {
background: #f5f5f2;
@ -663,10 +728,28 @@ onMounted(async () => {
font-size: 16px;
font-weight: 700;
border: none;
margin-top: 8px;
margin-top: 0;
letter-spacing: 0.5px;
transition: opacity 0.15s;
}
.submit-fixed {
position: fixed;
left: 0;
right: 0;
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
background: rgba(250, 250, 248, 0.96);
border-top: 1px solid #efefeb;
backdrop-filter: blur(2px);
z-index: 50;
transition: box-shadow 0.2s ease, background 0.2s ease, bottom 0.2s ease;
}
.submit-fixed--scrolled {
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
background: rgba(250, 250, 248, 0.985);
}
.submit-btn--fixed {
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.1);
}
.submit-btn:active { opacity: 0.85; }
.submit-btn[disabled] { background: #ddddd8; color: #999993; }