From 07ad42ea3b08c1283cc67e75fecb9504d0f165f5 Mon Sep 17 00:00:00 2001 From: MaDaLei Date: Sat, 18 Apr 2026 21:11:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=202026-04-18=2021:11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.production | 2 + README.md | 26 +- package-lock.json | 7 + package.json | 1 + src/components/report/ReminderCard.vue | 106 ++++++- src/components/report/ReportShareModal.vue | 337 +++++++++++++++++++++ src/pages/report-view/reportView.vue | 24 +- src/pages/report/Report.vue | 39 ++- src/utils/reportPublicUrl.js | 24 ++ src/utils/reportShare.js | 40 +++ 10 files changed, 581 insertions(+), 25 deletions(-) create mode 100644 src/components/report/ReportShareModal.vue create mode 100644 src/utils/reportPublicUrl.js create mode 100644 src/utils/reportShare.js diff --git a/.env.production b/.env.production index 6819b97..3384388 100644 --- a/.env.production +++ b/.env.production @@ -1 +1,3 @@ VITE_API_ORIGIN=https://api.s-good.com +# 部署了 report.html 的 H5 站点根(无尾斜杠),用于「生成报告」后复制链接与二维码;与 API 域名可不同 +# VITE_REPORT_PUBLIC_ORIGIN=https://www.example.com diff --git a/README.md b/README.md index 82648d8..1f60fed 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ npm run dev:h5 |------|------|------| | 登录 | /pages/login/login | Tab切换:员工登录/老板登录/注册老板/注册员工 | | 首页 | /pages/home/home | 预约列表 Tab(待开始/进行中/已完成)+ 新建预约 | -| 洗美报告 | /pages/report/report | 填写报告 + 提交后显示链接+二维码 | +| 洗美报告 | /pages/report/report | 填写报告;提交成功后弹出**发送给宠主**(复制链接 + 话术模板 + 二维码同屏) | | 报告页 | /pages/report-view/reportView | 独立访问的报告页(token=xxx) | | 我的 | /pages/mine/mine | 个人中心,老板有额外菜单 | | 员工管理 | /pages/mine/staff | 老板功能:管理员工 | @@ -104,3 +104,27 @@ npm run dev:h5 - 登录成功后,`petstore_user` 和 `petstore_store` 存入本地存储 - 用户 ID 决定能看到哪些数据(员工看自己,老板看全店) - role = 'boss' 时,显示员工管理/服务类型/店铺设置菜单 + +## 生产发版清单(小程序) + +后端发版、只更新服务器代码**不会**自动更新用户手机上的小程序界面;**必须**单独走微信侧上传与提审(或发布体验版)。 + +建议每次发版按下面核对: + +1. **本地构建小程序** + ```bash + npm run build:mp-weixin + ``` + 产物目录:`dist/build/mp-weixin/`(以构建日志为准)。 + +2. **用微信开发者工具打开上述目录**,确认能正常编译、关键页面自测通过。 + +3. **上传**:开发者工具 → 上传 → 填写版本号与项目备注(建议写本次前端变更摘要,便于回溯)。 + +4. **公众平台**:在小程序后台将本次上传设为**体验版**或提交**审核**/**发布**(按你们流程)。 + +5. **易漏项**:仅部署后端或 H5、忘记本步骤时,线上小程序仍是旧包,容易出现「代码已改、真机还是旧交互」的误判。 + +### 报告分享链接与二维码(小程序) + +「生成报告」弹层里的链接与二维码指向 **H5** `report.html?token=`,需与线上部署的静态站点一致。请在对应环境的 `.env` / `.env.production` 中配置 **`VITE_REPORT_PUBLIC_ORIGIN`**(例如 `https://www.example.com`,无尾斜杠)。未配置时仍可复制话术,但链接为空、二维码不展示,并会在弹层内提示。 diff --git a/package-lock.json b/package-lock.json index 246216c..a4eff53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@dcloudio/uni-components": "3.0.0-4060620250520001", "@dcloudio/uni-h5": "3.0.0-4060620250520001", "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001", + "uqrcodejs": "^4.0.7", "vue": "^3.4.21" }, "devDependencies": { @@ -6930,6 +6931,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uqrcodejs": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/uqrcodejs/-/uqrcodejs-4.0.7.tgz", + "integrity": "sha512-84+aZmD2godCVI+93lxE3YUAPNY8zAJvNA7xRS7R7U+q57KzMDepBSfNCwoRUhWOfR6eHFoAOcHRPwsP6ka1cA==", + "license": "Apache-2.0" + }, "node_modules/utif": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", diff --git a/package.json b/package.json index 128ecc7..ad87c80 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@dcloudio/uni-components": "3.0.0-4060620250520001", "@dcloudio/uni-h5": "3.0.0-4060620250520001", "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001", + "uqrcodejs": "^4.0.7", "vue": "^3.4.21" }, "devDependencies": { diff --git a/src/components/report/ReminderCard.vue b/src/components/report/ReminderCard.vue index e61db96..1785575 100644 --- a/src/components/report/ReminderCard.vue +++ b/src/components/report/ReminderCard.vue @@ -50,12 +50,15 @@ - 已为您设置提醒 + {{ successHeadline }} {{ formatChineseDate(savedDate) }} - 到点我们会通过微信/短信告诉你,无需惦记。 + {{ successDesc }} + + + 您此前已使用本手机号在本报告留资;本次已按最新建议日期更新提醒。 @@ -76,6 +79,8 @@ + + 若需用其他号码接收提醒,可更换后重新提交(同一报告仅保留各号码一条记录)。 @@ -97,8 +102,20 @@ const submitting = ref(false) const submitted = ref(false) const savedDate = ref('') const wechatBound = ref(false) +const repeatSubmit = ref(false) const errorMsg = ref('') +const successHeadline = computed(() => + repeatSubmit.value ? '已为您更新提醒' : '已为您设置提醒' +) +const successDesc = computed(() => + repeatSubmit.value + ? '提醒日期已同步为下方日期;到点我们会通过微信/短信联系你。' + : '到点我们会通过微信/短信告诉你,无需惦记。' +) + +const storageKey = () => `petstore_reminder_${props.token}` + const titleText = computed(() => { const name = suggestion.value.petName || props.petName || '毛孩子' const srv = friendlyServiceLabel(suggestion.value.serviceType) @@ -132,6 +149,41 @@ function formatChineseDate(dateStr) { return `${y}年${parseInt(m, 10)}月${parseInt(d, 10)}日` } +function tryRestoreFromStorage() { + try { + const raw = uni.getStorageSync(storageKey()) + if (!raw) return + const o = typeof raw === 'string' ? JSON.parse(raw) : raw + if (o && o.remindDate) { + savedDate.value = o.remindDate + wechatBound.value = !!o.wechatBound + repeatSubmit.value = false + if (o.phone && /^1[3-9]\d{9}$/.test(String(o.phone))) { + phone.value = String(o.phone).trim() + } + submitted.value = true + visible.value = true + } + } catch (e) { + // ignore + } +} + +function persistToStorage() { + try { + uni.setStorageSync( + storageKey(), + JSON.stringify({ + remindDate: savedDate.value, + wechatBound: wechatBound.value, + phone: phone.value + }) + ) + } catch (e) { + // ignore + } +} + async function loadSuggestion() { if (!props.token) return try { @@ -170,6 +222,19 @@ function goLoginWithPhone() { }) } +function changePhone() { + submitted.value = false + repeatSubmit.value = false + phone.value = '' + consent.value = false + errorMsg.value = '' + try { + uni.removeStorageSync(storageKey()) + } catch (e) { + // ignore + } +} + async function onSubmit() { if (!canSubmit.value) return errorMsg.value = '' @@ -180,7 +245,9 @@ async function onSubmit() { if (res && res.code === 200 && res.data && res.data.ok) { savedDate.value = res.data.remindDate || suggestion.value.remindDate wechatBound.value = !!res.data.wechatBound + repeatSubmit.value = !!res.data.repeatSubmit submitted.value = true + persistToStorage() } else { errorMsg.value = (res && res.message) || '提交失败,请稍后再试' } @@ -191,7 +258,10 @@ async function onSubmit() { } } -onMounted(loadSuggestion) +onMounted(async () => { + tryRestoreFromStorage() + await loadSuggestion() +}) diff --git a/src/components/report/ReportShareModal.vue b/src/components/report/ReportShareModal.vue new file mode 100644 index 0000000..f2584e5 --- /dev/null +++ b/src/components/report/ReportShareModal.vue @@ -0,0 +1,337 @@ + + + + + diff --git a/src/pages/report-view/reportView.vue b/src/pages/report-view/reportView.vue index 9d4b4bc..aa2feb4 100644 --- a/src/pages/report-view/reportView.vue +++ b/src/pages/report-view/reportView.vue @@ -223,6 +223,7 @@ import TestimonialModal from '../../components/report/TestimonialModal.vue' import ReminderCard from '../../components/report/ReminderCard.vue' import ReportHighlightBlock from '../../components/report/ReportHighlightBlock.vue' import { isLoggedIn } from '../../utils/session.js' +import { buildReportShareTitle, buildReportShareCoverUrl } from '../../utils/reportShare.js' const loading = ref(true) const loadError = ref(null) @@ -305,22 +306,15 @@ const staffInitial = computed(() => { return n.slice(0, 1) }) -const shareTitle = computed(() => { - const r = reportData.value - if (!r) return '洗护前后对比' - const store = r.store?.name || '宠伴生活馆' - const pet = r.petName || '爱宠' - if (r.highlightVideoStatus === 'done' && r.highlightVideoUrl) { - return `${pet}的服务回顾短片|${store}` - } - return `${pet}的洗护蜕变|${store}` -}) +const shareTitle = computed(() => buildReportShareTitle(reportData.value)) -const shareCoverUrl = computed(() => { - if (afterPhotos.value.length > 0) return imgUrl(afterPhotos.value[0]) - if (beforePhotos.value.length > 0) return imgUrl(beforePhotos.value[0]) - return '' -}) +const shareCoverUrl = computed(() => + buildReportShareCoverUrl(reportData.value, { + afterFirst: afterPhotos.value[0], + beforeFirst: beforePhotos.value[0], + storeLogo: reportData.value?.store?.logo + }) +) const brandBarSafe = (() => { const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20 diff --git a/src/pages/report/Report.vue b/src/pages/report/Report.vue index 6ac6ff3..380c562 100644 --- a/src/pages/report/Report.vue +++ b/src/pages/report/Report.vue @@ -179,6 +179,13 @@ + @@ -187,6 +194,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { onPageScroll } from '@dcloudio/uni-app' import { createReport, getServiceTypeList, imgUrl, BASE_URL } from '../../api/index.js' import AppIcon from '../../components/AppIcon.vue' +import ReportShareModal from '../../components/report/ReportShareModal.vue' import { getUserSession, getStoreSession } from '../../utils/session.js' import { useNavigator } from '../../composables/useNavigator.js' import { @@ -225,6 +233,9 @@ const report = ref({ const serviceTypes = ref([]) const currentAppointmentId = ref(null) const submitting = ref(false) +const shareModalOpen = ref(false) +const shareToken = ref('') +const sharePetName = ref('') const basicInfoCollapsed = ref(false) const hasScrolled = ref(false) const keyboardHeight = ref(0) @@ -688,18 +699,34 @@ const submitReport = async () => { submitting.value = false if (res.code === 200) { const token = res.data.reportToken + shareToken.value = token + sharePetName.value = report.value.petName || '' + report.value = { petName: '', serviceType: '', appointmentTime: '', beforeMedia: [], afterMedia: [], duringMedia: [], remark: '' } + currentAppointmentId.value = null + basicInfoCollapsed.value = false uni.showToast({ title: '已生成报告', icon: 'success' }) - 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) + shareModalOpen.value = true } else { uni.showToast({ title: res.message || '提交失败', icon: 'none' }) } } +const onShareModalClose = () => { + shareModalOpen.value = false + shareToken.value = '' + sharePetName.value = '' +} + +const onSharePreview = () => { + const t = shareToken.value + shareModalOpen.value = false + shareToken.value = '' + sharePetName.value = '' + if (t) { + uni.navigateTo({ url: `/pages/report-view/reportView?token=${encodeURIComponent(t)}` }) + } +} + const goBack = () => { const pages = getCurrentPages() if (pages.length > 1) { diff --git a/src/utils/reportPublicUrl.js b/src/utils/reportPublicUrl.js new file mode 100644 index 0000000..ac4570b --- /dev/null +++ b/src/utils/reportPublicUrl.js @@ -0,0 +1,24 @@ +/** + * 宠主打开的 H5 报告页完整 URL(report.html?token=),用于复制 / 二维码。 + * 请在生产环境配置 VITE_REPORT_PUBLIC_ORIGIN(部署了 report.html 的站点根,无末尾斜杠)。 + * 未配置时小程序内返回空字符串,弹层会提示配置。 + */ +export function getReportPublicPageOrigin() { + const fromEnv = import.meta.env.VITE_REPORT_PUBLIC_ORIGIN + if (fromEnv && String(fromEnv).trim()) { + return String(fromEnv).replace(/\/$/, '') + } + // #ifdef H5 + if (typeof window !== 'undefined' && window.location?.origin) { + return window.location.origin + } + // #endif + return '' +} + +export function buildReportH5FullUrl(token) { + if (!token) return '' + const origin = getReportPublicPageOrigin() + if (!origin) return '' + return `${origin}/report.html?token=${encodeURIComponent(token)}` +} diff --git a/src/utils/reportShare.js b/src/utils/reportShare.js new file mode 100644 index 0000000..6b7fb6d --- /dev/null +++ b/src/utils/reportShare.js @@ -0,0 +1,40 @@ +import { imgUrl } from '../api/index.js' + +/** + * 小程序转发标题:宠物名·服务|短片/报告|门店(与《本期主线》传播素材统一) + */ +export function buildReportShareTitle(report) { + if (!report) return '洗护前后对比' + const store = report.store?.name || '宠伴生活馆' + const pet = report.petName || '爱宠' + const svc = (report.serviceType || '').trim() + const svcPart = svc ? `·${svc}` : '' + if (report.highlightVideoStatus === 'done' && report.highlightVideoUrl) { + return `${pet}${svcPart}|服务回顾短片|${store}` + } + return `${pet}${svcPart}|洗护服务报告|${store}` +} + +/** + * 转发封面图 URL 优先级: + * 成片成功:highlightShareCoverUrl(若后端提供成片封面帧)> 服务后首图 > 门店 Logo + * 无成片:服务后首图 > 服务前首图 > 门店 Logo + */ +export function buildReportShareCoverUrl(report, paths) { + const afterFirst = paths?.afterFirst + const beforeFirst = paths?.beforeFirst + const storeLogo = paths?.storeLogo + const hlCover = report?.highlightShareCoverUrl || report?.highlightPosterUrl + const highlightDone = report?.highlightVideoStatus === 'done' && report?.highlightVideoUrl + + if (highlightDone) { + if (hlCover) return imgUrl(hlCover) + if (afterFirst) return imgUrl(afterFirst) + if (storeLogo) return imgUrl(storeLogo) + return '' + } + if (afterFirst) return imgUrl(afterFirst) + if (beforeFirst) return imgUrl(beforeFirst) + if (storeLogo) return imgUrl(storeLogo) + return '' +}