初始化
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view class="msgs" scroll-y :scroll-top="scrollTop">
|
||||
<view class="container">
|
||||
<view v-for="m in messages" :key="m.id" class="m" :class="{ me: m.role === 'user' }">
|
||||
<view class="bubble" :class="{ bme: m.role === 'user' }">
|
||||
<text class="txt">{{ m.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chips" v-if="showChips">
|
||||
<view class="chip" @tap="send('推荐爆款项目')">推荐爆款项目</view>
|
||||
<view class="chip" @tap="send('我想预约今天')">我想预约今天</view>
|
||||
<view class="chip" @tap="send('敏感肌适合做什么')">敏感肌适合做什么</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="quick row">
|
||||
<view class="q" @tap="send('查档期')">查档期</view>
|
||||
<view class="q" @tap="send('看价格')">看价格</view>
|
||||
<view class="q" @tap="send('我要预约')">我要预约</view>
|
||||
</view>
|
||||
|
||||
<view class="bar">
|
||||
<view class="voice" @tap="mockVoice">按住说话</view>
|
||||
<input class="ipt" v-model="text" confirm-type="send" @confirm="send(text)" placeholder="说出你的需求,例如:想做补水/我想预约今天 14:30" />
|
||||
<view class="send" @tap="send(text)">发送</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { aiReply } from '@/common/aiRules'
|
||||
import { projects } from '@/common/mockData'
|
||||
|
||||
function mid() {
|
||||
return `${Date.now()}_${Math.floor(Math.random() * 10000)}`
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
projectId: '',
|
||||
messages: [],
|
||||
text: '',
|
||||
scrollTop: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showChips() {
|
||||
return this.messages.length < 4
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
this.projectId = query.projectId || ''
|
||||
const base =
|
||||
'我是 AI 预约助手。你可以直接说“预约 + 时间 + 项目/需求”,我会把你带到预约/购买流程。'
|
||||
const ctx = this.projectId ? projects.find((x) => x.id === this.projectId) : null
|
||||
const intro = ctx ? `当前项目:${ctx.name}。你想预约哪个日期和时段?` : base
|
||||
this.messages = [
|
||||
{ id: mid(), role: 'assistant', text: intro }
|
||||
]
|
||||
this.bump()
|
||||
},
|
||||
methods: {
|
||||
bump() {
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = this.scrollTop + 99999
|
||||
})
|
||||
},
|
||||
send(raw) {
|
||||
const v = (raw || '').trim()
|
||||
if (!v) return
|
||||
this.messages.push({ id: mid(), role: 'user', text: v })
|
||||
this.text = ''
|
||||
|
||||
const r = aiReply(v)
|
||||
this.messages.push({ id: mid(), role: 'assistant', text: r.text })
|
||||
this.bump()
|
||||
|
||||
if (r.action?.type === 'go_booking') {
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: `/pages/booking/create?projectId=${r.action.projectId}` })
|
||||
}, 450)
|
||||
}
|
||||
},
|
||||
mockVoice() {
|
||||
uni.showToast({ title: '语音输入(原型演示)', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.msgs {
|
||||
flex: 1;
|
||||
}
|
||||
.m {
|
||||
display: flex;
|
||||
margin: 14rpx 0;
|
||||
}
|
||||
.me {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.bubble {
|
||||
max-width: 620rpx;
|
||||
padding: 18rpx 18rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow: 0 10rpx 28rpx rgba(17, 24, 39, 0.06);
|
||||
}
|
||||
.bme {
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border-color: rgba(59, 130, 246, 0.22);
|
||||
}
|
||||
.txt {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.quick {
|
||||
padding: 12rpx 18rpx 6rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
gap: 12rpx;
|
||||
}
|
||||
.q {
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
.bar {
|
||||
padding: 18rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.voice {
|
||||
width: 160rpx;
|
||||
height: 82rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
color: rgba(17, 24, 39, 0.78);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ipt {
|
||||
flex: 1;
|
||||
height: 82rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.05);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.send {
|
||||
width: 140rpx;
|
||||
height: 82rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(135deg, #111827 0%, #3b82f6 100%);
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin: 8rpx 0 12rpx;
|
||||
}
|
||||
.chip {
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card filters">
|
||||
<view class="row sc">
|
||||
<view
|
||||
v-for="s in tabs"
|
||||
:key="s.value"
|
||||
class="chip"
|
||||
:class="{ on: s.value === active }"
|
||||
hover-class="none"
|
||||
@tap="setActive(s.value)"
|
||||
>
|
||||
{{ s.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list">
|
||||
<view v-for="o in viewList" :key="o.id" class="card item" @tap="open(o.id)">
|
||||
<view class="row between">
|
||||
<view class="n">{{ o.projectName }}</view>
|
||||
<view
|
||||
class="st"
|
||||
:class="{
|
||||
w: o.uiStatus === '待到店',
|
||||
g: o.uiStatus === '已完成',
|
||||
d: o.uiStatus === '已取消',
|
||||
x: o.uiStatus === '已过期'
|
||||
}"
|
||||
>
|
||||
{{ o.uiStatus }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="muted meta">{{ o.appointmentDate }} {{ o.appointmentSlot }} · {{ o.technicianName }}</view>
|
||||
|
||||
<view class="ops row" v-if="o.uiStatus === '待到店'">
|
||||
<view class="op" @tap.stop="reschedule(o)">改约</view>
|
||||
<view class="op danger" @tap.stop="cancel(o)">取消预约</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
const rows = [
|
||||
{
|
||||
id: 'apt_demo_pending_001',
|
||||
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
||||
status: '待核销',
|
||||
uiStatus: '待到店',
|
||||
group: 'pending',
|
||||
amount: 238,
|
||||
projectId: 'p3',
|
||||
projectName: '补水修护屏障护理',
|
||||
durationMin: 80,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-22',
|
||||
appointmentSlot: '10:30',
|
||||
technicianName: '许言',
|
||||
note: '想要舒缓泛红',
|
||||
verifyCode: 'VCAP20260621001'
|
||||
},
|
||||
{
|
||||
id: 'apt_demo_pending_002',
|
||||
createdAt: Date.now() - 6 * 60 * 60 * 1000,
|
||||
status: '待核销',
|
||||
uiStatus: '待到店',
|
||||
group: 'pending',
|
||||
amount: 188,
|
||||
projectId: 'p4',
|
||||
projectName: '肩颈舒缓筋膜放松',
|
||||
durationMin: 60,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-24',
|
||||
appointmentSlot: '19:00',
|
||||
technicianName: '周晴',
|
||||
note: '',
|
||||
verifyCode: 'VCAP20260621002'
|
||||
},
|
||||
{
|
||||
id: 'apt_demo_done_001',
|
||||
createdAt: Date.now() - 9 * 24 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
uiStatus: '已完成',
|
||||
group: 'done',
|
||||
amount: 168,
|
||||
projectId: 'p2',
|
||||
projectName: '深层清洁黑头管理',
|
||||
durationMin: 75,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-12',
|
||||
appointmentSlot: '15:00',
|
||||
technicianName: '林安',
|
||||
note: 'T区出油严重',
|
||||
verifyCode: 'VCAP20260621003'
|
||||
},
|
||||
{
|
||||
id: 'apt_demo_done_002',
|
||||
createdAt: Date.now() - 15 * 24 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
uiStatus: '已完成',
|
||||
group: 'done',
|
||||
amount: 99,
|
||||
projectId: 'p1',
|
||||
projectName: '水氧净透体验',
|
||||
durationMin: 60,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-06',
|
||||
appointmentSlot: '11:30',
|
||||
technicianName: '系统分配',
|
||||
note: '',
|
||||
verifyCode: 'VCAP20260621004'
|
||||
},
|
||||
{
|
||||
id: 'apt_demo_canceled_001',
|
||||
createdAt: Date.now() - 3 * 24 * 60 * 60 * 1000,
|
||||
status: '已取消',
|
||||
uiStatus: '已取消',
|
||||
group: 'canceled',
|
||||
amount: 238,
|
||||
projectId: 'p3',
|
||||
projectName: '补水修护屏障护理',
|
||||
durationMin: 80,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-23',
|
||||
appointmentSlot: '16:00',
|
||||
technicianName: '许言',
|
||||
note: '',
|
||||
verifyCode: 'VCAP20260621005'
|
||||
},
|
||||
{
|
||||
id: 'apt_demo_canceled_002',
|
||||
createdAt: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
||||
status: '已取消',
|
||||
uiStatus: '已取消',
|
||||
group: 'canceled',
|
||||
amount: 188,
|
||||
projectId: 'p4',
|
||||
projectName: '肩颈舒缓筋膜放松',
|
||||
durationMin: 60,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-17',
|
||||
appointmentSlot: '13:30',
|
||||
technicianName: '周晴',
|
||||
note: '',
|
||||
verifyCode: 'VCAP20260621008'
|
||||
},
|
||||
{
|
||||
id: 'apt_demo_expired_001',
|
||||
createdAt: Date.now() - 26 * 60 * 60 * 1000,
|
||||
status: '待核销',
|
||||
uiStatus: '已过期',
|
||||
group: 'expired',
|
||||
amount: 188,
|
||||
projectId: 'p4',
|
||||
projectName: '肩颈舒缓筋膜放松',
|
||||
durationMin: 60,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-20',
|
||||
appointmentSlot: '13:30',
|
||||
technicianName: '周晴',
|
||||
note: '',
|
||||
verifyCode: 'VCAP20260621006'
|
||||
},
|
||||
{
|
||||
id: 'apt_demo_expired_002',
|
||||
createdAt: Date.now() - 4 * 24 * 60 * 60 * 1000,
|
||||
status: '待核销',
|
||||
uiStatus: '已过期',
|
||||
group: 'expired',
|
||||
amount: 99,
|
||||
projectId: 'p1',
|
||||
projectName: '水氧净透体验',
|
||||
durationMin: 60,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-19',
|
||||
appointmentSlot: '18:00',
|
||||
technicianName: '系统分配',
|
||||
note: '',
|
||||
verifyCode: 'VCAP20260621007'
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
tabs: [
|
||||
{ value: 'pending', label: '待到店' },
|
||||
{ value: 'done', label: '已完成' },
|
||||
{ value: 'canceled', label: '已取消' },
|
||||
{ value: 'expired', label: '已过期' }
|
||||
],
|
||||
active: 'pending',
|
||||
rows,
|
||||
viewList: rows.filter((x) => x.group === 'pending')
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.apply()
|
||||
},
|
||||
onShow() {
|
||||
this.apply()
|
||||
},
|
||||
methods: {
|
||||
apply() {
|
||||
const v = this.rows.filter((x) => x.group === this.active)
|
||||
this.viewList = v && v.length ? v : this.rows.slice(0, 3)
|
||||
},
|
||||
setActive(v) {
|
||||
if (!v || v === this.active) return
|
||||
this.active = v
|
||||
this.apply()
|
||||
},
|
||||
open(id) {
|
||||
const o = this.rows.find((x) => x.id === id) || this.rows[0]
|
||||
const payload = encodeURIComponent(JSON.stringify(o))
|
||||
uni.navigateTo({ url: `/pages/orders/detail?payload=${payload}` })
|
||||
},
|
||||
goProjects() {
|
||||
uni.switchTab({ url: '/pages/projects/list' })
|
||||
},
|
||||
reschedule(o) {
|
||||
uni.navigateTo({ url: `/pages/booking/create?projectId=${o.projectId}` })
|
||||
},
|
||||
cancel(o) {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: '取消后该预约将变更为已取消(原型演示)。',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.rows = this.rows.map((x) => (x.id === o.id ? { ...x, status: '已取消', uiStatus: '已取消', group: 'canceled' } : x))
|
||||
this.apply()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filters {
|
||||
padding: 16rpx;
|
||||
}
|
||||
.sc {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip {
|
||||
padding: 14rpx 18rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: #111827;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.on {
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.list {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.item {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.n {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
.st {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.w {
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.g {
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
color: #047857;
|
||||
}
|
||||
.d {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #b91c1c;
|
||||
}
|
||||
.x {
|
||||
background: rgba(17, 24, 39, 0.12);
|
||||
color: #111827;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.ops {
|
||||
margin-top: 16rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.op {
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
.danger {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card hero">
|
||||
<view class="brand">智约美服</view>
|
||||
<view class="slogan muted">AI 一键预约,24 小时接单</view>
|
||||
</view>
|
||||
|
||||
<view class="card box">
|
||||
<view class="wx row">
|
||||
<view class="wx-icon">W</view>
|
||||
<view class="wx-text">
|
||||
<view class="t1">微信一键登录</view>
|
||||
<view class="t2 muted">用于同步订单、卡券与会员档案</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn btn-primary login" @tap="doLogin">同意授权并登录</view>
|
||||
</view>
|
||||
|
||||
<view class="foot muted">
|
||||
<text @tap="goPrivacy">《用户隐私协议》</text>
|
||||
<text class="dot"> · </text>
|
||||
<text @tap="goTerms">《服务协议》</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const KEY = 'zy_authed_v1'
|
||||
|
||||
export default {
|
||||
onShow() {
|
||||
const ok = !!uni.getStorageSync(KEY)
|
||||
if (ok) {
|
||||
uni.switchTab({ url: '/pages/home/index' })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doLogin() {
|
||||
uni.setStorageSync(KEY, 1)
|
||||
uni.switchTab({ url: '/pages/home/index' })
|
||||
},
|
||||
goPrivacy() {
|
||||
uni.navigateTo({ url: '/pages/legal/privacy' })
|
||||
},
|
||||
goTerms() {
|
||||
uni.navigateTo({ url: '/pages/legal/terms' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hero {
|
||||
padding: 40rpx 28rpx;
|
||||
background: linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(59, 130, 246, 1) 100%);
|
||||
border: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.brand {
|
||||
font-size: 54rpx;
|
||||
font-weight: 950;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.slogan {
|
||||
margin-top: 14rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.box {
|
||||
margin-top: 22rpx;
|
||||
padding: 26rpx;
|
||||
}
|
||||
.wx {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.wx-icon {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 22rpx;
|
||||
background: rgba(16, 185, 129, 0.14);
|
||||
color: #059669;
|
||||
font-weight: 950;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
.t1 {
|
||||
font-size: 36rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.t2 {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.login {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
.foot {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.dot {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card block">
|
||||
<view class="row between">
|
||||
<view class="title">预约服务</view>
|
||||
<view class="muted">{{ project ? '已选择' : '请选择' }}</view>
|
||||
</view>
|
||||
<picker mode="selector" :range="projectNames" :value="projectIndex" @change="onPickProject">
|
||||
<view class="pick2">
|
||||
<view class="pname">{{ project ? project.name : '选择项目/套餐' }}</view>
|
||||
<view class="muted psub" v-if="project">{{ project.durationMin }} 分钟 · ¥{{ project.price }}</view>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="muted meta" v-if="project">适合人群:{{ project.fitFor }}</view>
|
||||
<view class="muted meta" v-if="project">禁忌提醒:{{ project.taboo }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="project">
|
||||
<view class="card block">
|
||||
<view class="title">选择日期</view>
|
||||
<scroll-view class="sc" scroll-x>
|
||||
<view class="row">
|
||||
<view
|
||||
v-for="d in dates"
|
||||
:key="d.value"
|
||||
class="date"
|
||||
:class="{ on: d.value === form.date }"
|
||||
@tap="selectDate(d.value)"
|
||||
>
|
||||
<view class="d1">{{ d.label1 }}</view>
|
||||
<view class="d2 muted">{{ d.label2 }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<picker mode="date" :value="form.date" @change="onPickDate">
|
||||
<view class="pick">自定义日期:{{ form.date }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">选择时段</view>
|
||||
<view class="grid">
|
||||
<view
|
||||
v-for="s in slotRows"
|
||||
:key="s.value"
|
||||
class="slot"
|
||||
:class="{ on: s.value === form.slot, off: s.disabled }"
|
||||
@tap="pickSlot(s)"
|
||||
>
|
||||
{{ s.value }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="hint muted">满档逻辑在商用版由实时档期接口返回,这里为原型演示。</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">选择技师</view>
|
||||
<view class="techs">
|
||||
<view class="tech" :class="{ on: form.techId === 'auto' }" @tap="form.techId = 'auto'">
|
||||
<view class="row between">
|
||||
<view class="t-name">系统自动分配</view>
|
||||
<view class="tag">推荐</view>
|
||||
</view>
|
||||
<view class="muted t-sub">根据项目与档期匹配最合适的技师</view>
|
||||
</view>
|
||||
<view
|
||||
v-for="t in techs"
|
||||
:key="t.id"
|
||||
class="tech"
|
||||
:class="{ on: form.techId === t.id }"
|
||||
@tap="form.techId = t.id"
|
||||
>
|
||||
<view class="row between">
|
||||
<view>
|
||||
<view class="t-name">{{ t.name }}</view>
|
||||
<view class="muted t-sub">{{ t.title }}</view>
|
||||
</view>
|
||||
<view class="tags">
|
||||
<text class="tag" v-for="tg in t.tags" :key="tg">{{ tg }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">备注需求</view>
|
||||
<textarea
|
||||
class="ta"
|
||||
placeholder="例如:敏感肌、易过敏、想做补水修护…"
|
||||
v-model="form.note"
|
||||
maxlength="120"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="space"></view>
|
||||
<view class="fixbar">
|
||||
<view class="btn btn-primary submit" @tap="submit">提交预约</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="card empty">
|
||||
<view class="e1">请选择项目后继续</view>
|
||||
<view class="muted e2">你也可以在“项目详情”里直接点击“立即预约”。</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { projects, technicians } from '@/common/mockData'
|
||||
|
||||
const fallbackProjects = [
|
||||
{
|
||||
id: 'fp1',
|
||||
name: '水氧净透体验',
|
||||
price: 99,
|
||||
durationMin: 60,
|
||||
fitFor: '初次体验、暗沉、出油',
|
||||
taboo: '近期医美术后需评估',
|
||||
desc: '轻盈水氧 + 净透护理,快速提升肤感与通透度。'
|
||||
},
|
||||
{
|
||||
id: 'fp2',
|
||||
name: '补水修护屏障护理',
|
||||
price: 238,
|
||||
durationMin: 80,
|
||||
fitFor: '敏感泛红、干燥紧绷',
|
||||
taboo: '过敏急性期请先咨询',
|
||||
desc: '修护屏障与舒缓敏感,适合换季与长期干燥人群。'
|
||||
},
|
||||
{
|
||||
id: 'fp3',
|
||||
name: '肩颈舒缓筋膜放松',
|
||||
price: 188,
|
||||
durationMin: 60,
|
||||
fitFor: '久坐办公、肩颈僵硬',
|
||||
taboo: '急性损伤与发热期不建议',
|
||||
desc: '深度放松肌群与筋膜,改善紧绷与酸胀。'
|
||||
}
|
||||
]
|
||||
|
||||
const runtimeProjects = Array.isArray(projects) && projects.length ? projects : fallbackProjects
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? `0${n}` : `${n}`
|
||||
}
|
||||
|
||||
function dayLabel(d) {
|
||||
const m = d.getMonth() + 1
|
||||
const dd = d.getDate()
|
||||
const w = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()]
|
||||
return { label1: `${m}/${dd}`, label2: `周${w}` }
|
||||
}
|
||||
|
||||
function isoDate(d) {
|
||||
const y = d.getFullYear()
|
||||
const m = pad2(d.getMonth() + 1)
|
||||
const dd = pad2(d.getDate())
|
||||
return `${y}-${m}-${dd}`
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
projectId: '',
|
||||
project: runtimeProjects[0] || null,
|
||||
allProjects: runtimeProjects,
|
||||
projectNames: runtimeProjects.length ? runtimeProjects.map((x) => x.name) : ['暂无项目'],
|
||||
projectIndex: 0,
|
||||
dates: [],
|
||||
slotRows: [],
|
||||
techs: technicians,
|
||||
form: {
|
||||
date: '',
|
||||
slot: '14:30',
|
||||
techId: 'auto',
|
||||
note: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
this.projectId = query.projectId || ''
|
||||
this.allProjects = runtimeProjects
|
||||
this.projectNames = this.allProjects.length ? this.allProjects.map((x) => x.name) : ['暂无项目']
|
||||
this.project = this.allProjects.find((x) => x.id === this.projectId) || this.allProjects[0] || null
|
||||
this.projectIndex = Math.max(
|
||||
0,
|
||||
this.allProjects.findIndex((x) => x.id === (this.project ? this.project.id : ''))
|
||||
)
|
||||
const today = new Date()
|
||||
const ds = []
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(today.getTime() + i * 24 * 60 * 60 * 1000)
|
||||
const l = dayLabel(d)
|
||||
ds.push({
|
||||
value: isoDate(d),
|
||||
label1: i === 0 ? '今天' : l.label1,
|
||||
label2: i === 0 ? l.label1 : l.label2
|
||||
})
|
||||
}
|
||||
this.dates = ds
|
||||
if (!this.form.date) this.form.date = ds[0]?.value || ''
|
||||
this.buildSlots()
|
||||
},
|
||||
methods: {
|
||||
onPickProject(e) {
|
||||
const idx = Number(e.detail.value || 0)
|
||||
this.projectIndex = idx
|
||||
this.project = this.allProjects[idx] || null
|
||||
},
|
||||
selectDate(v) {
|
||||
this.form.date = v
|
||||
this.buildSlots()
|
||||
},
|
||||
onPickDate(e) {
|
||||
const v = e.detail.value
|
||||
if (!v) return
|
||||
this.form.date = v
|
||||
this.buildSlots()
|
||||
},
|
||||
buildSlots() {
|
||||
const base = ['10:00', '11:30', '13:00', '14:30', '16:00', '17:30', '19:00', '20:30']
|
||||
const today = this.dates[0]?.value || ''
|
||||
const disabledSet = new Set()
|
||||
if (this.form.date === today) {
|
||||
disabledSet.add('10:00')
|
||||
disabledSet.add('11:30')
|
||||
}
|
||||
this.slotRows = base.map((x) => ({ value: x, disabled: disabledSet.has(x) }))
|
||||
if (disabledSet.has(this.form.slot)) this.form.slot = '14:30'
|
||||
},
|
||||
pickSlot(s) {
|
||||
if (s.disabled) return
|
||||
this.form.slot = s.value
|
||||
},
|
||||
submit() {
|
||||
if (!this.project) {
|
||||
uni.showToast({ title: '请选择项目', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.form.date || !this.form.slot) {
|
||||
uni.showToast({ title: '请选择日期和时段', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const tech =
|
||||
this.form.techId === 'auto' ? null : this.techs.find((x) => x.id === this.form.techId) || null
|
||||
const payload = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
type: 'booking',
|
||||
projectId: this.project.id,
|
||||
date: this.form.date,
|
||||
slot: this.form.slot,
|
||||
techId: this.form.techId,
|
||||
techName: tech?.name || '系统分配',
|
||||
note: this.form.note || ''
|
||||
})
|
||||
)
|
||||
uni.navigateTo({ url: `/pages/order/confirm?payload=${payload}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.pick2 {
|
||||
margin-top: 14rpx;
|
||||
padding: 18rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
background: #fff;
|
||||
}
|
||||
.pname {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.psub {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.empty {
|
||||
margin-top: 18rpx;
|
||||
padding: 34rpx 26rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.e1 {
|
||||
font-size: 34rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.e2 {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.price {
|
||||
font-size: 32rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.title {
|
||||
font-weight: 900;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.sc {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pick {
|
||||
margin-top: 14rpx;
|
||||
padding: 16rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.05);
|
||||
font-size: 26rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.date {
|
||||
width: 150rpx;
|
||||
padding: 16rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 18rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
background: rgba(17, 24, 39, 0.03);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.on {
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border-color: rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
.d1 {
|
||||
font-weight: 900;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.d2 {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
.slot {
|
||||
height: 70rpx;
|
||||
border-radius: 18rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.off {
|
||||
background: rgba(17, 24, 39, 0.05);
|
||||
color: rgba(17, 24, 39, 0.35);
|
||||
}
|
||||
.hint {
|
||||
margin-top: 14rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.techs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.tech {
|
||||
padding: 16rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
background: #fff;
|
||||
}
|
||||
.t-name {
|
||||
font-weight: 900;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.t-sub {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
max-width: 280rpx;
|
||||
}
|
||||
.ta {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
padding: 16rpx;
|
||||
border-radius: 18rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
background: #fff;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.space {
|
||||
height: 160rpx;
|
||||
}
|
||||
.fixbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 18rpx 24rpx 24rpx;
|
||||
background: linear-gradient(180deg, rgba(246, 247, 251, 0) 0%, rgba(246, 247, 251, 1) 46%);
|
||||
}
|
||||
.submit {
|
||||
height: 92rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card filters">
|
||||
<view class="row sc">
|
||||
<view v-for="s in tabs" :key="s.value" class="chip" :class="{ on: s.value === active }" hover-class="none" @tap="setActive(s.value)">
|
||||
{{ s.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list">
|
||||
<view v-for="c in viewList" :key="c.id" class="card item" @tap="open(c.id)">
|
||||
<view class="row between">
|
||||
<view class="n">{{ c.projectName }}</view>
|
||||
<view class="tag">剩余 {{ c.remainingTimes }}</view>
|
||||
</view>
|
||||
<view class="muted meta">核销码:{{ c.verifyCode }}</view>
|
||||
<view class="muted meta" v-if="c.validText">有效期:{{ c.validText }}</view>
|
||||
<view class="row between meta2">
|
||||
<view class="muted">状态:{{ c.uiStatus }}</view>
|
||||
<view class="amt">¥{{ c.amount }}</view>
|
||||
</view>
|
||||
<view class="row between ops" v-if="c.uiStatus === '未使用'">
|
||||
<view class="btn btn-ghost ob" @tap.stop="book(c)">去使用(预约)</view>
|
||||
<view class="btn btn-primary ob" @tap.stop="openCode(c)">核销码</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { demoOrders } from '@/common/demoOrders'
|
||||
|
||||
function buildCoupons() {
|
||||
const now = Date.now()
|
||||
const extra = [
|
||||
{
|
||||
id: 'cp_demo_unused_001',
|
||||
createdAt: now - 2 * 24 * 60 * 60 * 1000,
|
||||
status: '待核销',
|
||||
amount: 299,
|
||||
projectId: 'p9',
|
||||
projectName: '新客体验套餐 · 3 次',
|
||||
durationMin: 60,
|
||||
orderType: 'coupon',
|
||||
couponTitle: '新客体验套餐 · 3 次',
|
||||
couponPlanKey: 'package',
|
||||
couponPlanLabel: '套餐',
|
||||
validText: '有效期 90 天',
|
||||
remainingTimes: 3,
|
||||
verifyCode: 'VCCP20260621001'
|
||||
},
|
||||
{
|
||||
id: 'cp_demo_pending_001',
|
||||
createdAt: now - 6 * 24 * 60 * 60 * 1000,
|
||||
status: '待付款',
|
||||
amount: 168,
|
||||
projectId: 'p2',
|
||||
projectName: '深层清洁黑头管理',
|
||||
durationMin: 75,
|
||||
orderType: 'coupon',
|
||||
couponTitle: '深层清洁黑头管理',
|
||||
couponPlanKey: 'single',
|
||||
couponPlanLabel: '单次券',
|
||||
validText: '有效期 30 天',
|
||||
remainingTimes: 1,
|
||||
verifyCode: 'VCCP20260621002'
|
||||
},
|
||||
{
|
||||
id: 'cp_demo_used_001',
|
||||
createdAt: now - 18 * 24 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
amount: 899,
|
||||
projectId: 'p5',
|
||||
projectName: '皮肤管理次卡 5 次',
|
||||
durationMin: 60,
|
||||
orderType: 'coupon',
|
||||
couponTitle: '皮肤管理次卡 5 次',
|
||||
couponPlanKey: 'times5',
|
||||
couponPlanLabel: '次卡 5 次',
|
||||
validText: '有效期 180 天',
|
||||
remainingTimes: 0,
|
||||
verifyCode: 'VCCP20260621003'
|
||||
},
|
||||
{
|
||||
id: 'cp_demo_expired_001',
|
||||
createdAt: now - 65 * 24 * 60 * 60 * 1000,
|
||||
status: '已过期',
|
||||
amount: 99,
|
||||
projectId: 'p1',
|
||||
projectName: '水氧净透体验',
|
||||
durationMin: 60,
|
||||
orderType: 'coupon',
|
||||
couponTitle: '水氧净透体验',
|
||||
couponPlanKey: 'single',
|
||||
couponPlanLabel: '单次券',
|
||||
validText: '已过期(原型演示)',
|
||||
remainingTimes: 1,
|
||||
verifyCode: 'VCCP20260621004'
|
||||
}
|
||||
]
|
||||
|
||||
const all = [...demoOrders.filter((x) => x.orderType === 'coupon'), ...extra]
|
||||
return all.map((x) => {
|
||||
const uiStatus = x.status === '待核销' ? '未使用' : x.status === '已完成' ? '已核销' : x.status === '待付款' ? '待付款' : '已过期'
|
||||
const group = uiStatus === '已核销' ? 'used' : uiStatus === '待付款' ? 'pending' : uiStatus === '已过期' ? 'expired' : 'unused'
|
||||
return { ...x, uiStatus, group }
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
const coupons = buildCoupons()
|
||||
const init = coupons.filter((x) => x.group === 'unused')
|
||||
return {
|
||||
tabs: [
|
||||
{ value: 'unused', label: '未使用' },
|
||||
{ value: 'used', label: '已核销' },
|
||||
{ value: 'pending', label: '待付款' },
|
||||
{ value: 'expired', label: '已过期' }
|
||||
],
|
||||
active: 'unused',
|
||||
coupons,
|
||||
viewList: init && init.length ? init : coupons.slice(0, 3)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.apply()
|
||||
},
|
||||
onShow() {
|
||||
this.coupons = buildCoupons()
|
||||
this.apply()
|
||||
},
|
||||
methods: {
|
||||
apply() {
|
||||
const v = this.coupons.filter((x) => x.group === this.active)
|
||||
this.viewList = v && v.length ? v : this.coupons.slice(0, 3)
|
||||
},
|
||||
setActive(v) {
|
||||
if (!v || v === this.active) return
|
||||
this.active = v
|
||||
this.apply()
|
||||
},
|
||||
open(id) {
|
||||
const c = this.coupons.find((x) => x.id === id) || this.coupons[0]
|
||||
const payload = encodeURIComponent(JSON.stringify(c))
|
||||
uni.navigateTo({ url: `/pages/orders/detail?payload=${payload}` })
|
||||
},
|
||||
book(c) {
|
||||
uni.navigateTo({ url: `/pages/booking/create?projectId=${c.projectId}` })
|
||||
},
|
||||
openCode(c) {
|
||||
const payload = encodeURIComponent(JSON.stringify(c))
|
||||
uni.navigateTo({ url: `/pages/verify/code?payload=${payload}` })
|
||||
},
|
||||
goProjects() {
|
||||
uni.switchTab({ url: '/pages/projects/list' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filters {
|
||||
padding: 16rpx;
|
||||
}
|
||||
.sc {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip {
|
||||
padding: 14rpx 18rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: #111827;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.on {
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.empty {
|
||||
margin-top: 18rpx;
|
||||
padding: 26rpx;
|
||||
}
|
||||
.e1 {
|
||||
font-size: 36rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.e2 {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.ebtn {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.list {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.item {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.n {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.meta2 {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.ops {
|
||||
margin-top: 16rpx;
|
||||
gap: 14rpx;
|
||||
}
|
||||
.ob {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
}
|
||||
.amt {
|
||||
font-weight: 950;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card hero" @tap="goStore">
|
||||
<view class="row between">
|
||||
<view>
|
||||
<view class="trow">
|
||||
<image v-if="store.logo" class="logo" :src="store.logo" mode="aspectFill" />
|
||||
<view class="title">{{ store.name }}</view>
|
||||
</view>
|
||||
<view class="sub muted">{{ store.openHours }} · {{ store.address }}</view>
|
||||
</view>
|
||||
<view class="pill" @tap.stop="callStore">
|
||||
<text class="p-t">电话</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="locrow row between">
|
||||
<view class="loc muted">{{ locText }} · {{ distText }}</view>
|
||||
<view class="nav" @tap.stop="navToStore">导航</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card banner">
|
||||
<swiper class="sw" circular autoplay interval="3500" duration="500">
|
||||
<swiper-item>
|
||||
<view class="b b1">
|
||||
<view class="b-t">新客体验</view>
|
||||
<view class="b-s">水氧净透 ¥99</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
<swiper-item>
|
||||
<view class="b b2">
|
||||
<view class="b-t">皮肤管理</view>
|
||||
<view class="b-s">补水修护 ¥238</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
<swiper-item>
|
||||
<view class="b b3">
|
||||
<view class="b-t">次卡特惠</view>
|
||||
<view class="b-s">5 次 ¥899</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<view class="grid4">
|
||||
<view class="card q" @tap="goBooking">
|
||||
<view class="q-t">立即预约</view>
|
||||
<view class="q-s muted">选时间/技师</view>
|
||||
</view>
|
||||
<view class="card q" @tap="goProjects">
|
||||
<view class="q-t">全部项目</view>
|
||||
<view class="q-s muted">价格/时长</view>
|
||||
</view>
|
||||
<view class="card q" @tap="goCoupons">
|
||||
<view class="q-t">我的次卡</view>
|
||||
<view class="q-s muted">剩余次数</view>
|
||||
</view>
|
||||
<view class="card q" @tap="goMember">
|
||||
<view class="q-t">会员中心</view>
|
||||
<view class="q-s muted">积分/储值</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section row between">
|
||||
<view class="h">人气推荐项目</view>
|
||||
<view class="more" @tap="goProjects">全部</view>
|
||||
</view>
|
||||
|
||||
<view class="grid2">
|
||||
<ProjectGridCard v-for="p in hotProjects" :key="p.id" :project="p" />
|
||||
</view>
|
||||
|
||||
<view class="card store">
|
||||
<view class="row between">
|
||||
<view class="sh">门店信息</view>
|
||||
<view class="more" @tap="goStore">查看</view>
|
||||
</view>
|
||||
<view class="muted sline">营业时间:{{ store.openHours }}|休息:{{ store.restDay }}</view>
|
||||
<view class="muted sline">地址:{{ store.address }}</view>
|
||||
<view class="muted sline">电话:{{ store.phone }}</view>
|
||||
<view class="row between sline">
|
||||
<view class="muted">技师团队:点击查看</view>
|
||||
<view class="rt">
|
||||
<view class="star">★ {{ teamRating }}</view>
|
||||
<view class="like" @tap.stop="likeTeam">赞 {{ likeCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import ProjectGridCard from '@/components/ProjectGridCard.vue'
|
||||
import { storeProfile, projects } from '@/common/mockData'
|
||||
|
||||
export default {
|
||||
components: { AiFloat, ProjectGridCard },
|
||||
data() {
|
||||
return {
|
||||
store: storeProfile,
|
||||
hotProjects: projects.slice(0, 4),
|
||||
locText: '定位:获取中…',
|
||||
distText: '距离:计算中…',
|
||||
teamRating: 4.9,
|
||||
likeCount: 128
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.locText = '定位:获取中…'
|
||||
this.distText = '距离:计算中…'
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
this.locText = '定位:上海市 静安区(示例)'
|
||||
const d = this.calcDistanceMeter(res.latitude, res.longitude, this.store.latitude, this.store.longitude)
|
||||
this.distText = `距离:${this.formatDistance(d)}`
|
||||
},
|
||||
fail: () => {
|
||||
this.locText = '定位:未开启(可在“我的”页授权定位)'
|
||||
this.distText = '距离:--'
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
goProjects() {
|
||||
uni.switchTab({ url: '/pages/projects/list' })
|
||||
},
|
||||
goBooking() {
|
||||
const p = Array.isArray(projects) && projects.length ? projects[0] : null
|
||||
if (!p) {
|
||||
this.goProjects()
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/booking/create?projectId=${p.id}` })
|
||||
},
|
||||
goCoupons() {
|
||||
uni.navigateTo({ url: '/pages/coupons/list' })
|
||||
},
|
||||
goMember() {
|
||||
uni.switchTab({ url: '/pages/member/index' })
|
||||
},
|
||||
goStore() {
|
||||
uni.navigateTo({ url: '/pages/store/detail' })
|
||||
},
|
||||
callStore() {
|
||||
uni.makePhoneCall({ phoneNumber: this.store.phone })
|
||||
},
|
||||
navToStore() {
|
||||
const lat = this.store.latitude
|
||||
const lng = this.store.longitude
|
||||
if (typeof lat !== 'number' || typeof lng !== 'number') {
|
||||
uni.showToast({ title: '门店坐标未配置', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.openLocation({
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
name: this.store.name,
|
||||
address: this.store.address
|
||||
})
|
||||
},
|
||||
calcDistanceMeter(lat1, lng1, lat2, lng2) {
|
||||
if ([lat1, lng1, lat2, lng2].some((x) => typeof x !== 'number')) return NaN
|
||||
const toRad = (v) => (v * Math.PI) / 180
|
||||
const R = 6371000
|
||||
const dLat = toRad(lat2 - lat1)
|
||||
const dLng = toRad(lng2 - lng1)
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2)
|
||||
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
},
|
||||
formatDistance(m) {
|
||||
if (!Number.isFinite(m)) return '--'
|
||||
if (m < 1000) return `${Math.max(1, Math.round(m))}m`
|
||||
const km = m / 1000
|
||||
if (km < 10) return `${km.toFixed(1)}km`
|
||||
return `${Math.round(km)}km`
|
||||
},
|
||||
likeTeam() {
|
||||
this.likeCount += 1
|
||||
uni.showToast({ title: '已点赞', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hero {
|
||||
padding: 26rpx;
|
||||
}
|
||||
.trow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
.logo {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.loc {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.locrow {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.nav {
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.pill {
|
||||
padding: 14rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
}
|
||||
.p-t {
|
||||
font-weight: 700;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.banner {
|
||||
margin-top: 18rpx;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sw {
|
||||
height: 220rpx;
|
||||
}
|
||||
.b {
|
||||
height: 220rpx;
|
||||
padding: 26rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.b1 {
|
||||
background: linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(59, 130, 246, 1) 100%);
|
||||
}
|
||||
.b2 {
|
||||
background: linear-gradient(135deg, rgba(3, 105, 161, 1) 0%, rgba(16, 185, 129, 1) 100%);
|
||||
}
|
||||
.b3 {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 1) 0%, rgba(59, 130, 246, 1) 100%);
|
||||
}
|
||||
.b-t {
|
||||
color: #fff;
|
||||
font-weight: 950;
|
||||
font-size: 42rpx;
|
||||
}
|
||||
.b-s {
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.84);
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.grid4 {
|
||||
margin-top: 18rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 18rpx;
|
||||
}
|
||||
.q {
|
||||
padding: 18rpx;
|
||||
}
|
||||
.q-t {
|
||||
font-weight: 950;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.q-s {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.section {
|
||||
margin-top: 28rpx;
|
||||
padding: 6rpx 2rpx;
|
||||
}
|
||||
.h {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.more {
|
||||
color: #3b82f6;
|
||||
font-weight: 700;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.grid2 {
|
||||
margin-top: 14rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 18rpx;
|
||||
}
|
||||
.store {
|
||||
margin-top: 18rpx;
|
||||
padding: 22rpx;
|
||||
}
|
||||
.rt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
.star {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
background: rgba(245, 158, 11, 0.14);
|
||||
color: #b45309;
|
||||
}
|
||||
.like {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: rgba(17, 24, 39, 0.86);
|
||||
}
|
||||
.sh {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.sline {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card block">
|
||||
<view class="t">用户隐私协议(示例)</view>
|
||||
<view class="p muted">本页面用于原型演示。正式商用请替换为合规隐私政策全文。</view>
|
||||
<view class="p muted">1. 我们可能会收集:昵称/头像(授权后)、订单与预约信息。</view>
|
||||
<view class="p muted">2. 定位权限仅用于展示附近门店与距离,可在系统设置中关闭。</view>
|
||||
<view class="p muted">3. 我们不会向无关第三方出售你的个人信息。</view>
|
||||
<view class="p muted">4. 你可申请查询、更正、删除个人信息。</view>
|
||||
</view>
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
|
||||
export default {
|
||||
components: { AiFloat }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
}
|
||||
.t {
|
||||
font-size: 36rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.p {
|
||||
margin-top: 14rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card block">
|
||||
<view class="t">服务协议(示例)</view>
|
||||
<view class="p muted">本页面用于原型演示。正式商用请替换为合规服务条款全文。</view>
|
||||
<view class="p muted">1. 用户可通过本小程序进行预约与购买,具体服务以门店实际确认为准。</view>
|
||||
<view class="p muted">2. 预约改期与取消规则以订单页展示为准。</view>
|
||||
<view class="p muted">3. 卡券核销后视为完成服务,相关售后按门店规则执行。</view>
|
||||
<view class="p muted">4. 平台将尽力保障服务可用性,但不对不可抗力导致的中断负责。</view>
|
||||
</view>
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
|
||||
export default {
|
||||
components: { AiFloat }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
}
|
||||
.t {
|
||||
font-size: 36rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.p {
|
||||
margin-top: 14rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card profile">
|
||||
<view class="row between">
|
||||
<view class="row">
|
||||
<image class="avatar" :src="avatarUrl" mode="aspectFill" />
|
||||
<view class="uinfo">
|
||||
<view class="u">微信用户</view>
|
||||
<view class="muted s">会员档案 · {{ tag }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="badge">VIP</view>
|
||||
</view>
|
||||
|
||||
<view class="stats row">
|
||||
<view class="st">
|
||||
<view class="v">{{ points }}</view>
|
||||
<view class="muted l">积分</view>
|
||||
</view>
|
||||
<view class="sep"></view>
|
||||
<view class="st">
|
||||
<view class="v">¥{{ balance }}</view>
|
||||
<view class="muted l">储值</view>
|
||||
</view>
|
||||
<view class="sep"></view>
|
||||
<view class="st">
|
||||
<view class="v">{{ visitCount }}</view>
|
||||
<view class="muted l">到店</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card menu">
|
||||
<view class="cell row between" @tap="goAppointments">
|
||||
<view class="c-l">
|
||||
<view class="c-t">我的预约</view>
|
||||
<view class="muted c-s">待到店 / 改约 / 取消</view>
|
||||
</view>
|
||||
<view class="arr">›</view>
|
||||
</view>
|
||||
<view class="cell row between" @tap="goCoupons">
|
||||
<view class="c-l">
|
||||
<view class="c-t">我的卡券 / 次卡</view>
|
||||
<view class="muted c-s">剩余次数 / 去使用</view>
|
||||
</view>
|
||||
<view class="arr">›</view>
|
||||
</view>
|
||||
<view class="cell row between" @tap="goRecords">
|
||||
<view class="c-l">
|
||||
<view class="c-t">消费记录</view>
|
||||
<view class="muted c-s">近 7 天 / 30 天</view>
|
||||
</view>
|
||||
<view class="arr">›</view>
|
||||
</view>
|
||||
<view class="cell row between" @tap="goSkin">
|
||||
<view class="c-l">
|
||||
<view class="c-t">我的肤质档案</view>
|
||||
<view class="muted c-s">肤质 / 需求 / 建议</view>
|
||||
</view>
|
||||
<view class="arr">›</view>
|
||||
</view>
|
||||
<view class="cell row between" @tap="goMsg">
|
||||
<view class="c-l">
|
||||
<view class="c-t">消息提醒</view>
|
||||
<view class="muted c-s">预约成功 / 到店提醒</view>
|
||||
</view>
|
||||
<view class="arr">›</view>
|
||||
</view>
|
||||
<view class="cell row between" @tap="goStore">
|
||||
<view class="c-l">
|
||||
<view class="c-t">门店设置</view>
|
||||
<view class="muted c-s">地址 / 电话 / 技师团队</view>
|
||||
</view>
|
||||
<view class="arr">›</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">定位与附近流量</view>
|
||||
<view class="muted line">用于附近门店匹配与推荐展示</view>
|
||||
<view class="btn btn-ghost loc" @tap="getLoc">获取定位</view>
|
||||
<view class="muted line" v-if="locText">{{ locText }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card foot">
|
||||
<view class="row between">
|
||||
<view class="muted">版本 {{ version }}</view>
|
||||
<view class="kefu" @tap="call">联系客服</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { storeProfile } from '@/common/mockData'
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
avatarUrl:
|
||||
'https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=photorealistic%20portrait%20avatar%2C%20friendly%20woman%2C%20clean%20background%2C%20soft%20studio%20lighting%2C%20high-end%20beauty%20brand%20style%2C%2035mm%2C%20ultra%20detail&image_size=square',
|
||||
points: 1260,
|
||||
balance: 200,
|
||||
visitCount: 12,
|
||||
tag: '敏感肌 · 高复购',
|
||||
locText: '',
|
||||
version: '1.0.0',
|
||||
store: storeProfile
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goAppointments() {
|
||||
uni.navigateTo({ url: '/pages/appointments/list' })
|
||||
},
|
||||
goCoupons() {
|
||||
uni.navigateTo({ url: '/pages/coupons/list' })
|
||||
},
|
||||
goRecords() {
|
||||
uni.navigateTo({ url: '/pages/records/list' })
|
||||
},
|
||||
goSkin() {
|
||||
uni.navigateTo({ url: '/pages/profile/skin' })
|
||||
},
|
||||
goMsg() {
|
||||
uni.navigateTo({ url: '/pages/messages/settings' })
|
||||
},
|
||||
goStore() {
|
||||
uni.navigateTo({ url: '/pages/store/detail' })
|
||||
},
|
||||
getLoc() {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
this.locText = `已获取:${res.latitude.toFixed(6)}, ${res.longitude.toFixed(6)}`
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '未授权定位', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
call() {
|
||||
uni.makePhoneCall({ phoneNumber: this.store.phone })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile {
|
||||
padding: 26rpx;
|
||||
}
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
}
|
||||
.uinfo {
|
||||
margin-left: 18rpx;
|
||||
}
|
||||
.u {
|
||||
font-size: 38rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.s {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.badge {
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
font-size: 24rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.stats {
|
||||
margin-top: 22rpx;
|
||||
padding: 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.04);
|
||||
}
|
||||
.st {
|
||||
flex: 1;
|
||||
}
|
||||
.v {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.l {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.sep {
|
||||
width: 1rpx;
|
||||
height: 48rpx;
|
||||
background: rgba(17, 24, 39, 0.08);
|
||||
}
|
||||
.menu {
|
||||
margin-top: 18rpx;
|
||||
padding: 10rpx 22rpx;
|
||||
}
|
||||
.cell {
|
||||
padding: 18rpx 0;
|
||||
border-top: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
}
|
||||
.cell:first-of-type {
|
||||
border-top: 0;
|
||||
}
|
||||
.c-t {
|
||||
font-size: 30rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.c-s {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.arr {
|
||||
font-size: 40rpx;
|
||||
color: rgba(17, 24, 39, 0.4);
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.title {
|
||||
font-weight: 950;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.line {
|
||||
padding: 8rpx 0;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.loc {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.foot {
|
||||
margin-top: 18rpx;
|
||||
padding: 18rpx 22rpx;
|
||||
}
|
||||
.kefu {
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
font-size: 26rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card block">
|
||||
<view class="t">消息提醒</view>
|
||||
<view class="muted p">用于接收预约成功、到店提醒、过期提醒等通知。</view>
|
||||
<view class="row between line">
|
||||
<view>预约成功提醒</view>
|
||||
<switch :checked="on1" @change="toggle1" color="#3b82f6" />
|
||||
</view>
|
||||
<view class="row between line">
|
||||
<view>到店提醒</view>
|
||||
<switch :checked="on2" @change="toggle2" color="#3b82f6" />
|
||||
</view>
|
||||
<view class="row between line">
|
||||
<view>卡券过期提醒</view>
|
||||
<switch :checked="on3" @change="toggle3" color="#3b82f6" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="card block">
|
||||
<view class="t">订阅消息</view>
|
||||
<view class="muted p">商用版可在此引导用户订阅微信消息模板(本原型仅展示)。</view>
|
||||
<view class="btn btn-primary" @tap="mockSub">一键订阅(演示)</view>
|
||||
</view>
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
on1: true,
|
||||
on2: true,
|
||||
on3: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle1(e) {
|
||||
this.on1 = !!e.detail.value
|
||||
},
|
||||
toggle2(e) {
|
||||
this.on2 = !!e.detail.value
|
||||
},
|
||||
toggle3(e) {
|
||||
this.on3 = !!e.detail.value
|
||||
},
|
||||
mockSub() {
|
||||
uni.showToast({ title: '已订阅(演示)', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.t {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.p {
|
||||
margin-top: 14rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.line {
|
||||
padding: 18rpx 0;
|
||||
border-top: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
}
|
||||
.line:first-of-type {
|
||||
border-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card block">
|
||||
<view class="row between">
|
||||
<view class="name">{{ vm.project.name }}</view>
|
||||
<view class="price">¥{{ vm.amount }}</view>
|
||||
</view>
|
||||
<view class="muted meta" v-if="vm.type === 'booking'">
|
||||
{{ vm.date }} {{ vm.slot }} · {{ vm.techName }}
|
||||
</view>
|
||||
<view class="muted meta" v-else>{{ vm.planLabel }} · {{ vm.validText }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">订单信息</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">类型</view>
|
||||
<view>{{ vm.type === 'booking' ? '预约订单' : '购买卡券' }}</view>
|
||||
</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">单价</view>
|
||||
<view>¥{{ vm.amount }}</view>
|
||||
</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">数量</view>
|
||||
<view>1</view>
|
||||
</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">合计</view>
|
||||
<view class="sum">¥{{ vm.amount }}</view>
|
||||
</view>
|
||||
<view class="line row between" v-if="vm.note">
|
||||
<view class="muted">备注</view>
|
||||
<view class="note">{{ vm.note }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">规则摘要</view>
|
||||
<view class="muted rline" v-if="vm.type === 'booking'">改约:可在“我的预约”中发起改约(原型演示)。</view>
|
||||
<view class="muted rline" v-if="vm.type === 'booking'">取消:支持取消预约,取消后订单状态变为已取消。</view>
|
||||
<view class="muted rline" v-if="vm.type === 'booking'">爽约:商用版可按门店策略收取爽约金(此处占位)。</view>
|
||||
<view class="muted rline" v-if="vm.type !== 'booking'">有效期:以卡券类型展示为准,过期后不可核销(原型演示)。</view>
|
||||
<view class="muted rline" v-if="vm.type !== 'booking'">核销:到店出示核销码,核销后自动扣次。</view>
|
||||
</view>
|
||||
|
||||
<view class="card block" v-if="vm.type !== 'booking'">
|
||||
<view class="title">订单类型</view>
|
||||
<view class="plans">
|
||||
<view
|
||||
v-for="p in plans"
|
||||
:key="p.key"
|
||||
class="plan"
|
||||
:class="{ on: p.key === vm.planKey }"
|
||||
@tap="selectPlan(p.key)"
|
||||
>
|
||||
<view class="p1">{{ p.label }}</view>
|
||||
<view class="muted p2">{{ p.validText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="muted hint">购买后可在“我的卡券/次卡”里查看,预约后到店核销。</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">支付方式</view>
|
||||
<view class="pay row between">
|
||||
<view class="row">
|
||||
<view class="p-icon">W</view>
|
||||
<view class="p-text">微信支付(原型模拟)</view>
|
||||
</view>
|
||||
<view class="tag">默认</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn btn-primary submit" @tap="mockPay">去支付</view>
|
||||
<view class="btn btn-ghost submit2" @tap="saveUnpaid">先保存为待付款</view>
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { projects } from '@/common/mockData'
|
||||
|
||||
const fallbackProjects = [
|
||||
{
|
||||
id: 'p1',
|
||||
categoryId: 'c1',
|
||||
name: '水氧净透体验',
|
||||
price: 99,
|
||||
originPrice: 199,
|
||||
durationMin: 60,
|
||||
fitFor: '初次体验、暗沉、出油',
|
||||
taboo: '近期激光/微针术后 7 天内不建议',
|
||||
cover: '',
|
||||
desc: '轻盈水氧 + 净透护理,适合快速提升肤感与通透度。'
|
||||
}
|
||||
]
|
||||
|
||||
function safeJsonParse(s) {
|
||||
try {
|
||||
return JSON.parse(s)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
const safeProjects = Array.isArray(projects) && projects.length ? projects : fallbackProjects
|
||||
const initProject = safeProjects[0]
|
||||
const initPlan = { key: 'single', label: '单次券', validText: '有效期 30 天', times: 1, mul: 1 }
|
||||
return {
|
||||
safeProjects,
|
||||
vm: {
|
||||
type: 'coupon',
|
||||
project: initProject,
|
||||
note: '',
|
||||
planKey: initPlan.key,
|
||||
planLabel: initPlan.label,
|
||||
validText: initPlan.validText,
|
||||
times: initPlan.times,
|
||||
amount: initProject ? Math.round(initProject.price * initPlan.mul) : 99
|
||||
},
|
||||
plans: [
|
||||
{ key: 'single', label: '单次券', validText: '有效期 30 天', times: 1, mul: 1 },
|
||||
{ key: 'times5', label: '次卡 5 次', validText: '有效期 180 天', times: 5, mul: 4.2 },
|
||||
{ key: 'package', label: '套餐', validText: '有效期 90 天', times: 3, mul: 2.6 }
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
try {
|
||||
const payload = query.payload ? safeJsonParse(decodeURIComponent(query.payload)) : null
|
||||
const safeProjects = Array.isArray(this.safeProjects) && this.safeProjects.length ? this.safeProjects : fallbackProjects
|
||||
|
||||
if (payload && payload.type === 'booking') {
|
||||
const p = safeProjects.find((x) => x.id === payload.projectId) || safeProjects[0]
|
||||
const amount = p ? p.price : 99
|
||||
this.vm = {
|
||||
type: 'booking',
|
||||
project: p || fallbackProjects[0],
|
||||
amount,
|
||||
planKey: '',
|
||||
planLabel: '预约订单',
|
||||
validText: '',
|
||||
times: 0,
|
||||
date: payload.date || '',
|
||||
slot: payload.slot || '',
|
||||
techId: payload.techId || '',
|
||||
techName: payload.techName || '自动分配',
|
||||
note: payload.note || ''
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const projectId = query.projectId || ''
|
||||
const p = safeProjects.find((x) => x.id === projectId) || safeProjects.find((x) => x.categoryId === 'c5') || safeProjects[0]
|
||||
const basePlanKey = p && p.categoryId === 'c5' ? 'times5' : 'single'
|
||||
const plan = this.plans.find((x) => x.key === basePlanKey) || this.plans[0]
|
||||
const price = p ? p.price : 99
|
||||
const amount = Math.round(price * plan.mul)
|
||||
this.vm = {
|
||||
type: 'coupon',
|
||||
project: p || fallbackProjects[0],
|
||||
note: '',
|
||||
planKey: plan.key,
|
||||
planLabel: plan.label,
|
||||
validText: plan.validText,
|
||||
times: plan.times,
|
||||
amount
|
||||
}
|
||||
} catch (e) {
|
||||
const p = (Array.isArray(this.safeProjects) && this.safeProjects[0]) || fallbackProjects[0]
|
||||
const plan = this.plans[0]
|
||||
this.vm = {
|
||||
type: 'coupon',
|
||||
project: p,
|
||||
note: '',
|
||||
planKey: plan.key,
|
||||
planLabel: plan.label,
|
||||
validText: plan.validText,
|
||||
times: plan.times,
|
||||
amount: Math.round((p ? p.price : 99) * plan.mul)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectPlan(key) {
|
||||
const p = this.plans.find((x) => x.key === key) || this.plans[0]
|
||||
const amount = Math.round(this.vm.project.price * p.mul)
|
||||
this.vm = {
|
||||
...this.vm,
|
||||
planKey: p.key,
|
||||
planLabel: p.label,
|
||||
validText: p.validText,
|
||||
times: p.times,
|
||||
amount
|
||||
}
|
||||
},
|
||||
buildOrder(status) {
|
||||
const now = Date.now()
|
||||
const id = `ord_demo_${now}`
|
||||
const base = {
|
||||
id,
|
||||
createdAt: now,
|
||||
status,
|
||||
amount: this.vm.amount,
|
||||
projectId: this.vm.project.id,
|
||||
projectName: this.vm.project.name,
|
||||
durationMin: this.vm.project.durationMin
|
||||
}
|
||||
if (this.vm.type === 'booking') {
|
||||
return {
|
||||
...base,
|
||||
orderType: 'booking',
|
||||
appointmentDate: this.vm.date,
|
||||
appointmentSlot: this.vm.slot,
|
||||
technicianName: this.vm.techName,
|
||||
note: this.vm.note || '',
|
||||
verifyCode: `VC${now}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
orderType: 'coupon',
|
||||
couponTitle: this.vm.project.name,
|
||||
couponPlanKey: this.vm.planKey,
|
||||
couponPlanLabel: this.vm.planLabel,
|
||||
validText: this.vm.validText,
|
||||
remainingTimes: this.vm.times || 1,
|
||||
verifyCode: `VC${now}`
|
||||
}
|
||||
},
|
||||
mockPay() {
|
||||
const order = this.buildOrder('待核销')
|
||||
const payload = encodeURIComponent(JSON.stringify(order))
|
||||
uni.redirectTo({ url: `/pages/verify/code?payload=${payload}` })
|
||||
},
|
||||
saveUnpaid() {
|
||||
const order = this.buildOrder('待付款')
|
||||
const payload = encodeURIComponent(JSON.stringify(order))
|
||||
uni.redirectTo({ url: `/pages/orders/detail?payload=${payload}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 950;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
.price {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.title {
|
||||
font-weight: 950;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.line {
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
.sum {
|
||||
font-weight: 950;
|
||||
}
|
||||
.note {
|
||||
max-width: 520rpx;
|
||||
text-align: right;
|
||||
}
|
||||
.plans {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
.plan {
|
||||
padding: 14rpx 12rpx;
|
||||
border-radius: 18rpx;
|
||||
border: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
background: rgba(17, 24, 39, 0.03);
|
||||
}
|
||||
.on {
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border-color: rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
.p1 {
|
||||
font-size: 26rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.p2 {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 14rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.rline {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.pay {
|
||||
padding: 14rpx 0;
|
||||
}
|
||||
.p-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
color: #059669;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.p-text {
|
||||
font-weight: 800;
|
||||
}
|
||||
.submit {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.submit2 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card head">
|
||||
<view class="row between">
|
||||
<view
|
||||
class="st"
|
||||
:class="{
|
||||
w: o.status === '待付款',
|
||||
g: o.status === '已完成',
|
||||
d: o.status === '已取消',
|
||||
p: o.status !== '待付款' && o.status !== '已完成' && o.status !== '已取消'
|
||||
}"
|
||||
>
|
||||
{{ o.status }}
|
||||
</view>
|
||||
<view class="muted">订单号 {{ o.id }}</view>
|
||||
</view>
|
||||
<view class="name">{{ o.projectName }}</view>
|
||||
<view class="muted meta">{{ typeLabel(o.orderType) }} · ¥{{ o.amount }}</view>
|
||||
<view class="muted meta" v-if="o.orderType === 'booking'">
|
||||
{{ o.appointmentDate }} {{ o.appointmentSlot }} · {{ o.technicianName }}
|
||||
</view>
|
||||
<view class="muted meta" v-else>剩余次数:{{ o.remainingTimes }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">核销码</view>
|
||||
<view class="code row between">
|
||||
<view class="c">{{ o.verifyCode }}</view>
|
||||
<view class="tag" @tap="copy(o.verifyCode)">复制</view>
|
||||
</view>
|
||||
<view class="muted hint">到店出示核销码,由门店扫码/输码核销(此处为原型演示)。</view>
|
||||
<view class="btn btn-ghost more" @tap="openCode">查看大码</view>
|
||||
</view>
|
||||
|
||||
<view class="card block" v-if="o.note">
|
||||
<view class="title">备注</view>
|
||||
<view class="muted">{{ o.note }}</view>
|
||||
</view>
|
||||
|
||||
<view class="actions">
|
||||
<view v-if="o.status === '待付款'" class="btn btn-primary" @tap="pay">模拟支付</view>
|
||||
<view v-if="o.status === '待核销'" class="btn btn-primary" @tap="verify">模拟核销</view>
|
||||
<view v-if="o.orderType === 'booking' && o.status === '待核销'" class="btn btn-ghost" @tap="reschedule">改约</view>
|
||||
<view v-if="o.status !== '已取消' && o.status !== '已完成'" class="btn btn-ghost" @tap="cancel">取消订单</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { demoOrders } from '@/common/demoOrders'
|
||||
|
||||
function safeJsonParse(s) {
|
||||
try {
|
||||
return JSON.parse(s)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const __DBG_URL = 'http://127.0.0.1:7777/event'
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
order: null
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
//#region debug-point orders-detail-load
|
||||
try {
|
||||
uni.request({
|
||||
url: __DBG_URL,
|
||||
method: 'POST',
|
||||
timeout: 2000,
|
||||
data: {
|
||||
sessionId: 'orders-detail-blank',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'H2',
|
||||
msg: 'orders/detail onLoad',
|
||||
queryKeys: query ? Object.keys(query) : [],
|
||||
payloadLen: query && query.payload ? String(query.payload).length : 0
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
//#endregion debug-point orders-detail-load
|
||||
|
||||
const raw = query.payload ? safeJsonParse(decodeURIComponent(query.payload)) : null
|
||||
if (raw) this.order = raw
|
||||
},
|
||||
computed: {
|
||||
o() {
|
||||
return this.order || demoOrders[0] || {}
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
//#region debug-point orders-detail-ready
|
||||
try {
|
||||
uni.request({
|
||||
url: __DBG_URL,
|
||||
method: 'POST',
|
||||
timeout: 2000,
|
||||
data: {
|
||||
sessionId: 'orders-detail-blank',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'H2',
|
||||
msg: 'orders/detail onReady',
|
||||
hasOrder: !!this.order,
|
||||
oKeys: this.o ? Object.keys(this.o) : []
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
//#endregion debug-point orders-detail-ready
|
||||
},
|
||||
onError(err) {
|
||||
//#region debug-point orders-detail-error
|
||||
try {
|
||||
uni.request({
|
||||
url: __DBG_URL,
|
||||
method: 'POST',
|
||||
timeout: 2000,
|
||||
data: {
|
||||
sessionId: 'orders-detail-blank',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'H2',
|
||||
msg: 'orders/detail onError',
|
||||
err: String(err || '')
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
//#endregion debug-point orders-detail-error
|
||||
},
|
||||
methods: {
|
||||
typeLabel(t) {
|
||||
return t === 'booking' ? '预约订单' : '购买卡券'
|
||||
},
|
||||
openCode() {
|
||||
const payload = encodeURIComponent(JSON.stringify(this.o))
|
||||
uni.navigateTo({ url: `/pages/verify/code?payload=${payload}` })
|
||||
},
|
||||
copy(text) {
|
||||
uni.setClipboardData({ data: text })
|
||||
},
|
||||
pay() {
|
||||
this.order = { ...this.o, status: '待核销', paidAt: Date.now() }
|
||||
uni.showToast({ title: '支付成功(模拟)', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
const payload = encodeURIComponent(JSON.stringify(this.o))
|
||||
uni.navigateTo({ url: `/pages/verify/code?payload=${payload}` })
|
||||
}, 200)
|
||||
},
|
||||
verify() {
|
||||
if (this.o.orderType === 'coupon') {
|
||||
const left = Math.max(0, (this.o.remainingTimes || 0) - 1)
|
||||
this.order = { ...this.o, remainingTimes: left, status: left === 0 ? '已完成' : '待核销' }
|
||||
uni.showToast({ title: left === 0 ? '已核销完成' : '核销成功,已扣次', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.order = { ...this.o, status: '已完成', verifiedAt: Date.now() }
|
||||
uni.showToast({ title: '已核销完成(模拟)', icon: 'none' })
|
||||
},
|
||||
reschedule() {
|
||||
uni.navigateTo({ url: `/pages/booking/create?projectId=${this.o.projectId}` })
|
||||
},
|
||||
cancel() {
|
||||
uni.showModal({
|
||||
title: '确认取消',
|
||||
content: '原型演示:取消后订单状态将变为已取消。',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.order = { ...this.o, status: '已取消', canceledAt: Date.now() }
|
||||
}
|
||||
})
|
||||
},
|
||||
goMember() {
|
||||
uni.switchTab({ url: '/pages/member/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.head {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.st {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.w {
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
color: #b45309;
|
||||
}
|
||||
.p {
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.g {
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
color: #047857;
|
||||
}
|
||||
.d {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #b91c1c;
|
||||
}
|
||||
.name {
|
||||
margin-top: 14rpx;
|
||||
font-size: 38rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.title {
|
||||
font-weight: 950;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.code {
|
||||
padding: 16rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.04);
|
||||
border: 1rpx dashed rgba(17, 24, 39, 0.18);
|
||||
}
|
||||
.c {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.more {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card filters">
|
||||
<scroll-view class="sc" scroll-x>
|
||||
<view class="row">
|
||||
<view
|
||||
v-for="s in statuses"
|
||||
:key="s.value"
|
||||
class="chip"
|
||||
:class="{ on: s.value === activeStatus }"
|
||||
@tap="activeStatus = s.value"
|
||||
>
|
||||
{{ s.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length === 0" class="card empty">
|
||||
<view class="e1">暂无订单</view>
|
||||
<view class="e2 muted">从项目页预约或购买后,会在这里展示。</view>
|
||||
<view class="btn btn-primary ebtn" @tap="goProjects">去看看项目</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="list">
|
||||
<view v-for="o in list" :key="o.id" class="card item" @tap="open(o.id)">
|
||||
<view class="row between">
|
||||
<view class="n">{{ o.projectName }}</view>
|
||||
<view
|
||||
class="st"
|
||||
:class="{
|
||||
w: o.status === '待付款',
|
||||
g: o.status === '已完成',
|
||||
d: o.status === '已取消',
|
||||
p: o.status !== '待付款' && o.status !== '已完成' && o.status !== '已取消'
|
||||
}"
|
||||
>
|
||||
{{ o.status }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="row between meta">
|
||||
<view class="muted">{{ typeLabel(o.orderType) }}</view>
|
||||
<view class="amt">¥{{ o.amount }}</view>
|
||||
</view>
|
||||
<view class="muted meta2" v-if="o.orderType === 'booking'">
|
||||
{{ o.appointmentDate }} {{ o.appointmentSlot }} · {{ o.technicianName }}
|
||||
</view>
|
||||
<view class="muted meta2" v-else>核销码:{{ o.verifyCode }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { demoOrders } from '@/common/demoOrders'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statuses: [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: '待付款', label: '待付款' },
|
||||
{ value: '待核销', label: '待核销' },
|
||||
{ value: '已完成', label: '已完成' },
|
||||
{ value: '已取消', label: '已取消' }
|
||||
],
|
||||
activeStatus: 'all',
|
||||
orders: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
list() {
|
||||
if (this.activeStatus === 'all') return this.orders
|
||||
return this.orders.filter((x) => x.status === this.activeStatus)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.orders = demoOrders
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
const o = this.orders.find((x) => x.id === id) || this.orders[0]
|
||||
const payload = encodeURIComponent(JSON.stringify(o))
|
||||
uni.navigateTo({ url: `/pages/orders/detail?payload=${payload}` })
|
||||
},
|
||||
goProjects() {
|
||||
uni.switchTab({ url: '/pages/projects/list' })
|
||||
},
|
||||
typeLabel(t) {
|
||||
return t === 'booking' ? '预约订单' : '购买卡券'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filters {
|
||||
padding: 16rpx;
|
||||
}
|
||||
.sc {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip {
|
||||
padding: 14rpx 18rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: #111827;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.on {
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.empty {
|
||||
margin-top: 18rpx;
|
||||
padding: 26rpx;
|
||||
}
|
||||
.e1 {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.e2 {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.ebtn {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.list {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.item {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.n {
|
||||
font-size: 32rpx;
|
||||
font-weight: 900;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
.st {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
.w {
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
color: #b45309;
|
||||
}
|
||||
.p {
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.g {
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
color: #047857;
|
||||
}
|
||||
.d {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #b91c1c;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.amt {
|
||||
font-weight: 900;
|
||||
}
|
||||
.meta2 {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card block">
|
||||
<view class="t">我的肤质档案</view>
|
||||
<view class="muted p">肤质:敏感偏干</view>
|
||||
<view class="muted p">关注:补水修护、减少泛红</view>
|
||||
<view class="muted p">过敏史:无(示例)</view>
|
||||
<view class="muted p">备注:本页为原型演示,商用版可由门店持续更新</view>
|
||||
</view>
|
||||
<view class="card block">
|
||||
<view class="t">建议</view>
|
||||
<view class="muted p">优先选择:补水修护、舒缓敏感类项目</view>
|
||||
<view class="muted p">避免:爆痘炎症期的强刺激清洁项目</view>
|
||||
</view>
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
|
||||
export default {
|
||||
components: { AiFloat }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.t {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.p {
|
||||
margin-top: 14rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view>
|
||||
<view class="card head">
|
||||
<image v-if="p.cover" class="cover" :src="p.cover" mode="aspectFill" />
|
||||
<view class="name">{{ p.name }}</view>
|
||||
<view class="row between meta">
|
||||
<view class="muted">服务时长:{{ p.durationMin }} 分钟</view>
|
||||
<view class="price">
|
||||
<text class="yen">¥</text>
|
||||
<text class="num">{{ p.price }}</text>
|
||||
<text class="ori muted" v-if="p.originPrice">¥{{ p.originPrice }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="row between meta2">
|
||||
<view class="tag2">{{ categoryName }}</view>
|
||||
<view class="muted">评分 {{ rating }} / 5.0</view>
|
||||
</view>
|
||||
<view class="tags row">
|
||||
<view class="tag">{{ p.fitFor }}</view>
|
||||
<view class="tag warn">禁忌:{{ p.taboo }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="b-t">服务介绍</view>
|
||||
<view class="b-v muted">{{ p.desc }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="b-t">功效说明</view>
|
||||
<view class="b-v muted" v-for="x in effects" :key="x">· {{ x }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="b-t">服务流程</view>
|
||||
<view class="b-v muted" v-for="x in steps" :key="x">· {{ x }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="b-t">适合人群</view>
|
||||
<view class="b-v muted">{{ p.fitFor }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="b-t">禁忌说明</view>
|
||||
<view class="b-v muted">{{ p.taboo }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="b-t">用户评价</view>
|
||||
<view class="review">
|
||||
<view class="r1 row between">
|
||||
<view class="rn">顾客 A</view>
|
||||
<view class="muted">5.0</view>
|
||||
</view>
|
||||
<view class="muted rt">做完肤感很通透,过程舒服,推荐。</view>
|
||||
</view>
|
||||
<view class="review">
|
||||
<view class="r1 row between">
|
||||
<view class="rn">顾客 B</view>
|
||||
<view class="muted">4.8</view>
|
||||
</view>
|
||||
<view class="muted rt">店里环境很干净,技师很专业。</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="space"></view>
|
||||
|
||||
<view class="fixbar">
|
||||
<view class="bar card">
|
||||
<view class="btn btn-ghost a" @tap="book">立即预约</view>
|
||||
<view class="btn btn-primary b" @tap="buy">立即购买</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { categories, projects } from '@/common/mockData'
|
||||
|
||||
const fallbackCategories = [
|
||||
{ id: 'c1', name: '体验爆款' },
|
||||
{ id: 'c2', name: '面部护理' },
|
||||
{ id: 'c3', name: '身体养生' },
|
||||
{ id: 'c4', name: '美甲美睫' },
|
||||
{ id: 'c5', name: '特惠套餐' }
|
||||
]
|
||||
|
||||
const fallbackProjects = [
|
||||
{
|
||||
id: 'p1',
|
||||
categoryId: 'c1',
|
||||
name: '水氧净透体验',
|
||||
price: 99,
|
||||
originPrice: 199,
|
||||
durationMin: 60,
|
||||
fitFor: '初次体验、暗沉、出油',
|
||||
taboo: '近期激光/微针术后 7 天内不建议',
|
||||
cover: '',
|
||||
desc: '轻盈水氧 + 净透护理,适合快速提升肤感与通透度。'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
categoryId: 'c2',
|
||||
name: '深层清洁黑头管理',
|
||||
price: 168,
|
||||
originPrice: 268,
|
||||
durationMin: 75,
|
||||
fitFor: 'T 区油脂旺盛、黑头粉刺',
|
||||
taboo: '炎症爆痘期需评估后进行',
|
||||
cover: '',
|
||||
desc: '清洁、舒缓、收敛三步走,减少反复出油与毛孔困扰。'
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
categoryId: 'c2',
|
||||
name: '补水修护屏障护理',
|
||||
price: 238,
|
||||
originPrice: 368,
|
||||
durationMin: 80,
|
||||
fitFor: '敏感泛红、干燥紧绷',
|
||||
taboo: '过敏急性期请先咨询',
|
||||
cover: '',
|
||||
desc: '修护屏障与舒缓敏感,适合换季与长期干燥人群。'
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
categoryId: 'c3',
|
||||
name: '肩颈舒缓筋膜放松',
|
||||
price: 188,
|
||||
originPrice: 288,
|
||||
durationMin: 60,
|
||||
fitFor: '久坐办公、肩颈僵硬',
|
||||
taboo: '急性损伤与发热期不建议',
|
||||
cover: '',
|
||||
desc: '深度放松肌群与筋膜,改善紧绷与酸胀。'
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
categoryId: 'c5',
|
||||
name: '皮肤管理次卡 5 次',
|
||||
price: 899,
|
||||
originPrice: 1199,
|
||||
durationMin: 60,
|
||||
fitFor: '长期管理、复购人群',
|
||||
taboo: '具体项目以到店评估为准',
|
||||
cover: '',
|
||||
desc: '灵活使用,随时预约,到店核销自动扣次。'
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
categoryId: 'c4',
|
||||
name: '轻奢美甲 · 单色',
|
||||
price: 168,
|
||||
originPrice: 238,
|
||||
durationMin: 75,
|
||||
fitFor: '通勤、日常、显白',
|
||||
taboo: '甲面破损/感染需先处理',
|
||||
cover: '',
|
||||
desc: '干净利落的通勤单色,显白耐看,可按肤色搭配色卡。'
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
categoryId: 'c4',
|
||||
name: '自然单根美睫 · 清透款',
|
||||
price: 198,
|
||||
originPrice: 298,
|
||||
durationMin: 90,
|
||||
fitFor: '自然放大双眼、日常耐看',
|
||||
taboo: '眼部炎症/过敏期不建议',
|
||||
cover: '',
|
||||
desc: '清透自然的单根嫁接,整体更轻盈,适合新手与通勤。'
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
categoryId: 'c3',
|
||||
name: '全身精油舒压 · 90 分钟',
|
||||
price: 298,
|
||||
originPrice: 398,
|
||||
durationMin: 90,
|
||||
fitFor: '压力大、睡眠欠佳、疲劳',
|
||||
taboo: '孕期/发热/急性炎症期不建议',
|
||||
cover: '',
|
||||
desc: '精油舒压与深度放松结合,帮助缓解疲劳与紧绷。'
|
||||
},
|
||||
{
|
||||
id: 'p9',
|
||||
categoryId: 'c5',
|
||||
name: '新客体验套餐 · 3 次',
|
||||
price: 299,
|
||||
originPrice: 499,
|
||||
durationMin: 60,
|
||||
fitFor: '初次体验、想要快速改善肤感',
|
||||
taboo: '具体项目以到店评估为准',
|
||||
cover: '',
|
||||
desc: '高性价比新客套餐,适合建立基础皮肤管理节奏。'
|
||||
}
|
||||
]
|
||||
|
||||
const runtimeProjects = Array.isArray(projects) && projects.length ? projects : fallbackProjects
|
||||
const runtimeCategories = Array.isArray(categories) && categories.length ? categories : fallbackCategories
|
||||
|
||||
const detailPreset = {
|
||||
p1: {
|
||||
rating: 5.0,
|
||||
effects: ['净透提亮肤感', '补水保湿', '舒缓修护'],
|
||||
steps: ['洁面清洁', '水氧净透护理', '精华导入', '舒缓收尾与防护']
|
||||
},
|
||||
p2: {
|
||||
rating: 4.8,
|
||||
effects: ['减少黑头粉刺困扰', '清理油脂与角质', '舒缓收敛毛孔观感'],
|
||||
steps: ['皮肤评估与卸妆', '深层清洁与导出', '舒缓镇定', '收尾修护']
|
||||
},
|
||||
p3: {
|
||||
rating: 4.9,
|
||||
effects: ['补水修护屏障', '舒缓敏感泛红', '改善干燥紧绷'],
|
||||
steps: ['温和清洁', '舒缓修护导入', '补水面膜', '收尾锁水防护']
|
||||
},
|
||||
p4: {
|
||||
rating: 4.8,
|
||||
effects: ['放松肩颈肌群', '改善紧绷酸胀', '提升舒适度与精神状态'],
|
||||
steps: ['热敷放松', '筋膜松解', '肩颈重点放松', '收尾舒缓']
|
||||
},
|
||||
p5: {
|
||||
rating: 4.9,
|
||||
effects: ['长期皮肤管理更划算', '支持随时预约使用', '到店核销自动扣次'],
|
||||
steps: ['购买次卡', '在线预约', '到店出示核销码', '门店核销扣次']
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
id: '',
|
||||
project: runtimeProjects[0] || null,
|
||||
rating: 4.9,
|
||||
effects: ['效果以到店评估为准', '体验舒适、过程规范', '可按肤质做个性化调整'],
|
||||
steps: ['到店评估', '护理服务', '舒缓收尾', '给出居家建议'],
|
||||
categoryName: runtimeCategories[0]?.name || '项目'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
p() {
|
||||
return (
|
||||
this.project ||
|
||||
runtimeProjects[0] ||
|
||||
fallbackProjects[0] || {
|
||||
id: 'p1',
|
||||
categoryId: 'c1',
|
||||
name: '水氧净透体验',
|
||||
price: 99,
|
||||
originPrice: 199,
|
||||
durationMin: 60,
|
||||
fitFor: '初次体验、暗沉、出油',
|
||||
taboo: '近期激光/微针术后 7 天内不建议',
|
||||
cover: '',
|
||||
desc: '轻盈水氧 + 净透护理,适合快速提升肤感与通透度。'
|
||||
}
|
||||
)
|
||||
},
|
||||
displayProject() {
|
||||
return this.project || runtimeProjects[0] || null
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
this.id = query.id || query.projectId || ''
|
||||
if (!this.id && runtimeProjects.length) this.id = runtimeProjects[0].id
|
||||
this.project = runtimeProjects.find((x) => x.id === this.id) || runtimeProjects[0] || null
|
||||
const cat = this.p ? runtimeCategories.find((c) => c.id === this.p.categoryId) : null
|
||||
this.categoryName = cat ? cat.name : '项目'
|
||||
const p = detailPreset[this.id] || null
|
||||
this.rating = p ? p.rating : 4.9
|
||||
this.effects = p ? p.effects : ['效果以到店评估为准', '体验舒适、过程规范', '可按肤质做个性化调整']
|
||||
this.steps = p ? p.steps : ['到店评估', '护理服务', '舒缓收尾', '给出居家建议']
|
||||
},
|
||||
methods: {
|
||||
book() {
|
||||
uni.navigateTo({ url: `/pages/booking/create?projectId=${this.p.id}` })
|
||||
},
|
||||
buy() {
|
||||
uni.navigateTo({ url: `/pages/order/confirm?type=coupon&projectId=${this.p.id}` })
|
||||
},
|
||||
goList() {
|
||||
uni.switchTab({ url: '/pages/projects/list' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.head {
|
||||
padding: 26rpx;
|
||||
}
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
border-radius: 18rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.name {
|
||||
font-size: 42rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.meta2 {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.yen {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.num {
|
||||
font-size: 46rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.ori {
|
||||
margin-left: 10rpx;
|
||||
font-size: 24rpx;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.tag2 {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
font-weight: 900;
|
||||
}
|
||||
.tags {
|
||||
margin-top: 18rpx;
|
||||
gap: 12rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tag {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: #111827;
|
||||
}
|
||||
.warn {
|
||||
background: rgba(245, 158, 11, 0.14);
|
||||
color: #b45309;
|
||||
}
|
||||
.block {
|
||||
margin-top: 18rpx;
|
||||
padding: 22rpx;
|
||||
}
|
||||
.b-t {
|
||||
font-weight: 950;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.b-v {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.review {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
}
|
||||
.review:first-of-type {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
.r1 {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.rn {
|
||||
font-size: 28rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.rt {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.space {
|
||||
height: 150rpx;
|
||||
}
|
||||
.empty {
|
||||
margin-top: 18rpx;
|
||||
padding: 34rpx 26rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.en {
|
||||
font-size: 36rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.et {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.eb {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.fixbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 18rpx 24rpx 24rpx;
|
||||
background: linear-gradient(180deg, rgba(246, 247, 251, 0) 0%, rgba(246, 247, 251, 1) 46%);
|
||||
}
|
||||
.bar {
|
||||
padding: 18rpx;
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
}
|
||||
.a {
|
||||
flex: 1;
|
||||
height: 82rpx;
|
||||
}
|
||||
.b {
|
||||
flex: 1;
|
||||
height: 82rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card search">
|
||||
<view class="row between">
|
||||
<input class="ipt" v-model="kw" placeholder="搜索项目:补水/清洁/肩颈…" confirm-type="search" />
|
||||
<view class="sbtn" @tap="kw = ''">清空</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card filters">
|
||||
<scroll-view class="sc" scroll-x>
|
||||
<view class="row">
|
||||
<view
|
||||
v-for="c in allCategories"
|
||||
:key="c.id"
|
||||
class="chip"
|
||||
:class="{ on: c.id === activeCategoryId }"
|
||||
@tap="setCategory(c.id)"
|
||||
>
|
||||
{{ c.name }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="list">
|
||||
<ProjectCard v-for="p in filteredProjects" :key="p.id" :project="p" class="mb" />
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import ProjectCard from '@/components/ProjectCard.vue'
|
||||
import { categories, projects } from '@/common/mockData'
|
||||
|
||||
export default {
|
||||
components: { ProjectCard, AiFloat },
|
||||
data() {
|
||||
return {
|
||||
allCategories: [{ id: 'all', name: '全部' }, ...categories],
|
||||
activeCategoryId: 'all',
|
||||
kw: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredProjects() {
|
||||
const kw = (this.kw || '').trim()
|
||||
const list = this.activeCategoryId === 'all' ? projects : projects.filter((p) => p.categoryId === this.activeCategoryId)
|
||||
if (!kw) return list
|
||||
return list.filter((p) => `${p.name}${p.fitFor}${p.desc}`.includes(kw))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCategory(id) {
|
||||
this.activeCategoryId = id
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search {
|
||||
padding: 16rpx;
|
||||
}
|
||||
.ipt {
|
||||
flex: 1;
|
||||
height: 76rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.05);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.sbtn {
|
||||
margin-left: 12rpx;
|
||||
padding: 18rpx 14rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
.filters {
|
||||
margin-top: 16rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
.sc {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip {
|
||||
padding: 14rpx 18rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: #111827;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.on {
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.list {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.mb {
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card filters">
|
||||
<view class="row between">
|
||||
<view class="title">消费记录</view>
|
||||
<view class="row">
|
||||
<view class="chip2" :class="{ on: range === 7 }" @tap="setRange(7)">近7天</view>
|
||||
<view class="chip2" :class="{ on: range === 30 }" @tap="setRange(30)">近30天</view>
|
||||
<view class="chip2" :class="{ on: range === 0 }" @tap="setRange(0)">全部</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!viewList || !viewList.length" class="card empty">
|
||||
<view class="btn btn-primary ebtn" @tap="goProjects">去看看项目</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="list">
|
||||
<view v-for="o in viewList" :key="o.id" class="card item" @tap="open(o.id)">
|
||||
<view class="row between">
|
||||
<view class="n">{{ o.projectName }}</view>
|
||||
<view
|
||||
class="st"
|
||||
:class="{
|
||||
w: o.status === '待付款',
|
||||
g: o.status === '已完成',
|
||||
d: o.status === '已取消',
|
||||
p: o.status !== '待付款' && o.status !== '已完成' && o.status !== '已取消'
|
||||
}"
|
||||
>
|
||||
{{ o.status }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="row between meta">
|
||||
<view class="muted">{{ o.orderType === 'booking' ? '预约' : '购买' }}</view>
|
||||
<view class="amt">¥{{ o.amount }}</view>
|
||||
</view>
|
||||
<view class="muted meta2">{{ fmt(o.createdAt) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { demoOrders } from '@/common/demoOrders'
|
||||
|
||||
function tsToText(ts) {
|
||||
const d = new Date(ts)
|
||||
const y = d.getFullYear()
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const dd = `${d.getDate()}`.padStart(2, '0')
|
||||
const hh = `${d.getHours()}`.padStart(2, '0')
|
||||
const mm = `${d.getMinutes()}`.padStart(2, '0')
|
||||
return `${y}-${m}-${dd} ${hh}:${mm}`
|
||||
}
|
||||
|
||||
function buildOrders() {
|
||||
const now = Date.now()
|
||||
const extra = [
|
||||
{
|
||||
id: 'rc_demo_001',
|
||||
createdAt: now - 2 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
amount: 99,
|
||||
projectId: 'p1',
|
||||
projectName: '水氧净透体验',
|
||||
durationMin: 60,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-21',
|
||||
appointmentSlot: '12:30',
|
||||
technicianName: '系统分配',
|
||||
note: '',
|
||||
verifyCode: 'VCRC20260621001'
|
||||
},
|
||||
{
|
||||
id: 'rc_demo_002',
|
||||
createdAt: now - 20 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
amount: 168,
|
||||
projectId: 'p2',
|
||||
projectName: '深层清洁黑头管理',
|
||||
durationMin: 75,
|
||||
orderType: 'coupon',
|
||||
couponTitle: '深层清洁黑头管理',
|
||||
couponPlanKey: 'single',
|
||||
couponPlanLabel: '单次券',
|
||||
validText: '有效期 30 天',
|
||||
remainingTimes: 0,
|
||||
verifyCode: 'VCRC20260621002'
|
||||
},
|
||||
{
|
||||
id: 'rc_demo_003',
|
||||
createdAt: now - 5 * 24 * 60 * 60 * 1000,
|
||||
status: '已取消',
|
||||
amount: 238,
|
||||
projectId: 'p3',
|
||||
projectName: '补水修护屏障护理',
|
||||
durationMin: 80,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-06-18',
|
||||
appointmentSlot: '15:00',
|
||||
technicianName: '许言',
|
||||
note: '',
|
||||
verifyCode: 'VCRC20260621003'
|
||||
},
|
||||
{
|
||||
id: 'rc_demo_004',
|
||||
createdAt: now - 12 * 24 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
amount: 899,
|
||||
projectId: 'p5',
|
||||
projectName: '皮肤管理次卡 5 次',
|
||||
durationMin: 60,
|
||||
orderType: 'coupon',
|
||||
couponTitle: '皮肤管理次卡 5 次',
|
||||
couponPlanKey: 'times5',
|
||||
couponPlanLabel: '次卡 5 次',
|
||||
validText: '有效期 180 天',
|
||||
remainingTimes: 4,
|
||||
verifyCode: 'VCRC20260621004'
|
||||
},
|
||||
{
|
||||
id: 'rc_demo_005',
|
||||
createdAt: now - 35 * 24 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
amount: 188,
|
||||
projectId: 'p4',
|
||||
projectName: '肩颈舒缓筋膜放松',
|
||||
durationMin: 60,
|
||||
orderType: 'booking',
|
||||
appointmentDate: '2026-05-17',
|
||||
appointmentSlot: '19:00',
|
||||
technicianName: '周晴',
|
||||
note: '',
|
||||
verifyCode: 'VCRC20260621005'
|
||||
},
|
||||
{
|
||||
id: 'rc_demo_006',
|
||||
createdAt: now - 62 * 24 * 60 * 60 * 1000,
|
||||
status: '已完成',
|
||||
amount: 299,
|
||||
projectId: 'p9',
|
||||
projectName: '新客体验套餐 · 3 次',
|
||||
durationMin: 60,
|
||||
orderType: 'coupon',
|
||||
couponTitle: '新客体验套餐 · 3 次',
|
||||
couponPlanKey: 'package',
|
||||
couponPlanLabel: '套餐',
|
||||
validText: '有效期 90 天',
|
||||
remainingTimes: 2,
|
||||
verifyCode: 'VCRC20260621006'
|
||||
}
|
||||
]
|
||||
|
||||
return [...demoOrders, ...extra].slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
range: 30,
|
||||
orders: buildOrders(),
|
||||
viewList: []
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.apply()
|
||||
},
|
||||
onShow() {
|
||||
this.orders = buildOrders()
|
||||
this.apply()
|
||||
},
|
||||
methods: {
|
||||
apply() {
|
||||
if (this.range === 0) {
|
||||
this.viewList = this.orders && this.orders.length ? this.orders : []
|
||||
return
|
||||
}
|
||||
const from = Date.now() - this.range * 24 * 60 * 60 * 1000
|
||||
this.viewList = this.orders.filter((x) => (x.createdAt || 0) >= from)
|
||||
if (!this.viewList || !this.viewList.length) this.viewList = this.orders.slice(0, 6)
|
||||
},
|
||||
setRange(n) {
|
||||
this.range = n
|
||||
this.apply()
|
||||
},
|
||||
open(id) {
|
||||
const o = this.orders.find((x) => x.id === id) || this.orders[0]
|
||||
const payload = encodeURIComponent(JSON.stringify(o))
|
||||
uni.navigateTo({ url: `/pages/orders/detail?payload=${payload}` })
|
||||
},
|
||||
goProjects() {
|
||||
uni.switchTab({ url: '/pages/projects/list' })
|
||||
},
|
||||
fmt(ts) {
|
||||
return ts ? tsToText(ts) : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filters {
|
||||
padding: 18rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.chip2 {
|
||||
padding: 10rpx 14rpx;
|
||||
margin-left: 10rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: #111827;
|
||||
}
|
||||
.on {
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.empty {
|
||||
margin-top: 18rpx;
|
||||
padding: 26rpx;
|
||||
}
|
||||
.e1 {
|
||||
font-size: 36rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.e2 {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.ebtn {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.list {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.item {
|
||||
padding: 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
.n {
|
||||
font-size: 32rpx;
|
||||
font-weight: 950;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
.st {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
.w {
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
color: #b45309;
|
||||
}
|
||||
.p {
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.g {
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
color: #047857;
|
||||
}
|
||||
.d {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #b91c1c;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.amt {
|
||||
font-weight: 950;
|
||||
}
|
||||
.meta2 {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card hero">
|
||||
<view class="name">{{ store.name }}</view>
|
||||
<view class="muted sub">{{ store.address }}</view>
|
||||
<view class="row between meta">
|
||||
<view class="tag">营业 {{ store.openHours }}</view>
|
||||
<view class="tag" @tap="call">电话</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">门店信息</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">地址</view>
|
||||
<view class="val" @tap="copy(store.address)">复制</view>
|
||||
</view>
|
||||
<view class="muted tip">{{ store.address }}</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">电话</view>
|
||||
<view class="val" @tap="call">{{ store.phone }}</view>
|
||||
</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">营业时间</view>
|
||||
<view class="val">{{ store.openHours }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="title">技师团队</view>
|
||||
<view class="tech" v-for="t in techs" :key="t.id">
|
||||
<view class="row between">
|
||||
<view>
|
||||
<view class="tname">{{ t.name }}</view>
|
||||
<view class="muted tsub">{{ t.title }}</view>
|
||||
</view>
|
||||
<view class="tags">
|
||||
<text class="tag2" v-for="g in t.tags" :key="g">{{ g }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { storeProfile, technicians } from '@/common/mockData'
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
store: storeProfile,
|
||||
techs: technicians
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
call() {
|
||||
uni.makePhoneCall({ phoneNumber: this.store.phone })
|
||||
},
|
||||
copy(text) {
|
||||
uni.setClipboardData({ data: text })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hero {
|
||||
padding: 26rpx;
|
||||
}
|
||||
.name {
|
||||
font-size: 42rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.block {
|
||||
margin-top: 18rpx;
|
||||
padding: 22rpx;
|
||||
}
|
||||
.title {
|
||||
font-weight: 950;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.line {
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
.val {
|
||||
color: #1d4ed8;
|
||||
font-weight: 900;
|
||||
}
|
||||
.tip {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.tech {
|
||||
padding: 16rpx 0;
|
||||
border-top: 1rpx solid rgba(17, 24, 39, 0.08);
|
||||
}
|
||||
.tech:first-of-type {
|
||||
border-top: 0;
|
||||
}
|
||||
.tname {
|
||||
font-size: 30rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.tsub {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
max-width: 280rpx;
|
||||
}
|
||||
.tag2 {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="card hero">
|
||||
<view class="t1">支付成功</view>
|
||||
<view class="t2 muted">已为你生成订单,支持随时预约/到店核销</view>
|
||||
<view class="code">{{ o.verifyCode }}</view>
|
||||
<view class="row between info">
|
||||
<view class="muted">{{ o.projectName }}</view>
|
||||
<view class="amt">¥{{ o.amount }}</view>
|
||||
</view>
|
||||
<view class="muted info2" v-if="o.orderType === 'coupon'">
|
||||
{{ o.couponPlanLabel || '卡券' }} · {{ o.validText || '有效期以到店确认' }} · 剩余次数:{{ o.remainingTimes }}
|
||||
</view>
|
||||
<view class="muted info2" v-else>预约:{{ o.appointmentDate }} {{ o.appointmentSlot }}</view>
|
||||
</view>
|
||||
|
||||
<view class="card block">
|
||||
<view class="line row between">
|
||||
<view class="muted">订单状态</view>
|
||||
<view class="tag">{{ o.status }}</view>
|
||||
</view>
|
||||
<view class="line row between">
|
||||
<view class="muted">核销方式</view>
|
||||
<view>门店扫码 / 手动输码</view>
|
||||
</view>
|
||||
<view class="line row between" v-if="o.validText && o.orderType === 'coupon'">
|
||||
<view class="muted">有效期</view>
|
||||
<view>{{ o.validText }}</view>
|
||||
</view>
|
||||
<view class="line row between" v-if="o.orderType === 'booking'">
|
||||
<view class="muted">技师</view>
|
||||
<view>{{ o.technicianName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn btn-primary" @tap="goBooking">去预约</view>
|
||||
<view class="btn btn-ghost" @tap="goOrders">查看订单</view>
|
||||
<view class="btn btn-ghost" @tap="goMember">返回个人中心</view>
|
||||
|
||||
<AiFloat />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AiFloat from '@/components/AiFloat.vue'
|
||||
import { demoOrders } from '@/common/demoOrders'
|
||||
|
||||
function safeJsonParse(s) {
|
||||
try {
|
||||
return JSON.parse(s)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const __DBG_URL = 'http://127.0.0.1:7777/event'
|
||||
|
||||
export default {
|
||||
components: { AiFloat },
|
||||
data() {
|
||||
return {
|
||||
order: null
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
//#region debug-point verify-code-load
|
||||
try {
|
||||
uni.request({
|
||||
url: __DBG_URL,
|
||||
method: 'POST',
|
||||
timeout: 2000,
|
||||
data: {
|
||||
sessionId: 'orders-detail-blank',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'H2',
|
||||
msg: 'verify/code onLoad',
|
||||
queryKeys: query ? Object.keys(query) : [],
|
||||
payloadLen: query && query.payload ? String(query.payload).length : 0
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
//#endregion debug-point verify-code-load
|
||||
|
||||
const raw = query.payload ? safeJsonParse(decodeURIComponent(query.payload)) : null
|
||||
if (raw) this.order = raw
|
||||
},
|
||||
computed: {
|
||||
o() {
|
||||
const defaultCoupon = Array.isArray(demoOrders) ? demoOrders.find((x) => x && x.orderType === 'coupon') : null
|
||||
return (
|
||||
this.order ||
|
||||
defaultCoupon ||
|
||||
demoOrders[0] || {
|
||||
id: 'ord_demo_fallback',
|
||||
status: '待核销',
|
||||
amount: 99,
|
||||
projectId: 'p1',
|
||||
projectName: '水氧净透体验',
|
||||
durationMin: 60,
|
||||
orderType: 'coupon',
|
||||
couponPlanLabel: '单次券',
|
||||
validText: '有效期以到店确认',
|
||||
remainingTimes: 1,
|
||||
verifyCode: 'VC20260622000'
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBooking() {
|
||||
const o = this.o
|
||||
//#region debug-point verify-code-goBooking
|
||||
try {
|
||||
uni.request({
|
||||
url: __DBG_URL,
|
||||
method: 'POST',
|
||||
timeout: 2000,
|
||||
data: {
|
||||
sessionId: 'orders-detail-blank',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'H2',
|
||||
msg: 'verify/code goBooking',
|
||||
projectId: o && o.projectId ? String(o.projectId) : ''
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
//#endregion debug-point verify-code-goBooking
|
||||
uni.navigateTo({ url: `/pages/booking/create?projectId=${o.projectId}` })
|
||||
},
|
||||
goOrders() {
|
||||
//#region debug-point verify-code-goOrders
|
||||
try {
|
||||
uni.request({
|
||||
url: __DBG_URL,
|
||||
method: 'POST',
|
||||
timeout: 2000,
|
||||
data: {
|
||||
sessionId: 'orders-detail-blank',
|
||||
runId: 'pre-fix',
|
||||
hypothesisId: 'H2',
|
||||
msg: 'verify/code goOrders',
|
||||
orderId: this.o && this.o.id ? String(this.o.id) : ''
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
//#endregion debug-point verify-code-goOrders
|
||||
const payload = encodeURIComponent(JSON.stringify(this.o))
|
||||
uni.navigateTo({ url: `/pages/orders/detail?payload=${payload}` })
|
||||
},
|
||||
goMember() {
|
||||
uni.switchTab({ url: '/pages/member/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hero {
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(59, 130, 246, 1) 100%);
|
||||
border: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.t1 {
|
||||
font-size: 42rpx;
|
||||
font-weight: 950;
|
||||
}
|
||||
.t2 {
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.code {
|
||||
margin-top: 22rpx;
|
||||
padding: 22rpx;
|
||||
border-radius: 22rpx;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1rpx dashed rgba(255, 255, 255, 0.42);
|
||||
font-size: 50rpx;
|
||||
font-weight: 950;
|
||||
letter-spacing: 2rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.info {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.amt {
|
||||
font-weight: 950;
|
||||
}
|
||||
.info2 {
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.block {
|
||||
margin-top: 18rpx;
|
||||
padding: 22rpx;
|
||||
}
|
||||
.line {
|
||||
padding: 14rpx 0;
|
||||
}
|
||||
.tag {
|
||||
padding: 10rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user