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>
|
2025-11-20 13:55:46 +08:00
|
|
|
|
<view class="status-desc">
|
|
|
|
|
|
<text>请保持手机在线,等待刷卡设备将NFC卡号传递到本页面。</text>
|
|
|
|
|
|
<text v-if="nfcSupported" class="nfc-hint">
|
|
|
|
|
|
<text class="nfc-hint-text">或使用手机 NFC 功能直接读取卡片</text>
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="nfcSupported" class="nfc-status">
|
|
|
|
|
|
<view class="nfc-status-header">
|
2025-11-20 14:19:02 +08:00
|
|
|
|
<view :class="['status-dot', nfcEnabled ? 'online' : 'offline']" />
|
2025-11-20 13:55:46 +08:00
|
|
|
|
<text class="nfc-status-title">{{ nfcStatusText }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="nfcError" class="nfc-error-text">
|
2025-11-25 09:12:04 +08:00
|
|
|
|
<!-- <text class="error-content">{{ nfcError }}</text>-->
|
|
|
|
|
|
<text class="error-content">请打开NFC</text>
|
2025-11-20 13:55:46 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
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>
|
2025-11-20 11:40:31 +08:00
|
|
|
|
<view
|
|
|
|
|
|
v-if="connectionError"
|
|
|
|
|
|
class="text-btn"
|
|
|
|
|
|
@click="testServerConnection"
|
2025-11-20 10:06:56 +08:00
|
|
|
|
>测试服务器
|
|
|
|
|
|
</view>
|
2025-11-19 17:58:14 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="form-card">
|
2025-11-20 11:40:31 +08:00
|
|
|
|
<!-- <view class="field">-->
|
|
|
|
|
|
<!-- <text class="label">设备 MAC 地址</text>-->
|
|
|
|
|
|
<!-- <input-->
|
|
|
|
|
|
<!-- v-model.trim="deviceMac"-->
|
|
|
|
|
|
<!-- class="input"-->
|
|
|
|
|
|
<!-- maxlength="32"-->
|
|
|
|
|
|
<!-- placeholder="请输入设备 MAC"-->
|
|
|
|
|
|
<!-- placeholder-class="placeholder"-->
|
|
|
|
|
|
<!-- />-->
|
|
|
|
|
|
<!-- </view>-->
|
2025-11-19 17:58:14 +08:00
|
|
|
|
<view class="field">
|
|
|
|
|
|
<text class="label">NFC 卡号</text>
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="cardNo"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
placeholder="等待刷卡"
|
|
|
|
|
|
placeholder-class="placeholder"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-25 10:28:39 +08:00
|
|
|
|
<!-- 牌位信息显示 -->
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-if="
|
|
|
|
|
|
memorialInfo.regionName || memorialInfo.code || memorialInfo.mac
|
|
|
|
|
|
"
|
|
|
|
|
|
class="memorial-info-section"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="field readonly">
|
|
|
|
|
|
<text class="label">区域名称</text>
|
|
|
|
|
|
<text class="unit-value">{{ memorialInfo.regionName || "-" }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="field readonly">
|
|
|
|
|
|
<text class="label">编码</text>
|
|
|
|
|
|
<text class="unit-value">{{ memorialInfo.code || "-" }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="field readonly">
|
|
|
|
|
|
<text class="label">MAC 地址</text>
|
|
|
|
|
|
<text class="unit-value">{{ memorialInfo.mac || "-" }}</text>
|
|
|
|
|
|
</view>
|
2025-11-19 17:58:14 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2025-11-25 10:28:39 +08:00
|
|
|
|
<!-- <view v-if="unitId" class="field readonly">-->
|
|
|
|
|
|
<!-- <text class="label">绑定单元 ID</text>-->
|
|
|
|
|
|
<!-- <text class="unit-value">{{ unitId }}</text>-->
|
|
|
|
|
|
<!-- </view>-->
|
|
|
|
|
|
|
2025-11-19 17:58:14 +08:00
|
|
|
|
<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-25 10:28:39 +08:00
|
|
|
|
import {
|
|
|
|
|
|
bindNfcCard,
|
|
|
|
|
|
getMemorialDetail,
|
|
|
|
|
|
swipeNfcCard,
|
|
|
|
|
|
} from "@/api/memorial/index.js";
|
2025-11-19 17:58:14 +08:00
|
|
|
|
|
2025-11-20 16:37:14 +08:00
|
|
|
|
const WS_PATH = "/ws/ws/device";
|
2025-11-20 10:06:56 +08:00
|
|
|
|
const FIXED_MAC = "111111111111";
|
2025-11-19 17:58:14 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
components: {
|
|
|
|
|
|
BaseBackground,
|
|
|
|
|
|
CustomNavbar,
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
unitId: "",
|
2025-11-25 10:28:39 +08:00
|
|
|
|
memorialId: "", // 牌位ID
|
2025-11-19 17:58:14 +08:00
|
|
|
|
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-20 13:55:46 +08:00
|
|
|
|
// NFC 相关状态
|
|
|
|
|
|
nfcAdapter: null,
|
|
|
|
|
|
nfcSupported: false,
|
|
|
|
|
|
nfcEnabled: false,
|
|
|
|
|
|
nfcError: "",
|
2025-11-20 16:37:14 +08:00
|
|
|
|
reportingSwipe: false,
|
2025-11-25 10:28:39 +08:00
|
|
|
|
// 牌位信息
|
|
|
|
|
|
memorialInfo: {
|
|
|
|
|
|
regionName: "",
|
|
|
|
|
|
code: "",
|
|
|
|
|
|
mac: "",
|
|
|
|
|
|
},
|
2025-11-19 17:58:14 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
canSubmit() {
|
2025-11-20 11:40:31 +08:00
|
|
|
|
return !!this.cardNo;
|
2025-11-19 17:58:14 +08:00
|
|
|
|
},
|
|
|
|
|
|
connectionText() {
|
|
|
|
|
|
if (this.connectionError) {
|
|
|
|
|
|
return "连接异常";
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.socketConnected ? "已连接,等待刷卡" : "连接中...";
|
|
|
|
|
|
},
|
2025-11-20 13:55:46 +08:00
|
|
|
|
nfcStatusText() {
|
|
|
|
|
|
if (!this.nfcSupported) {
|
|
|
|
|
|
return "设备不支持 NFC";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.nfcError) {
|
2025-11-25 09:12:04 +08:00
|
|
|
|
// return `NFC 错误: ${this.nfcError}`;
|
|
|
|
|
|
return ``;
|
2025-11-20 13:55:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
return this.nfcEnabled ? "NFC 已开启,请将卡片贴近手机" : "NFC 未开启";
|
|
|
|
|
|
},
|
2025-11-19 17:58:14 +08:00
|
|
|
|
},
|
2025-11-25 10:28:39 +08:00
|
|
|
|
async onLoad(options = {}) {
|
|
|
|
|
|
// 优先从 id 参数获取牌位ID
|
|
|
|
|
|
if (options.id) {
|
|
|
|
|
|
this.memorialId = options.id;
|
|
|
|
|
|
await this.loadMemorialInfo(options.id);
|
|
|
|
|
|
} else if (options.unitId) {
|
2025-11-19 17:58:14 +08:00
|
|
|
|
this.unitId = options.unitId;
|
2025-11-25 10:28:39 +08:00
|
|
|
|
this.memorialId = options.unitId;
|
|
|
|
|
|
await this.loadMemorialInfo(options.unitId);
|
2025-11-19 17:58:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (options.mac) {
|
|
|
|
|
|
this.deviceMac = options.mac;
|
|
|
|
|
|
}
|
2025-11-25 09:12:04 +08:00
|
|
|
|
if (options.label) {
|
|
|
|
|
|
this.label = options.label;
|
|
|
|
|
|
}
|
2025-11-19 17:58:14 +08:00
|
|
|
|
this.initSocket();
|
2025-11-20 13:55:46 +08:00
|
|
|
|
this.initNFC();
|
2025-11-19 17:58:14 +08:00
|
|
|
|
},
|
|
|
|
|
|
onUnload() {
|
|
|
|
|
|
this.cleanupSocket();
|
2025-11-20 13:55:46 +08:00
|
|
|
|
this.cleanupNFC();
|
2025-11-19 17:58:14 +08:00
|
|
|
|
},
|
|
|
|
|
|
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
|
2025-11-20 16:37:14 +08:00
|
|
|
|
|
|
|
|
|
|
const protocol = "wss";
|
2025-11-19 17:58:14 +08:00
|
|
|
|
const host = baseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
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}`;
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
console.log("构建 WebSocket URL:", {
|
|
|
|
|
|
baseUrl,
|
|
|
|
|
|
protocol,
|
|
|
|
|
|
host,
|
|
|
|
|
|
path: WS_PATH,
|
|
|
|
|
|
token: token ? `${token.substring(0, 20)}...` : "无",
|
|
|
|
|
|
mac: FIXED_MAC,
|
|
|
|
|
|
finalUrl: url.replace(token, "***"), // 日志中隐藏完整token
|
|
|
|
|
|
});
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
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 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 添加连接超时处理
|
|
|
|
|
|
this.connectTimeout = setTimeout(() => {
|
|
|
|
|
|
if (!this.socketConnected) {
|
|
|
|
|
|
console.error("WebSocket 连接超时");
|
|
|
|
|
|
this.connectionError = "连接超时,请检查网络和服务器状态";
|
|
|
|
|
|
this.cleanupSocket();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10000); // 10秒超时
|
|
|
|
|
|
|
2025-11-20 11:40:31 +08:00
|
|
|
|
this.socketTask = uni.connectSocket({
|
2025-11-20 10:06:56 +08:00
|
|
|
|
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;
|
2025-11-20 11:40:31 +08:00
|
|
|
|
},
|
2025-11-20 10:06:56 +08:00
|
|
|
|
});
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
if (!this.socketTask) {
|
|
|
|
|
|
clearTimeout(this.connectTimeout);
|
|
|
|
|
|
this.connectionError = "当前环境不支持WebSocket";
|
|
|
|
|
|
console.error("initSocket: socketTask 为 null");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
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-20 11:40:31 +08:00
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
// 解析错误信息
|
|
|
|
|
|
let errorMsg = error.errMsg || error.message || "连接失败";
|
|
|
|
|
|
let userFriendlyMsg = "连接失败";
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
// 处理 Invalid HTTP status 错误
|
2025-11-20 11:40:31 +08:00
|
|
|
|
if (
|
|
|
|
|
|
errorMsg.includes("Invalid HTTP status") ||
|
|
|
|
|
|
error.errCode === 1004
|
|
|
|
|
|
) {
|
|
|
|
|
|
userFriendlyMsg =
|
|
|
|
|
|
"服务器不支持WebSocket或路径不存在\n请检查:\n1. 服务器是否正常运行\n2. WebSocket路径是否正确\n3. 服务器是否支持WebSocket协议";
|
2025-11-20 10:06:56 +08:00
|
|
|
|
console.error("WebSocket握手失败,可能原因:", {
|
|
|
|
|
|
url: this.buildSocketUrl(),
|
|
|
|
|
|
errorCode: error.errCode,
|
|
|
|
|
|
errorMsg: errorMsg,
|
2025-11-20 11:40:31 +08:00
|
|
|
|
suggestion: "服务器可能返回了404或500错误,请检查服务器日志",
|
2025-11-20 10:06:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
} else if (errorMsg.includes("timeout")) {
|
|
|
|
|
|
userFriendlyMsg = "连接超时,请检查网络连接";
|
|
|
|
|
|
} else if (errorMsg.includes("fail")) {
|
|
|
|
|
|
userFriendlyMsg = "网络连接失败,请检查网络和服务器地址";
|
|
|
|
|
|
}
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
this.connectionError = userFriendlyMsg;
|
2025-11-19 17:58:14 +08:00
|
|
|
|
this.socketConnected = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
this.socketTask.onMessage((event) => {
|
|
|
|
|
|
this.handleSocketMessage(event);
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
|
|
|
|
|
console.log("WebSocket <UNK>接收到事件", event);
|
2025-11-19 17:58:14 +08:00
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
// 解析错误信息
|
|
|
|
|
|
let errorMsg = error.errMsg || error.message || "连接失败";
|
|
|
|
|
|
let userFriendlyMsg = "连接失败";
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
// 处理 Invalid HTTP status 错误
|
2025-11-20 11:40:31 +08:00
|
|
|
|
if (
|
|
|
|
|
|
errorMsg.includes("Invalid HTTP status") ||
|
|
|
|
|
|
error.errCode === 1004
|
|
|
|
|
|
) {
|
|
|
|
|
|
userFriendlyMsg =
|
|
|
|
|
|
"服务器不支持WebSocket或路径不存在\n请检查:\n1. 服务器是否正常运行\n2. WebSocket路径是否正确\n3. 服务器是否支持WebSocket协议";
|
2025-11-20 10:06:56 +08:00
|
|
|
|
console.error("WebSocket握手失败,可能原因:", {
|
|
|
|
|
|
url: this.buildSocketUrl(),
|
|
|
|
|
|
errorCode: error.errCode,
|
|
|
|
|
|
errorMsg: errorMsg,
|
2025-11-20 11:40:31 +08:00
|
|
|
|
suggestion: "服务器可能返回了404或500错误,请检查服务器日志",
|
2025-11-20 10:06:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
} 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 {
|
2025-11-20 11:40:31 +08:00
|
|
|
|
this.cardNo = trimmed.replace(/^['"]|['"]$/g, "");
|
|
|
|
|
|
console.log("接收到的卡号", this.cardNo);
|
2025-11-19 17:58:14 +08:00
|
|
|
|
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);
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
const token = getToken();
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: "未登录",
|
|
|
|
|
|
content: "请先登录后再测试服务器连接",
|
|
|
|
|
|
showCancel: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
// 测试 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}`;
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
console.log("测试 WebSocket URL:", testUrl.replace(token, "***"));
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
uni.showLoading({ title: "测试连接中...", mask: true });
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
// 尝试连接 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,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
// 设置超时
|
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
|
|
testSocket.close();
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: "连接超时",
|
|
|
|
|
|
content: `WebSocket 连接超时\n\n请检查:\n1. 服务器地址: ${host}\n2. WebSocket 路径: ${WS_PATH}\n3. 服务器是否正常运行`,
|
|
|
|
|
|
showCancel: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 5000);
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
testSocket.onOpen(() => {
|
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
testSocket.close();
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "服务器连接正常",
|
|
|
|
|
|
icon: "success",
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-11-20 11:40:31 +08:00
|
|
|
|
|
2025-11-20 10:06:56 +08:00
|
|
|
|
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-25 10:28:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载牌位信息
|
|
|
|
|
|
* @param {string} id - 牌位ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadMemorialInfo(id) {
|
|
|
|
|
|
if (!id) {
|
|
|
|
|
|
console.warn("loadMemorialInfo: id 为空");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("开始加载牌位信息, id:", id);
|
|
|
|
|
|
const res = await getMemorialDetail(id);
|
|
|
|
|
|
console.log("牌位信息API响应:", res);
|
|
|
|
|
|
|
|
|
|
|
|
if (res && res.code === 200 && res.data) {
|
|
|
|
|
|
const data = res.data;
|
|
|
|
|
|
this.memorialInfo = {
|
|
|
|
|
|
regionName: data.regionName || "",
|
|
|
|
|
|
code: data.code || "",
|
|
|
|
|
|
mac: data.mac || "",
|
|
|
|
|
|
};
|
|
|
|
|
|
// 如果接口返回了 unitId,也更新
|
|
|
|
|
|
if (data.id) {
|
|
|
|
|
|
this.unitId = data.id;
|
|
|
|
|
|
this.memorialId = data.id;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果接口返回了 mac,也更新 deviceMac
|
|
|
|
|
|
if (data.mac) {
|
|
|
|
|
|
this.deviceMac = data.mac;
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log("牌位信息加载成功:", this.memorialInfo);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error("牌位信息API响应无效:", res);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "获取牌位信息失败",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("加载牌位信息失败:", error);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "获取牌位信息失败",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
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 = {
|
2025-11-20 11:40:31 +08:00
|
|
|
|
// memorialMac: this.deviceMac,
|
2025-11-19 17:58:14 +08:00
|
|
|
|
nfcMac: this.cardNo,
|
|
|
|
|
|
};
|
2025-11-25 10:28:39 +08:00
|
|
|
|
// 优先使用 memorialId,如果没有则使用 unitId
|
|
|
|
|
|
const memorialId = this.memorialId || this.unitId;
|
|
|
|
|
|
if (memorialId) {
|
|
|
|
|
|
payload.memorialId = memorialId;
|
2025-11-19 17:58:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
const res = await bindNfcCard(payload);
|
2025-11-20 16:37:14 +08:00
|
|
|
|
const success = res && (res.code === 200 || res.status === 200);
|
|
|
|
|
|
const message = (res && res.msg) || "";
|
|
|
|
|
|
const displayMessage = message || (success ? "读取成功" : "读取失败");
|
|
|
|
|
|
this.lastMessage = displayMessage;
|
|
|
|
|
|
const toastDuration = success ? 2600 : 2600;
|
|
|
|
|
|
await this.showStableToast({
|
|
|
|
|
|
title: displayMessage,
|
|
|
|
|
|
icon: success ? "success" : "none",
|
|
|
|
|
|
duration: toastDuration,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
uni.navigateBack({ delta: 1 });
|
2025-11-19 17:58:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("提交绑定失败", error);
|
|
|
|
|
|
uni.showToast({ title: "提交失败,请重试", icon: "none" });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.binding = false;
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-20 13:55:46 +08:00
|
|
|
|
// NFC 相关方法
|
|
|
|
|
|
initNFC() {
|
|
|
|
|
|
// 检查是否支持 NFC(微信小程序环境)
|
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
|
if (typeof wx === "undefined" || !wx.getNFCAdapter) {
|
|
|
|
|
|
console.warn("当前环境不支持 NFC 功能");
|
|
|
|
|
|
this.nfcSupported = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取 NFC 适配器
|
|
|
|
|
|
const nfcAdapter = wx.getNFCAdapter();
|
|
|
|
|
|
if (!nfcAdapter) {
|
|
|
|
|
|
console.warn("无法获取 NFC 适配器");
|
|
|
|
|
|
this.nfcSupported = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.nfcAdapter = nfcAdapter;
|
|
|
|
|
|
this.nfcSupported = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 开始发现 NFC 标签
|
|
|
|
|
|
this.startNFCDiscovery();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("初始化 NFC 失败:", error);
|
|
|
|
|
|
this.nfcSupported = false;
|
|
|
|
|
|
this.nfcError = error.message || "初始化失败";
|
|
|
|
|
|
}
|
|
|
|
|
|
// #endif
|
2025-11-20 14:19:02 +08:00
|
|
|
|
|
2025-11-20 13:55:46 +08:00
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
|
// 非微信小程序环境,不支持 NFC
|
|
|
|
|
|
console.warn("NFC 功能仅在微信小程序中支持");
|
|
|
|
|
|
this.nfcSupported = false;
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
startNFCDiscovery() {
|
|
|
|
|
|
if (!this.nfcAdapter) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 监听 NFC 标签发现事件
|
|
|
|
|
|
this.nfcAdapter.onDiscovered((res) => {
|
|
|
|
|
|
this.handleNfcDiscovered(res);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 开始发现 NFC 标签
|
|
|
|
|
|
this.nfcAdapter.startDiscovery({
|
|
|
|
|
|
success: () => {
|
|
|
|
|
|
console.log("NFC 发现已启动");
|
|
|
|
|
|
this.nfcEnabled = true;
|
|
|
|
|
|
this.nfcError = "";
|
|
|
|
|
|
this.lastMessage = "NFC 已开启,请将卡片贴近手机";
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error("启动 NFC 发现失败:", err);
|
|
|
|
|
|
this.nfcEnabled = false;
|
|
|
|
|
|
this.nfcError = err.errMsg || "启动失败";
|
2025-11-20 14:19:02 +08:00
|
|
|
|
|
2025-11-20 13:55:46 +08:00
|
|
|
|
// 如果是权限问题,给出提示
|
|
|
|
|
|
if (err.errMsg && err.errMsg.includes("permission")) {
|
|
|
|
|
|
this.nfcError = "需要 NFC 权限,请在设置中开启";
|
|
|
|
|
|
} else if (err.errMsg && err.errMsg.includes("not support")) {
|
|
|
|
|
|
this.nfcError = "设备不支持 NFC 功能";
|
|
|
|
|
|
this.nfcSupported = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("启动 NFC 发现异常:", error);
|
|
|
|
|
|
this.nfcEnabled = false;
|
|
|
|
|
|
this.nfcError = error.message || "启动异常";
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handleNfcDiscovered(res) {
|
|
|
|
|
|
console.log("发现 NFC 标签:", res);
|
2025-11-20 14:19:02 +08:00
|
|
|
|
|
2025-11-20 13:55:46 +08:00
|
|
|
|
try {
|
|
|
|
|
|
let cardNo = "";
|
|
|
|
|
|
|
|
|
|
|
|
console.log("移动号获取NFC:res", res);
|
|
|
|
|
|
console.log("res.id 类型:", typeof res.id, res.id);
|
|
|
|
|
|
console.log("res.techs:", res.techs);
|
|
|
|
|
|
|
|
|
|
|
|
// NFC 卡号主要存储在 res.id 中,格式为 ArrayBuffer
|
|
|
|
|
|
// 对于 MIFARE Classic 等卡片,UID 就存储在 id 字段
|
|
|
|
|
|
// 根据调试信息:[[ArrayBufferData]]: 1312 表示4字节的数值
|
|
|
|
|
|
if (res.id) {
|
|
|
|
|
|
// id 通常是 ArrayBuffer,需要转换为字符串
|
|
|
|
|
|
if (res.id instanceof ArrayBuffer) {
|
|
|
|
|
|
// 将 ArrayBuffer 转换为十六进制字符串
|
|
|
|
|
|
const uint8Array = new Uint8Array(res.id);
|
|
|
|
|
|
const bytes = Array.from(uint8Array);
|
2025-11-20 14:19:02 +08:00
|
|
|
|
|
2025-11-20 13:55:46 +08:00
|
|
|
|
console.log("=== NFC 卡号提取详情 ===");
|
|
|
|
|
|
console.log("ArrayBuffer 字节长度:", res.id.byteLength);
|
|
|
|
|
|
console.log("ArrayBuffer 字节数组(十进制):", bytes);
|
2025-11-20 14:19:02 +08:00
|
|
|
|
console.log(
|
|
|
|
|
|
"ArrayBuffer 字节数组(十六进制):",
|
|
|
|
|
|
bytes.map(
|
|
|
|
|
|
(b) => "0x" + b.toString(16).padStart(2, "0").toUpperCase(),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-20 14:56:06 +08:00
|
|
|
|
// 转换为十进制字符串
|
2025-11-20 13:55:46 +08:00
|
|
|
|
// NFC 卡号通常按照字节顺序(大端序)存储
|
2025-11-20 15:05:52 +08:00
|
|
|
|
const baseCardNo = bytes.join("").toUpperCase();
|
2025-11-20 14:19:02 +08:00
|
|
|
|
|
2025-11-20 15:05:52 +08:00
|
|
|
|
// 计算异或校验码(按位异或)
|
|
|
|
|
|
const checksum = bytes.reduce((acc, curr) => acc ^ curr, 0);
|
|
|
|
|
|
const checksumBinary = checksum.toString(2).padStart(8, "0");
|
|
|
|
|
|
|
|
|
|
|
|
cardNo = `${baseCardNo}${checksum}`;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("转换后的卡号(十进制字符串+校验):", cardNo);
|
|
|
|
|
|
console.log("基础卡号部分:", baseCardNo);
|
|
|
|
|
|
console.log("校验码(二进制):", checksumBinary);
|
|
|
|
|
|
console.log("校验码(十进制):", checksum);
|
2025-11-20 13:55:46 +08:00
|
|
|
|
console.log("卡号长度:", cardNo.length, "字符");
|
|
|
|
|
|
console.log("========================");
|
|
|
|
|
|
} else if (typeof res.id === "string") {
|
|
|
|
|
|
cardNo = res.id;
|
|
|
|
|
|
console.log("卡号(字符串):", cardNo);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("res.id 格式未知:", res.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 id 字段没有值,尝试从 techs 中获取
|
|
|
|
|
|
if (!cardNo && res.techs && res.techs.length > 0) {
|
|
|
|
|
|
// 尝试从 techs 中读取数据
|
|
|
|
|
|
for (const tech of res.techs) {
|
|
|
|
|
|
if (tech.id) {
|
|
|
|
|
|
if (tech.id instanceof ArrayBuffer) {
|
|
|
|
|
|
const uint8Array = new Uint8Array(tech.id);
|
|
|
|
|
|
cardNo = Array.from(uint8Array)
|
|
|
|
|
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
|
|
|
|
.join("")
|
|
|
|
|
|
.toUpperCase();
|
|
|
|
|
|
break;
|
|
|
|
|
|
} else if (typeof tech.id === "string") {
|
|
|
|
|
|
cardNo = tech.id;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果还是没有获取到,尝试从其他字段获取
|
|
|
|
|
|
if (!cardNo) {
|
|
|
|
|
|
// 尝试从 res 的其他字段获取
|
|
|
|
|
|
if (res.serialNumber) {
|
|
|
|
|
|
cardNo = res.serialNumber;
|
|
|
|
|
|
} else if (res.uid) {
|
|
|
|
|
|
cardNo = res.uid;
|
|
|
|
|
|
} else if (res.cardId) {
|
|
|
|
|
|
cardNo = res.cardId;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (cardNo) {
|
|
|
|
|
|
// 清理卡号格式(去除空格、冒号等)
|
|
|
|
|
|
cardNo = cardNo.replace(/[\s:]/g, "").toUpperCase();
|
|
|
|
|
|
this.cardNo = cardNo;
|
|
|
|
|
|
this.lastMessage = "已通过 NFC 读取卡号";
|
2025-11-20 17:20:31 +08:00
|
|
|
|
// uni.showToast({
|
|
|
|
|
|
// title: "读取成功",
|
|
|
|
|
|
// icon: "success",
|
|
|
|
|
|
// duration: 1500,
|
|
|
|
|
|
// });
|
2025-11-20 13:55:46 +08:00
|
|
|
|
console.log("NFC 读取到的卡号:", cardNo);
|
2025-11-20 16:37:14 +08:00
|
|
|
|
this.reportSwipeCard(cardNo);
|
2025-11-20 13:55:46 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("无法从 NFC 标签中提取卡号:", res);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "无法读取卡号",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("处理 NFC 标签数据失败:", error);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "读取失败",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
cleanupNFC() {
|
|
|
|
|
|
if (this.nfcAdapter) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 停止 NFC 发现
|
|
|
|
|
|
this.nfcAdapter.stopDiscovery({
|
|
|
|
|
|
success: () => {
|
|
|
|
|
|
console.log("NFC 发现已停止");
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.warn("停止 NFC 发现失败:", err);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn("清理 NFC 资源失败:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.nfcAdapter = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.nfcEnabled = false;
|
|
|
|
|
|
},
|
2025-11-20 16:37:14 +08:00
|
|
|
|
async showStableToast(options = {}) {
|
|
|
|
|
|
const { duration = 2000, ...rest } = options;
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
duration,
|
|
|
|
|
|
...rest,
|
|
|
|
|
|
complete: () => {
|
|
|
|
|
|
setTimeout(resolve, duration);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
async reportSwipeCard(cardNo) {
|
|
|
|
|
|
if (!cardNo || this.reportingSwipe) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.reportingSwipe = true;
|
|
|
|
|
|
uni.showLoading({
|
|
|
|
|
|
title: "处理中...",
|
|
|
|
|
|
mask: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
try {
|
2025-11-20 17:20:31 +08:00
|
|
|
|
const res = await swipeNfcCard({ nfcMac: cardNo });
|
2025-11-20 16:37:14 +08:00
|
|
|
|
if (res && res.code === 200) {
|
|
|
|
|
|
const message = (res && res.msg) || "";
|
|
|
|
|
|
const displayMessage = message || "读取成功";
|
|
|
|
|
|
this.lastMessage = displayMessage;
|
|
|
|
|
|
await this.showStableToast({
|
|
|
|
|
|
title: displayMessage,
|
|
|
|
|
|
icon: "success",
|
|
|
|
|
|
duration: 2600,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await this.showStableToast({
|
|
|
|
|
|
title: (res && res.msg) || "操作失败",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
duration: 2600,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("NFC 卡号上报失败:", error);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "请求失败,请重试",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
duration: 2000,
|
|
|
|
|
|
});
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.reportingSwipe = false;
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-19 17:58:14 +08:00
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
</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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 13:55:46 +08:00
|
|
|
|
.nfc-hint {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nfc-hint-text {
|
|
|
|
|
|
color: #4a90e2;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nfc-status {
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
background: rgba(74, 144, 226, 0.08);
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
border-left: 4rpx solid #4a90e2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nfc-status-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nfc-status-title {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nfc-error-text {
|
|
|
|
|
|
margin-top: 12rpx;
|
|
|
|
|
|
padding: 12rpx;
|
|
|
|
|
|
background: #fef0f0;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-19 17:58:14 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 10:28:39 +08:00
|
|
|
|
.memorial-info-section {
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
border-left: 4rpx solid #4a90e2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.memorial-info-section .field.readonly {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.memorial-info-section .field.readonly:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-19 17:58:14 +08:00
|
|
|
|
.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>
|