You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

927 lines
28 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="reservation-page" :class="{ 'wechat-browser': isWeixinBrowser }">
<view class="fixed-nav" v-if="!isWeixinBrowser">
<NavBar title="过闸预约" />
</view>
<view class="reservation-scroll">
<!-- 船舶信息卡片2详细信息 -->
<view class="card">
<view class="card-title">船舶信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">编号</text>
<view class="info-value">{{ currentShip.ship_number }} <text class="arrow"></text></view>
</view>
<view class="info-row">
<text class="info-label">总长度</text>
<view class="info-value">{{ currentShip.total_length }} <text class="arrow"></text></view>
</view>
<view class="info-row">
<text class="info-label">型宽</text>
<view class="info-value">{{ currentShip.total_width }} <text class="arrow"></text></view>
</view>
<view class="info-row">
<text class="info-label">型深</text>
<view class="info-value">{{ currentShip.molded_depth }} <text class="arrow"></text></view>
</view>
<view class="info-row">
<text class="info-label">载重</text>
<view class="info-value">{{ currentShip.total_tonnage }} <text class="arrow"></text></view>
</view>
<view class="info-row">
<text class="info-label">类型</text>
<view class="info-value">{{ getShipTypeName(currentShip.ship_type) }} <text class="arrow"></text></view>
</view>
</view>
</view>
<!-- 航行方向选择 -->
<view class="card">
<view class="card-title">航行方向</view>
<view class="direction-row">
<button
v-for="item in directionEnum"
:key="item.value"
class="direction-btn"
:class="{ active: direction === item.value }"
@click="setDirection(item.value)"
>
{{ item.label }}
</button>
</view>
</view>
<!-- 过闸日期选择 -->
<view class="card">
<view class="card-title">过闸日期</view>
<view class="direction-row">
<button
class="direction-btn"
:class="{ active: gateDate === 'today' }"
:disabled="isDateDisabled"
@click="setGateDate('today')"
>今天</button>
<button
class="direction-btn"
:class="{ active: gateDate === 'tomorrow', 'tomorrow-default': gateDate === 'tomorrow' && isDateDisabled }"
:disabled="isDateDisabled"
@click="setGateDate('tomorrow')"
>明天</button>
</view>
</view>
<!-- 预约须知 -->
<view class="notice-row">
<text class="notice-title">预约须知</text>
<view class="notice-check" style="position:relative;">
<checkbox :checked="agreeNotice" />
<text>我已阅读并同意《过闸预约服务协议》</text>
<view style="position:absolute;left:0;top:0;right:0;bottom:0;z-index:2;" @tap="toggleAgreeNotice"></view>
</view>
</view>
</view>
<view class="reservation-bottom-bar">
<button class="reservation-btn" @click="onReserve"></button>
</view>
</view>
</template>
<script>
import NavBar from '@/components/NavBar.vue'
import { API } from '@/config/index.js'
// #ifdef H5
import wx from 'jweixin-module'
// #endif
export default {
name: 'ReservationPage',
components: { NavBar },
data() {
return {
isWeixinBrowser: false,
direction: '',
shipList: [],
currentShip: {
total_length: '',
total_width: '',
molded_depth: '',
ship_number: '',
total_tonnage: '',
ship_type: ''
},
directionEnum: [],
shipTypeEnum: [],
agreeNotice: false,
gateDate: 'today', // 默认选中"今天"
deadlineTime: '', // 截止时间,格式如 "18:00"
isDateDisabled: false, // 是否禁用日期选择
userLocation: null, // 用户位置信息 {latitude, longitude}
isInGeofence: null, // 是否在围栏范围内null表示未检查true表示在范围内false表示不在范围内
}
},
onLoad() {
// #ifdef H5
this.isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent)
// #endif
},
async onShow() {
// 先拉取方向、船型和可用船舶信息,如果没有可用船舶,则直接返回,不再进行后续操作(如获取位置信息等)
await this.fetchDirectionEnum();
await this.fetchShipTypeEnum();
const hasShip = await this.fetchShipList();
if (!hasShip) {
// 已在 fetchShipList 内部给出提示并跳转
return;
}
// 有可用船舶时,再弹出获取位置信息的提示
uni.showModal({
title: '提示',
content: '预约前需要先获取您的位置信息',
showCancel: true,
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.getUserLocation();
} else {
// 用户拒绝获取位置,可根据需要给出提示
uni.showToast({
title: '未获取位置信息,可能无法完成预约',
icon: 'none'
});
}
}
});
// 最后再拉取每日预约截止时间并设置默认日期
this.fetchDailyReservationDeadline();
},
methods: {
// 获取用户位置
getUserLocation() {
// #ifdef H5
if (this.isWeixinBrowser) {
// 微信 H5 环境
console.log('微信 H5 环境');
this.getWeixinLocation();
} else {
// 普通 H5 环境
console.log('普通 H5 环境');
this.getBrowserLocation();
}
// #endif
// #ifdef MP-WEIXIN
// 微信小程序环境
console.log('微信小程序环境');
this.getMiniProgramLocation();
// #endif
},
// 微信 H5 获取位置
getWeixinLocation() {
// #ifdef H5
console.log('[WeixinLocation] 调用 getWeixinLocation');
const inWeixin = /MicroMessenger/i.test(navigator.userAgent);
console.log('[WeixinLocation] 是否微信环境 isWeixinBrowser =', this.isWeixinBrowser, 'UA检测 =', inWeixin ? '是' : '否');
// 优先使用 window.wx由 App.vue 挂载),否则使用导入的 wx
const hasWindowWx = (typeof window !== 'undefined' && !!window.wx);
const hasModuleWx = (typeof wx !== 'undefined');
console.log('[WeixinLocation] has window.wx =', hasWindowWx, ', has module wx =', hasModuleWx);
const wxObj = hasWindowWx ? window.wx : (hasModuleWx ? wx : null);
console.log('[WeixinLocation] wxObj =', wxObj);
if (!wxObj) {
console.error('微信 JS-SDK 未加载');
uni.showModal({
title: '提示',
content: '微信 JS-SDK 未加载,请刷新页面重试',
showCancel: false
});
return;
}
console.log('[WeixinLocation] wxObj.version =', wxObj.version, 'typeof wxObj.config =', typeof wxObj.config, 'typeof wxObj.ready =', typeof wxObj.ready, 'typeof wxObj.getLocation =', typeof wxObj.getLocation);
if (typeof wxObj.config !== 'function') {
console.warn('[WeixinLocation] wx.config 未定义说明微信JS-SDK可能还没有正确初始化wx.ready 可能不会触发,请确认后端是否已下发 JSSDK 配置并在 App.vue 中调用 initWechatJSSDK');
}
if (typeof wxObj.getLocation !== 'function') {
console.warn('[WeixinLocation] wx.getLocation 不是函数,说明当前 JSSDK 可能未加载完整或未包含该接口');
}
if (typeof wxObj.error === 'function') {
wxObj.error((err) => {
console.error('[WeixinLocation] wx.error 回调触发JSSDK 配置或调用出错:', err);
});
} else {
console.warn('[WeixinLocation] wx.error 不存在,无法捕获 JSSDK 配置错误');
}
// 如果 5 秒内 ready 没有回调,给出日志提示
let readyCalled = false;
setTimeout(() => {
if (!readyCalled) {
console.warn('[WeixinLocation] 5 秒内 wx.ready 未回调,可能是 wx.config 未正确执行或签名错误');
}
}, 5000);
// 使用微信 JS-SDK 获取位置
wxObj.ready(() => {
readyCalled = true;
console.log('[WeixinLocation] wx.ready 回调触发,开始调用 wx.getLocation');
wxObj.getLocation({
type: 'gcj02', // 返回可以用于uni.openLocation的经纬度
success: (res) => {
console.log('[WeixinLocation] wx.getLocation success 原始返回值:', res);
this.userLocation = {
latitude: res.latitude,
longitude: res.longitude,
speed: res.speed,
accuracy: res.accuracy
};
console.log('[WeixinLocation] 已保存 userLocation =', this.userLocation);
},
fail: (err) => {
console.error('[WeixinLocation] wx.getLocation fail:', err);
uni.showModal({
title: '提示',
content: '获取位置失败,请允许访问位置信息',
showCancel: false
});
}
});
});
// #endif
},
// 普通 H5 浏览器获取位置
getBrowserLocation() {
// #ifdef H5
if (typeof navigator === 'undefined' || !navigator.geolocation) {
uni.showModal({
title: '提示',
content: '您的浏览器不支持地理位置功能',
showCancel: false
});
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
this.userLocation = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
altitudeAccuracy: position.coords.altitudeAccuracy,
heading: position.coords.heading,
speed: position.coords.speed
};
console.log('浏览器获取位置成功:', this.userLocation);
},
(error) => {
console.error('浏览器获取位置失败:', error);
let errorMsg = '获取位置失败';
switch(error.code) {
case error.PERMISSION_DENIED:
errorMsg = '用户拒绝了位置请求';
break;
case error.POSITION_UNAVAILABLE:
errorMsg = '位置信息不可用';
break;
case error.TIMEOUT:
errorMsg = '获取位置超时';
break;
}
uni.showModal({
title: '提示',
content: errorMsg + ',请允许访问位置信息',
showCancel: false
});
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
// #endif
},
// 微信小程序获取位置
getMiniProgramLocation() {
// #ifdef MP-WEIXIN
uni.getLocation({
type: 'gcj02',
success: (res) => {
this.userLocation = {
latitude: res.latitude,
longitude: res.longitude,
speed: res.speed,
accuracy: res.accuracy
};
console.log('小程序获取位置成功:', this.userLocation);
},
fail: (err) => {
console.error('小程序获取位置失败:', err);
uni.showModal({
title: '提示',
content: '获取位置失败,请在设置中允许位置权限',
showCancel: false
});
}
});
// #endif
},
setDirection(dir) {
this.direction = dir;
// 重置围栏范围检查状态,因为切换方向后需要重新检查
this.isInGeofence = null;
// 用户选择航行方向后,调用接口
if (dir) {
this.fetchGeofenceByDirection();
}
},
async fetchGeofenceByDirection() {
const token = uni.getStorageSync('token');
if (!token || !this.direction) return;
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_GEOFENCE_BY_DIRECTION}?token=${token}&direction=${this.direction}`,
method: 'GET',
success: resolve,
fail: reject
});
});
if (res.data && res.data.errcode === 0) {
const geofenceData = res.data.data;
console.log(geofenceData);
// 如果数组为空,不做操作,允许提交
if (!geofenceData || !Array.isArray(geofenceData) || geofenceData.length === 0) {
this.isInGeofence = true; // 没有围栏数据,允许提交
return;
}
// 检查用户位置是否在围栏范围内
this.checkLocationInGeofence(geofenceData);
}
} catch (e) {
// 可选:错误处理
}
},
// 检查用户位置是否在围栏范围内
checkLocationInGeofence(geofenceData) {
// 检查用户位置是否存在
if (!this.userLocation || !this.userLocation.latitude || !this.userLocation.longitude) {
console.warn('用户位置信息不存在,无法进行范围判断');
return;
}
const userLat = parseFloat(this.userLocation.latitude);
const userLng = parseFloat(this.userLocation.longitude);
// 遍历围栏数据,查找 coordinates 字段
for (let i = 0; i < geofenceData.length; i++) {
const item = geofenceData[i];
if (item && item.coordinates && Array.isArray(item.coordinates) && item.coordinates.length > 0) {
// 判断用户位置是否在当前围栏范围内
if (this.isPointInPolygon(userLng, userLat, item.coordinates)) {
// 在范围内设置状态为true
this.isInGeofence = true;
console.log('用户位置在围栏范围内');
return;
}
}
}
// 如果遍历完所有围栏都不在范围内设置状态为false并提示用户
this.isInGeofence = false;
uni.showModal({
title: '提示',
content: '您的当前位置不在闸站可预约范围内',
showCancel: false
});
},
// 判断点是否在多边形内(使用射线法)
isPointInPolygon(lng, lat, coordinates) {
if (!coordinates || coordinates.length < 3) {
return false;
}
// 将字符串坐标转换为数字
const polygon = coordinates.map(coord => {
if (Array.isArray(coord) && coord.length >= 2) {
return [
parseFloat(coord[0]),
parseFloat(coord[1])
];
}
return null;
}).filter(coord => coord !== null);
if (polygon.length < 3) {
return false;
}
let inside = false;
const x = lng;
const y = lat;
// 射线法:从点向右发射一条射线,统计与多边形边界的交点数
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];
// 检查射线是否与边相交
const intersect = ((yi > y) !== (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) {
inside = !inside;
}
}
return inside;
},
async fetchDailyReservationDeadline() {
const token = uni.getStorageSync('token');
if (!token) return;
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_DAILY_RESERVATION_DEADLINE}?token=${token}`,
method: 'GET',
success: resolve,
fail: reject
});
});
if (res.data && res.data.errcode === 0) {
const deadlineData = res.data.data;
if (deadlineData && deadlineData.deadline_time) {
this.deadlineTime = deadlineData.deadline_time;
this.checkDeadlineAndSetDate();
}
}
} catch (e) {
// 可选:错误处理
}
},
checkDeadlineAndSetDate() {
if (!this.deadlineTime) return;
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// 解析截止时间,格式如 "18:00"
const [deadlineHour, deadlineMinute] = this.deadlineTime.split(':').map(Number);
// 计算当前时间(分钟)和截止时间(分钟)
const currentTimeMinutes = currentHour * 60 + currentMinute;
const deadlineTimeMinutes = deadlineHour * 60 + deadlineMinute;
// 如果当前时间晚于或等于截止时间,默认选择明天
if (currentTimeMinutes >= deadlineTimeMinutes) {
this.gateDate = 'tomorrow';
} else {
// 如果当前时间早于截止时间,默认选择今天
this.gateDate = 'today';
}
// 任何时候都不能更改日期
this.isDateDisabled = true;
},
async fetchShipList() {
const token = uni.getStorageSync('token');
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' });
return false;
}
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.AVAILABLE_SHIP}?token=${token}`,
method: 'GET',
success: resolve,
fail: reject
});
});
if (res.data && res.data.errcode === 0) {
if (!res.data.data) {
uni.showToast({ title: '暂无船舶信息', icon: 'none', duration: 1500 });
setTimeout(() => {
uni.redirectTo({ url: '/pages/index/ship_manage' });
}, 1000);
return false;
}
this.currentShip = {
id: res.data.data.id,
total_length: res.data.data.total_length,
total_width: res.data.data.total_width,
molded_depth: res.data.data.molded_depth,
ship_number: res.data.data.ship_number,
total_tonnage: res.data.data.total_tonnage,
ship_type: res.data.data.ship_type
};
return true;
} else {
uni.showToast({ title: (res.data && res.data.errmsg) || '获取船舶信息失败', icon: 'none' });
return false;
}
} catch (e) {
uni.showToast({ title: '网络错误', icon: 'none' });
return false;
}
},
async fetchDirectionEnum() {
const token = uni.getStorageSync('token');
if (!token) return;
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_DIRECTION_ENUM}?token=${token}`,
method: 'GET',
success: resolve,
fail: reject
});
});
if (res.data && res.data.errcode === 0) {
// 转为数组 [{label, value}]
this.directionEnum = Object.keys(res.data.data).map(label => ({
label,
value: res.data.data[label]
}));
// 默认不选择,等待用户选择
}
} catch (e) {}
},
async fetchShipTypeEnum() {
const token = uni.getStorageSync('token');
if (!token) return;
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.SHIP_PROPERTY_ENUM}?token=${token}`,
method: 'GET',
success: resolve,
fail: reject
});
});
if (res.data && res.data.errcode === 0) {
const shipTypeRaw = res.data.data.ship_type || {};
if (Array.isArray(shipTypeRaw)) {
this.shipTypeEnum = shipTypeRaw;
} else {
this.shipTypeEnum = Object.keys(shipTypeRaw).map(label => ({
label,
value: shipTypeRaw[label]
}));
}
}
} catch (e) {
// 可选:错误处理
}
},
getShipTypeName(type) {
const found = this.shipTypeEnum.find(item => item.value === type || item.value == type);
return found ? found.label : type;
},
onReserve() {
if (!this.agreeNotice) {
uni.showToast({ title: '请先阅读并同意预约须知', icon: 'none' });
return;
}
if (!this.direction) {
uni.showToast({ title: '请先选择航行方向', icon: 'none' });
return;
}
// 检查是否在围栏范围内
if (this.isInGeofence === false) {
uni.showToast({ title: '您的当前位置不在闸站可预约范围内', icon: 'none' });
return;
}
const token = uni.getStorageSync('token');
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' });
return;
}
if (!this.currentShip || !this.currentShip.ship_number || !this.currentShip.id) {
uni.showToast({ title: '无效的船舶信息', icon: 'none' });
return;
}
// direction: north->in, south->out
const directionValue = this.direction || 'in';
// 计算过闸日期
let gateDateStr = '';
const today = new Date();
if (this.gateDate === 'today') {
gateDateStr = today.toISOString().slice(0, 10);
} else if (this.gateDate === 'tomorrow') {
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
gateDateStr = tomorrow.toISOString().slice(0, 10);
}
// 准备提交数据
const requestData = {
token,
ship_id: this.currentShip.id,
direction: directionValue,
passage_date: gateDateStr
};
// 如果用户位置存在,添加经纬度
if (this.userLocation && this.userLocation.latitude && this.userLocation.longitude) {
requestData.latitude = this.userLocation.latitude;
requestData.longitude = this.userLocation.longitude;
}
uni.showLoading({ title: '提交中...' });
uni.request({
url: `${API.RESERVATION_CREATE}`,
method: 'POST',
data: requestData,
success: (res) => {
uni.hideLoading();
if (res.data && res.data.errcode === 0) {
uni.showToast({ title: '预约成功', icon: 'success' });
setTimeout(() => {
uni.switchTab({ url: '/pages/order/index' });
}, 800);
} else {
// 处理包含字段错误信息的返回,例如:
// res.data.data = { latitude: ['纬度不能为空'], longitude: ['经度不能为空'] }
const resp = res.data || {};
const detail = resp.data || {};
let msg = '';
if (detail && typeof detail === 'object') {
const parts = [];
if (Array.isArray(detail.latitude) && detail.latitude.length) {
parts.push(`纬度:${detail.latitude.join('')}`);
}
if (Array.isArray(detail.longitude) && detail.longitude.length) {
parts.push(`经度:${detail.longitude.join('')}`);
}
if (parts.length) {
msg = parts.join('');
}
}
if (!msg) {
msg = resp.errmsg || '预约失败';
}
uni.showModal({
title: '提示',
content: msg,
showCancel: false
});
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: '网络错误', icon: 'none' });
}
});
},
toggleAgreeNotice() {
this.agreeNotice = !this.agreeNotice;
},
setGateDate(val) {
// 如果日期选择被禁用,不允许更改
if (this.isDateDisabled) {
return;
}
this.gateDate = val;
},
}
}
</script>
<style lang="scss" scoped>
.reservation-page {
background: linear-gradient(180deg, #cbe6ff 0%, #f6faff 100%);
min-height: 100vh;
padding-bottom: 140rpx;
padding-top: 90px;
font-family: 'SourceHanSansCN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.wechat-browser {
padding-top: 10px;
}
.fixed-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: linear-gradient(180deg, #cbe6ff 0%, #f6faff 100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 16px 10px 16px;
background: linear-gradient(180deg, #cbe6ff 0%, #f6faff 100%);
padding-top: 7vh;
}
.back-btn, .more-btn {
font-size: 24px;
color: #333;
}
.title {
font-size: 22px;
font-weight: bold;
color: #222;
}
.card {
background: #fff;
border-radius: 18px;
margin: 0 16px 16px 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 18px 18px 12px 18px;
margin-top: 20px;
}
.card-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: #222;
}
.water-info-row {
display: flex;
justify-content: center;
align-items: flex-start;
width: fit-content;
margin: 0 auto;
gap: 150rpx;
}
.water-info-col {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.label {
color: #888;
font-size: 15px;
margin-bottom: 2px;
text-align: left;
}
.value.blue {
color: #217aff;
font-size: 14px;
text-align: left;
}
.info-list {
border-top: 1px solid #f0f0f0;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #222;
font-size: 16px;
}
.info-value {
color: #222;
font-size: 16px;
display: flex;
align-items: center;
}
.arrow {
color: #bdbdbd;
font-size: 18px;
margin-left: 4px;
}
.direction-row, .batch-row {
display: flex;
margin-bottom: 8px;
justify-content: center;
}
.direction-btn, .batch-btn {
border: none;
border-radius: 24px;
padding: 0;
font-size: 14px;
background: #f2f6fa;
color: #888;
height: 24px;
width: 110px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 20rpx;
border: none;
outline: none;
&::after {
border: none;
}
}
.direction-btn.active, .batch-btn.active {
background: #217aff;
color: #fff;
}
.direction-btn:disabled {
opacity: 0.6;
pointer-events: none;
}
.direction-btn.tomorrow-default {
background: linear-gradient(90deg, rgb(255, 185, 128) 0%, rgb(255, 201, 154) 100%) !important;
color: #fff;
}
.notice-row {
margin: 24px 16px 0 16px;
}
.notice-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
color: #222;
}
.notice-check {
display: flex;
align-items: center;
margin-top: 8px;
font-size: 13px;
color: #888;
}
.tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 60px;
background: #fff;
display: flex;
border-top: 1px solid #eaeaea;
z-index: 10;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #888;
font-size: 14px;
}
.tab-item.active {
color: #217aff;
}
.icon {
font-size: 22px;
margin-bottom: 2px;
}
.reservation-bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fcfcfc;
box-shadow: 0 -2rpx 16rpx rgba(59,124,255,0.08);
padding: 24rpx 24rpx 32rpx 24rpx;
z-index: 999;
display: flex;
justify-content: center;
}
.reservation-btn {
min-width: 320rpx;
height: 80rpx;
border-radius: 40rpx;
background: #217aff;
color: #fff;
font-size: 32rpx;
font-weight: 500;
border: none;
outline: none;
box-shadow: 0 4rpx 16rpx rgba(33,122,255,0.08);
transition: background 0.2s;
}
.reservation-scroll {
padding-bottom: 80rpx;
}
.date-btn.active {
background: #217aff;
color: #fff;
}
</style>