deploy: update logo and latest build
This commit is contained in:
parent
b486dd313a
commit
2a4545bc00
@ -1,7 +1,7 @@
|
|||||||
# petstore-frontend
|
# petstore-frontend
|
||||||
|
|
||||||
#### Description
|
#### Description
|
||||||
宠伴生活馆
|
宠小它
|
||||||
|
|
||||||
#### Software Architecture
|
#### Software Architecture
|
||||||
Software architecture description
|
Software architecture description
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
# 宠伴生活馆 - UniApp 项目
|
# 宠小它 - UniApp 项目
|
||||||
|
|
||||||
|
> 品牌:宠小它 · 用心宠小它,温暖伴一生 · 智慧宠物门店服务系统
|
||||||
|
|
||||||
从 H5 (Vite+Vue3+Vant) 迁移到 UniApp+Vue3,用于生成微信小程序。
|
从 H5 (Vite+Vue3+Vant) 迁移到 UniApp+Vue3,用于生成微信小程序。
|
||||||
|
|
||||||
@ -128,3 +130,8 @@ npm run dev:h5
|
|||||||
### 报告分享链接与二维码(小程序)
|
### 报告分享链接与二维码(小程序)
|
||||||
|
|
||||||
「生成报告」弹层里的链接与二维码指向 **H5** `report.html?token=`,需与线上部署的静态站点一致。请在对应环境的 `.env` / `.env.production` 中配置 **`VITE_REPORT_PUBLIC_ORIGIN`**(例如 `https://www.example.com`,无尾斜杠)。未配置时仍可复制话术,但链接为空、二维码不展示,并会在弹层内提示。
|
「生成报告」弹层里的链接与二维码指向 **H5** `report.html?token=`,需与线上部署的静态站点一致。请在对应环境的 `.env` / `.env.production` 中配置 **`VITE_REPORT_PUBLIC_ORIGIN`**(例如 `https://www.example.com`,无尾斜杠)。未配置时仍可复制话术,但链接为空、二维码不展示,并会在弹层内提示。
|
||||||
|
|
||||||
|
### 宠主预约草稿(P0 C3)
|
||||||
|
|
||||||
|
当前实现:**已登录**用户在 `CustAppointmentCreate` 填写的内容会 debounce 写入本地 `uni.storage`(7 天有效),离开页/重进可恢复;提交成功会清除草稿。
|
||||||
|
产品文档中的 **「未登录可先填单、提交时再登录并恢复草稿」** 需同时将「新建预约」页加入公开路由(如 `App.vue` 的 `isPublicRoute`)并在登录回跳后合并草稿;若未做公开路由,未登录用户仍会在进入预约页前被拦截到登录。
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>宠伴生活馆</title>
|
<title>宠小它</title>
|
||||||
<meta name="description" content="宠伴生活馆 - 宠物服务,让爱更专业">
|
<meta name="description" content="宠小它|用心宠小它,温暖伴一生。智慧宠物门店服务系统">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>登录 - 宠伴生活馆</title>
|
<title>登录 - 宠小它</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||||
@ -45,8 +45,8 @@
|
|||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<div class="logo-text">宠伴生活馆</div>
|
<div class="logo-text">宠小它</div>
|
||||||
<div class="logo-sub">宠物服务,让爱更专业</div>
|
<div class="logo-sub">用心宠小它,温暖伴一生</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-card">
|
<div class="form-card">
|
||||||
@ -150,7 +150,7 @@
|
|||||||
// 模拟微信授权,演示用
|
// 模拟微信授权,演示用
|
||||||
const demoUser = { id: 99, name: '微信用户', phone: '', role: 'staff' }
|
const demoUser = { id: 99, name: '微信用户', phone: '', role: 'staff' }
|
||||||
localStorage.setItem('petstore_user', JSON.stringify(demoUser))
|
localStorage.setItem('petstore_user', JSON.stringify(demoUser))
|
||||||
localStorage.setItem('petstore_store', JSON.stringify({ id: 2, name: '宠伴生活馆测试店' }))
|
localStorage.setItem('petstore_store', JSON.stringify({ id: 2, name: '宠小它测试店' }))
|
||||||
window.location.href = 'index.html'
|
window.location.href = 'index.html'
|
||||||
}, 1500)
|
}, 1500)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>入驻申请 - 宠伴生活馆</title>
|
<title>入驻申请 - 宠小它</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||||
@ -84,7 +84,7 @@
|
|||||||
|
|
||||||
<div class="success-card">
|
<div class="success-card">
|
||||||
<div class="success-icon">🎉</div>
|
<div class="success-icon">🎉</div>
|
||||||
<div class="success-title">欢迎加入宠伴生活馆</div>
|
<div class="success-title">欢迎加入宠小它</div>
|
||||||
<div class="success-sub">店铺创建成功,您可以开始管理您的宠物店了</div>
|
<div class="success-sub">店铺创建成功,您可以开始管理您的宠物店了</div>
|
||||||
|
|
||||||
<div class="success-info">
|
<div class="success-info">
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>员工注册 - 宠伴生活馆</title>
|
<title>员工注册 - 宠小它</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>服务报告 - 宠伴生活馆</title>
|
<title>服务报告 - 宠小它</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@ -98,6 +98,10 @@ export const postReportHighlightStart = (data) => post('/report/highlight/start'
|
|||||||
// 获取报告(通过token)
|
// 获取报告(通过token)
|
||||||
export const getReportByToken = (token) => get('/report/get', { token })
|
export const getReportByToken = (token) => get('/report/get', { token })
|
||||||
|
|
||||||
|
/** P0:报告打开埋点(服务端记日志,可对接日志平台) */
|
||||||
|
export const postReportOpenTrack = (token, visitType) =>
|
||||||
|
post('/report/open-track', { token, visitType: visitType || 'unknown' })
|
||||||
|
|
||||||
// 报告列表
|
// 报告列表
|
||||||
export const getReportList = (params) => get('/report/list', params)
|
export const getReportList = (params) => get('/report/list', params)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/* ============================================================
|
/* ============================================================
|
||||||
宠伴生活馆 — 设计系统 v2 · 简约 · 温暖 · 专业
|
宠小它 — 设计系统 v2 · 简约 · 温暖 · 专业
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@ -62,7 +62,11 @@ import { buildReportH5FullUrl } from '../../utils/reportPublicUrl.js'
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: { type: Boolean, default: false },
|
show: { type: Boolean, default: false },
|
||||||
reportToken: { type: String, default: '' },
|
reportToken: { type: String, default: '' },
|
||||||
petName: { type: String, default: '' }
|
petName: { type: String, default: '' },
|
||||||
|
/** 门店名(话术变量) */
|
||||||
|
storeName: { type: String, default: '' },
|
||||||
|
/** 服务类型(话术变量) */
|
||||||
|
serviceType: { type: String, default: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'preview'])
|
const emit = defineEmits(['close', 'preview'])
|
||||||
@ -88,14 +92,17 @@ const qrGridStyle = computed(() => {
|
|||||||
const displayUrl = computed(() => buildReportH5FullUrl(props.reportToken))
|
const displayUrl = computed(() => buildReportH5FullUrl(props.reportToken))
|
||||||
|
|
||||||
const petLabel = computed(() => (props.petName && String(props.petName).trim()) || '毛孩子')
|
const petLabel = computed(() => (props.petName && String(props.petName).trim()) || '毛孩子')
|
||||||
|
const storeLabel = computed(() => (props.storeName && String(props.storeName).trim()) || '门店')
|
||||||
|
const svcLabel = computed(() => (props.serviceType && String(props.serviceType).trim()) || '洗护服务')
|
||||||
|
|
||||||
const templateLines = computed(() => {
|
const templateLines = computed(() => {
|
||||||
const link = displayUrl.value || '(请配置域名后显示链接)'
|
const link = displayUrl.value || '(请配置域名后显示链接)'
|
||||||
const name = petLabel.value
|
const pet = petLabel.value
|
||||||
|
const store = storeLabel.value
|
||||||
|
const svc = svcLabel.value
|
||||||
return [
|
return [
|
||||||
`【${name}】的洗护服务报告已生成,点链接查看:${link}`,
|
`您好,【${pet}】的「${svc}」报告已生成,门店:${store}。点击查看:${link}`,
|
||||||
`您好,这是「${name}」本次服务报告,欢迎查阅:${link}`,
|
`【${store}】${pet} 本次「${svc}」服务报告已完成,欢迎查阅:${link}`
|
||||||
`毛孩子的服务记录在这里,方便时打开看看:${link}`
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "宠伴生活馆",
|
"name": "宠小它",
|
||||||
"appid": "wx8ca2dfa89af72edf",
|
"appid": "wx8ca2dfa89af72edf",
|
||||||
"description": "宠物服务小程序",
|
"description": "宠小它|智慧宠物门店服务系统",
|
||||||
"versionName": "1.0.0",
|
"versionName": "1.0.0",
|
||||||
"versionCode": "100",
|
"versionCode": "100",
|
||||||
"transformPx": false,
|
"transformPx": false,
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTextStyle": "white",
|
"navigationBarTextStyle": "white",
|
||||||
"navigationBarTitleText": "宠伴生活馆",
|
"navigationBarTitleText": "宠小它",
|
||||||
"navigationBarBackgroundColor": "#2db96d",
|
"navigationBarBackgroundColor": "#2db96d",
|
||||||
"backgroundColor": "#fafaf8"
|
"backgroundColor": "#fafaf8"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,8 +118,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
import {
|
import {
|
||||||
createAppointment,
|
createAppointment,
|
||||||
getServiceTypeList,
|
getServiceTypeList,
|
||||||
@ -580,6 +580,9 @@ const confirmNewAppt = async () => {
|
|||||||
try {
|
try {
|
||||||
uni.setStorageSync(lastServiceStorageKey(storeId), a.serviceType)
|
uni.setStorageSync(lastServiceStorageKey(storeId), a.serviceType)
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync(apptDraftStorageKey())
|
||||||
|
} catch (_) {}
|
||||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||||
setTimeout(() => uni.navigateBack(), 600)
|
setTimeout(() => uni.navigateBack(), 600)
|
||||||
} else {
|
} else {
|
||||||
@ -601,6 +604,77 @@ const loadMyPets = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** P0 C3:意外退出 / 返回栈恢复草稿(7 天);与「未登录可填单」全量能力见 README) */
|
||||||
|
const apptDraftStorageKey = () => `petstore_cust_appt_draft_v1_${userInfo.id || '0'}`
|
||||||
|
|
||||||
|
const saveApptDraft = () => {
|
||||||
|
if (!userInfo?.id) return
|
||||||
|
try {
|
||||||
|
const row = orderedStores.value[selectedStoreIndex.value]
|
||||||
|
uni.setStorageSync(
|
||||||
|
apptDraftStorageKey(),
|
||||||
|
JSON.stringify({
|
||||||
|
v: 1,
|
||||||
|
at: Date.now(),
|
||||||
|
petName: newAppt.value.petName,
|
||||||
|
petType: newAppt.value.petType,
|
||||||
|
serviceType: newAppt.value.serviceType,
|
||||||
|
appointmentTime: newAppt.value.appointmentTime,
|
||||||
|
remark: newAppt.value.remark,
|
||||||
|
selectedDateStr: selectedDateStr.value,
|
||||||
|
storeIndex: selectedStoreIndex.value,
|
||||||
|
storeId: row?.id
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreApptDraft = () => {
|
||||||
|
if (!userInfo?.id) return
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(apptDraftStorageKey())
|
||||||
|
if (!raw) return
|
||||||
|
const d = JSON.parse(raw)
|
||||||
|
if (!d || d.v !== 1) return
|
||||||
|
if (Date.now() - (d.at || 0) > 7 * 86400000) {
|
||||||
|
uni.removeStorageSync(apptDraftStorageKey())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof d.remark === 'string') newAppt.value.remark = d.remark
|
||||||
|
if (d.serviceType) newAppt.value.serviceType = d.serviceType
|
||||||
|
if (d.appointmentTime) newAppt.value.appointmentTime = d.appointmentTime
|
||||||
|
if (d.petName) newAppt.value.petName = d.petName
|
||||||
|
if (d.petType) newAppt.value.petType = d.petType
|
||||||
|
if (typeof d.selectedDateStr === 'string' && d.selectedDateStr) selectedDateStr.value = d.selectedDateStr
|
||||||
|
if (typeof d.storeIndex === 'number' && orderedStores.value[d.storeIndex]) {
|
||||||
|
selectedStoreIndex.value = d.storeIndex
|
||||||
|
applySelectedStoreToSession()
|
||||||
|
} else if (d.storeId && orderedStores.value.some((x) => String(x.id) === String(d.storeId))) {
|
||||||
|
selectedStoreIndex.value = orderedStores.value.findIndex((x) => String(x.id) === String(d.storeId))
|
||||||
|
applySelectedStoreToSession()
|
||||||
|
}
|
||||||
|
selectedPetId.value = null
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let apptDraftTimer = null
|
||||||
|
watch(
|
||||||
|
() =>
|
||||||
|
JSON.stringify({
|
||||||
|
n: newAppt.value,
|
||||||
|
si: selectedStoreIndex.value,
|
||||||
|
ds: selectedDateStr.value
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
if (apptDraftTimer) clearTimeout(apptDraftTimer)
|
||||||
|
apptDraftTimer = setTimeout(saveApptDraft, 650)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
saveApptDraft()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
selectedDateStr.value = ymdLocal()
|
selectedDateStr.value = ymdLocal()
|
||||||
await initStoresAndLocation()
|
await initStoresAndLocation()
|
||||||
@ -608,6 +682,10 @@ onMounted(async () => {
|
|||||||
await loadServiceTypesForSelected()
|
await loadServiceTypesForSelected()
|
||||||
}
|
}
|
||||||
await loadMyPets()
|
await loadMyPets()
|
||||||
|
restoreApptDraft()
|
||||||
|
if (orderedStores.value.length) {
|
||||||
|
await loadServiceTypesForSelected()
|
||||||
|
}
|
||||||
await loadAvailableSlots()
|
await loadAvailableSlots()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<!-- C端:服务预约列表 + 加号新建 -->
|
<!-- C端:服务预约列表 + 加号新建 -->
|
||||||
<view v-if="userInfo.role === 'customer'" class="c-home">
|
<view v-if="userInfo.role === 'customer'" class="c-home">
|
||||||
<view class="home-nav home-nav--center nav-gradient" :style="navSafeStyle">
|
<view class="home-nav home-nav--center nav-gradient" :style="navSafeStyle">
|
||||||
<text class="nav-title">{{ storeInfo.name || '宠伴生活馆' }}</text>
|
<text class="nav-title">{{ storeInfo.name || '宠小它' }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="c-hero">
|
<view class="c-hero">
|
||||||
@ -54,6 +54,11 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="listMode === 'tab' && currentStatus === 'done'" class="done-filter-row">
|
||||||
|
<text class="done-filter-label">包含已取消</text>
|
||||||
|
<switch :checked="includeCancelledInDone" color="#2db96d" @change="onIncludeCancelledChange" />
|
||||||
|
</view>
|
||||||
|
|
||||||
<AppPageState
|
<AppPageState
|
||||||
class="home-list-state"
|
class="home-list-state"
|
||||||
:loading="appointmentsLoading"
|
:loading="appointmentsLoading"
|
||||||
@ -64,7 +69,9 @@
|
|||||||
@retry="fetchAppointments"
|
@retry="fetchAppointments"
|
||||||
>
|
>
|
||||||
<template v-if="custEmptyGuidance" #empty>
|
<template v-if="custEmptyGuidance" #empty>
|
||||||
<text class="c-empty-emoji">📋</text>
|
<view class="c-empty-icon-wrap">
|
||||||
|
<AppIcon name="report" :size="44" color="#a8a89f" />
|
||||||
|
</view>
|
||||||
<text class="c-empty-title">当前没有可展示的预约</text>
|
<text class="c-empty-title">当前没有可展示的预约</text>
|
||||||
<text class="c-empty-hint">请使用上方「预约服务」创建;提交后会在本列表显示进度</text>
|
<text class="c-empty-hint">请使用上方「预约服务」创建;提交后会在本列表显示进度</text>
|
||||||
</template>
|
</template>
|
||||||
@ -94,7 +101,7 @@
|
|||||||
<!-- B端:看板(新建预约改为右下角悬浮按钮,避免与小程序胶囊重叠) -->
|
<!-- B端:看板(新建预约改为右下角悬浮按钮,避免与小程序胶囊重叠) -->
|
||||||
<view v-else>
|
<view v-else>
|
||||||
<view class="home-nav home-nav--center nav-gradient" :style="navSafeStyle">
|
<view class="home-nav home-nav--center nav-gradient" :style="navSafeStyle">
|
||||||
<text class="nav-title">{{ storeInfo.name || '宠伴生活馆' }}</text>
|
<text class="nav-title">{{ storeInfo.name || '宠小它' }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="home-dash">
|
<view class="home-dash">
|
||||||
@ -142,6 +149,11 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="listMode === 'tab' && currentStatus === 'done'" class="done-filter-row">
|
||||||
|
<text class="done-filter-label">包含已取消</text>
|
||||||
|
<switch :checked="includeCancelledInDone" color="#2db96d" @change="onIncludeCancelledChange" />
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<AppPageState
|
<AppPageState
|
||||||
class="home-list-state"
|
class="home-list-state"
|
||||||
@ -264,6 +276,7 @@ import {
|
|||||||
} from '../../api/index.js'
|
} from '../../api/index.js'
|
||||||
import TabBar from '../../components/TabBar.vue'
|
import TabBar from '../../components/TabBar.vue'
|
||||||
import AppPageState from '../../components/AppPageState.vue'
|
import AppPageState from '../../components/AppPageState.vue'
|
||||||
|
import AppIcon from '../../components/AppIcon.vue'
|
||||||
import { messageFromApi, NETWORK_ERROR_MSG } from '../../utils/apiResult.js'
|
import { messageFromApi, NETWORK_ERROR_MSG } from '../../utils/apiResult.js'
|
||||||
import { getUserSession, getStoreSession } from '../../utils/session.js'
|
import { getUserSession, getStoreSession } from '../../utils/session.js'
|
||||||
import { useNavigator } from '../../composables/useNavigator.js'
|
import { useNavigator } from '../../composables/useNavigator.js'
|
||||||
@ -308,6 +321,13 @@ const navSafeStyle = (() => {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const currentStatus = ref(userInfo.role === 'customer' ? 'new' : 'doing')
|
const currentStatus = ref(userInfo.role === 'customer' ? 'new' : 'doing')
|
||||||
|
/** 「已完成」Tab:默认不含已取消;打开开关后含 cancel(P0 B1) */
|
||||||
|
const includeCancelledInDone = ref(false)
|
||||||
|
|
||||||
|
const onIncludeCancelledChange = (e) => {
|
||||||
|
const v = e?.detail?.value
|
||||||
|
includeCancelledInDone.value = v === true || v === 'true'
|
||||||
|
}
|
||||||
const statusTabs = [
|
const statusTabs = [
|
||||||
{ title: '待开始', name: 'new' },
|
{ title: '待开始', name: 'new' },
|
||||||
{ title: '进行中', name: 'doing' },
|
{ title: '进行中', name: 'doing' },
|
||||||
@ -397,7 +417,10 @@ const staffSelectedSlotRangeLabel = computed(() =>
|
|||||||
const filteredOrders = computed(() => orders.value.filter(o => {
|
const filteredOrders = computed(() => orders.value.filter(o => {
|
||||||
if (currentStatus.value === 'new') return o.status === 'new'
|
if (currentStatus.value === 'new') return o.status === 'new'
|
||||||
if (currentStatus.value === 'doing') return o.status === 'doing'
|
if (currentStatus.value === 'doing') return o.status === 'doing'
|
||||||
if (currentStatus.value === 'done') return o.status === 'done' || o.status === 'cancel'
|
if (currentStatus.value === 'done') {
|
||||||
|
if (includeCancelledInDone.value) return o.status === 'done' || o.status === 'cancel'
|
||||||
|
return o.status === 'done'
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -422,6 +445,7 @@ const selectTab = (name) => {
|
|||||||
currentStatus.value = name
|
currentStatus.value = name
|
||||||
listMode.value = 'tab'
|
listMode.value = 'tab'
|
||||||
activeDashKey.value = null
|
activeDashKey.value = null
|
||||||
|
if (name !== 'done') includeCancelledInDone.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDashCardClick = (key) => {
|
const onDashCardClick = (key) => {
|
||||||
@ -796,12 +820,22 @@ onShow(() => {
|
|||||||
color: #3d5a4a;
|
color: #3d5a4a;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
.c-empty-emoji {
|
.c-empty-icon-wrap {
|
||||||
display: block;
|
display: flex;
|
||||||
font-size: 36px;
|
justify-content: center;
|
||||||
text-align: center;
|
margin-bottom: 12px;
|
||||||
margin-bottom: 10px;
|
}
|
||||||
opacity: 0.9;
|
.done-filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 8px 16px 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.done-filter-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b6b65;
|
||||||
}
|
}
|
||||||
.c-empty-title {
|
.c-empty-title {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="login-page">
|
<view class="login-page">
|
||||||
<view class="login-brand">
|
<view class="login-brand">
|
||||||
<text class="brand-name">宠伴生活馆</text>
|
<text class="brand-name">宠小它</text>
|
||||||
<text class="brand-sub">宠物服务,让爱更专业</text>
|
<text class="brand-sub">用心宠小它,温暖伴一生</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 登录 -->
|
<!-- 登录 -->
|
||||||
|
|||||||
@ -15,15 +15,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 店铺管理(老板) -->
|
<!-- 店铺管理(老板):分组见 P0 B4 -->
|
||||||
<view v-if="userInfo.role === 'boss'" class="menu-group">
|
<view v-if="userInfo.role === 'boss'" class="menu-group">
|
||||||
<text class="menu-label">店铺管理</text>
|
<text class="menu-label module-title">店铺与经营</text>
|
||||||
<view class="menu-card">
|
<view class="menu-card">
|
||||||
<view class="menu-item" @click="goStaffPage">
|
|
||||||
<view class="menu-icon-wrap"><AppIcon name="staff" :size="16" color="#666660" /></view>
|
|
||||||
<text class="menu-text">员工管理</text>
|
|
||||||
<text class="menu-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="menu-item" @click="goServiceTypePage">
|
<view class="menu-item" @click="goServiceTypePage">
|
||||||
<view class="menu-icon-wrap"><AppIcon name="service" :size="16" color="#666660" /></view>
|
<view class="menu-icon-wrap"><AppIcon name="service" :size="16" color="#666660" /></view>
|
||||||
<text class="menu-text">服务类型</text>
|
<text class="menu-text">服务类型</text>
|
||||||
@ -47,6 +42,17 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="userInfo.role === 'boss'" class="menu-group">
|
||||||
|
<text class="menu-label module-title">团队与账号</text>
|
||||||
|
<view class="menu-card">
|
||||||
|
<view class="menu-item" @click="goStaffPage">
|
||||||
|
<view class="menu-icon-wrap"><AppIcon name="staff" :size="16" color="#666660" /></view>
|
||||||
|
<text class="menu-text">员工管理</text>
|
||||||
|
<text class="menu-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 员工:仅门店设置(含营业时间) -->
|
<!-- 员工:仅门店设置(含营业时间) -->
|
||||||
<view v-else-if="userInfo.role === 'staff'" class="menu-group">
|
<view v-else-if="userInfo.role === 'staff'" class="menu-group">
|
||||||
<text class="menu-label">门店</text>
|
<text class="menu-label">门店</text>
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部信息 -->
|
<!-- 底部信息 -->
|
||||||
<div class="footer-tip">宠伴生活馆 · {{ editionLabel }}</div>
|
<div class="footer-tip">宠小它 · {{ editionLabel }}</div>
|
||||||
|
|
||||||
<!-- 修改姓名弹窗 -->
|
<!-- 修改姓名弹窗 -->
|
||||||
<view v-if="showEditName" class="popup-mask profile-popup-mask" @click="showEditName = false">
|
<view v-if="showEditName" class="popup-mask profile-popup-mask" @click="showEditName = false">
|
||||||
|
|||||||
@ -110,7 +110,7 @@
|
|||||||
<view class="store-actions-hint">保存后立即生效,并写入本地缓存。</view>
|
<view class="store-actions-hint">保存后立即生效,并写入本地缓存。</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="store-footer-tip">宠伴生活馆 · 商家版</view>
|
<view class="store-footer-tip">宠小它 · 商家版</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<view class="brand-text-col">
|
<view class="brand-text-col">
|
||||||
<text class="brand-name-line">{{ reportData.store?.name || '宠伴生活馆' }}</text>
|
<text class="brand-name-line">{{ reportData.store?.name || '宠小它' }}</text>
|
||||||
<text class="brand-sub-line">服务报告 · {{ formatTime(reportData.appointmentTime) }}</text>
|
<text class="brand-sub-line">服务报告 · {{ formatTime(reportData.appointmentTime) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -34,6 +34,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="report-privacy-bar">
|
||||||
|
<text class="report-privacy-bar-text">本页含宠物服务影像,请勿公开转发或发群</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 技师情感卡片(与 H5 对齐) -->
|
<!-- 技师情感卡片(与 H5 对齐) -->
|
||||||
<view v-if="reportData.staffName || staffAvatarDisplay" class="staff-card-mp">
|
<view v-if="reportData.staffName || staffAvatarDisplay" class="staff-card-mp">
|
||||||
<image
|
<image
|
||||||
@ -185,8 +189,8 @@
|
|||||||
<!-- 底部品牌 -->
|
<!-- 底部品牌 -->
|
||||||
<view class="footer-brand">
|
<view class="footer-brand">
|
||||||
<text class="footer-line"></text>
|
<text class="footer-line"></text>
|
||||||
<text class="footer-name">{{ reportData.store?.name || '宠伴生活馆' }}</text>
|
<text class="footer-name">{{ reportData.store?.name || '宠小它' }}</text>
|
||||||
<text class="footer-sub">宠物服务 · 让爱更专业</text>
|
<text class="footer-sub">用心宠小它,温暖伴一生</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</AppPageState>
|
</AppPageState>
|
||||||
@ -212,6 +216,7 @@
|
|||||||
import { ref, computed, onMounted, getCurrentInstance, nextTick } from 'vue'
|
import { ref, computed, onMounted, getCurrentInstance, nextTick } from 'vue'
|
||||||
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import { getReportByToken, imgUrl, submitReportTestimonial } from '../../api/index.js'
|
import { getReportByToken, imgUrl, submitReportTestimonial } from '../../api/index.js'
|
||||||
|
import { trackReportOpen, getReportByTokenWithTimeout } from '../../utils/reportOpenTrack.js'
|
||||||
import { formatDateTimeYMDHM } from '../../utils/datetime.js'
|
import { formatDateTimeYMDHM } from '../../utils/datetime.js'
|
||||||
import { drawReportPoster, POSTER_W, computePosterLogicalHeight } from '../../utils/reportPosterDraw.js'
|
import { drawReportPoster, POSTER_W, computePosterLogicalHeight } from '../../utils/reportPosterDraw.js'
|
||||||
import AppIcon from '../../components/AppIcon.vue'
|
import AppIcon from '../../components/AppIcon.vue'
|
||||||
@ -335,9 +340,10 @@ const loadReport = async () => {
|
|||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getReportByToken(token)
|
const res = await getReportByTokenWithTimeout(getReportByToken, token)
|
||||||
if (res && res.code === 200 && res.data) {
|
if (res && res.code === 200 && res.data) {
|
||||||
reportData.value = res.data
|
reportData.value = res.data
|
||||||
|
trackReportOpen(token)
|
||||||
} else if (res && res.code === 404) {
|
} else if (res && res.code === 404) {
|
||||||
reportData.value = null
|
reportData.value = null
|
||||||
notFound.value = true
|
notFound.value = true
|
||||||
@ -347,7 +353,8 @@ const loadReport = async () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportData.value = null
|
reportData.value = null
|
||||||
loadError.value = NETWORK_ERROR_MSG
|
loadError.value =
|
||||||
|
e && e.message === 'TIMEOUT' ? '加载超时,请检查网络后重试' : NETWORK_ERROR_MSG
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -382,7 +389,7 @@ const navToStore = () => {
|
|||||||
uni.openLocation({
|
uni.openLocation({
|
||||||
latitude: Number(store.latitude),
|
latitude: Number(store.latitude),
|
||||||
longitude: Number(store.longitude),
|
longitude: Number(store.longitude),
|
||||||
name: store.name || '宠伴生活馆',
|
name: store.name || '宠小它',
|
||||||
address: store.address || ''
|
address: store.address || ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1047,4 +1054,17 @@ onMounted(() => loadReport())
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-privacy-bar {
|
||||||
|
margin: 0 16px 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.report-privacy-bar-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -167,6 +167,9 @@
|
|||||||
:class="{ 'submit-fixed--scrolled': hasScrolled }"
|
:class="{ 'submit-fixed--scrolled': hasScrolled }"
|
||||||
:style="submitFixedStyle"
|
:style="submitFixedStyle"
|
||||||
>
|
>
|
||||||
|
<view class="report-lock-hint">
|
||||||
|
<text>提交后不可修改,请确认照片与备注无误。</text>
|
||||||
|
</view>
|
||||||
<button class="app-btn-main app-btn-primary submit-btn--fixed" :disabled="submitting" @click="submitReport">
|
<button class="app-btn-main app-btn-primary submit-btn--fixed" :disabled="submitting" @click="submitReport">
|
||||||
{{ submitting ? '提交中…' : '生成报告' }}
|
{{ submitting ? '提交中…' : '生成报告' }}
|
||||||
</button>
|
</button>
|
||||||
@ -183,6 +186,8 @@
|
|||||||
:show="shareModalOpen"
|
:show="shareModalOpen"
|
||||||
:report-token="shareToken"
|
:report-token="shareToken"
|
||||||
:pet-name="sharePetName"
|
:pet-name="sharePetName"
|
||||||
|
:store-name="shareStoreName"
|
||||||
|
:service-type="shareServiceType"
|
||||||
@close="onShareModalClose"
|
@close="onShareModalClose"
|
||||||
@preview="onSharePreview"
|
@preview="onSharePreview"
|
||||||
/>
|
/>
|
||||||
@ -236,6 +241,8 @@ const submitting = ref(false)
|
|||||||
const shareModalOpen = ref(false)
|
const shareModalOpen = ref(false)
|
||||||
const shareToken = ref('')
|
const shareToken = ref('')
|
||||||
const sharePetName = ref('')
|
const sharePetName = ref('')
|
||||||
|
const shareStoreName = ref('')
|
||||||
|
const shareServiceType = ref('')
|
||||||
const basicInfoCollapsed = ref(false)
|
const basicInfoCollapsed = ref(false)
|
||||||
const hasScrolled = ref(false)
|
const hasScrolled = ref(false)
|
||||||
const keyboardHeight = ref(0)
|
const keyboardHeight = ref(0)
|
||||||
@ -701,6 +708,8 @@ const submitReport = async () => {
|
|||||||
const token = res.data.reportToken
|
const token = res.data.reportToken
|
||||||
shareToken.value = token
|
shareToken.value = token
|
||||||
sharePetName.value = report.value.petName || ''
|
sharePetName.value = report.value.petName || ''
|
||||||
|
shareStoreName.value = storeInfo?.name || ''
|
||||||
|
shareServiceType.value = report.value.serviceType || ''
|
||||||
report.value = { petName: '', serviceType: '', appointmentTime: '', beforeMedia: [], afterMedia: [], duringMedia: [], remark: '' }
|
report.value = { petName: '', serviceType: '', appointmentTime: '', beforeMedia: [], afterMedia: [], duringMedia: [], remark: '' }
|
||||||
currentAppointmentId.value = null
|
currentAppointmentId.value = null
|
||||||
basicInfoCollapsed.value = false
|
basicInfoCollapsed.value = false
|
||||||
@ -715,6 +724,8 @@ const onShareModalClose = () => {
|
|||||||
shareModalOpen.value = false
|
shareModalOpen.value = false
|
||||||
shareToken.value = ''
|
shareToken.value = ''
|
||||||
sharePetName.value = ''
|
sharePetName.value = ''
|
||||||
|
shareStoreName.value = ''
|
||||||
|
shareServiceType.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSharePreview = () => {
|
const onSharePreview = () => {
|
||||||
@ -722,6 +733,8 @@ const onSharePreview = () => {
|
|||||||
shareModalOpen.value = false
|
shareModalOpen.value = false
|
||||||
shareToken.value = ''
|
shareToken.value = ''
|
||||||
sharePetName.value = ''
|
sharePetName.value = ''
|
||||||
|
shareStoreName.value = ''
|
||||||
|
shareServiceType.value = ''
|
||||||
if (t) {
|
if (t) {
|
||||||
uni.navigateTo({ url: `/pages/report-view/reportView?token=${encodeURIComponent(t)}` })
|
uni.navigateTo({ url: `/pages/report-view/reportView?token=${encodeURIComponent(t)}` })
|
||||||
}
|
}
|
||||||
@ -1014,6 +1027,14 @@ const onRemarkBlur = () => {
|
|||||||
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
|
||||||
background: rgba(250, 250, 248, 0.985);
|
background: rgba(250, 250, 248, 0.985);
|
||||||
}
|
}
|
||||||
|
.report-lock-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a84;
|
||||||
|
line-height: 1.45;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
.submit-btn--fixed {
|
.submit-btn--fixed {
|
||||||
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/utils/brand.js
Normal file
4
src/utils/brand.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** 品牌与 Slogan(单一来源,供页面与分享兜底引用) */
|
||||||
|
export const BRAND_NAME = '宠小它'
|
||||||
|
export const BRAND_SLOGAN_MAIN = '用心宠小它,温暖伴一生'
|
||||||
|
export const BRAND_SLOGAN_SUB = '智慧宠物门店服务系统'
|
||||||
39
src/utils/reportOpenTrack.js
Normal file
39
src/utils/reportOpenTrack.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { postReportOpenTrack } from '../api/index.js'
|
||||||
|
|
||||||
|
const STORAGE_PREFIX = 'petstore_report_seen_'
|
||||||
|
|
||||||
|
function storageKeyForToken(token) {
|
||||||
|
let h = 0
|
||||||
|
const s = String(token || '')
|
||||||
|
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0
|
||||||
|
return STORAGE_PREFIX + Math.abs(h).toString(36)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P0 埋点:报告打开(首次 / 再次)。失败静默,不影响阅读。
|
||||||
|
* @param {string} token report_token
|
||||||
|
*/
|
||||||
|
export function trackReportOpen(token) {
|
||||||
|
if (!token) return
|
||||||
|
const key = storageKeyForToken(token)
|
||||||
|
let visitType = 'first'
|
||||||
|
try {
|
||||||
|
const prev = uni.getStorageSync(key)
|
||||||
|
if (prev) visitType = 'repeat'
|
||||||
|
else uni.setStorageSync(key, '1')
|
||||||
|
} catch (_) {
|
||||||
|
visitType = 'repeat'
|
||||||
|
}
|
||||||
|
postReportOpenTrack(token, visitType).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPORT_FETCH_MS = 10000
|
||||||
|
|
||||||
|
export function getReportByTokenWithTimeout(getReportByToken, token) {
|
||||||
|
return Promise.race([
|
||||||
|
getReportByToken(token),
|
||||||
|
new Promise((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('TIMEOUT')), REPORT_FETCH_MS)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
@ -95,7 +95,7 @@ function drawBgGradient(ctx, height) {
|
|||||||
export function drawReportPoster(ctx, data) {
|
export function drawReportPoster(ctx, data) {
|
||||||
const { storeName, petName, beforeImg, afterImg, qrImg, testimonial } = data
|
const { storeName, petName, beforeImg, afterImg, qrImg, testimonial } = data
|
||||||
|
|
||||||
const name = storeName || '宠伴生活馆'
|
const name = storeName || '宠小它'
|
||||||
const PAD = 28
|
const PAD = 28
|
||||||
const pet = petName || '宝贝'
|
const pet = petName || '宝贝'
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { imgUrl } from '../api/index.js'
|
|||||||
*/
|
*/
|
||||||
export function buildReportShareTitle(report) {
|
export function buildReportShareTitle(report) {
|
||||||
if (!report) return '洗护前后对比'
|
if (!report) return '洗护前后对比'
|
||||||
const store = report.store?.name || '宠伴生活馆'
|
const store = report.store?.name || '宠小它'
|
||||||
const pet = report.petName || '爱宠'
|
const pet = report.petName || '爱宠'
|
||||||
const svc = (report.serviceType || '').trim()
|
const svc = (report.serviceType || '').trim()
|
||||||
const svcPart = svc ? `·${svc}` : ''
|
const svcPart = svc ? `·${svc}` : ''
|
||||||
|
|||||||
@ -28,8 +28,8 @@
|
|||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<div class="brand-text">
|
<div class="brand-text">
|
||||||
<div class="brand-logo">{{ reportData.store?.name || '宠伴生活馆' }}</div>
|
<div class="brand-logo">{{ reportData.store?.name || '宠小它' }}</div>
|
||||||
<div class="brand-sub">宠物服务,让爱更专业</div>
|
<div class="brand-sub">用心宠小它,温暖伴一生</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="brand-contact">
|
<div class="brand-contact">
|
||||||
@ -38,6 +38,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="report-privacy-bar">
|
||||||
|
本页含宠物服务影像,请勿公开转发或发群
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 报告标题 -->
|
<!-- 报告标题 -->
|
||||||
<div class="report-title-wrap">
|
<div class="report-title-wrap">
|
||||||
<div class="report-title">服务报告</div>
|
<div class="report-title">服务报告</div>
|
||||||
@ -134,6 +138,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { getReportByToken, imgUrl, submitReportTestimonial } from '../api/index.js'
|
import { getReportByToken, imgUrl, submitReportTestimonial } from '../api/index.js'
|
||||||
|
import { trackReportOpen, getReportByTokenWithTimeout } from '../utils/reportOpenTrack.js'
|
||||||
import { drawReportPoster, POSTER_W, computePosterLogicalHeight } from '../utils/reportPosterDraw.js'
|
import { drawReportPoster, POSTER_W, computePosterLogicalHeight } from '../utils/reportPosterDraw.js'
|
||||||
import { formatDateTimeYMDHM } from '../utils/datetime.js'
|
import { formatDateTimeYMDHM } from '../utils/datetime.js'
|
||||||
import AppIcon from '../components/AppIcon.vue'
|
import AppIcon from '../components/AppIcon.vue'
|
||||||
@ -235,9 +240,10 @@ const loadReport = async () => {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getReportByToken(token)
|
const res = await getReportByTokenWithTimeout(getReportByToken, token)
|
||||||
if (res && res.code === 200 && res.data) {
|
if (res && res.code === 200 && res.data) {
|
||||||
reportData.value = res.data
|
reportData.value = res.data
|
||||||
|
trackReportOpen(token)
|
||||||
} else if (res && res.code === 404) {
|
} else if (res && res.code === 404) {
|
||||||
reportData.value = null
|
reportData.value = null
|
||||||
notFound.value = true
|
notFound.value = true
|
||||||
@ -247,7 +253,8 @@ const loadReport = async () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportData.value = null
|
reportData.value = null
|
||||||
loadError.value = NETWORK_ERROR_MSG
|
loadError.value =
|
||||||
|
e && e.message === 'TIMEOUT' ? '加载超时,请检查网络后重试' : NETWORK_ERROR_MSG
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -469,4 +476,14 @@ onMounted(() => loadReport())
|
|||||||
.section-label { font-size: 15px; font-weight: 700; color: #1f2937; margin-bottom: 10px; }
|
.section-label { font-size: 15px; font-weight: 700; color: #1f2937; margin-bottom: 10px; }
|
||||||
.remark-content { background: #f8fafc; border: 1px solid #e8edf4; border-radius: 12px; padding: 14px; font-size: 14px; color: #64748b; line-height: 1.6; min-height: 60px; }
|
.remark-content { background: #f8fafc; border: 1px solid #e8edf4; border-radius: 12px; padding: 14px; font-size: 14px; color: #64748b; line-height: 1.6; min-height: 60px; }
|
||||||
.action-section { margin: 0 16px 24px; }
|
.action-section { margin: 0 16px 24px; }
|
||||||
|
.report-privacy-bar {
|
||||||
|
margin: 0 16px 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #92400e;
|
||||||
|
line-height: 1.45;
|
||||||
|
background: #fffbeb;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user