第一个版本
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 景区基础信息卡片 -->
|
||||
<view class="scenic-card">
|
||||
<image class="scenic-img" :src="scenic.image" mode="aspectFill" />
|
||||
<view class="scenic-info">
|
||||
@@ -13,11 +12,10 @@
|
||||
<view class="scenic-icon scenic-icon--clock" />
|
||||
<text class="scenic-text">{{ scenic.hours }}</text>
|
||||
</view>
|
||||
<text class="scenic-tip">持赣湘全域旅游年卡免费入园,单人单日仅可预约1次</text>
|
||||
<text class="scenic-tip">持{{ cardModelName }}免费入园,单人单日仅可预约1次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期选择模块 -->
|
||||
<view class="date-card">
|
||||
<text class="section-title">选择预约日期</text>
|
||||
<scroll-view scroll-x class="date-scroll">
|
||||
@@ -27,19 +25,21 @@
|
||||
:key="d.key"
|
||||
class="date-item"
|
||||
:class="{
|
||||
'date-item--active': selectedDate === d.key && d.stock > 0,
|
||||
'date-item--disabled': d.stock <= 0
|
||||
'date-item--active': selectedDate === d.key,
|
||||
'date-item--disabled': d.disabled || (d.stock != null && d.stock <= 0)
|
||||
}"
|
||||
@tap="d.stock > 0 && (selectedDate = d.key)"
|
||||
@tap="!(d.disabled || (d.stock != null && d.stock <= 0)) && (selectedDate = d.key)"
|
||||
>
|
||||
<text class="date-week">{{ d.week }}</text>
|
||||
<text class="date-day">{{ d.label }}</text>
|
||||
<text v-if="d.stock > 0" class="date-stock">余{{ d.stock }}位</text>
|
||||
<text v-if="d.disabled" class="date-stock date-stock--empty">不可约</text>
|
||||
<text v-else-if="d.stock == null" class="date-stock">无限库存</text>
|
||||
<text v-else-if="d.stock >= 50" class="date-stock">可预约</text>
|
||||
<text v-else-if="d.stock > 0" class="date-stock">余{{ d.stock }}位</text>
|
||||
<text v-else class="date-stock date-stock--empty">已约满</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 场次选择 -->
|
||||
<view v-if="selectedSessions.length > 0" class="session-section">
|
||||
<text class="session-title">选择入园场次</text>
|
||||
<view class="session-list">
|
||||
@@ -48,59 +48,50 @@
|
||||
:key="s.id"
|
||||
class="session-item"
|
||||
:class="{
|
||||
'session-item--active': selectedSession === s.id && s.stock > 0,
|
||||
'session-item--disabled': s.stock <= 0
|
||||
'session-item--active': selectedSession === s.id && (s.stock == null || s.stock > 0),
|
||||
'session-item--disabled': s.stock != null && s.stock <= 0
|
||||
}"
|
||||
@tap="s.stock > 0 && (selectedSession = s.id)"
|
||||
@tap="(s.stock == null || s.stock > 0) && (selectedSession = s.id)"
|
||||
>
|
||||
<text class="session-time">{{ s.time }}</text>
|
||||
<text v-if="s.stock > 0" class="session-stock">余{{ s.stock }}位</text>
|
||||
<text v-if="s.stock == null" class="session-stock session-stock--available">可预约</text>
|
||||
<text v-else-if="s.stock >= 50" class="session-stock session-stock--available">可预约</text>
|
||||
<text v-else-if="s.stock > 0" class="session-stock session-stock--limited">余{{ s.stock }}位</text>
|
||||
<text v-else class="session-stock session-stock--empty">已约满</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="selectedDate && selectedDateStock <= 0" class="no-session-tip">
|
||||
<text class="no-session-tip__text">该日期暂无可预约场次</text>
|
||||
<view v-else-if="selectedDateObj?.disabled" class="no-session-tip">
|
||||
<text class="no-session-tip__text no-session-tip__text--error">{{ selectedDateObj.reason || '该日期不可预约' }}</text>
|
||||
</view>
|
||||
<view v-else-if="selectedDate" class="no-session-tip">
|
||||
<view v-else-if="selectedDate && selectedDateStock === 0" class="no-session-tip">
|
||||
<text class="no-session-tip__text">该日期已约满</text>
|
||||
</view>
|
||||
<view v-else-if="selectedDate && selectedDateStock > 0 && !selectedSessions.length" class="no-session-tip">
|
||||
<text class="no-session-tip__text">该日期无需选择场次,直接预约即可</text>
|
||||
</view>
|
||||
<text class="date-tip">仅可预约近3日内时段,当日预约需提前1天取消</text>
|
||||
<text class="date-tip">仅可预约近{{ appointmentDays || 3 }}日内时段</text>
|
||||
</view>
|
||||
|
||||
<!-- 预约人实名信息 -->
|
||||
<view class="user-card">
|
||||
<text class="section-title">预约人信息</text>
|
||||
<view class="user-row">
|
||||
<text class="user-label">姓名</text>
|
||||
<text class="user-value">{{ userInfo.name }}</text>
|
||||
<text class="user-value">{{ userName }}</text>
|
||||
</view>
|
||||
<view class="user-row">
|
||||
<text class="user-label">身份证号</text>
|
||||
<text class="user-value">{{ maskedId }}</text>
|
||||
<text class="user-value">{{ userIdCard }}</text>
|
||||
</view>
|
||||
<text class="user-warn">预约信息必须与持卡实名一致,不可更换他人</text>
|
||||
</view>
|
||||
|
||||
<!-- 预约须知 -->
|
||||
<view class="notice-card">
|
||||
<view class="notice-card" v-if="bookingNotice">
|
||||
<text class="notice-title">预约须知</text>
|
||||
<view class="notice-list">
|
||||
<view class="notice-item">
|
||||
<view class="notice-dot" />
|
||||
<text class="notice-text">赣湘全域旅游年卡单人每日仅可预约该景区1次</text>
|
||||
</view>
|
||||
<view class="notice-item">
|
||||
<view class="notice-dot" />
|
||||
<text class="notice-text">需至少提前1天线上取消预约,预约当日无法取消</text>
|
||||
</view>
|
||||
<view class="notice-item">
|
||||
<view class="notice-dot" />
|
||||
<text class="notice-text">未提前取消且未入园,将扣除1次年卡使用次数,权益不可恢复</text>
|
||||
</view>
|
||||
<view class="notice-item">
|
||||
<view class="notice-dot" />
|
||||
<text class="notice-text">入园需携带本人身份证+年卡入园二维码,人证合一核验</text>
|
||||
<text class="notice-text">{{ bookingNotice }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="notice-link" @tap="goFaq">
|
||||
@@ -108,10 +99,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区占位 -->
|
||||
<view class="safe-bottom" />
|
||||
|
||||
<!-- 底部固定预约按钮 -->
|
||||
<view class="bottom-bar">
|
||||
<button
|
||||
class="btn-submit"
|
||||
@@ -128,100 +117,212 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { post } from '../../../../api/request'
|
||||
import { BASE_URL } from '../../../../config'
|
||||
|
||||
const scenic = ref({
|
||||
name: '武功山金顶景区',
|
||||
address: '江西省萍乡市芦溪县武功山风景名胜区',
|
||||
hours: '08:00-18:00',
|
||||
image: 'https://images.unsplash.com/photo-1513415564515-763d91423bdd?fm=jpg&q=80&w=800&h=400&fit=crop'
|
||||
})
|
||||
|
||||
const userInfo = ref({
|
||||
name: '张三',
|
||||
idCard: '360302199001011234'
|
||||
})
|
||||
|
||||
const maskedId = computed(() => {
|
||||
const id = userInfo.value.idCard
|
||||
return id.slice(0, 6) + '****' + id.slice(-4)
|
||||
})
|
||||
|
||||
// 生成近3天日期选项(明天起,含库存,第3天已约满)
|
||||
const weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
const mockStocks = [0, 200, 88]
|
||||
const dateOptions = Array.from({ length: 3 }, (_, i) => {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() + i + 1) // 从明天开始
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
return {
|
||||
key: `${d.getFullYear()}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
|
||||
week: i === 0 ? '明天' : weekNames[d.getDay()],
|
||||
label: `${month}月${day}日`,
|
||||
stock: mockStocks[i]
|
||||
}
|
||||
})
|
||||
|
||||
// 默认选中第一个有库存的日期
|
||||
const firstAvailable = dateOptions.find(d => d.stock > 0)
|
||||
const selectedDate = ref(firstAvailable ? firstAvailable.key : '')
|
||||
|
||||
// 场次数据(根据选中日期动态展示)
|
||||
interface Session {
|
||||
id: string
|
||||
interface ReserveOptionSession {
|
||||
id: string | number
|
||||
time: string
|
||||
stock: number
|
||||
}
|
||||
|
||||
const sessionMap: Record<string, Session[]> = {
|
||||
[dateOptions[0].key]: [
|
||||
{ id: 's1', time: '08:00-10:00', stock: 56 },
|
||||
{ id: 's2', time: '10:00-12:00', stock: 42 },
|
||||
{ id: 's3', time: '14:00-16:00', stock: 58 },
|
||||
],
|
||||
[dateOptions[1].key]: [
|
||||
{ id: 's4', time: '08:00-12:00', stock: 100 },
|
||||
{ id: 's5', time: '12:00-18:00', stock: 100 },
|
||||
],
|
||||
interface DateOption {
|
||||
key: string
|
||||
week: string
|
||||
label: string
|
||||
stock: number | null
|
||||
sessions: ReserveOptionSession[]
|
||||
disabled?: boolean
|
||||
reason?: string
|
||||
}
|
||||
|
||||
const selectedSession = ref('')
|
||||
const scenic = ref({ name: '', address: '', hours: '', image: '' })
|
||||
const userName = ref('')
|
||||
const userIdCard = ref('')
|
||||
const bookingNotice = ref('')
|
||||
const cardModelName = ref('')
|
||||
const appointmentDays = ref<number | null>(null)
|
||||
|
||||
const reserveParams = ref({
|
||||
scenicId: 0,
|
||||
channel: '',
|
||||
thirdId: '',
|
||||
cardNum: '',
|
||||
mobile: '',
|
||||
cardModelName: ''
|
||||
})
|
||||
|
||||
const dateOptions = ref<DateOption[]>([])
|
||||
const selectedDate = ref('')
|
||||
const selectedSession = ref<string | number>('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
|
||||
function formatDateDisplay(dateStr: string): { week: string; label: string } {
|
||||
const d = new Date(dateStr)
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const tomorrow = new Date(today)
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
const diff = Math.round((d.getTime() - today.getTime()) / 86400000)
|
||||
const week = diff === 1 ? '明天' : diff === 2 ? '后天' : weekNames[d.getDay()]
|
||||
const label = `${d.getMonth() + 1}月${d.getDate()}日`
|
||||
return { week, label }
|
||||
}
|
||||
|
||||
const selectedDateObj = computed(() => {
|
||||
if (!selectedDate.value) return null
|
||||
return dateOptions.value.find(o => o.key === selectedDate.value) || null
|
||||
})
|
||||
|
||||
const selectedSessions = computed(() => {
|
||||
if (!selectedDate.value) return []
|
||||
const sessions = sessionMap[selectedDate.value] || []
|
||||
// 切换日期时自动选中第一个有库存的场次
|
||||
if (!selectedDate.value || selectedDateObj.value?.disabled) return []
|
||||
const d = dateOptions.value.find(d => d.key === selectedDate.value)
|
||||
const sessions = d?.sessions || []
|
||||
if (sessions.length > 0 && !sessions.find(s => s.id === selectedSession.value)) {
|
||||
const available = sessions.find(s => s.stock > 0)
|
||||
const available = sessions.find(s => s.stock == null || s.stock > 0)
|
||||
selectedSession.value = available ? available.id : ''
|
||||
}
|
||||
return sessions
|
||||
})
|
||||
|
||||
const selectedDateStock = computed(() => {
|
||||
if (!selectedDate.value) return 0
|
||||
const d = dateOptions.find(o => o.key === selectedDate.value)
|
||||
return d ? d.stock : 0
|
||||
if (!selectedDate.value) return -1
|
||||
const d = dateOptions.value.find(o => o.key === selectedDate.value)
|
||||
if (!d) return 0
|
||||
return d.stock == null ? 1 : d.stock
|
||||
})
|
||||
|
||||
onLoad((query: any) => {
|
||||
if (query?.scenicId) {
|
||||
// 实际项目中根据 scenicId 请求景区详情
|
||||
onLoad(async (query: any) => {
|
||||
const resourceId = query?.resourceId
|
||||
if (!resourceId) return
|
||||
const cardNumber = query?.cardNumber || ''
|
||||
const cardModelId = query?.cardModelId || ''
|
||||
const phone = (uni.getStorageSync('phone') as string) || ''
|
||||
|
||||
try {
|
||||
const res = await post<any>('/mpReserve/queryReserveOptions', {
|
||||
resourceId: parseInt(resourceId),
|
||||
phone,
|
||||
cardNumber: cardNumber || undefined,
|
||||
})
|
||||
if (res) {
|
||||
scenic.value = {
|
||||
name: res.scenicName || '',
|
||||
address: res.address || '',
|
||||
hours: res.hours || '',
|
||||
image: res.scenicImage || ''
|
||||
}
|
||||
userName.value = res.userName || ''
|
||||
userIdCard.value = res.userIdCard || ''
|
||||
bookingNotice.value = Array.isArray(res.bookingNotice) ? res.bookingNotice.join('\n') : (res.bookingNotice || '')
|
||||
cardModelName.value = res.cardModelName || ''
|
||||
appointmentDays.value = (res.dates || []).length
|
||||
reserveParams.value = {
|
||||
scenicId: parseInt(resourceId),
|
||||
channel: 'Channel_SZH',
|
||||
thirdId: cardModelId,
|
||||
cardNum: cardNumber,
|
||||
mobile: phone,
|
||||
cardModelName: res.cardModelName || ''
|
||||
}
|
||||
dateOptions.value = (res.dates || []).map((d: any) => ({
|
||||
key: d.key,
|
||||
week: d.week,
|
||||
label: d.label,
|
||||
stock: d.stock,
|
||||
sessions: d.sessions || [],
|
||||
disabled: d.disabled,
|
||||
reason: d.reason,
|
||||
}))
|
||||
const firstAvailable = dateOptions.value.find(d => d.stock == null || d.stock > 0)
|
||||
if (firstAvailable) selectedDate.value = firstAvailable.key
|
||||
}
|
||||
} catch (err: any) {
|
||||
const mockStocks: (number | null)[] = [null, 200, 0]
|
||||
const mockSessions: Record<string, ReserveOptionSession[]> = {
|
||||
['mock0']: [
|
||||
{ id: 's1', time: '08:00-10:00', stock: 56 },
|
||||
{ id: 's2', time: '10:00-12:00', stock: 42 },
|
||||
{ id: 's3', time: '14:00-16:00', stock: 58 },
|
||||
],
|
||||
['mock1']: [
|
||||
{ id: 's4', time: '08:00-12:00', stock: 100 },
|
||||
{ id: 's5', time: '12:00-18:00', stock: 100 },
|
||||
],
|
||||
}
|
||||
dateOptions.value = Array.from({ length: 3 }, (_, i) => {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() + i + 1)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const key = `${d.getFullYear()}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||
const { week, label } = formatDateDisplay(key)
|
||||
return {
|
||||
key,
|
||||
week,
|
||||
label,
|
||||
stock: mockStocks[i],
|
||||
sessions: mockSessions[`mock${i}`] || [],
|
||||
}
|
||||
})
|
||||
appointmentDays.value = 3
|
||||
scenic.value = {
|
||||
name: '武功山金顶景区',
|
||||
address: '江西省萍乡市芦溪县武功山风景名胜区',
|
||||
hours: '08:00-18:00',
|
||||
image: 'https://images.unsplash.com/photo-1513415564515-763d91423bdd?fm=jpg&q=80&w=800&h=400&fit=crop',
|
||||
}
|
||||
bookingNotice.value = '请提前10分钟到达景区检票口;\n入园需出示预约码和身份证;\n如无法按时入园请提前取消预约。'
|
||||
userName.value = '张三'
|
||||
userIdCard.value = '360302199001011234'
|
||||
cardModelName.value = '赣湘全域旅游年卡'
|
||||
const firstAvailable = dateOptions.value.find(d => d.stock == null || d.stock > 0)
|
||||
if (firstAvailable) selectedDate.value = firstAvailable.key
|
||||
}
|
||||
})
|
||||
|
||||
function handleSubmit() {
|
||||
if (!selectedDate.value) return
|
||||
async function handleSubmit() {
|
||||
if (!selectedDate.value || submitting.value) return
|
||||
|
||||
// Mock 预约成功
|
||||
const reserveId = 'YP' + Date.now().toString().slice(-8)
|
||||
// 获取选中场次的时间段
|
||||
const session = selectedSessions.value.find(s => s.id === selectedSession.value)
|
||||
const sessionTime = session ? session.time : ''
|
||||
const sessionParam = sessionTime ? `&session=${encodeURIComponent(sessionTime)}` : ''
|
||||
uni.navigateTo({
|
||||
url: `/packages/user/pages/reserve/success?reserveId=${reserveId}&date=${selectedDate.value}&scenic=${encodeURIComponent(scenic.value.name)}${sessionParam}`
|
||||
})
|
||||
const dateObj = dateOptions.value.find(d => d.key === selectedDate.value)
|
||||
if (dateObj?.disabled) {
|
||||
uni.showToast({ title: dateObj.reason || '该日期不可预约', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (dateObj && dateObj.stock != null && dateObj.stock <= 0) {
|
||||
uni.showToast({ title: '该日期已约满', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const session = selectedSessions.value.find(s => s.id === selectedSession.value)
|
||||
const p = reserveParams.value
|
||||
const result = await post<any>(`${BASE_URL}/zhy-api/cardBag/appoint`, {
|
||||
scenicId: String(p.scenicId),
|
||||
channel: p.channel,
|
||||
thirdId: p.thirdId,
|
||||
cardNum: p.cardNum,
|
||||
mobile: p.mobile,
|
||||
cardModelName: p.cardModelName,
|
||||
appointDate: selectedDate.value,
|
||||
timeId: session ? String(session.id) : '',
|
||||
name: userName.value,
|
||||
idNo: userIdCard.value,
|
||||
})
|
||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/packages/user/pages/reserve/success?appointId=${result}&cardNumber=${p.cardNum}&cardModelId=${p.thirdId}`
|
||||
})
|
||||
}, 1500)
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message || '预约失败', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goFaq() {
|
||||
@@ -239,7 +340,6 @@ $bar-height: 120rpx;
|
||||
font-family: $font-family-body;
|
||||
}
|
||||
|
||||
/* ===== 景区信息卡片 ===== */
|
||||
.scenic-card {
|
||||
margin: 24rpx 24rpx 0;
|
||||
background: $color-card;
|
||||
@@ -337,7 +437,6 @@ $bar-height: 120rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ===== 日期选择 ===== */
|
||||
.date-card {
|
||||
margin: 24rpx 32rpx 0;
|
||||
background: $color-card;
|
||||
@@ -371,7 +470,7 @@ $bar-height: 120rpx;
|
||||
align-items: center;
|
||||
padding: 16rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid $uv-border-color;
|
||||
border: 2rpx solid $uv-primary;
|
||||
transition: all $transition-fast;
|
||||
|
||||
&--active {
|
||||
@@ -406,10 +505,10 @@ $bar-height: 120rpx;
|
||||
.date-stock {
|
||||
font-family: $font-family-body;
|
||||
font-size: $font-size-xxs;
|
||||
color: $uv-placeholder-color;
|
||||
color: $uv-main-color;
|
||||
|
||||
&--empty {
|
||||
color: $uv-error;
|
||||
color: $uv-placeholder-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,15 +517,15 @@ $bar-height: 120rpx;
|
||||
}
|
||||
|
||||
.date-item--disabled {
|
||||
opacity: 0.5;
|
||||
border-color: $uv-border-color;
|
||||
|
||||
.date-week,
|
||||
.date-day {
|
||||
.date-day,
|
||||
.date-stock {
|
||||
color: $uv-placeholder-color;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 场次选择 ===== */
|
||||
.session-section {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
@@ -501,6 +600,11 @@ $bar-height: 120rpx;
|
||||
color: $uv-placeholder-color;
|
||||
}
|
||||
|
||||
.no-session-tip__text--error {
|
||||
color: $uv-error;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.date-tip {
|
||||
display: block;
|
||||
font-family: $font-family-body;
|
||||
@@ -509,7 +613,6 @@ $bar-height: 120rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== 预约人信息 ===== */
|
||||
.user-card {
|
||||
margin: 24rpx 32rpx 0;
|
||||
background: $color-card;
|
||||
@@ -545,7 +648,6 @@ $bar-height: 120rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* ===== 预约须知 ===== */
|
||||
.notice-card {
|
||||
margin: 24rpx 32rpx 0;
|
||||
background: #eef4ff;
|
||||
@@ -600,7 +702,6 @@ $bar-height: 120rpx;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
/* ===== 底部 ===== */
|
||||
.safe-bottom {
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user