deploy: update logo and latest build
This commit is contained in:
parent
b486dd313a
commit
2a4545bc00
@ -1,7 +1,7 @@
|
||||
# petstore-frontend
|
||||
|
||||
#### Description
|
||||
宠伴生活馆
|
||||
宠小它
|
||||
|
||||
#### Software Architecture
|
||||
Software architecture description
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
# 宠伴生活馆 - UniApp 项目
|
||||
# 宠小它 - UniApp 项目
|
||||
|
||||
> 品牌:宠小它 · 用心宠小它,温暖伴一生 · 智慧宠物门店服务系统
|
||||
|
||||
从 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`,无尾斜杠)。未配置时仍可复制话术,但链接为空、二维码不展示,并会在弹层内提示。
|
||||
|
||||
### 宠主预约草稿(P0 C3)
|
||||
|
||||
当前实现:**已登录**用户在 `CustAppointmentCreate` 填写的内容会 debounce 写入本地 `uni.storage`(7 天有效),离开页/重进可恢复;提交成功会清除草稿。
|
||||
产品文档中的 **「未登录可先填单、提交时再登录并恢复草稿」** 需同时将「新建预约」页加入公开路由(如 `App.vue` 的 `isPublicRoute`)并在登录回跳后合并草稿;若未做公开路由,未登录用户仍会在进入预约页前被拦截到登录。
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>宠伴生活馆</title>
|
||||
<meta name="description" content="宠伴生活馆 - 宠物服务,让爱更专业">
|
||||
<title>宠小它</title>
|
||||
<meta name="description" content="宠小它|用心宠小它,温暖伴一生。智慧宠物门店服务系统">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>登录 - 宠伴生活馆</title>
|
||||
<title>登录 - 宠小它</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||
@ -45,8 +45,8 @@
|
||||
<div id="app">
|
||||
<div class="page">
|
||||
<div class="logo">
|
||||
<div class="logo-text">宠伴生活馆</div>
|
||||
<div class="logo-sub">宠物服务,让爱更专业</div>
|
||||
<div class="logo-text">宠小它</div>
|
||||
<div class="logo-sub">用心宠小它,温暖伴一生</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
@ -150,7 +150,7 @@
|
||||
// 模拟微信授权,演示用
|
||||
const demoUser = { id: 99, name: '微信用户', phone: '', role: 'staff' }
|
||||
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'
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>入驻申请 - 宠伴生活馆</title>
|
||||
<title>入驻申请 - 宠小它</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||
@ -84,7 +84,7 @@
|
||||
|
||||
<div class="success-card">
|
||||
<div class="success-icon">🎉</div>
|
||||
<div class="success-title">欢迎加入宠伴生活馆</div>
|
||||
<div class="success-title">欢迎加入宠小它</div>
|
||||
<div class="success-sub">店铺创建成功,您可以开始管理您的宠物店了</div>
|
||||
|
||||
<div class="success-info">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>员工注册 - 宠伴生活馆</title>
|
||||
<title>员工注册 - 宠小它</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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.gstatic.com" crossorigin />
|
||||
<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)
|
||||
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)
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* ============================================================
|
||||
宠伴生活馆 — 设计系统 v2 · 简约 · 温暖 · 专业
|
||||
宠小它 — 设计系统 v2 · 简约 · 温暖 · 专业
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
|
||||
@ -62,7 +62,11 @@ import { buildReportH5FullUrl } from '../../utils/reportPublicUrl.js'
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
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'])
|
||||
@ -88,14 +92,17 @@ const qrGridStyle = computed(() => {
|
||||
const displayUrl = computed(() => buildReportH5FullUrl(props.reportToken))
|
||||
|
||||
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 link = displayUrl.value || '(请配置域名后显示链接)'
|
||||
const name = petLabel.value
|
||||
const pet = petLabel.value
|
||||
const store = storeLabel.value
|
||||
const svc = svcLabel.value
|
||||
return [
|
||||
`【${name}】的洗护服务报告已生成,点链接查看:${link}`,
|
||||
`您好,这是「${name}」本次服务报告,欢迎查阅:${link}`,
|
||||
`毛孩子的服务记录在这里,方便时打开看看:${link}`
|
||||
`您好,【${pet}】的「${svc}」报告已生成,门店:${store}。点击查看:${link}`,
|
||||
`【${store}】${pet} 本次「${svc}」服务报告已完成,欢迎查阅:${link}`
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "宠伴生活馆",
|
||||
"name": "宠小它",
|
||||
"appid": "wx8ca2dfa89af72edf",
|
||||
"description": "宠物服务小程序",
|
||||
"description": "宠小它|智慧宠物门店服务系统",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
"globalStyle": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "宠伴生活馆",
|
||||
"navigationBarTitleText": "宠小它",
|
||||
"navigationBarBackgroundColor": "#2db96d",
|
||||
"backgroundColor": "#fafaf8"
|
||||
}
|
||||
|
||||
@ -118,8 +118,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import {
|
||||
createAppointment,
|
||||
getServiceTypeList,
|
||||
@ -580,6 +580,9 @@ const confirmNewAppt = async () => {
|
||||
try {
|
||||
uni.setStorageSync(lastServiceStorageKey(storeId), a.serviceType)
|
||||
} catch (_) {}
|
||||
try {
|
||||
uni.removeStorageSync(apptDraftStorageKey())
|
||||
} catch (_) {}
|
||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 600)
|
||||
} 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 () => {
|
||||
selectedDateStr.value = ymdLocal()
|
||||
await initStoresAndLocation()
|
||||
@ -608,6 +682,10 @@ onMounted(async () => {
|
||||
await loadServiceTypesForSelected()
|
||||
}
|
||||
await loadMyPets()
|
||||
restoreApptDraft()
|
||||
if (orderedStores.value.length) {
|
||||
await loadServiceTypesForSelected()
|
||||
}
|
||||
await loadAvailableSlots()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<!-- C端:服务预约列表 + 加号新建 -->
|
||||
<view v-if="userInfo.role === 'customer'" class="c-home">
|
||||
<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 class="c-hero">
|
||||
@ -54,6 +54,11 @@
|
||||
</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
|
||||
class="home-list-state"
|
||||
:loading="appointmentsLoading"
|
||||
@ -64,7 +69,9 @@
|
||||
@retry="fetchAppointments"
|
||||
>
|
||||
<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-hint">请使用上方「预约服务」创建;提交后会在本列表显示进度</text>
|
||||
</template>
|
||||
@ -94,7 +101,7 @@
|
||||
<!-- B端:看板(新建预约改为右下角悬浮按钮,避免与小程序胶囊重叠) -->
|
||||
<view v-else>
|
||||
<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 class="home-dash">
|
||||
@ -142,6 +149,11 @@
|
||||
</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
|
||||
class="home-list-state"
|
||||
@ -264,6 +276,7 @@ import {
|
||||
} from '../../api/index.js'
|
||||
import TabBar from '../../components/TabBar.vue'
|
||||
import AppPageState from '../../components/AppPageState.vue'
|
||||
import AppIcon from '../../components/AppIcon.vue'
|
||||
import { messageFromApi, NETWORK_ERROR_MSG } from '../../utils/apiResult.js'
|
||||
import { getUserSession, getStoreSession } from '../../utils/session.js'
|
||||
import { useNavigator } from '../../composables/useNavigator.js'
|
||||
@ -308,6 +321,13 @@ const navSafeStyle = (() => {
|
||||
})()
|
||||
|
||||
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 = [
|
||||
{ title: '待开始', name: 'new' },
|
||||
{ title: '进行中', name: 'doing' },
|
||||
@ -397,7 +417,10 @@ const staffSelectedSlotRangeLabel = computed(() =>
|
||||
const filteredOrders = computed(() => orders.value.filter(o => {
|
||||
if (currentStatus.value === 'new') return o.status === 'new'
|
||||
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
|
||||
}))
|
||||
|
||||
@ -422,6 +445,7 @@ const selectTab = (name) => {
|
||||
currentStatus.value = name
|
||||
listMode.value = 'tab'
|
||||
activeDashKey.value = null
|
||||
if (name !== 'done') includeCancelledInDone.value = false
|
||||
}
|
||||
|
||||
const onDashCardClick = (key) => {
|
||||
@ -796,12 +820,22 @@ onShow(() => {
|
||||
color: #3d5a4a;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.c-empty-emoji {
|
||||
display: block;
|
||||
font-size: 36px;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.9;
|
||||
.c-empty-icon-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.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 {
|
||||
display: block;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-brand">
|
||||
<text class="brand-name">宠伴生活馆</text>
|
||||
<text class="brand-sub">宠物服务,让爱更专业</text>
|
||||
<text class="brand-name">宠小它</text>
|
||||
<text class="brand-sub">用心宠小它,温暖伴一生</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录 -->
|
||||
|
||||
@ -15,15 +15,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺管理(老板) -->
|
||||
<!-- 店铺管理(老板):分组见 P0 B4 -->
|
||||
<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-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-icon-wrap"><AppIcon name="service" :size="16" color="#666660" /></view>
|
||||
<text class="menu-text">服务类型</text>
|
||||
@ -47,6 +42,17 @@
|
||||
</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">
|
||||
<text class="menu-label">门店</text>
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
</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">
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
<view class="store-actions-hint">保存后立即生效,并写入本地缓存。</view>
|
||||
</view>
|
||||
|
||||
<view class="store-footer-tip">宠伴生活馆 · 商家版</view>
|
||||
<view class="store-footer-tip">宠小它 · 商家版</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
@ -34,6 +34,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="report-privacy-bar">
|
||||
<text class="report-privacy-bar-text">本页含宠物服务影像,请勿公开转发或发群</text>
|
||||
</view>
|
||||
|
||||
<!-- 技师情感卡片(与 H5 对齐) -->
|
||||
<view v-if="reportData.staffName || staffAvatarDisplay" class="staff-card-mp">
|
||||
<image
|
||||
@ -185,8 +189,8 @@
|
||||
<!-- 底部品牌 -->
|
||||
<view class="footer-brand">
|
||||
<text class="footer-line"></text>
|
||||
<text class="footer-name">{{ reportData.store?.name || '宠伴生活馆' }}</text>
|
||||
<text class="footer-sub">宠物服务 · 让爱更专业</text>
|
||||
<text class="footer-name">{{ reportData.store?.name || '宠小它' }}</text>
|
||||
<text class="footer-sub">用心宠小它,温暖伴一生</text>
|
||||
</view>
|
||||
</view>
|
||||
</AppPageState>
|
||||
@ -212,6 +216,7 @@
|
||||
import { ref, computed, onMounted, getCurrentInstance, nextTick } from 'vue'
|
||||
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import { getReportByToken, imgUrl, submitReportTestimonial } from '../../api/index.js'
|
||||
import { trackReportOpen, getReportByTokenWithTimeout } from '../../utils/reportOpenTrack.js'
|
||||
import { formatDateTimeYMDHM } from '../../utils/datetime.js'
|
||||
import { drawReportPoster, POSTER_W, computePosterLogicalHeight } from '../../utils/reportPosterDraw.js'
|
||||
import AppIcon from '../../components/AppIcon.vue'
|
||||
@ -335,9 +340,10 @@ const loadReport = async () => {
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getReportByToken(token)
|
||||
const res = await getReportByTokenWithTimeout(getReportByToken, token)
|
||||
if (res && res.code === 200 && res.data) {
|
||||
reportData.value = res.data
|
||||
trackReportOpen(token)
|
||||
} else if (res && res.code === 404) {
|
||||
reportData.value = null
|
||||
notFound.value = true
|
||||
@ -347,7 +353,8 @@ const loadReport = async () => {
|
||||
}
|
||||
} catch (e) {
|
||||
reportData.value = null
|
||||
loadError.value = NETWORK_ERROR_MSG
|
||||
loadError.value =
|
||||
e && e.message === 'TIMEOUT' ? '加载超时,请检查网络后重试' : NETWORK_ERROR_MSG
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -382,7 +389,7 @@ const navToStore = () => {
|
||||
uni.openLocation({
|
||||
latitude: Number(store.latitude),
|
||||
longitude: Number(store.longitude),
|
||||
name: store.name || '宠伴生活馆',
|
||||
name: store.name || '宠小它',
|
||||
address: store.address || ''
|
||||
})
|
||||
}
|
||||
@ -1047,4 +1054,17 @@ onMounted(() => loadReport())
|
||||
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>
|
||||
|
||||
@ -167,6 +167,9 @@
|
||||
:class="{ 'submit-fixed--scrolled': hasScrolled }"
|
||||
: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">
|
||||
{{ submitting ? '提交中…' : '生成报告' }}
|
||||
</button>
|
||||
@ -183,6 +186,8 @@
|
||||
:show="shareModalOpen"
|
||||
:report-token="shareToken"
|
||||
:pet-name="sharePetName"
|
||||
:store-name="shareStoreName"
|
||||
:service-type="shareServiceType"
|
||||
@close="onShareModalClose"
|
||||
@preview="onSharePreview"
|
||||
/>
|
||||
@ -236,6 +241,8 @@ const submitting = ref(false)
|
||||
const shareModalOpen = ref(false)
|
||||
const shareToken = ref('')
|
||||
const sharePetName = ref('')
|
||||
const shareStoreName = ref('')
|
||||
const shareServiceType = ref('')
|
||||
const basicInfoCollapsed = ref(false)
|
||||
const hasScrolled = ref(false)
|
||||
const keyboardHeight = ref(0)
|
||||
@ -701,6 +708,8 @@ const submitReport = async () => {
|
||||
const token = res.data.reportToken
|
||||
shareToken.value = token
|
||||
sharePetName.value = report.value.petName || ''
|
||||
shareStoreName.value = storeInfo?.name || ''
|
||||
shareServiceType.value = report.value.serviceType || ''
|
||||
report.value = { petName: '', serviceType: '', appointmentTime: '', beforeMedia: [], afterMedia: [], duringMedia: [], remark: '' }
|
||||
currentAppointmentId.value = null
|
||||
basicInfoCollapsed.value = false
|
||||
@ -715,6 +724,8 @@ const onShareModalClose = () => {
|
||||
shareModalOpen.value = false
|
||||
shareToken.value = ''
|
||||
sharePetName.value = ''
|
||||
shareStoreName.value = ''
|
||||
shareServiceType.value = ''
|
||||
}
|
||||
|
||||
const onSharePreview = () => {
|
||||
@ -722,6 +733,8 @@ const onSharePreview = () => {
|
||||
shareModalOpen.value = false
|
||||
shareToken.value = ''
|
||||
sharePetName.value = ''
|
||||
shareStoreName.value = ''
|
||||
shareServiceType.value = ''
|
||||
if (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);
|
||||
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 {
|
||||
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) {
|
||||
const { storeName, petName, beforeImg, afterImg, qrImg, testimonial } = data
|
||||
|
||||
const name = storeName || '宠伴生活馆'
|
||||
const name = storeName || '宠小它'
|
||||
const PAD = 28
|
||||
const pet = petName || '宝贝'
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { imgUrl } from '../api/index.js'
|
||||
*/
|
||||
export function buildReportShareTitle(report) {
|
||||
if (!report) return '洗护前后对比'
|
||||
const store = report.store?.name || '宠伴生活馆'
|
||||
const store = report.store?.name || '宠小它'
|
||||
const pet = report.petName || '爱宠'
|
||||
const svc = (report.serviceType || '').trim()
|
||||
const svcPart = svc ? `·${svc}` : ''
|
||||
|
||||
@ -28,8 +28,8 @@
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<div class="brand-text">
|
||||
<div class="brand-logo">{{ reportData.store?.name || '宠伴生活馆' }}</div>
|
||||
<div class="brand-sub">宠物服务,让爱更专业</div>
|
||||
<div class="brand-logo">{{ reportData.store?.name || '宠小它' }}</div>
|
||||
<div class="brand-sub">用心宠小它,温暖伴一生</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="brand-contact">
|
||||
@ -38,6 +38,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-privacy-bar">
|
||||
本页含宠物服务影像,请勿公开转发或发群
|
||||
</div>
|
||||
|
||||
<!-- 报告标题 -->
|
||||
<div class="report-title-wrap">
|
||||
<div class="report-title">服务报告</div>
|
||||
@ -134,6 +138,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
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 { formatDateTimeYMDHM } from '../utils/datetime.js'
|
||||
import AppIcon from '../components/AppIcon.vue'
|
||||
@ -235,9 +240,10 @@ const loadReport = async () => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getReportByToken(token)
|
||||
const res = await getReportByTokenWithTimeout(getReportByToken, token)
|
||||
if (res && res.code === 200 && res.data) {
|
||||
reportData.value = res.data
|
||||
trackReportOpen(token)
|
||||
} else if (res && res.code === 404) {
|
||||
reportData.value = null
|
||||
notFound.value = true
|
||||
@ -247,7 +253,8 @@ const loadReport = async () => {
|
||||
}
|
||||
} catch (e) {
|
||||
reportData.value = null
|
||||
loadError.value = NETWORK_ERROR_MSG
|
||||
loadError.value =
|
||||
e && e.message === 'TIMEOUT' ? '加载超时,请检查网络后重试' : NETWORK_ERROR_MSG
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -469,4 +476,14 @@ onMounted(() => loadReport())
|
||||
.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; }
|
||||
.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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user