main
lion 4 months ago
parent 2437d21cc4
commit 8ce98d425d

@ -39,6 +39,17 @@
},
onShow: function() {
console.log('App Show')
// #ifdef H5
// onShow onLaunch
const isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent)
if (isWeixinBrowser && process.env.NODE_ENV !== 'development') {
const token = uni.getStorageSync('token')
if (!token) {
console.log('[App] onShow 检测到没有 token重新尝试登录')
this.wxH5AuthLogin()
}
}
// #endif
},
onHide: function() {
console.log('App Hide')
@ -145,50 +156,108 @@
//
const isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent)
if (!isWeixinBrowser) {
uni.showModal({
title: '提示',
content: '请在微信客户端中打开',
showCancel: false
})
console.log('[App] 非微信环境,跳过登录')
return
}
// token
const existingToken = uni.getStorageSync('token')
if (existingToken) {
console.log('[App] 已存在 token跳过登录')
return
}
let link = window.location.href;
if (/code=/.test(link) || link.indexOf("code") > -1) {
let temp = decodeURIComponent((new RegExp('[?|&]' + 'code' + '=' + '([^&;]+?)(&|#|;|$)').exec(
link) || [, ''])[1].replace(/\+/g, '%20')) || null;
console.log("code",temp)
console.log('[App] 当前 URL:', link)
// code URL
let code = null;
try {
// 1: 使 URLSearchParams ()
if (typeof URLSearchParams !== 'undefined') {
const url = new URL(link)
code = url.searchParams.get('code')
}
// 2: 1使
if (!code) {
const match = link.match(/[?&]code=([^&?#]+)/)
if (match && match[1]) {
code = decodeURIComponent(match[1].replace(/\+/g, '%20'))
}
}
// 3:
if (!code) {
const regex = new RegExp('[?|&]' + 'code' + '=' + '([^&;]+?)(&|#|;|$)')
const match = regex.exec(link)
if (match && match[1]) {
code = decodeURIComponent(match[1].replace(/\+/g, '%20'))
}
}
} catch (e) {
console.error('[App] 提取 code 失败:', e)
}
console.log('[App] 提取到的 code:', code)
if (code) {
// URL code
try {
const url = new URL(link)
url.searchParams.delete('code')
url.searchParams.delete('state')
// 使 replaceState URL
if (window.history && window.history.replaceState) {
window.history.replaceState({}, '', url.toString())
}
} catch (e) {
console.warn('[App] 清理 URL 参数失败:', e)
}
// code token
console.log('[App] 开始调用登录接口code:', code)
uni.request({
url: API.WX_LOGIN,
method: 'POST',
data: { code: temp },
success: (res) => {
const result = res.data
if (result.errcode === 0) {
const token = result.data.access_token
console.log('获取 token 成功:', token)
uni.setStorageSync('token', token)
} else {
console.error('登录失败:', result.errmsg)
uni.showToast({ title: result.errmsg, icon: 'none' })
}
},
fail: (err) => {
console.error('获取 token 失败:', err)
url: API.WX_LOGIN,
method: 'POST',
data: { code: code },
success: (res) => {
console.log('[App] 登录接口响应:', res.data)
const result = res.data
if (result.errcode === 0) {
const token = result.data.access_token
console.log('[App] 获取 token 成功:', token)
uni.setStorageSync('token', token)
// token
uni.$emit('loginSuccess', { token })
} else {
console.error('[App] 登录失败:', result.errmsg)
uni.showToast({ title: result.errmsg || '登录失败', icon: 'none' })
}
})
}else{
},
fail: (err) => {
console.error('[App] 获取 token 失败:', err)
uni.showToast({ title: '网络错误,请重试', icon: 'none' })
}
})
} else {
// code
console.log('[App] 未找到 code跳转到授权页面')
const appId = 'wx9538bc740fe87fce'
const currentUrl = window.location.href
const redirectUri = encodeURIComponent(currentUrl.replace(/#\//, ""));
// URL hash code
let currentUrl = window.location.href.split('#')[0]
// code state
currentUrl = currentUrl.replace(/[?&]code=[^&]*/g, '').replace(/[?&]state=[^&]*/g, '')
// ? ?
currentUrl = currentUrl.replace(/\?$/, '')
const redirectUri = encodeURIComponent(currentUrl)
const scope = 'snsapi_userinfo'
const state = 'STATE'
console.log(redirectUri)
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&
response_type=code&scope=${scope}&state=${state}#wechat_redirect`
console.log('[App] redirectUri:', redirectUri)
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`
//
console.log('[App] 跳转到授权页面:', authUrl)
window.location.href = authUrl
}
},
@ -206,8 +275,11 @@
// this.handleWxH5Login(res.data.code)
} else if (res.data && res.data.data.access_token) {
// token
uni.setStorageSync('token', res.data.data.access_token)
const token = res.data.data.access_token
uni.setStorageSync('token', token)
uni.showToast({ title: '登录成功', icon: 'success' })
//
uni.$emit('loginSuccess', { token })
} else {
uni.showToast({ title: '登录失败', icon: 'none' })
}

@ -35,5 +35,7 @@ export const API = {
GET_INVOICE: `${BASE_URL}/api/customer/reservation/get-invoice`,
GET_DAILY_RESERVATION_DEADLINE: `${BASE_URL}/api/customer/setting/get-daily-reservation-deadline`,
GET_GEOFENCE_BY_DIRECTION: `${BASE_URL}/api/customer/geofence/get-by-direction`,
GET_WATER_LEVEL: `${BASE_URL}/api/customer/setting/get-water-level`,
GET_UNIT_PRICE: `${BASE_URL}/api/customer/setting/get-unit-price`,
GET_SHIP_INSPECTION_EXAMPLES: `${BASE_URL}/api/customer/setting/get-ship-inspection-examples`,
}

@ -5,27 +5,58 @@
<view class="header-title">胥口枢纽闸站状态</view>
<view class="header-info">
<view class="info-item">
<text class="info-label">今日太湖水位</text>
<text class="info-value">{{ statistics.water_level.taihu }}m</text>
<text class="info-label">太湖</text>
<view class="info-value-group">
<text class="info-value"
>水位深度{{
statistics.taihu_to_xujiang
? statistics.taihu_to_xujiang.water_level
: "-"
}}m</text
>
<text class="info-value"
>吃水深度{{
statistics.taihu_to_xujiang
? statistics.taihu_to_xujiang.draft_depth
: "-"
}}m</text
>
</view>
</view>
<view class="info-item">
<text class="info-label">今日胥江水位</text>
<text class="info-value">{{ statistics.water_level.xujiang }}m</text>
<text class="info-label">胥江</text>
<view class="info-value-group">
<text class="info-value"
>水位深度{{
statistics.xujiang_to_taihu
? statistics.xujiang_to_taihu.water_level
: "-"
}}m</text
>
<text class="info-value"
>吃水深度{{
statistics.xujiang_to_taihu
? statistics.xujiang_to_taihu.draft_depth
: "-"
}}m</text
>
</view>
</view>
</view>
<view class="batch-row-strict">
<block v-for="(item,index) in statistics.batches" :key="item.id">
<block v-for="(item, index) in statistics.batches || []" :key="item.id">
<block v-if="index < 2">
<view class="batch-col">
<view class="batch-tag-strict orange">
<text class="tag-orange">{{ item.direction==='in'?'去胥江':'去太湖' }}</text>{{ item.name }}
</view>
</view>
<view v-if="index < 1" class="batch-divider-strict"></view>
<view class="batch-tag-strict orange">
<text class="tag-orange">{{
item.direction === "in" ? "去胥江" : "去太湖"
}}</text>
<view class="batch-num-strict">{{ item.name }}</view>
</view>
</view>
<view v-if="index < 1" class="batch-divider-strict"></view>
</block>
</block>
</view>
</view>
@ -36,17 +67,29 @@
</view>
<view class="process-flow">
<view class="process-step">
<image class="icon" src="/static/icon_step_index1.png" mode="aspectFit" />
<image
class="icon"
src="/static/icon_step_index1.png"
mode="aspectFit"
/>
<text class="process-label">先预约</text>
</view>
<view class="arrow">&gt;</view>
<view class="process-step">
<image class="icon" src="/static/icon_step_index2.png" mode="aspectFit" />
<image
class="icon"
src="/static/icon_step_index2.png"
mode="aspectFit"
/>
<text class="process-label">再购票</text>
</view>
<view class="arrow">&gt;</view>
<view class="process-step">
<image class="icon" src="/static/icon_step_index3.png" mode="aspectFit" />
<image
class="icon"
src="/static/icon_step_index3.png"
mode="aspectFit"
/>
<text class="process-label">排队过闸</text>
</view>
</view>
@ -55,7 +98,11 @@
<!-- 四个功能卡片 -->
<view class="card-grid">
<view class="func-card" @click="goReservation">
<image class="card-bg" src="/static/index_radius_green.png" mode="aspectFill" />
<image
class="card-bg"
src="/static/index_radius_green.png"
mode="aspectFill"
/>
<view class="func-card-content">
<text class="func-title">过闸预约</text>
<text class="func-num">{{ statistics.total_count }}</text>
@ -63,7 +110,11 @@
<view class="func-bg-icon clock"></view>
</view>
<view class="func-card" @click="goOrder">
<image class="card-bg" src="/static/index_radius_blue.png" mode="aspectFill" />
<image
class="card-bg"
src="/static/index_radius_blue.png"
mode="aspectFill"
/>
<view class="func-card-content">
<text class="func-title">在线付款</text>
<text class="func-num">{{ statistics.unpaid_count }}</text>
@ -71,7 +122,11 @@
<view class="func-bg-icon ticket"></view>
</view>
<view class="func-card" @click="goWaitPass">
<image class="card-bg" src="/static/index_radius_orange.png" mode="aspectFill" />
<image
class="card-bg"
src="/static/index_radius_orange.png"
mode="aspectFill"
/>
<view class="func-card-content">
<text class="func-title">排队过闸</text>
<text class="func-num">{{ statistics.paid_count }}</text>
@ -79,7 +134,11 @@
<view class="func-bg-icon ship"></view>
</view>
<view class="func-card" @click="goInvoiceManage">
<image class="card-bg" src="/static/index_radius_purple.png" mode="aspectFill" />
<image
class="card-bg"
src="/static/index_radius_purple.png"
mode="aspectFill"
/>
<view class="func-card-content">
<text class="func-title">我的开票</text>
<text class="func-num">{{ statistics.billed_count }}</text>
@ -103,7 +162,7 @@
<text class="info-text">北向南2025040102准备过闸</text>
</view>
</view> -->
<!-- 编辑信息弹窗 -->
<view v-if="showEditPopup" class="edit-popup-mask" @click="closeEditPopup">
<view class="edit-popup" @click.stop>
@ -114,7 +173,11 @@
<view class="edit-popup-content">
<view class="edit-field">
<text class="edit-label">姓名</text>
<input class="edit-input" v-model="editForm.name" placeholder="请输入姓名" />
<input
class="edit-input"
v-model="editForm.name"
placeholder="请输入姓名"
/>
</view>
<view class="edit-field">
<text class="edit-label">交款人类型</text>
@ -164,190 +227,303 @@
</template>
<script>
import { API } from '@/config/index.js'
import { API } from "@/config/index.js";
export default {
data() {
return {
batches: [
{
id: 1,
direction: "in",
name: "2025010101",
},
{
id: 2,
direction: "out",
name: "2025010102",
},
],
isWeixinBrowser: false,
userInfo: null,
showEditPopup: false,
editForm: {
name: '',
name: "",
payer_type: 1,
phone: '',
id_card: ''
phone: "",
id_card: "",
},
statistics: {
water_level: {
taihu: '-',
xujiang: '-'
},
taihu_to_xujiang: null, // { water_level, draft_depth }
xujiang_to_taihu: null, // { water_level, draft_depth }
total_count: 0,
unpaid_count: 0,
paid_count: 0,
billed_count: 0
}
}
billed_count: 0,
// - batches
batches: [
{
id: 1,
direction: "in",
name: "2025010101",
},
{
id: 2,
direction: "out",
name: "2025010102",
},
],
},
};
},
onLoad() {
// #ifdef H5
this.isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent)
this.isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent);
// #endif
this.fetchUserInfo()
this.fetchStatistics()
// token
this.waitForTokenAndFetch();
},
onShow() {
//
const token = uni.getStorageSync("token");
if (token) {
this.fetchUserInfo();
this.fetchStatistics();
this.fetchWaterLevel();
}
},
onUnload() {
//
uni.$off("loginSuccess", this.onLoginSuccess);
},
methods: {
// token
waitForTokenAndFetch() {
const token = uni.getStorageSync("token");
if (token) {
// token
this.fetchUserInfo();
this.fetchStatistics();
this.fetchWaterLevel();
} else {
// token
uni.$on("loginSuccess", this.onLoginSuccess);
//
let retryCount = 0;
const maxRetries = 20; // 20500ms10
const checkToken = setInterval(() => {
retryCount++;
const currentToken = uni.getStorageSync("token");
if (currentToken) {
clearInterval(checkToken);
this.fetchUserInfo();
this.fetchStatistics();
this.fetchWaterLevel();
} else if (retryCount >= maxRetries) {
clearInterval(checkToken);
console.warn("等待 token 超时,可能登录失败");
}
}, 500);
}
},
//
onLoginSuccess() {
//
uni.$off("loginSuccess", this.onLoginSuccess);
//
this.fetchUserInfo();
this.fetchStatistics();
this.fetchWaterLevel();
},
async fetchWaterLevel() {
const token = uni.getStorageSync("token");
if (!token) {
return;
}
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_WATER_LEVEL}?token=${token}`,
method: "get",
success: resolve,
fail: reject,
});
});
if (res.data && res.data.errcode === 0) {
const data = res.data.data;
// statistics
if (data) {
console.log("data", data);
this.statistics.taihu_to_xujiang = data.taihu_to_xujiang || null;
this.statistics.xujiang_to_taihu = data.xujiang_to_taihu || null;
}
console.log(this.statistics);
}
},
async fetchStatistics() {
const token = uni.getStorageSync('token')
const token = uni.getStorageSync("token");
if (!token) {
return
return;
}
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.STATISTICS}?token=${token}`,
method: 'get',
method: "get",
success: resolve,
fail: reject
})
})
fail: reject,
});
});
if (res.data && res.data.errcode === 0) {
this.statistics = res.data.data
}
//
const existingWaterData = {
taihu_to_xujiang: this.statistics.taihu_to_xujiang,
xujiang_to_taihu: this.statistics.xujiang_to_taihu,
};
this.statistics = {
...res.data.data,
...existingWaterData,
};
}
},
async fetchUserInfo() {
const token = uni.getStorageSync('token')
const token = uni.getStorageSync("token");
if (!token) {
this.userInfo = null
return
this.userInfo = null;
return;
}
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_USER_INFO}?token=${token}`,
method: 'POST',
method: "POST",
success: resolve,
fail: reject
})
})
fail: reject,
});
});
if (res.data) {
const data = res.data || {}
const payerType = Number(data.payer_type ?? data.payerType ?? 1)
this.userInfo = { ...data, payer_type: payerType }
const data = res.data || {};
const payerType = Number(data.payer_type ?? data.payerType ?? 1);
this.userInfo = { ...data, payer_type: payerType };
this.editForm = {
name: data.name || '',
name: data.name || "",
payer_type: payerType,
phone: data.phone || '',
id_card: data.id_card || ''
}
phone: data.phone || "",
id_card: data.id_card || "",
};
}
} catch (e) {
this.userInfo = null
this.userInfo = null;
}
},
requireUserInfoComplete() {
const u = this.userInfo || {}
const ok = !!(u.name && (u.payer_type ?? u.payerType) && u.phone && u.id_card)
return ok
const u = this.userInfo || {};
const ok = !!(
u.name &&
(u.payer_type ?? u.payerType) &&
u.phone &&
u.id_card
);
return ok;
},
openEditPopup() {
this.showEditPopup = true
this.showEditPopup = true;
},
closeEditPopup() {
this.showEditPopup = false
this.showEditPopup = false;
},
validateForm() {
if (!this.editForm.name) {
uni.showToast({ title: '请输入姓名', icon: 'none' })
return false
uni.showToast({ title: "请输入姓名", icon: "none" });
return false;
}
if (![1, 2].includes(Number(this.editForm.payer_type))) {
uni.showToast({ title: '请选择交款人类型', icon: 'none' })
return false
uni.showToast({ title: "请选择交款人类型", icon: "none" });
return false;
}
const phoneReg = /^1\d{10}$/
const phoneReg = /^1\d{10}$/;
if (!phoneReg.test(this.editForm.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return false
uni.showToast({ title: "请输入正确的手机号", icon: "none" });
return false;
}
const idReg = /^(?:\d{15}|\d{17}[\dXx])$/
const idReg = /^(?:\d{15}|\d{17}[\dXx])$/;
if (!idReg.test(this.editForm.id_card)) {
uni.showToast({ title: '请输入正确的证件号', icon: 'none' })
return false
uni.showToast({ title: "请输入正确的证件号", icon: "none" });
return false;
}
return true
return true;
},
async submitUserInfo() {
if (!this.validateForm()) return
const token = uni.getStorageSync('token')
if (!this.validateForm()) return;
const token = uni.getStorageSync("token");
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
uni.showToast({ title: "请先登录", icon: "none" });
return;
}
try {
uni.showLoading({ title: '提交中...' })
uni.showLoading({ title: "提交中..." });
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.UPDATE_USER_INFO}?token=${token}`,
method: 'POST',
method: "POST",
data: {
name: this.editForm.name,
payer_type: Number(this.editForm.payer_type),
phone: this.editForm.phone,
id_card: this.editForm.id_card
id_card: this.editForm.id_card,
},
success: resolve,
fail: reject
})
})
uni.hideLoading()
fail: reject,
});
});
uni.hideLoading();
if (res.data && res.data.errcode === 0) {
uni.showToast({ title: '更新成功', icon: 'success' })
this.closeEditPopup()
await this.fetchUserInfo()
uni.showToast({ title: "更新成功", icon: "success" });
this.closeEditPopup();
await this.fetchUserInfo();
} else {
uni.showToast({ title: (res.data && res.data.errmsg) || '更新失败', icon: 'none' })
uni.showToast({
title: (res.data && res.data.errmsg) || "更新失败",
icon: "none",
});
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '更新失败', icon: 'none' })
uni.hideLoading();
uni.showToast({ title: "更新失败", icon: "none" });
}
},
goReservation() {
const afterCheck = () => {
if (this.requireUserInfoComplete()) {
uni.navigateTo({ url: '/pages/reservation/index' });
uni.navigateTo({ url: "/pages/reservation/index" });
} else {
uni.showModal({
title: '提示',
content: '为了后续流程的使用,请先完善个人信息',
confirmText: '去完善',
cancelText: '取消',
title: "提示",
content: "为了后续流程的使用,请先完善个人信息",
confirmText: "去完善",
cancelText: "取消",
success: (res) => {
if (res.confirm) {
this.openEditPopup()
this.openEditPopup();
}
}
})
},
});
}
}
};
if (!this.userInfo) {
this.fetchUserInfo().then(afterCheck).catch(afterCheck)
this.fetchUserInfo().then(afterCheck).catch(afterCheck);
} else {
afterCheck()
afterCheck();
}
},
goWaitPass() {
uni.navigateTo({ url: '/pages/order/pay_order_list?status=paid' });
uni.navigateTo({ url: "/pages/order/pay_order_list?status=paid" });
},
goOrder() {
uni.navigateTo({ url: '/pages/order/pay_order_list?status=unpaid' });
uni.navigateTo({ url: "/pages/order/pay_order_list?status=unpaid" });
},
goInvoiceManage() {
uni.navigateTo({ url: '/pages/index/invoice_manage' });
}
}
}
uni.navigateTo({ url: "/pages/index/invoice_manage" });
},
},
};
</script>
<style scoped>
@ -387,7 +563,7 @@ export default {
justify-content: center;
align-items: flex-start;
width: fit-content;
margin: 5vh auto 0 auto; /* 水平居中 */
margin: 2vh auto 0 auto; /* 水平居中 */
gap: 154rpx; /* 控制每组间距 */
}
@ -399,25 +575,36 @@ export default {
}
.info-label {
font-size: 24rpx;
font-size: 32rpx;
opacity: 0.95;
color: #e6eaff;
}
.info-value {
font-size: 32rpx;
font-size: 24rpx;
font-weight: normal;
margin-top: 8rpx;
display: block;
color: #fff;
font-family: sans-serif;
}
.info-value-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4rpx;
margin-top: 8rpx;
}
.info-value-group .info-value {
margin-top: 0;
font-size: 28rpx;
}
.batch-row-strict {
display: flex;
align-items: flex-start;
justify-content: center;
margin: 4vh 48rpx 0 48rpx;
margin: 2vh 48rpx 0 48rpx;
position: relative;
}
@ -439,7 +626,8 @@ export default {
font-size: 32rpx;
font-weight: normal;
margin-bottom: 8rpx;
font-family: 'PangMenZhengDao', 'SourceHanSansCN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-family: "PangMenZhengDao", "SourceHanSansCN", "PingFang SC",
"Microsoft YaHei", sans-serif;
letter-spacing: 3rpx;
}
@ -448,21 +636,21 @@ export default {
padding: 4rpx 12rpx;
border-radius: 20rpx;
background: #2b70ee;
display: flex;
/* display: flex; */
align-items: center;
gap: 4rpx;
}
.tag-orange {
color: #FF9F43;
color: #ff9f43;
}
.tag-green {
color: #28C76F;
color: #28c76f;
}
.tag-blue {
color: #4FC3FF;
color: #4fc3ff;
}
.process-card {
@ -470,7 +658,7 @@ export default {
border-radius: 22rpx;
margin: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.08);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
position: relative;
margin-top: -140rpx;
height: 138px;
@ -488,7 +676,7 @@ export default {
border-radius: 0 0 20rpx 20rpx;
padding: 12rpx 0;
text-align: center;
box-shadow: 0 4rpx 16rpx rgba(59,124,255,0.12);
box-shadow: 0 4rpx 16rpx rgba(59, 124, 255, 0.12);
z-index: 2;
}
@ -526,7 +714,7 @@ export default {
}
.arrow {
color: #B0B8C6;
color: #b0b8c6;
font-size: 40rpx;
font-weight: bold;
margin: 0 12rpx;
@ -545,7 +733,7 @@ export default {
border-radius: 32rpx;
height: 200rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.08);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
justify-content: space-between;
@ -582,7 +770,8 @@ export default {
font-size: 58rpx;
font-weight: normal;
color: #fff;
font-family: 'PangMenZhengDao', 'SourceHanSansCN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-family: "PangMenZhengDao", "SourceHanSansCN", "PingFang SC",
"Microsoft YaHei", sans-serif;
letter-spacing: 5rpx;
}
@ -734,7 +923,7 @@ export default {
}
.notice {
background: linear-gradient(90deg, #2b70ee 0%, #4FC3FF 100%);
background: linear-gradient(90deg, #2b70ee 0%, #4fc3ff 100%);
color: white;
}

File diff suppressed because it is too large Load Diff

@ -4,6 +4,17 @@
<NavBar title="船舶详情" />
</view>
<view class="content-area">
<!-- 单价和计算规则 -->
<view v-if="unitPrice || calculationDescription" class="price-info-section">
<view v-if="unitPrice" class="price-info-item">
<text class="price-label">计算单价</text>
<text class="price-value">{{ unitPrice }}</text>
</view>
<view v-if="calculationDescription" class="price-info-item">
<text class="price-label">计算规则</text>
<text class="price-desc">{{ calculationDescription }}</text>
</view>
</view>
<view v-if="ship" class="ship-section">
<view class="section-title">基本信息</view>
<view class="section-row"><text>船舶所有人</text><text>{{ ship.owner_name }}</text></view>
@ -13,7 +24,7 @@
</view>
<view v-if="ship" class="ship-section">
<view class="section-title">船舶参数</view>
<view class="section-row"><text>吨位</text><text>{{ ship.total_tonnage }}</text></view>
<view class="section-row"><text>载重吨位</text><text>{{ ship.total_tonnage }}</text></view>
<view class="section-row"><text>总长度</text><text>{{ ship.total_length }}</text></view>
<view class="section-row"><text>总宽</text><text>{{ ship.total_width }}</text></view>
<view class="section-row"><text>型深</text><text>{{ ship.molded_depth }}</text></view>
@ -69,10 +80,13 @@ export default {
picture1_file: {},
picture2_file: {},
picture3_file: {},
unitPrice: '', //
calculationDescription: '', //
}
},
onLoad(options) {
this.id = options.id;
this.fetchUnitPrice()
if (options.item) {
try {
this.ship = JSON.parse(decodeURIComponent(options.item));
@ -90,6 +104,25 @@ export default {
});
},
methods: {
async fetchUnitPrice() {
const token = uni.getStorageSync('token')
if (!token) {
return
}
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_UNIT_PRICE}?token=${token}`,
method: 'get',
success: resolve,
fail: reject
})
})
if (res.data && res.data.errcode === 0) {
const data = res.data.data;
this.unitPrice = data.unit_price || '';
this.calculationDescription = data.calculation_description || '';
}
},
getShipTypeName(type) {
if (!Array.isArray(this.shipTypeEnum)) return type;
const found = this.shipTypeEnum.find(item => item.value === type?.toString());
@ -314,6 +347,38 @@ export default {
padding-top: 90px;
padding-bottom: 20px;
}
.price-info-section {
background: #fff;
border-radius: 24rpx;
margin: 0 24rpx 32rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(59,124,255,0.08);
padding: 32rpx 24rpx;
margin-top: 20px;
}
.price-info-item {
display: flex;
align-items: flex-start;
margin-bottom: 16rpx;
font-size: 28rpx;
}
.price-info-item:last-child {
margin-bottom: 0;
}
.price-label {
color: #666;
min-width: 140rpx;
flex-shrink: 0;
}
.price-value {
color: #217aff;
font-weight: 600;
flex: 1;
}
.price-desc {
color: #222;
flex: 1;
line-height: 1.6;
}
.fixed-bottom-btn-bar {
position: fixed;
left: 0;

@ -21,7 +21,7 @@
</view>
<view class="ship-row">所有人: {{ item.owner_name }}</view>
<view class="ship-row">联系电话: {{ item.phone }}</view>
<view class="ship-row">吨位: {{ item.total_tonnage }} </view>
<view class="ship-row">载重吨位: {{ item.total_tonnage }} </view>
</view>
</view>
</view>

@ -4,6 +4,17 @@
<NavBar title="订单详情" />
</view>
<view class="pay-scroll">
<!-- 单价和计算规则 -->
<view v-if="unitPrice || calculationDescription" class="price-info-section">
<view v-if="unitPrice" class="price-info-item">
<text class="price-label">计算单价</text>
<text class="price-value">{{ unitPrice }}</text>
</view>
<view v-if="calculationDescription" class="price-info-item">
<text class="price-label">计算规则</text>
<text class="price-desc">{{ calculationDescription }}</text>
</view>
</view>
<!-- 预约信息 -->
<view class="pay-section">
<view class="pay-title">预约信息</view>
@ -157,6 +168,8 @@ export default {
shipTypeEnum: [],
reservationStatusEnum: [],
qrContent: "",
unitPrice: '', //
calculationDescription: '', //
};
},
onLoad(options) {
@ -175,6 +188,8 @@ export default {
// #endif
},
onShow() {
//
this.fetchUnitPrice();
this.fetchShipTypeEnum().then(() => {
if (this.item.id && this.item.status === "unpaid") {
this.fetchQrcode(this.item.id);
@ -184,6 +199,26 @@ export default {
},
methods: {
formatChinaDate: base.formatChinaDate,
//
async fetchUnitPrice() {
const token = uni.getStorageSync('token')
if (!token) {
return
}
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_UNIT_PRICE}?token=${token}`,
method: 'get',
success: resolve,
fail: reject
})
})
if (res.data && res.data.errcode === 0) {
const data = res.data.data;
this.unitPrice = data.unit_price || '';
this.calculationDescription = data.calculation_description || '';
}
},
async fetchQrcode(id) {
const token = uni.getStorageSync("token");
if (!token || !id) return;
@ -639,4 +674,36 @@ export default {
border: none;
}
}
.price-info-section {
background: #fff;
border-radius: 24rpx;
margin: 0 24rpx 32rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(59, 124, 255, 0.08);
padding: 32rpx 24rpx;
margin-top: 20px;
}
.price-info-item {
display: flex;
align-items: flex-start;
margin-bottom: 16rpx;
font-size: 28rpx;
}
.price-info-item:last-child {
margin-bottom: 0;
}
.price-label {
color: #666;
min-width: 140rpx;
flex-shrink: 0;
}
.price-value {
color: #217aff;
font-weight: 600;
flex: 1;
}
.price-desc {
color: #222;
flex: 1;
line-height: 1.6;
}
</style>

@ -4,7 +4,17 @@
<NavBar title="过闸预约" />
</view>
<view class="reservation-scroll">
<!-- 单价和计算规则 -->
<view v-if="unitPrice || calculationDescription" class="price-info-section">
<view v-if="unitPrice" class="price-info-item">
<text class="price-label">计算单价</text>
<text class="price-value">{{ unitPrice }}</text>
</view>
<view v-if="calculationDescription" class="price-info-item">
<text class="price-label">计算规则</text>
<text class="price-desc">{{ calculationDescription }}</text>
</view>
</view>
<!-- 船舶信息卡片2详细信息 -->
<view class="card">
<view class="card-title">船舶信息</view>
@ -118,6 +128,8 @@ export default {
isDateDisabled: false, //
userLocation: null, // {latitude, longitude}
isInGeofence: null, // nulltruefalse
unitPrice: '', //
calculationDescription: '', //
}
},
onLoad() {
@ -126,6 +138,8 @@ export default {
// #endif
},
async onShow() {
//
this.fetchUnitPrice();
//
await this.fetchDirectionEnum();
await this.fetchShipTypeEnum();
@ -159,6 +173,26 @@ export default {
this.fetchDailyReservationDeadline();
},
methods: {
//
async fetchUnitPrice() {
const token = uni.getStorageSync('token')
if (!token) {
return
}
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_UNIT_PRICE}?token=${token}`,
method: 'get',
success: resolve,
fail: reject
})
})
if (res.data && res.data.errcode === 0) {
const data = res.data.data;
this.unitPrice = data.unit_price || '';
this.calculationDescription = data.calculation_description || '';
}
},
//
getUserLocation() {
// #ifdef H5
@ -924,4 +958,36 @@ export default {
background: #217aff;
color: #fff;
}
.price-info-section {
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;
}
.price-info-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
font-size: 14px;
}
.price-info-item:last-child {
margin-bottom: 0;
}
.price-label {
color: #666;
min-width: 100px;
flex-shrink: 0;
}
.price-value {
color: #217aff;
font-weight: 600;
flex: 1;
}
.price-desc {
color: #222;
flex: 1;
line-height: 1.6;
}
</style>

@ -0,0 +1,35 @@
后台:
1. 价格计算参数、吃水深度计算参数;
2. 短信通知、模版消息/订阅消息;
3. 初审之后就可以预约;
4. 退款处理流程
前台新增的三个接口在swagger的[用户端系统设置](https://xukoushuniu.115.langye.net/swagger/#/用户端系统设置)
-1. “总吨位”改成“载重吨位”,直接给出价格算法和结果,分别在填写船舶信息、查看船舶信息、预约界面展示;
2. 船检簿示例及文字说明,通过接口获取,展示在录入船舶信息的页面;
3. 订阅消息优先级低相关文档请参照公众号订阅消息文档后端接口暂未提供1_v39TguWNu6ALEpdXSuy8hHFep0m4NrARAutSvl5sRE预约成功之后订阅后台排班确认后触发
_v39TguWNu6ALEpdXSuy8hHFep0m4NrARAutSvl5sRE
![image-20251215231038839](/Users/weizongsong/Library/Application Support/typora-user-images/image-20251215231038839.png)
14ydgM4-nHNOfYiotYFmvxZR_fOwHlQcgc-aXu4KZ8Hs支付成功之后订阅后台开始检票后触发
![image-20251215231103582](/Users/weizongsong/Library/Application Support/typora-user-images/image-20251215231103582.png)
4. 初次进入页面未自动登录的BUG初次进入首页的时候携带了code但是没有自动登录。
5. 水位信息展示修改:通过/api/customer/setting/get-water-level获取数据前端展示水位和吃水深度
其他:
1. 操作手册
2. 上线节点梳理
3. 后台用户清单

@ -0,0 +1,406 @@
# 胥口闸站购票系统 - 用户操作手册
## 目录
1. [系统简介](#系统简介)
2. [首次使用](#首次使用)
3. [主要功能](#主要功能)
- [3.1 首页](#31-首页)
- [3.2 船舶管理](#32-船舶管理)
- [3.3 过闸预约](#33-过闸预约)
- [3.4 订单管理](#34-订单管理)
- [3.5 在线付款](#35-在线付款)
- [3.6 发票管理](#36-发票管理)
- [3.7 消息中心](#37-消息中心)
- [3.8 个人中心](#38-个人中心)
---
## 系统简介
胥口闸站购票系统是一款为船舶过闸提供预约、购票、支付等服务的移动端应用。系统支持微信小程序和H5网页两种使用方式。
**核心流程:先预约 → 再购票 → 排队过闸**
---
## 首次使用
### 1. 登录/授权
#### 微信小程序用户
- 打开微信,搜索"胥口闸站购票"小程序
- 首次打开会自动弹出微信授权提示
- 点击"允许"完成授权登录
#### 微信H5用户
- 在微信中打开系统链接
- 系统会自动跳转到微信授权页面
- 点击"同意"完成授权登录
**注意:** 系统需要获取您的位置信息用于预约验证,请允许位置权限。
---
## 主要功能
### 3.1 首页
首页是系统的核心入口,显示以下信息:
#### 3.1.1 闸站状态信息
- **今日太湖水位**:显示当前太湖水位高度(单位:米)
- **今日胥江水位**:显示当前胥江水位高度(单位:米)
- **当前批次**显示正在进行的过闸批次信息最多显示2个
#### 3.1.2 闸站流程指引
系统提供清晰的流程指引:
1. **先预约** - 选择航行方向和过闸日期进行预约
2. **再购票** - 预约成功后进行在线支付
3. **排队过闸** - 支付完成后等待过闸
#### 3.1.3 功能入口卡片
首页提供四个主要功能入口:
1. **过闸预约**
- 显示预约总数
- 点击进入预约页面
2. **在线付款**
- 显示待支付订单数量
- 点击进入付款列表
3. **排队过闸**
- 显示已支付订单数量
- 点击查看排队状态
4. **我的开票**
- 显示已开票数量
- 点击进入发票管理
---
### 3.2 船舶管理
在预约过闸前,您需要先添加并审核通过船舶信息。
#### 3.2.1 进入船舶管理
- 方式一:从首页点击"过闸预约",如果没有可用船舶会自动跳转到船舶管理
- 方式二:从"我的"页面进入船舶管理
#### 3.2.2 添加船舶
1. 点击"添加船只"按钮
2. 按步骤填写信息:
**第一步:基本信息**
- 船舶所有人(必填):姓名需与身份证一致
- 身份证号必填18位身份证号码
- 联系电话必填11位手机号码
- 船舶编号(必填)
- 船舶类型(必填):选择货船或客船
**第二步:船舶参数**
- 总吨位(必填,单位:吨)
- 总长度(必填,单位:米)
- 总宽(必填,单位:米)
- 型深(必填,单位:米)
- 参考载重吨位必填选择A/B/C/D等级
- 船型(必填):根据实际情况选择
**第三步:船检簿上传**
- 第一页(必填):上传船检簿第一页照片
- 第二页(必填):上传船检簿第二页照片
- 第三页(必填):上传船检簿第三页照片
- 可点击"查看示例"了解上传要求
**第四步:签名确认**
- 勾选承诺声明:"本人承诺所提供材料皆真实有效;如有虚假,本人承担因此造成的全部责任。"
- 手写签名:在签名区域完成手写签名
- 可点击"重新签名"清除重签
- 可点击"预览签名"查看签名效果
3. 点击"提交"完成添加
4. 提交后等待审核,审核状态可在船舶列表中查看
#### 3.2.3 查看船舶详情
- 在船舶列表中点击任意船舶卡片
- 查看船舶的详细信息、审核状态和签名图片
#### 3.2.4 编辑船舶
- 在船舶详情页点击"编辑"按钮
- 或从船舶管理列表点击"编辑"
- 修改信息后重新提交审核
**注意:** 编辑时如果已有签名,系统会显示原有签名,您可以选择使用原签名或重新签名。
#### 3.2.5 删除船舶
- 在船舶详情页点击"删除"按钮
- 确认删除后,该船舶信息将被永久删除
#### 3.2.6 船舶审核状态
- **待审核**:已提交,等待审核
- **已通过**:审核通过,可用于预约
- **已拒绝**:审核未通过,需修改后重新提交
---
### 3.3 过闸预约
预约是过闸的第一步,需要选择航行方向和过闸日期。
#### 3.3.1 进入预约页面
- 从首页点击"过闸预约"卡片
- 系统会先检查是否有可用船舶:
- 如果没有可用船舶,会提示并跳转到船舶管理页面
- 如果有可用船舶,继续预约流程
#### 3.3.2 位置信息获取
- 进入预约页面时,系统会弹出提示:"预约前需要先获取您的位置信息"
- 点击"确定"允许获取位置
- 系统会自动获取您当前的经纬度信息
- **重要:** 位置信息用于验证您是否在闸站可预约范围内
#### 3.3.3 选择航行方向
- 系统会显示可选的航行方向(如:去胥江、去太湖等)
- 点击选择您要前往的方向
- **注意:** 必须选择航行方向才能继续
#### 3.3.4 选择过闸日期
- 系统会根据当前时间和截止时间自动设置默认日期:
- 如果当前时间早于截止时间:默认选择"今天",可更改
- 如果当前时间晚于截止时间:默认选择"明天",不可更改(按钮显示为橙色渐变背景)
- **注意:** 过闸日期一旦确定后不可更改
#### 3.3.5 位置范围验证
- 选择航行方向后,系统会自动验证您的位置是否在可预约范围内
- 如果不在范围内,会弹出提示:"您的当前位置不在闸站可预约范围内"
- 此时无法提交预约,请移动到可预约范围内后重试
#### 3.3.6 阅读并同意预约须知
- 勾选"我已阅读并同意《过闸预约服务协议》"
- 必须同意才能提交预约
#### 3.3.7 提交预约
- 确认所有信息无误后,点击底部"预约"按钮
- 系统会验证:
- 是否同意预约须知
- 是否选择航行方向
- 位置是否在可预约范围内
- 是否有可用船舶
- 验证通过后提交成功,跳转到订单页面
---
### 3.4 订单管理
在"订单"页面可以查看所有预约订单,进行支付、取消、重新预约等操作。
#### 3.4.1 查看订单列表
- 点击底部导航栏"订单"标签
- 显示所有预约订单,按时间倒序排列
- 每个订单卡片显示:
- 订单状态(待支付、已支付、已取消等)
- 开票状态(如已开票)
- 创建时间
- 订单编号和船舶编号
- 航行方向和批次信息
#### 3.4.2 订单状态说明
- **待支付**:预约成功,等待支付
- **已支付**:已支付,等待过闸
- **已取消**:订单已取消
- **其他状态**:根据实际情况显示
#### 3.4.3 订单操作
**取消预约**
- 仅"待支付"状态的订单可以取消
- 点击"取消预约"按钮
- 确认后订单状态变为"已取消"
**重新预约**
- 某些状态下可以重新预约
- 点击"重新预约"按钮
- 跳转到预约页面,重新填写信息
**去支付**
- "待支付"状态的订单可以支付
- 点击"去支付"按钮
- 跳转到支付页面
**去开票**
- 已支付的订单可以开具发票
- 点击"去开票"按钮
- 跳转到发票开具页面
**查看详情**
- 点击"查看详情"按钮
- 查看订单的完整信息
#### 3.4.4 下拉刷新和上拉加载
- 下拉页面可以刷新订单列表
- 上拉到底部可以加载更多订单
---
### 3.5 在线付款
#### 3.5.1 进入付款页面
- 方式一:从首页点击"在线付款"卡片
- 方式二:从订单列表点击"去支付"按钮
#### 3.5.2 查看待支付订单
- 显示所有待支付的订单列表
- 每个订单显示:
- 订单编号
- 船舶信息
- 航行方向
- 过闸日期
- 应付金额
#### 3.5.3 支付订单
1. 选择要支付的订单
2. 确认订单信息
3. 选择支付方式
4. 完成支付
5. 支付成功后订单状态更新为"已支付"
#### 3.5.4 查看支付详情
- 点击订单可以查看支付详情
- 包括支付时间、支付金额、支付方式等信息
---
### 3.6 发票管理
#### 3.6.1 进入发票管理
- 方式一:从首页点击"我的开票"卡片
- 方式二:从"我的"页面进入发票管理
#### 3.6.2 查看发票列表
- 显示所有已开具的发票
- 每个发票显示:
- 发票类型
- 发票金额
- 开具时间
- 发票状态
#### 3.6.3 开具发票
1. 从订单详情页点击"去开票"
2. 或从发票管理页面点击"开具发票"
3. 选择要开具发票的订单
4. 填写发票信息:
- 发票类型(个人/单位)
- 发票抬头
- 税号(单位必填)
- 联系方式
5. 提交开具申请
6. 开具成功后可在发票列表中查看
#### 3.6.4 查看发票详情
- 点击发票卡片查看发票详情
- 可以查看发票的完整信息
- 可以下载或打印发票
---
### 3.7 消息中心
#### 3.7.1 进入消息中心
- 点击底部导航栏"消息"标签
#### 3.7.2 查看消息
- 显示所有系统通知和公告消息
- 每条消息显示:
- 消息类型标签(带颜色区分)
- 消息标题
- 消息内容
- 发布时间
#### 3.7.3 消息类型
- 系统会根据消息类型显示不同颜色的标签
- 包括:公告、通知、提醒等类型
#### 3.7.4 刷新和加载
- 下拉可以刷新消息列表
- 上拉可以加载更多消息
---
### 3.8 个人中心
#### 3.8.1 进入个人中心
- 点击底部导航栏"我的"标签
#### 3.8.2 个人信息
- 显示头像、昵称、手机号等信息
- 点击个人信息区域可以编辑
#### 3.8.3 编辑个人信息
1. 点击个人信息区域或设置图标
2. 在弹出的编辑窗口中修改:
- 姓名
- 交款人类型(个人/单位)
- 手机号
- 证件号(身份证号)
3. 点击"保存"完成修改
#### 3.8.4 功能菜单
个人中心提供以下功能入口:
- **船舶管理**:管理您的船舶信息
- **发票管理**:查看和管理发票
- **关于我们**:查看系统相关信息
---
## 常见问题
### Q1: 为什么无法预约?
**A:** 可能的原因:
1. 没有可用船舶或船舶未审核通过
2. 未选择航行方向
3. 当前位置不在闸站可预约范围内
4. 未同意预约须知
### Q2: 为什么过闸日期不能更改?
**A:** 系统根据当前时间和截止时间自动设置过闸日期,一旦确定后不可更改,这是为了确保预约的准确性。
### Q3: 如何知道我的位置是否在可预约范围内?
**A:** 选择航行方向后,系统会自动验证。如果不在范围内,会弹出提示信息。
### Q4: 船舶审核需要多长时间?
**A:** 审核时间由管理员决定,请耐心等待。审核结果会通过消息通知您。
### Q5: 可以取消已支付的订单吗?
**A:** 已支付的订单无法取消,如需取消请联系客服。
### Q6: 发票可以重复开具吗?
**A:** 每个订单只能开具一次发票,请谨慎操作。
### Q7: 忘记登录怎么办?
**A:** 系统使用微信授权登录,重新打开应用会自动登录。
---
## 注意事项
1. **位置权限**:预约功能需要获取您的位置信息,请务必允许位置权限
2. **船舶信息**:请确保船舶信息真实有效,虚假信息将承担相应责任
3. **预约时间**:请在截止时间前完成预约,过期将无法预约当日
4. **支付时效**:预约成功后请及时支付,避免订单过期
5. **网络环境**:建议在良好的网络环境下使用,避免操作失败
---
## 技术支持
如遇到问题,可通过以下方式联系:
- 查看系统消息中的通知
- 联系客服(如有提供联系方式)
---
**版本信息**
- 文档版本v1.0
- 更新日期2025年
- 适用平台微信小程序、微信H5
Loading…
Cancel
Save