426 lines
11 KiB
Vue
426 lines
11 KiB
Vue
<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>
|
||
|