初始化

This commit is contained in:
leiking
2026-06-29 10:54:33 +08:00
parent 761cee968e
commit 4983006317
156 changed files with 25687 additions and 0 deletions
+425
View File
@@ -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>