buddhism/pages/memorial/nfcPairing.vue
2025-11-20 11:40:31 +08:00

728 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<base-background />
<custom-navbar title="NFC配对" />
<view class="content">
<view class="status-card">
<view class="status-header">
<view
:class="['status-dot', socketConnected ? 'online' : 'offline']"
/>
<text class="status-title">{{ connectionText }}</text>
</view>
<text class="status-desc">
请保持手机在线等待刷卡设备将NFC卡号传递到本页面
</text>
<view :class="{ ready: !!cardNo }" class="card-box">
<text class="card-label">NFC卡号</text>
<text class="card-value">{{ cardNo || "等待刷卡..." }}</text>
</view>
<view v-if="connectionError" class="error-text">
<text class="error-content">{{ connectionError }}</text>
</view>
<view v-else-if="lastMessage" class="hint-text">{{ lastMessage }}</view>
<view class="status-actions">
<view class="text-btn" @click="handleRetry">重新连接</view>
<view v-if="cardNo" class="text-btn" @click="resetCard"
>清空卡号
</view>
<view
v-if="connectionError"
class="text-btn"
@click="testServerConnection"
>测试服务器
</view>
</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";
import { getRequestConfig, getToken } from "@/utils/request.js";
import { bindNfcCard } from "@/api/memorial/index.js";
const WS_PATH = "/ws/device";
const FIXED_MAC = "111111111111";
export default {
components: {
BaseBackground,
CustomNavbar,
},
data() {
return {
unitId: "",
socketTask: null,
socketConnected: false,
deviceMac: "",
cardNo: "",
binding: false,
connectionError: "",
lastMessage: "",
usingGlobalSocketEvents: false,
globalSocketHandlers: null,
connectTimeout: null,
};
},
computed: {
canSubmit() {
return !!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();
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";
const host = baseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
// 构建查询参数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;
} catch (error) {
console.error("构建WebSocket地址失败", error);
return "";
}
},
initSocket() {
this.connectionError = "";
this.lastMessage = "";
const url = this.buildSocketUrl();
if (!url) {
this.connectionError = "缺少WebSocket地址请检查配置";
console.error("initSocket: URL 构建失败");
return;
}
this.cleanupSocket();
console.log("NFC配对页面发起WebSocket连接:", url);
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 || "未知错误"}`;
}
},
cleanupSocket() {
// 清除连接超时定时器
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
if (this.socketTask && typeof this.socketTask.close === "function") {
try {
this.socketTask.close();
console.log("WebSocket 连接已关闭 (Task级别)");
} catch (error) {
console.warn("关闭WebSocket失败", error);
}
} else {
try {
uni.closeSocket && uni.closeSocket({});
console.log("WebSocket 连接已关闭 (全局)");
} catch (error) {
console.warn("关闭WebSocket失败", error);
}
}
this.unbindGlobalSocketEvents();
this.socketTask = null;
this.socketConnected = false;
},
bindTaskSocketEvents() {
if (!this.socketTask) return;
this.socketTask.onOpen(() => {
console.log("NFC WebSocket 已连接 (Task级别)");
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.socketConnected = true;
this.connectionError = "";
this.lastMessage = "连接成功,等待刷卡...";
});
this.socketTask.onClose((event) => {
console.warn("NFC WebSocket 连接关闭 (Task级别)", event);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.socketConnected = false;
this.socketTask = null;
if (!this.connectionError) {
this.connectionError = "连接已断开";
}
});
this.socketTask.onError((error) => {
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;
this.socketConnected = false;
});
this.socketTask.onMessage((event) => {
this.handleSocketMessage(event);
console.log("WebSocket <UNK>接收到事件", event);
});
},
bindGlobalSocketEvents() {
if (this.usingGlobalSocketEvents) return;
this.usingGlobalSocketEvents = true;
this.globalSocketHandlers = {
open: () => {
console.log("NFC WebSocket 已连接 (全局事件)");
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.socketConnected = true;
this.connectionError = "";
this.lastMessage = "连接成功,等待刷卡...";
},
close: (event) => {
console.warn("NFC WebSocket 连接关闭 (全局事件)", event);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = null;
}
this.socketConnected = false;
if (!this.connectionError) {
this.connectionError = "连接已断开";
}
},
error: (error) => {
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;
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.replace(/^['"]|['"]$/g, "");
console.log("接收到的卡号", this.cardNo);
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();
},
// 测试服务器连接(用于诊断)
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",
});
}
},
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.memorialId = 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;
padding: 16rpx;
background: #fef0f0;
border-radius: 12rpx;
border-left: 4rpx solid #f56c6c;
}
.error-content {
color: #f56c6c;
font-size: 24rpx;
line-height: 1.6;
white-space: pre-line;
word-break: break-all;
}
.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>