Files
2026-06-29 10:54:33 +08:00

426 lines
11 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>