buddhism/pages/memorial/nfcPairing.vue
2025-11-19 17:58:14 +08:00

465 lines
12 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="card-box" :class="{ ready: !!cardNo }">
<text class="card-label">NFC卡号</text>
<text class="card-value">{{ cardNo || "等待刷卡..." }}</text>
</view>
<view v-if="connectionError" class="error-text">{{ connectionError }}</view>
<view v-else-if="lastMessage" class="hint-text">{{ lastMessage }}</view>
<view class="status-actions">
<view class="text-btn" @click="handleRetry">重新连接</view>
<view class="text-btn" @click="resetCard" v-if="cardNo">清空卡号</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 } from "@/utils/request.js";
import { bindNfcCard } from "@/api/memorial/index.js";
const NFC_WS_PATH = "/ws/nfc/swipeCard";
export default {
components: {
BaseBackground,
CustomNavbar,
},
data() {
return {
unitId: "",
socketTask: null,
socketConnected: false,
deviceMac: "",
cardNo: "",
binding: false,
connectionError: "",
lastMessage: "",
usingGlobalSocketEvents: false,
globalSocketHandlers: null,
};
},
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();
if (!baseUrl) return "";
const protocol = baseUrl.startsWith("https") ? "wss" : "ws";
const host = baseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
const query = this.unitId ? `?unitId=${this.unitId}` : "";
return `${protocol}://${host}${NFC_WS_PATH}${query}`;
} catch (error) {
console.error("构建WebSocket地址失败", error);
return "";
}
},
initSocket() {
this.connectionError = "";
this.lastMessage = "";
const url = this.buildSocketUrl();
if (!url) {
this.connectionError = "缺少WebSocket地址请检查配置";
return;
}
this.cleanupSocket();
console.log("NFC配对页面发起WebSocket连接:", url);
this.socketTask = uni.connectSocket({ url });
if (!this.socketTask) {
this.connectionError = "当前环境不支持WebSocket";
return;
}
if (typeof this.socketTask.onOpen === "function") {
this.bindTaskSocketEvents();
} else {
this.bindGlobalSocketEvents();
}
},
cleanupSocket() {
if (this.socketTask && typeof this.socketTask.close === "function") {
try {
this.socketTask.close();
} catch (error) {
console.warn("关闭WebSocket失败", error);
}
} else {
uni.closeSocket && uni.closeSocket({});
}
this.unbindGlobalSocketEvents();
this.socketTask = null;
this.socketConnected = false;
},
bindTaskSocketEvents() {
if (!this.socketTask) return;
this.socketTask.onOpen(() => {
console.log("NFC WebSocket 已连接");
this.socketConnected = true;
this.connectionError = "";
});
this.socketTask.onClose((event) => {
console.warn("NFC WebSocket 连接关闭", event);
this.socketConnected = false;
this.socketTask = null;
});
this.socketTask.onError((error) => {
console.error("NFC WebSocket 错误", error);
this.connectionError = "连接失败,请重试";
this.socketConnected = false;
});
this.socketTask.onMessage((event) => {
this.handleSocketMessage(event);
});
},
bindGlobalSocketEvents() {
if (this.usingGlobalSocketEvents) return;
this.usingGlobalSocketEvents = true;
this.globalSocketHandlers = {
open: () => {
console.log("NFC WebSocket 已连接 (global handler)");
this.socketConnected = true;
this.connectionError = "";
},
close: (event) => {
console.warn("NFC WebSocket 连接关闭", event);
this.socketConnected = false;
},
error: (error) => {
console.error("NFC WebSocket 错误", error);
this.connectionError = "连接失败,请重试";
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();
},
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;
color: #f56c6c;
font-size: 24rpx;
}
.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>