feat: 新增预约系统三页面 + 订单/使用记录优化
- 新增 pages/reserve/reserve-index 景区预约页(日期选择/场次/库存/实名信息) - 新增 pages/reserve/reserve-success 预约成功页(二维码/规则/返回拦截) - 新增 pages/reserve/reserve-list 预约使用记录页(Tab筛选/取消/详情弹窗) - 预约日期仅展示明天起3天,支持已约满/无场次状态 - 使用详情增加卡种/卡号/核销照片/时间字段,照片可放大预览 - 订单列表/详情全部激活改绑定,卡号CJ格式,Tab均分布局 - 景区详情立即预约跳转预约页,使用说明跳转独立页面 - 统一景区信息区字号间距规范 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,663 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 景区基础信息卡片 -->
|
||||
<view class="scenic-card">
|
||||
<image class="scenic-img" :src="scenic.image" mode="aspectFill" />
|
||||
<view class="scenic-info">
|
||||
<text class="scenic-name">{{ scenic.name }}</text>
|
||||
<view class="scenic-row">
|
||||
<view class="scenic-icon scenic-icon--pin" />
|
||||
<text class="scenic-text">{{ scenic.address }}</text>
|
||||
</view>
|
||||
<view class="scenic-row">
|
||||
<view class="scenic-icon scenic-icon--clock" />
|
||||
<text class="scenic-text">{{ scenic.hours }}</text>
|
||||
</view>
|
||||
<text class="scenic-tip">持赣湘全域旅游年卡免费入园,单人单日仅可预约1次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期选择模块 -->
|
||||
<view class="date-card">
|
||||
<text class="section-title">选择预约日期</text>
|
||||
<scroll-view scroll-x class="date-scroll">
|
||||
<view class="date-list">
|
||||
<view
|
||||
v-for="d in dateOptions"
|
||||
:key="d.key"
|
||||
class="date-item"
|
||||
:class="{
|
||||
'date-item--active': selectedDate === d.key && d.stock > 0,
|
||||
'date-item--disabled': d.stock <= 0
|
||||
}"
|
||||
@tap="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-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">
|
||||
<view
|
||||
v-for="s in selectedSessions"
|
||||
:key="s.id"
|
||||
class="session-item"
|
||||
:class="{
|
||||
'session-item--active': selectedSession === s.id && s.stock > 0,
|
||||
'session-item--disabled': s.stock <= 0
|
||||
}"
|
||||
@tap="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-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>
|
||||
<view v-else-if="selectedDate" class="no-session-tip">
|
||||
<text class="no-session-tip__text">该日期无需选择场次,直接预约即可</text>
|
||||
</view>
|
||||
<text class="date-tip">仅可预约近3日内时段,当日预约需提前1天取消</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>
|
||||
</view>
|
||||
<view class="user-row">
|
||||
<text class="user-label">身份证号</text>
|
||||
<text class="user-value">{{ maskedId }}</text>
|
||||
</view>
|
||||
<text class="user-warn">预约信息必须与持卡实名一致,不可更换他人</text>
|
||||
</view>
|
||||
|
||||
<!-- 预约须知 -->
|
||||
<view class="notice-card">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="notice-link" @tap="goFaq">
|
||||
<text class="notice-link__text">查看完整FAQ</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区占位 -->
|
||||
<view class="safe-bottom" />
|
||||
|
||||
<!-- 底部固定预约按钮 -->
|
||||
<view class="bottom-bar">
|
||||
<button
|
||||
class="btn-submit"
|
||||
:class="{ 'btn-submit--disabled': !selectedDate }"
|
||||
:disabled="!selectedDate"
|
||||
@tap="handleSubmit"
|
||||
>
|
||||
<text class="btn-submit__text">提交预约</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
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
|
||||
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 },
|
||||
],
|
||||
}
|
||||
|
||||
const selectedSession = ref('')
|
||||
|
||||
const selectedSessions = computed(() => {
|
||||
if (!selectedDate.value) return []
|
||||
const sessions = sessionMap[selectedDate.value] || []
|
||||
// 切换日期时自动选中第一个有库存的场次
|
||||
if (sessions.length > 0 && !sessions.find(s => s.id === selectedSession.value)) {
|
||||
const available = sessions.find(s => 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
|
||||
})
|
||||
|
||||
onLoad((query: any) => {
|
||||
if (query?.scenicId) {
|
||||
// 实际项目中根据 scenicId 请求景区详情
|
||||
}
|
||||
})
|
||||
|
||||
function handleSubmit() {
|
||||
if (!selectedDate.value) return
|
||||
|
||||
// Mock 预约成功
|
||||
const reserveId = 'YP' + Date.now().toString().slice(-8)
|
||||
uni.navigateTo({
|
||||
url: `/pages/reserve/reserve-success?reserveId=${reserveId}&date=${selectedDate.value}&scenic=${scenic.value.name}`
|
||||
})
|
||||
}
|
||||
|
||||
function goFaq() {
|
||||
uni.navigateTo({ url: '/pages/faq/faq' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$font: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||
$primary: #3c9cff;
|
||||
$text-main: #111111;
|
||||
$text-label: #222222;
|
||||
$text-body: #333333;
|
||||
$text-hint: #666666;
|
||||
$text-weak: #999999;
|
||||
$divider: #eeeeee;
|
||||
$card-radius: 24rpx;
|
||||
$bar-height: 120rpx;
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom));
|
||||
font-family: $font;
|
||||
}
|
||||
|
||||
/* ===== 景区信息卡片 ===== */
|
||||
.scenic-card {
|
||||
margin: 24rpx 24rpx 0;
|
||||
background: #ffffff;
|
||||
border-radius: $card-radius;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.scenic-img {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
}
|
||||
|
||||
.scenic-info {
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.scenic-name {
|
||||
display: block;
|
||||
font-family: $font;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.scenic-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.scenic-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--pin {
|
||||
border: 3rpx solid $text-hint;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg);
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid $text-hint;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
&--clock {
|
||||
border: 3rpx solid $text-hint;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
width: 2rpx;
|
||||
height: 10rpx;
|
||||
background: $text-hint;
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
width: 8rpx;
|
||||
height: 2rpx;
|
||||
background: $text-hint;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scenic-text {
|
||||
font-family: $font;
|
||||
font-size: 24rpx;
|
||||
color: $text-hint;
|
||||
}
|
||||
|
||||
.scenic-tip {
|
||||
display: block;
|
||||
font-family: $font;
|
||||
font-size: 22rpx;
|
||||
color: $text-weak;
|
||||
margin-top: 16rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ===== 日期选择 ===== */
|
||||
.date-card {
|
||||
margin: 24rpx 32rpx 0;
|
||||
background: #ffffff;
|
||||
border-radius: $card-radius;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-family: $font;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: $text-label;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.date-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-list {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.date-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid $divider;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--active {
|
||||
background: $primary;
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.date-week {
|
||||
font-family: $font;
|
||||
font-size: 18rpx;
|
||||
color: $text-hint;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.date-item--active .date-week {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.date-day {
|
||||
font-family: $font;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.date-item--active .date-day {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.date-stock {
|
||||
font-family: $font;
|
||||
font-size: 18rpx;
|
||||
color: $text-weak;
|
||||
|
||||
&--empty {
|
||||
color: #f53f3f;
|
||||
}
|
||||
}
|
||||
|
||||
.date-item--active .date-stock {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.date-item--disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
.date-week,
|
||||
.date-day {
|
||||
color: $text-weak;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 场次选择 ===== */
|
||||
.session-section {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid $divider;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
display: block;
|
||||
font-family: $font;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: $text-label;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx solid $divider;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--active {
|
||||
background: rgba(60, 156, 255, 0.08);
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-family: $font;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
color: $text-main;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.session-item--active .session-time {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.session-stock {
|
||||
font-family: $font;
|
||||
font-size: 18rpx;
|
||||
color: $text-weak;
|
||||
|
||||
&--empty {
|
||||
color: #f53f3f;
|
||||
}
|
||||
}
|
||||
|
||||
.no-session-tip {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid $divider;
|
||||
}
|
||||
|
||||
.no-session-tip__text {
|
||||
font-family: $font;
|
||||
font-size: 18rpx;
|
||||
color: $text-weak;
|
||||
}
|
||||
|
||||
.date-tip {
|
||||
display: block;
|
||||
font-family: $font;
|
||||
font-size: 18rpx;
|
||||
color: $text-weak;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== 预约人信息 ===== */
|
||||
.user-card {
|
||||
margin: 24rpx 32rpx 0;
|
||||
background: #ffffff;
|
||||
border-radius: $card-radius;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.user-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.user-label {
|
||||
font-family: $font;
|
||||
font-size: 26rpx;
|
||||
color: $text-label;
|
||||
}
|
||||
|
||||
.user-value {
|
||||
font-family: $font;
|
||||
font-size: 26rpx;
|
||||
color: $text-body;
|
||||
}
|
||||
|
||||
.user-warn {
|
||||
display: block;
|
||||
font-family: $font;
|
||||
font-size: 24rpx;
|
||||
color: #e53935;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* ===== 预约须知 ===== */
|
||||
.notice-card {
|
||||
margin: 24rpx 32rpx 0;
|
||||
background: #eef4ff;
|
||||
border-radius: $card-radius;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
display: block;
|
||||
font-family: $font;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.notice-dot {
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 50%;
|
||||
background: $primary;
|
||||
flex-shrink: 0;
|
||||
margin-top: 14rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
font-family: $font;
|
||||
font-size: 26rpx;
|
||||
color: $text-hint;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.notice-link {
|
||||
&:active { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.notice-link__text {
|
||||
font-family: $font;
|
||||
font-size: 26rpx;
|
||||
color: $primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 底部 ===== */
|
||||
.safe-bottom {
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: $bar-height;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 48rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: $primary;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&::after { display: none; }
|
||||
|
||||
&:active:not(.btn-submit--disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-submit__text {
|
||||
font-family: $font;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-submit--disabled .btn-submit__text {
|
||||
color: $text-weak;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user