buddhism/pages/memorial/nfcPairing.vue

714 lines
21 KiB
Vue
Raw Normal View History

2025-11-19 17:58:14 +08:00
<template>
<view class="page">
<base-background />
<custom-navbar title="NFC配对" />
<view class="content">
<view class="status-card">
<view class="status-header">
2025-11-20 10:06:56 +08:00
<view
:class="['status-dot', socketConnected ? 'online' : 'offline']"
/>
2025-11-19 17:58:14 +08:00
<text class="status-title">{{ connectionText }}</text>
</view>
<text class="status-desc">
请保持手机在线等待刷卡设备将NFC卡号传递到本页面
</text>
2025-11-20 10:06:56 +08:00
<view :class="{ ready: !!cardNo }" class="card-box">
2025-11-19 17:58:14 +08:00
<text class="card-label">NFC卡号</text>
<text class="card-value">{{ cardNo || "等待刷卡..." }}</text>
</view>
2025-11-20 10:06:56 +08:00
<view v-if="connectionError" class="error-text">
<text class="error-content">{{ connectionError }}</text>
</view>
2025-11-19 17:58:14 +08:00
<view v-else-if="lastMessage" class="hint-text">{{ lastMessage }}</view>
<view class="status-actions">
<view class="text-btn" @click="handleRetry">重新连接</view>
2025-11-20 10:06:56 +08:00
<view v-if="cardNo" class="text-btn" @click="resetCard"
>清空卡号
</view>
<view v-if="connectionError" class="text-btn" @click="testServerConnection"
>测试服务器
</view>
2025-11-19 17:58:14 +08:00
</view>
</view>
<view class="form-card">
<view class="field">
<text class="label">设备 MAC 地址</text>
<input
v-model.trim="deviceMac"
class="input"
maxlength="32"
placeholder="请输入设备 MAC"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">NFC 卡号</text>
<input
v-model="cardNo"
class="input"
disabled
placeholder="等待刷卡"
placeholder-class="placeholder"
/>
</view>
<view v-if="unitId" class="field readonly">
<text class="label">绑定单元 ID</text>
<text class="unit-value">{{ unitId }}</text>
</view>
<view
:class="['primary-btn', { disabled: !canSubmit || binding }]"
@click="handleBind"
>
{{ binding ? "提交中..." : "提交绑定" }}
</view>
</view>
</view>
</view>
</template>
<script>
import BaseBackground from "@/components/base-background/base-background.vue";
import CustomNavbar from "@/components/custom-navbar/custom-navbar.vue";
2025-11-20 10:06:56 +08:00
import { getRequestConfig, getToken } from "@/utils/request.js";
2025-11-19 17:58:14 +08:00
import { bindNfcCard } from "@/api/memorial/index.js";
2025-11-20 10:06:56 +08:00
const WS_PATH = "/ws/device";
const FIXED_MAC = "111111111111";
2025-11-19 17:58:14 +08:00
export default {
components: {
BaseBackground,
CustomNavbar,
},
data() {
return {
unitId: "",
socketTask: null,
socketConnected: false,
deviceMac: "",
cardNo: "",
binding: false,
connectionError: "",
lastMessage: "",
usingGlobalSocketEvents: false,
globalSocketHandlers: null,
2025-11-20 10:06:56 +08:00
connectTimeout: null,
2025-11-19 17:58:14 +08:00
};
},
computed: {
canSubmit() {
return !!this.deviceMac && !!this.cardNo;
},
connectionText() {
if (this.connectionError) {
return "连接异常";
}
return this.socketConnected ? "已连接,等待刷卡" : "连接中...";
},
},
onLoad(options = {}) {
if (options.unitId) {
this.unitId = options.unitId;
}
if (options.mac) {
this.deviceMac = options.mac;
}
this.initSocket();
},
onUnload() {
this.cleanupSocket();
},
methods: {
buildSocketUrl() {
try {
const { baseUrl } = getRequestConfig();
2025-11-20 10:06:56 +08:00
if (!baseUrl) {
console.error("buildSocketUrl: baseUrl 为空");
return "";
}
// 获取当前登录的 token
const token = getToken();
if (!token) {
console.error("buildSocketUrl: token 为空,请先登录");
this.connectionError = "未登录,请先登录后再试";
return "";
}
// 根据 baseUrl 的协议自动选择 ws 或 wss
const isHttps = baseUrl.startsWith("https://");
const protocol = isHttps ? "wss" : "ws";
2025-11-19 17:58:14 +08:00
const host = baseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
2025-11-20 10:06:56 +08:00
// 构建查询参数token 和固定的 mac
const query = `?token=${encodeURIComponent(token)}&mac=${FIXED_MAC}`;
const url = `${protocol}://${host}${WS_PATH}${query}`;
console.log("构建 WebSocket URL:", {
baseUrl,
protocol,
host,
path: WS_PATH,
token: token ? `${token.substring(0, 20)}...` : "无",
mac: FIXED_MAC,
finalUrl: url.replace(token, "***"), // 日志中隐藏完整token
});
return url;
2025-11-19 17:58:14 +08:00
} catch (error) {
console.error("构建WebSocket地址失败", error);
return "";
}
},
initSocket() {
this.connectionError = "";
this.lastMessage = "";
const url = this.buildSocketUrl();
if (!url) {
this.connectionError = "缺少WebSocket地址请检查配置";
2025-11-20 10:06:56 +08:00
console.error("initSocket: URL 构建失败");
2025-11-19 17:58:14 +08:00
return;
}
this.cleanupSocket();
console.log("NFC配对页面发起WebSocket连接:", url);
2025-11-20 10:06:56 +08:00
try {
// 添加连接超时处理
this.connectTimeout = setTimeout(() => {
if (!this.socketConnected) {
console.error("WebSocket 连接超时");
this.connectionError = "连接超时,请检查网络和服务器状态";
this.cleanupSocket();
}
}, 10000); // 10秒超时
this.socketTask = uni.connectSocket({
url,
success: (res) => {
console.log("uni.connectSocket success:", res);
},
fail: (err) => {
console.error("uni.connectSocket fail:", err);
clearTimeout(this.connectTimeout);
this.connectionError = `连接失败: ${err.errMsg || "未知错误"}`;
this.socketConnected = false;
}
});
if (!this.socketTask) {
clearTimeout(this.connectTimeout);
this.connectionError = "当前环境不支持WebSocket";
console.error("initSocket: socketTask 为 null");
return;
}
if (typeof this.socketTask.onOpen === "function") {
console.log("使用 Task 级别事件绑定", this.socketTask);
this.bindTaskSocketEvents();
} else {
console.log("使用全局事件绑定", this.socketTask);
this.bindGlobalSocketEvents();
}
} catch (error) {
console.error("initSocket 异常:", error);
clearTimeout(this.connectTimeout);
this.connectionError = `连接异常: ${error.message || "未知错误"}`;
2025-11-19 17:58:14 +08:00
}
},
cleanupSocket() {
2025-11-20 10:06:56 +08:00
// 清除连接超时定时器
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
2025-11-19 17:58:14 +08:00
if (this.socketTask && typeof this.socketTask.close === "function") {
try {
this.socketTask.close();
2025-11-20 10:06:56 +08:00
console.log("WebSocket 连接已关闭 (Task级别)");
2025-11-19 17:58:14 +08:00
} catch (error) {
console.warn("关闭WebSocket失败", error);
}
} else {
2025-11-20 10:06:56 +08:00
try {
uni.closeSocket && uni.closeSocket({});
console.log("WebSocket 连接已关闭 (全局)");
} catch (error) {
console.warn("关闭WebSocket失败", error);
}
2025-11-19 17:58:14 +08:00
}
this.unbindGlobalSocketEvents();
this.socketTask = null;
this.socketConnected = false;
},
bindTaskSocketEvents() {
if (!this.socketTask) return;
this.socketTask.onOpen(() => {
2025-11-20 10:06:56 +08:00
console.log("NFC WebSocket 已连接 (Task级别)");
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
2025-11-19 17:58:14 +08:00
this.socketConnected = true;
this.connectionError = "";
2025-11-20 10:06:56 +08:00
this.lastMessage = "连接成功,等待刷卡...";
2025-11-19 17:58:14 +08:00
});
this.socketTask.onClose((event) => {
2025-11-20 10:06:56 +08:00
console.warn("NFC WebSocket 连接关闭 (Task级别)", event);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
2025-11-19 17:58:14 +08:00
this.socketConnected = false;
this.socketTask = null;
2025-11-20 10:06:56 +08:00
if (!this.connectionError) {
this.connectionError = "连接已断开";
}
2025-11-19 17:58:14 +08:00
});
this.socketTask.onError((error) => {
2025-11-20 10:06:56 +08:00
console.error("NFC WebSocket 错误 (Task级别)", error);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
// 解析错误信息
let errorMsg = error.errMsg || error.message || "连接失败";
let userFriendlyMsg = "连接失败";
// 处理 Invalid HTTP status 错误
if (errorMsg.includes("Invalid HTTP status") || error.errCode === 1004) {
userFriendlyMsg = "服务器不支持WebSocket或路径不存在\n请检查\n1. 服务器是否正常运行\n2. WebSocket路径是否正确\n3. 服务器是否支持WebSocket协议";
console.error("WebSocket握手失败可能原因", {
url: this.buildSocketUrl(),
errorCode: error.errCode,
errorMsg: errorMsg,
suggestion: "服务器可能返回了404或500错误请检查服务器日志"
});
} else if (errorMsg.includes("timeout")) {
userFriendlyMsg = "连接超时,请检查网络连接";
} else if (errorMsg.includes("fail")) {
userFriendlyMsg = "网络连接失败,请检查网络和服务器地址";
}
this.connectionError = userFriendlyMsg;
2025-11-19 17:58:14 +08:00
this.socketConnected = false;
});
this.socketTask.onMessage((event) => {
this.handleSocketMessage(event);
});
},
bindGlobalSocketEvents() {
if (this.usingGlobalSocketEvents) return;
this.usingGlobalSocketEvents = true;
this.globalSocketHandlers = {
open: () => {
2025-11-20 10:06:56 +08:00
console.log("NFC WebSocket 已连接 (全局事件)");
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
2025-11-19 17:58:14 +08:00
this.socketConnected = true;
this.connectionError = "";
2025-11-20 10:06:56 +08:00
this.lastMessage = "连接成功,等待刷卡...";
2025-11-19 17:58:14 +08:00
},
close: (event) => {
2025-11-20 10:06:56 +08:00
console.warn("NFC WebSocket 连接关闭 (全局事件)", event);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
2025-11-19 17:58:14 +08:00
this.socketConnected = false;
2025-11-20 10:06:56 +08:00
if (!this.connectionError) {
this.connectionError = "连接已断开";
}
2025-11-19 17:58:14 +08:00
},
error: (error) => {
2025-11-20 10:06:56 +08:00
console.error("NFC WebSocket 错误 (全局事件)", error);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
// 解析错误信息
let errorMsg = error.errMsg || error.message || "连接失败";
let userFriendlyMsg = "连接失败";
// 处理 Invalid HTTP status 错误
if (errorMsg.includes("Invalid HTTP status") || error.errCode === 1004) {
userFriendlyMsg = "服务器不支持WebSocket或路径不存在\n请检查\n1. 服务器是否正常运行\n2. WebSocket路径是否正确\n3. 服务器是否支持WebSocket协议";
console.error("WebSocket握手失败可能原因", {
url: this.buildSocketUrl(),
errorCode: error.errCode,
errorMsg: errorMsg,
suggestion: "服务器可能返回了404或500错误请检查服务器日志"
});
} else if (errorMsg.includes("timeout")) {
userFriendlyMsg = "连接超时,请检查网络连接";
} else if (errorMsg.includes("fail")) {
userFriendlyMsg = "网络连接失败,请检查网络和服务器地址";
}
this.connectionError = userFriendlyMsg;
2025-11-19 17:58:14 +08:00
this.socketConnected = false;
},
message: (event) => {
this.handleSocketMessage(event);
},
};
uni.onSocketOpen && uni.onSocketOpen(this.globalSocketHandlers.open);
uni.onSocketClose && uni.onSocketClose(this.globalSocketHandlers.close);
uni.onSocketError && uni.onSocketError(this.globalSocketHandlers.error);
uni.onSocketMessage &&
uni.onSocketMessage(this.globalSocketHandlers.message);
},
unbindGlobalSocketEvents() {
if (!this.usingGlobalSocketEvents || !this.globalSocketHandlers) return;
const { open, close, error, message } = this.globalSocketHandlers;
uni.offSocketOpen && open && uni.offSocketOpen(open);
uni.offSocketClose && close && uni.offSocketClose(close);
uni.offSocketError && error && uni.offSocketError(error);
uni.offSocketMessage && message && uni.offSocketMessage(message);
this.usingGlobalSocketEvents = false;
this.globalSocketHandlers = null;
},
handleSocketMessage(event) {
let message = event?.data;
this.lastMessage = "";
try {
if (typeof message === "string") {
const trimmed = message.trim();
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
const parsed = JSON.parse(trimmed);
if (parsed.cardNo || parsed.cardNumber || parsed.nfcNo) {
this.cardNo = parsed.cardNo || parsed.cardNumber || parsed.nfcNo;
this.lastMessage = "已接收卡号";
uni.showToast({ title: "收到卡号", icon: "success" });
return;
}
this.lastMessage = parsed.msg || trimmed;
return;
} else {
this.cardNo = trimmed;
this.lastMessage = "已接收卡号";
uni.showToast({ title: "收到卡号", icon: "success" });
return;
}
} else if (typeof message === "object" && message) {
const data = message.data || message;
if (data.cardNo || data.cardNumber || data.nfcNo) {
this.cardNo = data.cardNo || data.cardNumber || data.nfcNo;
this.lastMessage = "已接收卡号";
uni.showToast({ title: "收到卡号", icon: "success" });
return;
}
}
this.lastMessage = "收到未知消息";
} catch (error) {
console.error("解析WebSocket消息失败", error, message);
this.lastMessage = "消息解析失败";
}
},
resetCard() {
this.cardNo = "";
this.lastMessage = "";
},
handleRetry() {
this.initSocket();
},
2025-11-20 10:06:56 +08:00
// 测试服务器连接(用于诊断)
async testServerConnection() {
try {
const { baseUrl } = getRequestConfig();
console.log("测试服务器连接:", baseUrl);
const token = getToken();
if (!token) {
uni.showModal({
title: "未登录",
content: "请先登录后再测试服务器连接",
showCancel: false,
});
return;
}
// 测试 WebSocket 服务器是否可达
const isHttps = baseUrl.startsWith("https://");
const protocol = isHttps ? "wss" : "ws";
const host = baseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
const testUrl = `${protocol}://${host}${WS_PATH}?token=${encodeURIComponent(token)}&mac=${FIXED_MAC}`;
console.log("测试 WebSocket URL:", testUrl.replace(token, "***"));
uni.showLoading({ title: "测试连接中...", mask: true });
// 尝试连接 WebSocket 来测试服务器
const testSocket = uni.connectSocket({
url: testUrl,
success: () => {
console.log("WebSocket 连接测试:连接请求已发送");
},
fail: (err) => {
console.error("WebSocket 连接测试失败:", err);
uni.hideLoading();
uni.showModal({
title: "服务器连接测试",
content: `无法连接到 WebSocket 服务器\n错误: ${err.errMsg || "未知错误"}\n\n请检查\n1. 服务器地址是否正确 (${host})\n2. WebSocket 服务是否运行\n3. 网络是否正常\n4. Token 是否有效`,
showCancel: false,
});
},
});
// 设置超时
const timeout = setTimeout(() => {
testSocket.close();
uni.hideLoading();
uni.showModal({
title: "连接超时",
content: `WebSocket 连接超时\n\n请检查\n1. 服务器地址: ${host}\n2. WebSocket 路径: ${WS_PATH}\n3. 服务器是否正常运行`,
showCancel: false,
});
}, 5000);
testSocket.onOpen(() => {
clearTimeout(timeout);
testSocket.close();
uni.hideLoading();
uni.showToast({
title: "服务器连接正常",
icon: "success",
duration: 2000,
});
});
testSocket.onError((err) => {
clearTimeout(timeout);
uni.hideLoading();
uni.showModal({
title: "服务器连接测试",
content: `WebSocket 连接失败\n错误: ${err.errMsg || "未知错误"}\n\n请检查\n1. 服务器地址是否正确\n2. WebSocket 服务是否运行\n3. Token 是否有效`,
showCancel: false,
});
});
} catch (error) {
console.error("测试连接异常:", error);
uni.hideLoading();
uni.showToast({
title: "测试失败",
icon: "none",
});
}
},
2025-11-19 17:58:14 +08:00
async handleBind() {
if (!this.canSubmit || this.binding) return;
this.binding = true;
uni.showLoading({ title: "提交中...", mask: true });
try {
const payload = {
memorialMac: this.deviceMac,
nfcMac: this.cardNo,
};
if (this.unitId) {
payload.unitId = this.unitId;
}
const res = await bindNfcCard(payload);
if (res && (res.code === 200 || res.status === 200)) {
uni.showToast({ title: res.msg || "绑定成功", icon: "success" });
setTimeout(() => {
uni.navigateBack({ delta: 1 });
}, 800);
} else {
uni.showToast({
title: (res && res.msg) || "绑定失败",
icon: "none",
});
}
} catch (error) {
console.error("提交绑定失败", error);
uni.showToast({ title: "提交失败,请重试", icon: "none" });
} finally {
this.binding = false;
uni.hideLoading();
}
},
},
};
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
width: 100%;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.content {
padding: 0 32rpx 60rpx;
box-sizing: border-box;
}
.status-card,
.form-card {
background: rgba(255, 255, 255, 0.92);
border-radius: 24rpx;
padding: 32rpx;
margin-top: 32rpx;
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.06);
}
.status-header {
display: flex;
align-items: center;
gap: 16rpx;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #f0b400;
}
.status-dot.online {
background: #3ac569;
}
.status-dot.offline {
background: #f56c6c;
}
.status-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.status-desc {
margin-top: 12rpx;
font-size: 24rpx;
color: #999;
line-height: 1.5;
}
.card-box {
margin-top: 24rpx;
border: 2rpx dashed #f0b400;
border-radius: 20rpx;
padding: 24rpx;
background: #fff9eb;
}
.card-box.ready {
border-color: #3ac569;
background: #effbf4;
}
.card-label {
font-size: 24rpx;
color: #666;
}
.card-value {
display: block;
margin-top: 16rpx;
font-size: 36rpx;
font-weight: 600;
color: #333;
word-break: break-all;
}
.error-text {
margin-top: 16rpx;
2025-11-20 10:06:56 +08:00
padding: 16rpx;
background: #fef0f0;
border-radius: 12rpx;
border-left: 4rpx solid #f56c6c;
}
.error-content {
2025-11-19 17:58:14 +08:00
color: #f56c6c;
font-size: 24rpx;
2025-11-20 10:06:56 +08:00
line-height: 1.6;
white-space: pre-line;
word-break: break-all;
2025-11-19 17:58:14 +08:00
}
.hint-text {
margin-top: 16rpx;
color: #999;
font-size: 24rpx;
}
.status-actions {
margin-top: 24rpx;
display: flex;
gap: 32rpx;
}
.text-btn {
font-size: 26rpx;
color: #4a90e2;
}
.field {
margin-bottom: 32rpx;
}
.field.readonly {
padding: 24rpx;
background: #f9f9f9;
border-radius: 16rpx;
}
.label {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
.input {
width: 100%;
height: 88rpx;
border-radius: 16rpx;
background: #f8f8f8;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.placeholder {
color: #bbb;
}
.unit-value {
font-size: 30rpx;
color: #333;
word-break: break-all;
}
.primary-btn {
height: 96rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #f0b400, #f08400);
color: #fff;
font-size: 30rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
margin-top: 12rpx;
}
.primary-btn.disabled {
opacity: 0.5;
}
</style>