petstore-frontend/pages/report/report.vue
2026-04-12 22:57:48 +08:00

334 lines
10 KiB
Vue

<template>
<view class="report-page">
<!-- 提交结果 -->
<view v-if="reportResult" class="result-wrap">
<view class="notice-bar">✓ 提交成功,可复制链接或扫描二维码分享给宠主</view>
<view class="result-content">
<view class="result-link-row">
<text class="result-link-label">报告链接</text>
<text class="result-link-val" selectable>{{ reportResult.reportUrl }}</text>
</view>
<view class="qr-wrap">
<image :src="qrUrl" mode="aspectFit" class="qr-img" />
</view>
<button class="btn-primary-full" @click="downloadQr">📥 保存二维码到相册</button>
<button class="btn-default-full" @click="goHome" style="margin-top:12px;">返回首页</button>
</view>
</view>
<!-- 填写表单 -->
<view v-else class="report-form">
<!-- 基本信息 -->
<view class="form-section">
<view class="form-title">基本信息</view>
<view class="form-card">
<view class="form-row">
<text class="row-label">宠物名字</text>
<input class="row-input" v-model="report.petName" placeholder="请输入宠物名字" />
</view>
<view class="form-row">
<text class="row-label">服务类型</text>
<picker :value="serviceTypeIdx" :range="serviceTypeOptions" :range-key="'name'" @change="onServiceTypeChange">
<view class="picker-val">{{ report.serviceType || '请选择' }} ▾</view>
</picker>
</view>
<view class="form-row">
<text class="row-label">服务时间</text>
<picker mode="datetime" :value="report.appointmentTime" @change="onDateChange">
<view class="picker-val">{{ report.appointmentTime || '请选择' }} ▾</view>
</picker>
</view>
</view>
</view>
<!-- 服务照片 -->
<view class="form-section">
<view class="form-title">服务照片</view>
<view class="form-card">
<view class="photo-row">
<text class="row-label">服务前</text>
<view class="photo-upload" @click="chooseImage('before')">
<image v-if="report.before" :src="report.before" class="photo-preview" mode="aspectFill" />
<view v-else class="photo-placeholder">
<text class="plus-icon">+</text>
<text class="upload-tip">上传照片</text>
</view>
</view>
</view>
<view class="photo-row">
<text class="row-label">服务后</text>
<view class="photo-upload" @click="chooseImage('after')">
<image v-if="report.after" :src="report.after" class="photo-preview" mode="aspectFill" />
<view v-else class="photo-placeholder">
<text class="plus-icon">+</text>
<text class="upload-tip">上传照片</text>
</view>
</view>
</view>
</view>
</view>
<!-- 备注 -->
<view class="form-section">
<view class="form-title">备注</view>
<view class="form-card">
<textarea class="remark-input" v-model="report.remark" placeholder="请输入备注信息..." rows="3" />
</view>
</view>
<view class="submit-wrap">
<button class="btn-submit" :class="{ loading: submitting }" @click="submitReport">提交报告</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { createReport, getServiceTypeList, uploadImage } from '../../utils/api.js'
import { imgUrl } from '../../utils/api.js'
const userInfo = JSON.parse(uni.getStorageSync('petstore_user') || '{}')
const storeInfo = JSON.parse(uni.getStorageSync('petstore_store') || '{}')
const report = ref({ petName: '', serviceType: '', appointmentTime: '', before: '', after: '', remark: '' })
const reportResult = ref(null)
const serviceTypes = ref([])
const serviceTypeIdx = ref(0)
const submitting = ref(false)
const currentAppointmentId = ref(null)
const serviceTypeOptions = computed(() =>
serviceTypes.value.map(s => ({
name: `${serviceEmoji(s.name)} ${s.name}`,
value: s.name
}))
)
const serviceEmoji = (name) => {
if (!name) return ''
if (name.includes('洗澡') && name.includes('美容')) return '🛁✂️'
if (name.includes('洗澡')) return '🛁'
if (name.includes('美容')) return '✂️'
if (name.includes('剪指甲')) return '💅'
if (name.includes('驱虫')) return '🐛'
return '✨'
}
const qrUrl = computed(() => reportResult.value
? `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(reportResult.value.reportUrl)}`
: '')
const loadServiceTypes = async () => {
if (!storeInfo.id) return
const res = await getServiceTypeList(storeInfo.id)
if (res.code === 200) serviceTypes.value = res.data
}
const onServiceTypeChange = (e) => {
serviceTypeIdx.value = e.detail.value
report.value.serviceType = serviceTypes.value[e.detail.value]?.name || ''
}
const onDateChange = (e) => {
report.value.appointmentTime = e.detail.value
}
const chooseImage = async (field) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const filePath = res.tempFilePaths[0]
uni.showLoading({ title: '上传中...' })
const r = await uploadImage(filePath)
uni.hideLoading()
if (r.code === 200) {
report.value[field] = imgUrl(r.data.url)
uni.showToast({ title: '上传成功', icon: 'success' })
} else {
uni.showToast({ title: r.message || '上传失败', icon: 'none' })
}
}
})
}
const submitReport = async () => {
if (!report.value.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return }
if (!report.value.serviceType) { uni.showToast({ title: '请选择服务类型', icon: 'none' }); return }
submitting.value = true
const payload = {
appointmentId: currentAppointmentId.value || null,
userId: userInfo.id,
petName: report.value.petName,
serviceType: report.value.serviceType,
appointmentTime: report.value.appointmentTime || null,
beforePhoto: report.value.before,
afterPhoto: report.value.after,
remark: report.value.remark
}
const res = await createReport(payload)
submitting.value = false
if (res.code === 200) {
const token = res.data.reportToken
// #ifdef MP-WEIXIN
const reportUrl = `/pages/report-view/reportView?token=${token}`
// #endif
// #ifdef H5
const reportUrl = `${window.location.origin}/#/pages/report-view/reportView?token=${token}`
// #endif
reportResult.value = { token, reportUrl }
} else {
uni.showToast({ title: res.message || '提交失败', icon: 'none' })
}
}
const copyLink = () => {
uni.setClipboardData({ data: reportResult.value.reportUrl, success: () => uni.showToast({ title: '链接已复制', icon: 'success' }) })
}
const downloadQr = () => {
uni.showToast({ title: '请长按二维码图片保存', icon: 'none' })
}
const goHome = () => {
reportResult.value = null
report.value = { petName: '', serviceType: '', appointmentTime: '', before: '', after: '', remark: '' }
uni.switchTab({ url: '/pages/home/home' })
}
onMounted(async () => {
await loadServiceTypes()
const prefill = JSON.parse(uni.getStorageSync('petstore_report_prefill') || 'null')
if (prefill) {
currentAppointmentId.value = prefill.appointmentId
report.value.petName = prefill.petName || ''
report.value.serviceType = prefill.serviceType || ''
report.value.appointmentTime = prefill.appointmentTime ? prefill.appointmentTime.slice(0, 16) : ''
uni.removeStorageSync('petstore_report_prefill')
}
})
</script>
<style scoped>
.report-page { padding-bottom: 20px; background: #f5f5f5; min-height: 100vh; }
.notice-bar {
background: #f6ffed;
color: #52c41a;
font-size: 13px;
padding: 10px 16px;
border-bottom: 1px solid #b7eb8f;
}
.result-content { padding: 16px; }
.result-link-row { background: #f9f9f9; border-radius: 8px; padding: 12px; margin-bottom: 16px; }
.result-link-label { font-size: 12px; color: #999; display: block; margin-bottom: 4px; }
.result-link-val { font-size: 12px; color: #07c160; word-break: break-all; }
.qr-wrap { text-align: center; padding: 20px; background: #f9f9f9; border-radius: 12px; margin: 16px 0; }
.qr-img { width: 180px; height: 180px; }
.form-section { padding: 12px 16px 0; }
.form-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 8px; padding-left: 4px; }
.form-card { background: #fff; border-radius: 12px; padding: 4px 16px; }
.form-row {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.form-row:last-child { border-bottom: none; }
.row-label { font-size: 14px; color: #666; width: 80px; flex-shrink: 0; }
.row-input {
flex: 1;
font-size: 14px;
color: #333;
text-align: right;
}
.picker-val {
flex: 1;
font-size: 14px;
color: #333;
text-align: right;
}
.photo-row {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.photo-row:last-child { border-bottom: none; }
.photo-upload {
flex: 1;
display: flex;
justify-content: flex-end;
}
.photo-preview {
width: 80px;
height: 80px;
border-radius: 8px;
}
.photo-placeholder {
width: 80px;
height: 80px;
border: 1px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
}
.plus-icon { font-size: 24px; line-height: 1; }
.upload-tip { font-size: 11px; margin-top: 2px; }
.remark-input {
width: 100%;
padding: 8px 0;
font-size: 14px;
color: #333;
border: none;
resize: none;
box-sizing: border-box;
}
.submit-wrap { padding: 16px; }
.btn-submit {
width: 100%;
height: 46px;
background: #07c160;
color: #fff;
border: none;
border-radius: 23px;
font-size: 16px;
font-weight: 600;
}
.btn-submit.loading { opacity: 0.6; }
.btn-primary-full {
width: 100%;
height: 44px;
background: #07c160;
color: #fff;
border: none;
border-radius: 8px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-default-full {
width: 100%;
height: 44px;
background: #fff;
color: #333;
border: 1px solid #eee;
border-radius: 8px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
</style>