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.

975 lines
24 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="home-container" :class="{ 'wechat-browser': isWeixinBrowser }">
<!-- 顶部蓝色渐变背景及数据 -->
<view class="header-gradient">
<view class="header-title">胥口枢纽闸站状态</view>
<view class="header-info">
<view class="info-item">
<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>
<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-if="index < 2">
<view class="batch-col">
<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>
<!-- 闸站流程 -->
<view class="process-card">
<view class="process-title-bar">
<text>闸站流程</text>
</view>
<view class="process-flow">
<view class="process-step">
<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"
/>
<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"
/>
<text class="process-label">排队过闸</text>
</view>
</view>
</view>
<!-- 四个功能卡片 -->
<view class="card-grid">
<view class="func-card" @click="goReservation">
<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>
</view>
<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"
/>
<view class="func-card-content">
<text class="func-title">在线付款</text>
<text class="func-num">{{ statistics.unpaid_count }}</text>
</view>
<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"
/>
<view class="func-card-content">
<text class="func-title">排队过闸</text>
<text class="func-num">{{ statistics.paid_count }}</text>
</view>
<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"
/>
<view class="func-card-content">
<text class="func-title">我的开票</text>
<text class="func-num">{{ statistics.billed_count }}</text>
</view>
<view class="func-bg-icon invoice"></view>
</view>
</view>
<!-- 实时/公告信息 -->
<!-- <view class="info-list">
<view class="info-item-row">
<view class="info-tag realtime">
<text class="info-tag-text">实时</text>
</view>
<text class="info-text">北向南2025040102准备过闸</text>
</view>
<view class="info-item-row">
<view class="info-tag notice">
<text class="info-tag-text">公告</text>
</view>
<text class="info-text">北向南2025040102准备过闸</text>
</view>
</view> -->
<!-- 编辑信息弹窗 -->
<view v-if="showEditPopup" class="edit-popup-mask" @click="closeEditPopup">
<view class="edit-popup" @click.stop>
<view class="edit-popup-header">
<text>更新信息</text>
<text class="edit-popup-close" @click="closeEditPopup">×</text>
</view>
<view class="edit-popup-content">
<view class="edit-field">
<text class="edit-label">姓名</text>
<input
class="edit-input"
v-model="editForm.name"
placeholder="请输入姓名"
/>
</view>
<view class="edit-field">
<text class="edit-label">交款人类型</text>
<view class="payer-type-group">
<view
class="payer-type-item"
:class="{ active: Number(editForm.payer_type) === 1 }"
@click="editForm.payer_type = 1"
>
个人
</view>
<view
class="payer-type-item"
:class="{ active: Number(editForm.payer_type) === 2 }"
@click="editForm.payer_type = 2"
>
单位
</view>
</view>
</view>
<view class="edit-field">
<text class="edit-label">手机号</text>
<input
class="edit-input"
v-model="editForm.phone"
type="number"
maxlength="11"
placeholder="请输入手机号"
/>
</view>
<view class="edit-field">
<text class="edit-label">证件号</text>
<input
class="edit-input"
v-model="editForm.id_card"
placeholder="请输入身份证号"
maxlength="18"
/>
</view>
</view>
<view class="edit-popup-footer">
<button class="edit-submit-btn" @click="submitUserInfo"></button>
</view>
</view>
</view>
</view>
</template>
<script>
import { API } from "@/config/index.js";
export default {
data() {
return {
isWeixinBrowser: false,
userInfo: null,
showEditPopup: false,
editForm: {
name: "",
payer_type: 1,
phone: "",
id_card: "",
},
statistics: {
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,
// 测试数据 - batches
batches: [],
},
};
},
onLoad() {
// #ifdef H5
this.isWeixinBrowser = /MicroMessenger/i.test(navigator.userAgent);
// #endif
// 检查 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 失效统一处理
handleTokenInvalid() {
// 清理本地 token
uni.removeStorageSync("token");
// 简单清空当前页关键数据
this.userInfo = null;
// #ifdef H5
// H5 微信环境下,尝试重新发起授权登录
try {
const app = getApp();
if (app && typeof app.wxH5AuthLogin === "function") {
app.wxH5AuthLogin();
return;
}
} catch (e) {
console.warn("handleTokenInvalid 调用 wxH5AuthLogin 失败:", e);
}
// #endif
// 其他环境给出提示
uni.showToast({
title: "登录已失效,请重新进入",
icon: "none",
});
},
// 等待 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; // 最多重试20次每次500ms总共10秒
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) {
if (res.data.errcode === 4000) {
// token 鉴权失败,重新登录
this.handleTokenInvalid();
return;
}
if (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");
if (!token) {
return;
}
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.STATISTICS}?token=${token}`,
method: "get",
success: resolve,
fail: reject,
});
});
if (res.data) {
if (res.data.errcode === 4000) {
// token 鉴权失败,重新登录
this.handleTokenInvalid();
return;
}
if (res.data.errcode === 0) {
// 保留已有的水位数据,合并新的统计数据
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");
if (!token) {
this.userInfo = null;
return;
}
try {
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.GET_USER_INFO}?token=${token}`,
method: "POST",
success: resolve,
fail: reject,
});
});
if (res.data) {
// 兼容两种返回结构:直接返回用户数据或带 errcode 包裹
if (typeof res.data.errcode !== "undefined") {
if (res.data.errcode === 4000) {
// token 鉴权失败
this.handleTokenInvalid();
return;
}
if (res.data.errcode !== 0) {
// 其他错误直接提示
uni.showToast({
title: res.data.errmsg || "获取用户信息失败",
icon: "none",
});
this.userInfo = null;
return;
}
}
const raw = res.data.data || res.data;
const payerType = Number(raw.payer_type ?? raw.payerType ?? 1);
this.userInfo = { ...raw, payer_type: payerType };
this.editForm = {
name: raw.name || "",
payer_type: payerType,
phone: raw.phone || "",
id_card: raw.id_card || "",
};
}
} catch (e) {
this.userInfo = null;
}
},
requireUserInfoComplete() {
const u = this.userInfo || {};
const ok = !!(
u.name &&
(u.payer_type ?? u.payerType) &&
u.phone &&
u.id_card
);
return ok;
},
openEditPopup() {
this.showEditPopup = true;
},
closeEditPopup() {
this.showEditPopup = false;
},
validateForm() {
if (!this.editForm.name) {
uni.showToast({ title: "请输入姓名", icon: "none" });
return false;
}
if (![1, 2].includes(Number(this.editForm.payer_type))) {
uni.showToast({ title: "请选择交款人类型", icon: "none" });
return false;
}
const phoneReg = /^1\d{10}$/;
if (!phoneReg.test(this.editForm.phone)) {
uni.showToast({ title: "请输入正确的手机号", icon: "none" });
return false;
}
const idReg = /^(?:\d{15}|\d{17}[\dXx])$/;
if (!idReg.test(this.editForm.id_card)) {
uni.showToast({ title: "请输入正确的证件号", icon: "none" });
return false;
}
return true;
},
async submitUserInfo() {
if (!this.validateForm()) return;
const token = uni.getStorageSync("token");
if (!token) {
uni.showToast({ title: "请先登录", icon: "none" });
return;
}
try {
uni.showLoading({ title: "提交中..." });
const res = await new Promise((resolve, reject) => {
uni.request({
url: `${API.UPDATE_USER_INFO}?token=${token}`,
method: "POST",
data: {
name: this.editForm.name,
payer_type: Number(this.editForm.payer_type),
phone: this.editForm.phone,
id_card: this.editForm.id_card,
},
success: resolve,
fail: reject,
});
});
uni.hideLoading();
if (res.data && res.data.errcode === 0) {
uni.showToast({ title: "更新成功", icon: "success" });
this.closeEditPopup();
await this.fetchUserInfo();
} else {
uni.showToast({
title: (res.data && res.data.errmsg) || "更新失败",
icon: "none",
});
}
} catch (e) {
uni.hideLoading();
uni.showToast({ title: "更新失败", icon: "none" });
}
},
goReservation() {
const afterCheck = () => {
if (this.requireUserInfoComplete()) {
uni.navigateTo({ url: "/pages/reservation/index" });
} else {
uni.showModal({
title: "提示",
content: "为了后续流程的使用,请先完善个人信息",
confirmText: "去完善",
cancelText: "取消",
success: (res) => {
if (res.confirm) {
this.openEditPopup();
}
},
});
}
};
if (!this.userInfo) {
this.fetchUserInfo().then(afterCheck).catch(afterCheck);
} else {
afterCheck();
}
},
goWaitPass() {
uni.navigateTo({ url: "/pages/order/pay_order_list?status=paid" });
},
goOrder() {
uni.navigateTo({ url: "/pages/order/pay_order_list?status=unpaid" });
},
goInvoiceManage() {
uni.navigateTo({ url: "/pages/index/invoice_manage" });
},
},
};
</script>
<style scoped>
.home-container {
background: #f6f8fc;
min-height: 100vh;
padding-bottom: 24rpx;
position: relative;
}
.wechat-browser {
margin-top: -88rpx;
}
.header-gradient {
background: linear-gradient(180deg, #2f50ff 0%, #28a8fa 100%);
border-bottom-left-radius: 20rpx;
border-bottom-right-radius: 20rpx;
padding: 0 0 36rpx 0;
color: #fff;
position: relative;
height: 630rpx;
min-height: 400rpx;
max-height: 650rpx;
}
.header-title {
text-align: center;
font-size: 36rpx;
font-weight: bold;
padding-top: 7vh;
letter-spacing: 2rpx;
}
.header-info {
display: flex;
justify-content: center;
align-items: flex-start;
width: fit-content;
margin: 2vh auto 0 auto; /* 水平居中 */
gap: 154rpx; /* 控制每组间距 */
}
.info-item {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.info-label {
font-size: 32rpx;
opacity: 0.95;
color: #e6eaff;
}
.info-value {
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: 2vh 48rpx 0 48rpx;
position: relative;
}
.batch-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.batch-divider-strict {
width: 2rpx;
height: 82rpx;
background: rgba(255, 255, 255, 0.2);
margin: 0 48rpx;
}
.batch-num-strict {
font-size: 32rpx;
font-weight: normal;
margin-bottom: 8rpx;
font-family: "PangMenZhengDao", "SourceHanSansCN", "PingFang SC",
"Microsoft YaHei", sans-serif;
letter-spacing: 3rpx;
}
.batch-tag-strict {
font-size: 32rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
background: #2b70ee;
/* display: flex; */
align-items: center;
gap: 4rpx;
}
.tag-orange {
color: #ff9f43;
}
.tag-green {
color: #28c76f;
}
.tag-blue {
color: #4fc3ff;
}
.process-card {
background: #fff;
border-radius: 22rpx;
margin: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
position: relative;
margin-top: -140rpx;
height: 138px;
}
.process-title-bar {
width: 60%;
margin: 0 auto;
position: relative;
top: -32rpx;
background: linear-gradient(180deg, #2f4dff 0%, #4b65ff 100%);
color: #fff;
font-size: 28rpx;
font-weight: bold;
border-radius: 0 0 20rpx 20rpx;
padding: 12rpx 0;
text-align: center;
box-shadow: 0 4rpx 16rpx rgba(59, 124, 255, 0.12);
z-index: 2;
}
.process-flow {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12rpx;
margin-top: 10rpx;
}
.process-step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.icon {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: linear-gradient(135deg, #2c51ff 0%, #2991fd 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
/* 这里放svg或字体icon颜色为白色 */
}
.process-label {
color: #445fff;
font-size: 26rpx;
margin-top: 4rpx;
}
.arrow {
color: #b0b8c6;
font-size: 40rpx;
font-weight: bold;
margin: 0 12rpx;
margin-top: -30rpx;
}
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32rpx;
margin: 32rpx 24rpx 24rpx 24rpx;
}
.func-card {
position: relative;
border-radius: 32rpx;
height: 200rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 32rpx 0 32rpx 32rpx;
}
.card-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.func-card-content {
z-index: 2;
position: relative;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
}
.func-title {
font-size: 28rpx;
color: #fff;
font-weight: 500;
margin-bottom: 20rpx;
margin-top: 10rpx;
}
.func-num {
font-size: 58rpx;
font-weight: normal;
color: #fff;
font-family: "PangMenZhengDao", "SourceHanSansCN", "PingFang SC",
"Microsoft YaHei", sans-serif;
letter-spacing: 5rpx;
}
.func-bg-icon {
position: absolute;
right: 12rpx;
bottom: 12rpx;
width: 100rpx;
height: 100rpx;
opacity: 0.18;
z-index: 1;
/* 这里放svg或字体icon */
}
.edit-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
justify-content: center;
align-items: flex-end;
z-index: 9999;
}
.edit-popup {
background: #fff;
width: 100%;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
padding: 32rpx;
box-sizing: border-box;
max-height: 80vh;
overflow-y: auto;
}
.edit-popup-header {
display: flex;
justify-content: center;
position: relative;
font-size: 32rpx;
font-weight: 600;
color: #222;
}
.edit-popup-close {
position: absolute;
right: 0;
top: 0;
font-size: 44rpx;
color: #999;
padding: 0 16rpx;
}
.edit-popup-content {
margin-top: 32rpx;
}
.edit-field {
margin-bottom: 28rpx;
}
.edit-label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
}
.edit-input {
width: 100%;
height: 80rpx;
border-radius: 12rpx;
border: 1rpx solid #e5e6eb;
padding: 0 24rpx;
font-size: 28rpx;
box-sizing: border-box;
background: #fafafa;
}
.payer-type-group {
display: flex;
gap: 20rpx;
}
.payer-type-item {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
border: 1rpx solid #e5e6eb;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #666;
}
.payer-type-item.active {
border-color: #3b7cff;
color: #3b7cff;
background: #edf3ff;
}
.edit-popup-footer {
margin-top: 12rpx;
}
.edit-submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background: linear-gradient(90deg, #3b7cff 0%, #5bb6ff 100%);
color: #fff;
font-size: 32rpx;
font-weight: 500;
border: none;
}
.edit-submit-btn::after {
border: none;
}
.info-list {
background: #fff;
border-radius: 24rpx;
margin: 24rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.info-item-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
padding: 12rpx 0;
}
.info-item-row:last-child {
margin-bottom: 0;
}
.info-tag {
font-size: 22rpx;
padding: 4rpx 18rpx;
border-radius: 8rpx;
margin-right: 12rpx;
white-space: nowrap;
color: #fff;
display: inline-block;
background: #e68c6e;
transform: skewX(-20deg);
font-weight: 500;
border: none;
margin-right: 32rpx;
}
.info-tag-text {
display: inline-block;
transform: skewX(20deg);
}
.notice {
background: linear-gradient(90deg, #2b70ee 0%, #4fc3ff 100%);
color: white;
}
.realtime {
background: linear-gradient(90deg, #e68d6e 0%, #ffb86c 100%);
color: white;
}
.info-text {
font-size: 26rpx;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>