Files
pingxiangcard/src/packages/user/pages/reserve/index.vue
T

659 lines
15 KiB
Vue
Raw 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="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)
// 获取选中场次的时间段
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}`
})
}
function goFaq() {
uni.navigateTo({ url: '/packages/content/pages/faq' })
}
</script>
<style lang="scss" scoped>
$bar-height: 120rpx;
.page {
min-height: 100vh;
background: $color-background;
padding-bottom: calc(#{$bar-height} + env(safe-area-inset-bottom));
font-family: $font-family-body;
}
/* ===== 景区信息卡片 ===== */
.scenic-card {
margin: 24rpx 24rpx 0;
background: $color-card;
border-radius: $radius-lg;
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-family-body;
font-size: $font-size-base;
font-weight: $font-weight-bold;
color: $uv-main-color;
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 $uv-tips-color;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
position: relative;
&::after {
content: '';
width: 10rpx;
height: 10rpx;
border-radius: 50%;
border: 2rpx solid $uv-tips-color;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
&--clock {
border: 3rpx solid $uv-tips-color;
border-radius: 50%;
position: relative;
&::before {
content: '';
width: 2rpx;
height: 10rpx;
background: $uv-tips-color;
position: absolute;
top: 4rpx;
left: 50%;
transform: translateX(-50%);
}
&::after {
content: '';
width: 8rpx;
height: 2rpx;
background: $uv-tips-color;
position: absolute;
top: 50%;
left: 50%;
}
}
}
.scenic-text {
font-family: $font-family-body;
font-size: $font-size-sm;
color: $uv-tips-color;
}
.scenic-tip {
display: block;
font-family: $font-family-body;
font-size: 22rpx;
color: $uv-placeholder-color;
margin-top: 16rpx;
line-height: 1.6;
}
/* ===== 日期选择 ===== */
.date-card {
margin: 24rpx 32rpx 0;
background: $color-card;
border-radius: $radius-lg;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.section-title {
display: block;
font-family: $font-family-body;
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $uv-main-color;
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 $uv-border-color;
transition: all $transition-fast;
&--active {
background: $uv-primary;
border-color: $uv-primary;
}
}
.date-week {
font-family: $font-family-body;
font-size: $font-size-xxs;
color: $uv-tips-color;
margin-bottom: 4rpx;
}
.date-item--active .date-week {
color: rgba(255, 255, 255, 0.8);
}
.date-day {
font-family: $font-family-body;
font-size: $font-size-xs;
font-weight: $font-weight-bold;
color: $uv-main-color;
margin-bottom: 4rpx;
}
.date-item--active .date-day {
color: #ffffff;
}
.date-stock {
font-family: $font-family-body;
font-size: $font-size-xxs;
color: $uv-placeholder-color;
&--empty {
color: $uv-error;
}
}
.date-item--active .date-stock {
color: rgba(255, 255, 255, 0.7);
}
.date-item--disabled {
opacity: 0.5;
.date-week,
.date-day {
color: $uv-placeholder-color;
}
}
/* ===== 场次选择 ===== */
.session-section {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid $uv-border-color;
}
.session-title {
display: block;
font-family: $font-family-body;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: $uv-main-color;
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 $uv-border-color;
transition: all $transition-fast;
&--active {
background: rgba(60, 156, 255, 0.08);
border-color: $uv-primary;
}
&--disabled {
opacity: 0.5;
}
}
.session-time {
font-family: $font-family-body;
font-size: $font-size-xs;
font-weight: $font-weight-medium;
color: $uv-main-color;
margin-bottom: 4rpx;
}
.session-item--active .session-time {
color: $uv-primary;
}
.session-stock {
font-family: $font-family-body;
font-size: $font-size-xxs;
color: $uv-placeholder-color;
&--empty {
color: $uv-error;
}
}
.no-session-tip {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid $uv-border-color;
}
.no-session-tip__text {
font-family: $font-family-body;
font-size: $font-size-xxs;
color: $uv-placeholder-color;
}
.date-tip {
display: block;
font-family: $font-family-body;
font-size: $font-size-xxs;
color: $uv-placeholder-color;
margin-top: 16rpx;
}
/* ===== 预约人信息 ===== */
.user-card {
margin: 24rpx 32rpx 0;
background: $color-card;
border-radius: $radius-lg;
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-family-body;
font-size: $font-size-sm;
color: $uv-main-color;
}
.user-value {
font-family: $font-family-body;
font-size: $font-size-sm;
color: $uv-content-color;
}
.user-warn {
display: block;
font-family: $font-family-body;
font-size: $font-size-sm;
color: $uv-error;
margin-top: 12rpx;
}
/* ===== 预约须知 ===== */
.notice-card {
margin: 24rpx 32rpx 0;
background: #eef4ff;
border-radius: $radius-lg;
padding: 32rpx;
}
.notice-title {
display: block;
font-family: $font-family-body;
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $uv-main-color;
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: $uv-primary;
flex-shrink: 0;
margin-top: 14rpx;
margin-right: 12rpx;
}
.notice-text {
font-family: $font-family-body;
font-size: $font-size-sm;
color: $uv-tips-color;
line-height: 1.6;
}
.notice-link {
&:active { opacity: 0.7; }
}
.notice-link__text {
font-family: $font-family-body;
font-size: $font-size-sm;
color: $uv-primary;
font-weight: $font-weight-medium;
}
/* ===== 底部 ===== */
.safe-bottom {
height: 24rpx;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
width: 100%;
box-sizing: border-box;
min-height: $bar-height;
background: $color-card;
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: $uv-primary;
border: none;
display: flex;
align-items: center;
justify-content: center;
transition: all $transition-fast;
&::after { display: none; }
&:active:not(.btn-submit--disabled) {
transform: scale(0.98);
}
&--disabled {
background: $uv-bg-color;
}
}
.btn-submit__text {
font-family: $font-family-body;
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: #ffffff;
}
.btn-submit--disabled .btn-submit__text {
color: $uv-placeholder-color;
}
</style>