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.

517 lines
13 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">
<!-- 船舶信息卡片1水位等 -->
<view class="card">
<view class="card-title">船舶信息</view>
<view class="water-info-row">
<view class="water-info-col">
<text class="label">今日水位</text>
<text class="value blue">4.2m</text>
</view>
<view class="water-info-col">
<text class="label">水深</text>
<text class="value blue">3.8m</text>
</view>
<view class="water-info-col">
<text class="label">限高</text>
<text class="value blue">7.5m</text>
</view>
</view>
</view>
<!-- 船舶信息卡片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' }"
@click="setGateDate('today')"
>今天</button>
<button
class="direction-btn"
:class="{ active: gateDate === 'tomorrow' }"
@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'
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', // 默认选中"今天"
}
},
onLoad() {
// #ifdef H5
this.isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent)
// #endif
},
onShow() {
this.fetchDirectionEnum().then(() => {
this.fetchShipTypeEnum().then(() => {
this.fetchShipList();
});
});
},
methods: {
setDirection(dir) {
this.direction = dir
},
async fetchShipList() {
const token = uni.getStorageSync('token');
if (!token) return;
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' });
setTimeout(() => {
uni.navigateBack();
uni.navigateTo({ url: '/pages/index/ship_manage' });
}, 1000);
return;
}
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
};
}
} catch (e) {
// 可选:错误处理
}
},
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]
}));
// 默认选中第一个
if (this.directionEnum.length && !this.direction) {
this.direction = this.directionEnum[0].value;
}
}
} 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;
}
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);
}
uni.showLoading({ title: '提交中...' });
uni.request({
url: `${API.RESERVATION_CREATE}`,
method: 'POST',
data: {
token,
ship_id: this.currentShip.id,
direction: directionValue,
passage_date: gateDateStr
},
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 {
uni.hideLoading();
uni.showToast({ title: res.data.errmsg || '预约失败', icon: 'none' });
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: '网络错误', icon: 'none' });
}
});
},
toggleAgreeNotice() {
this.agreeNotice = !this.agreeNotice;
},
setGateDate(val) {
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;
}
.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>