1049 lines
46 KiB
HTML
1049 lines
46 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>智能读证网页终端</title>
|
||
<!-- 1. 预加载关键资源 -->
|
||
<link rel="preconnect" href="https://cdn.tailwindcss.com">
|
||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||
|
||
<!-- 2. 异步加载外部资源,避免阻塞渲染 -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet" media="print" onload="this.media='all'">
|
||
|
||
<!-- 3. 精简Tailwind配置,仅保留必要扩展 -->
|
||
<script>
|
||
// 提前初始化Tailwind配置,减少运行时计算
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
primary: '#3b82f6',
|
||
success: '#10b981',
|
||
danger: '#ef4444',
|
||
warning: '#f59e0b',
|
||
dark: '#1e293b',
|
||
},
|
||
fontFamily: {
|
||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||
},
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- 4. 提取核心样式,精简工具类,移除冗余 -->
|
||
<style type="text/tailwindcss">
|
||
@layer utilities {
|
||
.content-auto { content-visibility: auto; }
|
||
.scrollbar-hide {
|
||
-ms-overflow-style: none;
|
||
scrollbar-width: none;
|
||
}
|
||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||
.card-shadow { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); }
|
||
.bg-gradient { background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%); }
|
||
.json-log { white-space: pre-wrap; word-wrap: break-word; font-family: 'Consolas', 'Monaco', monospace; }
|
||
.avatar-frame {
|
||
width: 100px; height: 133px;
|
||
border: 2px solid white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
border-radius: 0.5rem; overflow: hidden;
|
||
}
|
||
.avatar-container {
|
||
display: flex; align-items: center; justify-content: center;
|
||
width: 100%; height: 100%; background-color: #f3f4f6;
|
||
}
|
||
.avatar-img { width: 100%; height: 100%; object-fit: contain; object-position: center; }
|
||
.base64-short {
|
||
color: #6b7280; font-style: italic; cursor: pointer;
|
||
display: inline-block; margin: 4px 0;
|
||
}
|
||
.base64-full {
|
||
max-height: 200px; overflow-y: auto; margin: 4px 0 8px 20px;
|
||
padding: 8px; background-color: #f9fafb; border-radius: 4px;
|
||
font-size: 12px; border-left: 2px solid #e5e7eb;
|
||
}
|
||
.data-table {
|
||
width: 100%; border-collapse: collapse; font-size: 12px;
|
||
}
|
||
.data-table tbody {
|
||
width: 100% !important; /* 强制tbody 100%宽度,优先级最高 */
|
||
}
|
||
.data-table th {
|
||
background-color: #f3f4f6; padding: 6px 8px; text-align: left;
|
||
border: 1px solid #e5e7eb; font-weight: 600;
|
||
width: 100%;
|
||
}
|
||
.data-table td {
|
||
padding: 6px 8px; border: 1px solid #e5e7eb;
|
||
vertical-align: center;
|
||
width: 45%;
|
||
}
|
||
.table-row-group {
|
||
background-color: #f9fafb;
|
||
}
|
||
.table-row-group:nth-child(even) {
|
||
background-color: #ffffff;
|
||
}
|
||
.required-field {
|
||
color: #ef4444; font-size: 10px; margin-left: 2px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- 5. 内联关键CSS,避免FOUC -->
|
||
<style>
|
||
/* 首屏关键样式内联,优先渲染 */
|
||
body { min-height: 100vh; }
|
||
.status { transition: all 0.3s ease; }
|
||
button:disabled { cursor: not-allowed !important; }
|
||
#messageLog { transition: height 0.2s ease; }
|
||
.base64-short { transition: color 0.2s ease; }
|
||
.base64-short:hover { color: #3b82f6; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-50 font-sans text-gray-800 min-h-screen">
|
||
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
||
<!-- 头部 -->
|
||
<header class="mb-8">
|
||
<div class="bg-gradient text-white rounded-xl p-6 card-shadow">
|
||
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold flex items-center">
|
||
<i class="fa fa-exchange mr-3"></i>智能读证网页终端
|
||
</h1>
|
||
<p class="mt-2 text-blue-100">实时数据通信,头像显示与日志记录</p>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 主内容区 -->
|
||
<main class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<!-- 左侧:连接控制区 -->
|
||
<div class="lg:col-span-1">
|
||
<div class="bg-white rounded-xl p-6 card-shadow h-full">
|
||
<h2 class="text-xl font-semibold mb-6 flex items-center text-dark">
|
||
<i class="fa fa-plug mr-2 text-primary"></i>连接控制
|
||
</h2>
|
||
|
||
<!-- 连接状态 -->
|
||
<div id="connectionStatus" class="status mb-6 p-4 rounded-lg bg-danger/10 text-danger flex items-center">
|
||
<i class="fa fa-circle-o-notch fa-spin mr-2"></i>
|
||
<span>未连接</span>
|
||
</div>
|
||
|
||
<!-- 连接配置 -->
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">WebSocket 服务器地址</label>
|
||
<input type="text" id="serverUrl" value="ws://127.0.0.1:9090"
|
||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary outline-none transition">
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="flex flex-wrap gap-3 mb-6">
|
||
<button onclick="connectWebSocket()" class="flex-1 bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg transition flex items-center justify-center">
|
||
<i class="fa fa-connectdevelop mr-2"></i>连接设备
|
||
</button>
|
||
<button onclick="disconnectWebSocket()" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition flex items-center justify-center">
|
||
<i class="fa fa-unlink mr-2"></i>断开连接
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 命令选择 -->
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">预设命令</label>
|
||
<select id="commandSelect" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary outline-none transition">
|
||
<option value="set_auto" selected>设置自动模式 (set auto=1)</option>
|
||
<option value="get_name">获取名称 (get name)</option>
|
||
<option value="get_serialNo">获取序列号 (get serialNo)</option>
|
||
<option value="get_model">获取型号 (get model)</option>
|
||
<option value="get_type">获取类型 (get type)</option>
|
||
<option value="set_timeout">设置超时 (set timeout=5)</option>
|
||
<option value="read">读取数据 (read=5)</option>
|
||
<option value="scan">扫描 (scan timeout=5)</option>
|
||
<option value="readCode">读取数据 (readCode=5)</option>
|
||
<option value="getPhoto">获取照片 (getPhoto=5)</option>
|
||
<option value="scanRaw">高级扫描 (scanRaw)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 发送按钮 -->
|
||
<button onclick="sendData()" disabled id="sendButton"
|
||
class="w-full bg-success hover:bg-success/90 disabled:bg-gray-400 disabled:cursor-not-allowed text-white px-4 py-3 rounded-lg transition flex items-center justify-center font-medium">
|
||
<i class="fa fa-paper-plane mr-2"></i>发送命令
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:头像+日志组合区 -->
|
||
<div class="lg:col-span-2">
|
||
<div class="bg-white rounded-xl p-6 card-shadow">
|
||
<h2 class="text-xl font-semibold mb-6 flex items-center text-dark">
|
||
<i class="fa fa-history mr-2 text-primary"></i>消息日志与头像展示
|
||
</h2>
|
||
|
||
<!-- 头像+日志布局 - 固定尺寸 -->
|
||
<div class="flex flex-col md:flex-row gap-8">
|
||
<!-- 头像展示区 - 懒加载非首屏内容 -->
|
||
<div class="flex flex-col justify-center items-center" loading="lazy">
|
||
<div class="text-sm font-medium text-gray-500 mb-3 text-center">头像展示</div>
|
||
<div class="avatar-frame">
|
||
<div class="avatar-container">
|
||
<i class="fa fa-user text-4xl text-gray-300"></i>
|
||
<img id="avatarImage" class="hidden avatar-img" alt="用户头像">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日志展示区 - 固定高度,不伸缩 -->
|
||
<div class="flex-1 min-w-[300px]">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<div class="text-sm font-medium text-gray-500">日志记录</div>
|
||
<div class="flex gap-2">
|
||
<button onclick="clearLogs()" class="text-sm text-gray-500 hover:text-danger transition">
|
||
<i class="fa fa-trash-o mr-1"></i>清空
|
||
</button>
|
||
<button onclick="downloadLogs()" class="text-sm text-primary hover:text-primary/80 transition">
|
||
<i class="fa fa-download mr-1"></i>导出
|
||
</button>
|
||
<button onclick="toggleLogView()" class="text-sm text-primary hover:text-primary/80 transition">
|
||
<i class="fa fa-table mr-1"></i>
|
||
<span id="viewToggleText">文本视图</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- 初始隐藏文本日志,显示表格视图 -->
|
||
<div id="messageLog" class="border border-gray-200 rounded-lg p-4 h-[400px] overflow-auto font-mono text-sm bg-gray-50 hidden" data-lazy-render="true"></div>
|
||
<!-- 表格视图容器(默认显示) -->
|
||
<div id="tableView" class="border border-gray-200 rounded-lg p-4 h-[400px] overflow-auto bg-gray-50">
|
||
<div id="dataTableContainer" class="mb-4"></div>
|
||
<div id="logTableContainer" class="mt-4 text-sm"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- 页脚 -->
|
||
<footer class="mt-8 text-center text-gray-500 text-sm">
|
||
<p>智能读证网页终端 © 2025</p>
|
||
</footer>
|
||
</div>
|
||
|
||
<!-- 6. JS代码延迟加载,非阻塞执行 -->
|
||
<script defer>
|
||
// 全局变量(精简初始化)
|
||
let webSocket = null;
|
||
let logHistory = [];
|
||
let base64DataMap = new Map();
|
||
let base64ReplaceMap = new Map();
|
||
let dataRecords = []; // 存储解析后的结构化数据
|
||
let isTableView = true; // 核心修改:默认显示表格视图
|
||
|
||
// 协议映射表
|
||
const protocolMappings = {
|
||
// 旅客类型映射
|
||
guestType: {
|
||
'100': '国内旅客',
|
||
'200': '港澳台旅客',
|
||
'300': '国外旅客'
|
||
},
|
||
// 性别映射
|
||
sex: {
|
||
'1': '男',
|
||
'2': '女'
|
||
},
|
||
// 证件类型映射
|
||
cardType: {
|
||
// 国内旅客
|
||
'11': '身份证',
|
||
'12': '居住证',
|
||
'13': '户口本',
|
||
'55': '港澳台居民居住证',
|
||
'90': '军官证',
|
||
'91': '警官证',
|
||
'92': '士兵证',
|
||
'93': '国内护照',
|
||
'94': '驾照',
|
||
'95': '港澳通行证',
|
||
'99': '其他',
|
||
'A1': '临时身份证',
|
||
// 港澳台旅客
|
||
'16': '台湾居民来往大陆通行证',
|
||
'60': '港澳居民来往内地通行证',
|
||
// 国外旅客
|
||
'14': '国外护照',
|
||
'34': '外国人永久居留身份证'
|
||
},
|
||
// 民族映射
|
||
nation: {
|
||
'01': '汉',
|
||
'02': '蒙古',
|
||
'03': '回',
|
||
'04': '藏',
|
||
'05': '维吾尔',
|
||
'06': '苗',
|
||
'07': '彝',
|
||
'08': '壮',
|
||
'09': '布依',
|
||
'10': '朝鲜',
|
||
'11': '满',
|
||
'12': '侗',
|
||
'13': '瑶',
|
||
'14': '白',
|
||
'15': '土家',
|
||
'16': '哈尼',
|
||
'17': '哈萨克',
|
||
'18': '傣',
|
||
'19': '黎',
|
||
'20': '傈僳',
|
||
'21': '佤',
|
||
'22': '畲',
|
||
'23': '高山',
|
||
'24': '拉祜',
|
||
'25': '水',
|
||
'26': '东乡',
|
||
'27': '纳西',
|
||
'28': '景颇',
|
||
'29': '柯尔克孜',
|
||
'30': '土',
|
||
'31': '达翰尔',
|
||
'32': '仫佬',
|
||
'33': '羌',
|
||
'34': '布朗',
|
||
'35': '撒拉',
|
||
'36': '毛难',
|
||
'37': '仡佬',
|
||
'38': '锡伯',
|
||
'39': '阿昌',
|
||
'40': '普米',
|
||
'41': '塔吉克',
|
||
'42': '怒',
|
||
'43': '乌孜别克',
|
||
'44': '俄罗斯',
|
||
'45': '鄂温克',
|
||
'46': '崩龙',
|
||
'47': '保安',
|
||
'48': '裕固',
|
||
'49': '京',
|
||
'50': '塔塔尔',
|
||
'51': '独龙',
|
||
'52': '鄂伦春',
|
||
'53': '赫哲',
|
||
'54': '门巴',
|
||
'55': '珞巴',
|
||
'56': '基诺',
|
||
'59': '穿青人',
|
||
'98': '外国血统',
|
||
'99': '其它'
|
||
}
|
||
};
|
||
|
||
// DOM元素(延迟获取,避免提前查询)
|
||
let DOM = {
|
||
statusElement: null,
|
||
sendButton: null,
|
||
messageLog: null,
|
||
serverUrlInput: null,
|
||
commandSelect: null,
|
||
avatarImage: null,
|
||
tableView: null,
|
||
dataTableContainer: null,
|
||
logTableContainer: null,
|
||
viewToggleText: null
|
||
};
|
||
|
||
// ========== 工具函数(精简核心逻辑) ==========
|
||
function clearAvatar() {
|
||
if (!DOM.avatarImage) DOM.avatarImage = document.getElementById('avatarImage');
|
||
DOM.avatarImage.src = '';
|
||
DOM.avatarImage.classList.add('hidden');
|
||
|
||
const avatarContainer = document.querySelector('.avatar-container');
|
||
avatarContainer.innerHTML = `
|
||
<i class="fa fa-user text-4xl text-gray-300"></i>
|
||
<img id="avatarImage" class="hidden avatar-img" alt="用户头像">
|
||
`;
|
||
DOM.avatarImage = document.getElementById('avatarImage');
|
||
}
|
||
|
||
function generateBase64Marker(base64Str) {
|
||
if (!base64Str || base64Str.length < 100) return null;
|
||
const markerId = '[[BASE64_' + Date.now() + '_' + Math.floor(Math.random() * 1000) + ']]';
|
||
base64DataMap.set(markerId, base64Str);
|
||
base64ReplaceMap.set(markerId, {
|
||
length: base64Str.length,
|
||
preview: base64Str.substring(0, 50) + '...' + base64Str.substring(base64Str.length - 20)
|
||
});
|
||
return markerId;
|
||
}
|
||
|
||
function createBase64Html(markerId, length) {
|
||
const htmlId = markerId.replace(/[\[\]]/g, '').toLowerCase();
|
||
return `
|
||
<span class="base64-short" onclick="toggleBase64Display('${htmlId}')">
|
||
[Base64图片数据 - 共${length}字符] 点击查看完整数据
|
||
<span id="base64_${htmlId}_toggle" class="ml-2">▼</span>
|
||
</span>
|
||
<div id="base64_${htmlId}_content" class="base64-full json-log hidden" data-marker="${markerId}">
|
||
加载中...
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
window.toggleBase64Display = function(htmlId) {
|
||
const contentEl = document.getElementById(`base64_${htmlId}_content`);
|
||
const toggleEl = document.getElementById(`base64_${htmlId}_toggle`);
|
||
|
||
if (contentEl.classList.contains('hidden')) {
|
||
contentEl.classList.remove('hidden');
|
||
toggleEl.textContent = '▲';
|
||
const markerId = contentEl.getAttribute('data-marker');
|
||
contentEl.textContent = base64DataMap.get(markerId) || '数据不存在';
|
||
} else {
|
||
contentEl.classList.add('hidden');
|
||
toggleEl.textContent = '▼';
|
||
}
|
||
};
|
||
|
||
function isBase64ImageData(str) {
|
||
if (!str || typeof str !== 'string') return false;
|
||
const base64ImagePrefixes = ['data:image/jpeg;base64,', 'data:image/png;base64,', 'data:image/gif;base64,', 'data:image/bmp;base64,'];
|
||
const isLongBase64 = str.length > 1000 && /^[A-Za-z0-9+/=]+$/.test(str);
|
||
return base64ImagePrefixes.some(prefix => str.startsWith(prefix)) || isLongBase64;
|
||
}
|
||
|
||
function displayAvatar(base64Data) {
|
||
try {
|
||
clearAvatar();
|
||
if (!base64Data || base64Data.length === 0) {
|
||
logMessage('头像数据为空,无法显示');
|
||
return;
|
||
}
|
||
|
||
logMessage(`尝试加载头像数据 - 大小: ${base64Data.length} 字符`);
|
||
let imageUrl = base64Data;
|
||
|
||
if (!base64Data.startsWith('data:image')) {
|
||
const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, '');
|
||
imageUrl = `data:image/jpeg;base64,${cleanBase64}`;
|
||
}
|
||
|
||
const avatarContainer = document.querySelector('.avatar-container');
|
||
const img = document.createElement('img');
|
||
img.id = 'avatarImage';
|
||
img.className = 'avatar-img';
|
||
img.alt = '用户头像';
|
||
|
||
img.onload = function() {
|
||
logMessage('头像图片加载成功 - 完整显示');
|
||
avatarContainer.innerHTML = '';
|
||
avatarContainer.appendChild(img);
|
||
};
|
||
|
||
img.onerror = function(error) {
|
||
logMessage(`头像加载失败: ${error.message}`);
|
||
clearAvatar();
|
||
};
|
||
|
||
img.src = imageUrl;
|
||
} catch (error) {
|
||
logMessage(`头像显示异常: ${error.message}`);
|
||
clearAvatar();
|
||
}
|
||
}
|
||
|
||
function processJsonForBase64(obj) {
|
||
if (typeof obj === 'string' && isBase64ImageData(obj)) {
|
||
const marker = generateBase64Marker(obj);
|
||
return marker || obj;
|
||
} else if (Array.isArray(obj)) {
|
||
return obj.map(item => processJsonForBase64(item));
|
||
} else if (obj && typeof obj === 'object' && obj !== null) {
|
||
const result = {};
|
||
for (const key in obj) {
|
||
if (obj.hasOwnProperty(key)) result[key] = processJsonForBase64(obj[key]);
|
||
}
|
||
return result;
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
function formatJsonWithBase64Markers(jsonStr) {
|
||
let formatted = jsonStr;
|
||
base64ReplaceMap.forEach((info, marker) => {
|
||
const escapedMarker = marker.replace(/[[\]]/g, '\\$&');
|
||
const regex = new RegExp(`"${escapedMarker}"`, 'g');
|
||
formatted = formatted.replace(regex, `"[Base64图片数据 - 共${info.length}字符]"`);
|
||
});
|
||
return formatted;
|
||
}
|
||
|
||
function formatData(data) {
|
||
try {
|
||
if (isBase64ImageData(data)) {
|
||
return {
|
||
content: generateBase64Marker(data),
|
||
isBase64: true,
|
||
isJson: false
|
||
};
|
||
}
|
||
|
||
const parsed = JSON.parse(data);
|
||
// 只解析data对象中的字段
|
||
if (parsed.data && typeof parsed.data === 'object') {
|
||
parseAndSaveStructuredData(parsed.data, data);
|
||
}
|
||
|
||
const processed = processJsonForBase64(parsed);
|
||
let jsonStr = JSON.stringify(processed, null, 2);
|
||
const formattedJson = formatJsonWithBase64Markers(jsonStr);
|
||
|
||
return {
|
||
content: formattedJson,
|
||
isBase64: false,
|
||
isJson: true
|
||
};
|
||
} catch (e) {
|
||
return {
|
||
content: data,
|
||
isBase64: isBase64ImageData(data),
|
||
isJson: false
|
||
};
|
||
}
|
||
}
|
||
|
||
// 解析并保存结构化数据(只处理data对象)
|
||
function parseAndSaveStructuredData(dataObj, rawData) {
|
||
if (!dataObj || typeof dataObj !== 'object') {
|
||
return; // 不是对象则跳过
|
||
}
|
||
|
||
const record = {
|
||
timestamp: new Date().toLocaleString(),
|
||
raw: rawData,
|
||
data: {...dataObj} // 只保存data对象的内容
|
||
};
|
||
|
||
dataRecords.unshift(record); // 添加到数组开头
|
||
if (dataRecords.length > 10) {
|
||
dataRecords.pop(); // 只保留最近10条记录
|
||
}
|
||
|
||
// 如果有头像数据,自动加载
|
||
if (dataObj.curPhoto && isBase64ImageData(dataObj.curPhoto)) {
|
||
displayAvatar(dataObj.curPhoto);
|
||
} else if (dataObj.photo && isBase64ImageData(dataObj.photo)) {
|
||
displayAvatar(dataObj.photo);
|
||
}
|
||
|
||
// 如果当前是表格视图,更新表格
|
||
if (isTableView) {
|
||
renderDataTable();
|
||
}
|
||
}
|
||
|
||
// 获取字段的格式化显示值
|
||
function getFormattedValue(fieldName, value) {
|
||
if (value === undefined || value === null || value === '') {
|
||
return '-';
|
||
}
|
||
|
||
// // 检查是否有映射表
|
||
// if (protocolMappings[fieldName] && protocolMappings[fieldName][value]) {
|
||
// return `${value} (${protocolMappings[fieldName][value]})`;
|
||
// }
|
||
if (protocolMappings[fieldName] && protocolMappings[fieldName][value]) {
|
||
return `${protocolMappings[fieldName][value]}`;
|
||
}
|
||
|
||
// 特殊处理日期格式(如果看起来是日期)
|
||
if ((fieldName.includes('Date') || fieldName.includes('date') ||
|
||
fieldName === 'birthday' || fieldName === 'validDate') &&
|
||
value.length === 10 && value.includes('-')) {
|
||
return value;
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
// 渲染数据表格(只显示data中的字段)
|
||
function renderDataTable() {
|
||
if (!DOM.dataTableContainer) {
|
||
DOM.dataTableContainer = document.getElementById('dataTableContainer');
|
||
}
|
||
|
||
if (dataRecords.length === 0) {
|
||
DOM.dataTableContainer.innerHTML = '<div class="text-gray-500 text-center py-4">暂无证件数据</div>';
|
||
return;
|
||
}
|
||
|
||
// 使用最新的一条记录渲染详情表格
|
||
const latestRecord = dataRecords[0];
|
||
const data = latestRecord.data;
|
||
|
||
// let tableHtml = `
|
||
// <div class="mb-2 text-sm font-medium text-gray-700">
|
||
// 证件信息 (${latestRecord.timestamp})
|
||
// </div>
|
||
// <table class="data-table mb-4">
|
||
// <thead>
|
||
// <tr>
|
||
// <th class="w-1/4">字段</th>
|
||
// <th>值</th>
|
||
// </tr>
|
||
// </thead>
|
||
// <tbody>
|
||
// `;
|
||
let tableHtml = `
|
||
<div class="mb-2 text-sm font-medium text-gray-700">
|
||
证件信息 (${latestRecord.timestamp})
|
||
</div>
|
||
<table class="data-table mb-4">
|
||
<tbody>
|
||
`;
|
||
|
||
// 证件信息字段定义(按协议顺序)
|
||
const fields = [
|
||
{key: 'guestType', label: '旅客类型', required: true},
|
||
{key: 'name', label: '姓名', required: true},
|
||
{key: 'sex', label: '性别', required: true},
|
||
{key: 'birthday', label: '出生日期', required: true},
|
||
{key: 'cardType', label: '证件类型', required: true},
|
||
{key: 'cardNo', label: '证件号码', required: true},
|
||
{key: 'nation', label: '民族'},
|
||
{key: 'adminDivision', label: '行政区划'},
|
||
{key: 'firstName', label: '英文姓'},
|
||
{key: 'lastName', label: '英文名'},
|
||
{key: 'nationalityArea', label: '国籍/地区', required: true},
|
||
{key: 'address', label: '住址'},
|
||
{key: 'signDate', label: '签证日期'},
|
||
{key: 'beginDate', label: '有效期开始时间'},
|
||
{key: 'validDate', label: '有效期结束时间'},
|
||
{key: 'signOrg', label: '签发机关'},
|
||
{key: 'signPlace', label: '签发地址'},
|
||
{key: 'phone', label: '联系电话'}
|
||
];
|
||
|
||
// 渲染有值的字段
|
||
let hasData = false;
|
||
fields.forEach(field => {
|
||
const value = data[field.key];
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
hasData = true;
|
||
const displayValue = getFormattedValue(field.key, value);
|
||
// const displayValue = field.key;
|
||
|
||
tableHtml += `
|
||
<tr class="table-row-group">
|
||
<td class="font-medium">
|
||
${field.label}
|
||
${field.required ? '<sup class="required-field">*</sup>' : ''}
|
||
</td>
|
||
<td class="font-medium">${displayValue}</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
});
|
||
|
||
// // 处理照片字段
|
||
// if (data.curPhoto) {
|
||
// hasData = true;
|
||
// const marker = generateBase64Marker(data.curPhoto);
|
||
// const base64Html = createBase64Html(marker, data.curPhoto.length);
|
||
// tableHtml += `
|
||
// <tr class="table-row-group">
|
||
// <td class="font-medium">
|
||
// 证件头像
|
||
// <sup class="required-field">*</sup>
|
||
// </td>
|
||
// <td>${base64Html}</td>
|
||
// </tr>
|
||
// `;
|
||
// }
|
||
|
||
// if (data.photo) {
|
||
// hasData = true;
|
||
// const marker = generateBase64Marker(data.photo);
|
||
// const base64Html = createBase64Html(marker, data.photo.length);
|
||
// tableHtml += `
|
||
// <tr class="table-row-group">
|
||
// <td class="font-medium">现场头像</td>
|
||
// <td>${base64Html}</td>
|
||
// </tr>
|
||
// `;
|
||
// }
|
||
|
||
// 如果没有任何数据
|
||
if (!hasData) {
|
||
tableHtml += `
|
||
<tr>
|
||
<td colspan="2" class="text-gray-500 text-center py-4">
|
||
暂无有效证件数据
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
tableHtml += `
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
DOM.dataTableContainer.innerHTML = tableHtml;
|
||
|
||
// // 渲染日志表格
|
||
// renderLogTable();
|
||
}
|
||
|
||
// 渲染日志表格
|
||
function renderLogTable() {
|
||
if (!DOM.logTableContainer) {
|
||
DOM.logTableContainer = document.getElementById('logTableContainer');
|
||
}
|
||
|
||
if (logHistory.length === 0) {
|
||
DOM.logTableContainer.innerHTML = '<div class="text-gray-500 text-center py-2">暂无日志记录</div>';
|
||
return;
|
||
}
|
||
|
||
// 只显示最近20条日志
|
||
const recentLogs = logHistory.slice(-20).reverse();
|
||
|
||
let logTableHtml = `
|
||
<div class="mb-2 text-sm font-medium text-gray-700 mt-4">
|
||
操作日志 (最近${recentLogs.length}条)
|
||
</div>
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-1/5">时间</th>
|
||
<th>日志内容</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
recentLogs.forEach(log => {
|
||
let content = log.isRawData ? '[数据内容]' : log.message;
|
||
// 截断过长的日志内容
|
||
if (content.length > 100) {
|
||
content = content.substring(0, 100) + '...';
|
||
}
|
||
|
||
logTableHtml += `
|
||
<tr class="table-row-group">
|
||
<td>${log.time}</td>
|
||
<td>${content}</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
logTableHtml += `
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
DOM.logTableContainer.innerHTML = logTableHtml;
|
||
}
|
||
|
||
// 切换视图(文本/表格)
|
||
window.toggleLogView = function() {
|
||
if (!DOM.messageLog) DOM.messageLog = document.getElementById('messageLog');
|
||
if (!DOM.tableView) DOM.tableView = document.getElementById('tableView');
|
||
if (!DOM.viewToggleText) DOM.viewToggleText = document.getElementById('viewToggleText');
|
||
|
||
isTableView = !isTableView;
|
||
|
||
if (isTableView) {
|
||
DOM.messageLog.classList.add('hidden');
|
||
DOM.tableView.classList.remove('hidden');
|
||
DOM.viewToggleText.textContent = '文本视图';
|
||
renderDataTable();
|
||
} else {
|
||
DOM.messageLog.classList.remove('hidden');
|
||
DOM.tableView.classList.add('hidden');
|
||
DOM.viewToggleText.textContent = '表格视图';
|
||
}
|
||
}
|
||
|
||
function getCommandData() {
|
||
if (!DOM.commandSelect) DOM.commandSelect = document.getElementById('commandSelect');
|
||
const command = DOM.commandSelect.value;
|
||
const commandMap = {
|
||
'get_name': { "command": "get", "operand": "name" },
|
||
'get_serialNo': { "command": "get", "operand": "serialNo" },
|
||
'get_model': { "command": "get", "operand": "model" },
|
||
'get_type': { "command": "get", "operand": "type" },
|
||
'set_timeout': { "command": "set", "operand": "timeout", "param": 5 },
|
||
'set_auto': { "command": "set", "operand": "auto", "param": 1 },
|
||
'read': { "command": "read", "param": 5 },
|
||
'scan': { "command": "scan", "param": "{\"timeout\":5}" },
|
||
'readCode': { "command": "readCode", "param": 5 },
|
||
'getPhoto': { "command": "getPhoto", "param": 5 },
|
||
'scanRaw': { "command": "scan", "param": "{\"timeout\":5, \"dpi\":\"300\",\"colorMode\":\"1\", \"cardType\": \"02\"}" }
|
||
};
|
||
return commandMap[command] || commandMap['get_name'];
|
||
}
|
||
|
||
// ========== 核心业务逻辑 ==========
|
||
function connectWebSocket() {
|
||
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
|
||
logMessage('已经处于连接状态');
|
||
return;
|
||
}
|
||
|
||
if (!DOM.serverUrlInput) DOM.serverUrlInput = document.getElementById('serverUrl');
|
||
const serverUrl = DOM.serverUrlInput.value.trim();
|
||
if (!serverUrl) {
|
||
logMessage('请输入有效的服务器地址');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
webSocket = new WebSocket(serverUrl);
|
||
|
||
webSocket.onopen = function(event) {
|
||
if (!DOM.statusElement) DOM.statusElement = document.getElementById('connectionStatus');
|
||
DOM.statusElement.innerHTML = '<i class="fa fa-check-circle mr-2"></i><span>已连接到服务器</span>';
|
||
DOM.statusElement.className = 'status mb-6 p-4 rounded-lg bg-success/10 text-success flex items-center';
|
||
if (!DOM.sendButton) DOM.sendButton = document.getElementById('sendButton');
|
||
DOM.sendButton.disabled = false;
|
||
logMessage(`已连接到: ${serverUrl}`);
|
||
logMessage(`WebSocket 状态: OPEN (readyState: ${webSocket.readyState})`);
|
||
};
|
||
|
||
webSocket.onmessage = function(event) {
|
||
const rawData = event.data;
|
||
const formatted = formatData(rawData);
|
||
|
||
logMessage(`收到服务器响应 [${event.type}]: `);
|
||
|
||
if (formatted.isBase64) {
|
||
const markerInfo = base64ReplaceMap.get(formatted.content) || { length: rawData.length };
|
||
const base64Html = createBase64Html(formatted.content, markerInfo.length);
|
||
logMessage(base64Html, false, true);
|
||
|
||
try {
|
||
displayAvatar(rawData);
|
||
} catch (e) {
|
||
logMessage(`头像显示失败: ${e.message}`);
|
||
}
|
||
} else if (formatted.isJson) {
|
||
logMessage(formatted.content, true);
|
||
|
||
const jsonLogItems = DOM.messageLog.querySelectorAll('.json-log:last-child');
|
||
if (jsonLogItems.length > 0) {
|
||
const jsonLogItem = jsonLogItems[0];
|
||
base64ReplaceMap.forEach((info, marker) => {
|
||
if (formatted.content.includes(marker)) {
|
||
const base64Html = createBase64Html(marker, info.length);
|
||
const base64Element = document.createElement('div');
|
||
base64Element.innerHTML = base64Html;
|
||
jsonLogItem.parentNode.insertBefore(base64Element, jsonLogItem.nextSibling);
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
logMessage(formatted.content, true);
|
||
}
|
||
};
|
||
|
||
webSocket.onclose = function(event) {
|
||
if (!DOM.statusElement) DOM.statusElement = document.getElementById('connectionStatus');
|
||
DOM.statusElement.innerHTML = '<i class="fa fa-circle-o-notch fa-spin mr-2"></i><span>连接已关闭 (代码: ' + event.code + ')</span>';
|
||
DOM.statusElement.className = 'status mb-6 p-4 rounded-lg bg-danger/10 text-danger flex items-center';
|
||
if (!DOM.sendButton) DOM.sendButton = document.getElementById('sendButton');
|
||
DOM.sendButton.disabled = true;
|
||
logMessage(`连接已关闭 - 代码: ${event.code}, 原因: ${event.reason}, 干净关闭: ${event.wasClean}`);
|
||
logMessage(`WebSocket 状态: CLOSED (readyState: ${webSocket.readyState})`);
|
||
webSocket = null;
|
||
};
|
||
|
||
webSocket.onerror = function(error) {
|
||
logMessage(`WebSocket 错误: ${JSON.stringify(error, null, 2)}`);
|
||
if (error.message) logMessage(`错误详情: ${error.message}`);
|
||
};
|
||
} catch (error) {
|
||
logMessage(`连接失败: ${error.message}`);
|
||
logMessage(`错误堆栈: ${error.stack || '无'}`);
|
||
}
|
||
}
|
||
|
||
function sendData() {
|
||
if (!webSocket || webSocket.readyState !== WebSocket.OPEN) {
|
||
logMessage('发送失败: 未连接到服务器或连接已关闭');
|
||
logMessage(`当前WebSocket状态: ${webSocket ? `readyState: ${webSocket.readyState}` : '未初始化'}`);
|
||
return;
|
||
}
|
||
|
||
const data = getCommandData();
|
||
const jsonData = JSON.stringify(data, null, 2);
|
||
|
||
try {
|
||
logMessage(`准备发送数据: `);
|
||
logMessage(jsonData, true);
|
||
const sendResult = webSocket.send(JSON.stringify(data));
|
||
logMessage(`数据发送成功 (返回值: ${sendResult})`);
|
||
|
||
if (data.command === 'getPhoto') {
|
||
clearAvatar();
|
||
logMessage('清空现有头像,准备接收新头像数据');
|
||
}
|
||
} catch (error) {
|
||
logMessage(`发送失败: ${error.message}`);
|
||
logMessage(`错误类型: ${error.name}, 堆栈: ${error.stack || '无'}`);
|
||
}
|
||
}
|
||
|
||
function disconnectWebSocket() {
|
||
if (webSocket) {
|
||
logMessage('主动关闭连接,当前状态: ' + webSocket.readyState);
|
||
webSocket.close(1000, '客户端主动断开');
|
||
} else {
|
||
logMessage('当前未连接到服务器');
|
||
}
|
||
}
|
||
|
||
function logMessage(message, isRawData = false, isHtml = false) {
|
||
// 延迟初始化日志容器
|
||
if (!DOM.messageLog) {
|
||
DOM.messageLog = document.getElementById('messageLog');
|
||
// 初始化日志容器样式
|
||
DOM.messageLog.removeAttribute('data-lazy-render');
|
||
}
|
||
|
||
const timestamp = new Date().toLocaleString();
|
||
const logEntry = {
|
||
time: timestamp,
|
||
message: message,
|
||
timestamp: Date.now(),
|
||
isRawData: isRawData,
|
||
isHtml: isHtml,
|
||
rawContent: message
|
||
};
|
||
|
||
logHistory.push(logEntry);
|
||
|
||
const logItem = document.createElement('div');
|
||
let baseClass = 'mb-1.5 border-l-2 pl-2 py-0.5 text-sm';
|
||
|
||
if (message.includes('错误') || message.includes('失败') || message.includes('Error')) {
|
||
logItem.className = `${baseClass} text-danger border-danger`;
|
||
} else if (message.includes('已发送') || message.includes('发送成功')) {
|
||
logItem.className = `${baseClass} text-primary border-primary`;
|
||
} else if (message.includes('收到') || message.includes('原始数据')) {
|
||
logItem.className = `${baseClass} text-success border-success`;
|
||
} else if (message.includes('连接') || message.includes('状态')) {
|
||
logItem.className = `${baseClass} text-warning border-warning`;
|
||
} else if (isRawData) {
|
||
logItem.className = `${baseClass} text-gray-700 border-gray-200 json-log`;
|
||
} else {
|
||
logItem.className = `${baseClass} text-gray-700 border-gray-200`;
|
||
}
|
||
|
||
if (isHtml) {
|
||
logItem.innerHTML = isRawData ? message : `[${timestamp}] ${message}`;
|
||
} else {
|
||
logItem.textContent = isRawData ? message : `[${timestamp}] ${message}`;
|
||
}
|
||
|
||
DOM.messageLog.appendChild(logItem);
|
||
DOM.messageLog.scrollTop = DOM.messageLog.scrollHeight;
|
||
|
||
// 如果当前是表格视图,更新日志表格
|
||
if (isTableView) {
|
||
// renderLogTable();
|
||
}
|
||
}
|
||
|
||
function clearLogs() {
|
||
logMessage('日志已手动清空');
|
||
if (!DOM.messageLog) DOM.messageLog = document.getElementById('messageLog');
|
||
DOM.messageLog.innerHTML = '';
|
||
logHistory = [];
|
||
dataRecords = []; // 清空结构化数据
|
||
base64DataMap.clear();
|
||
base64ReplaceMap.clear();
|
||
|
||
// 清空表格视图
|
||
if (DOM.dataTableContainer) {
|
||
DOM.dataTableContainer.innerHTML = '<div class="text-gray-500 text-center py-4">暂无证件数据</div>';
|
||
}
|
||
if (DOM.logTableContainer) {
|
||
DOM.logTableContainer.innerHTML = '<div class="text-gray-500 text-center py-2">暂无日志记录</div>';
|
||
}
|
||
}
|
||
|
||
function downloadLogs() {
|
||
if (logHistory.length === 0) {
|
||
logMessage('日志为空,无法导出');
|
||
return;
|
||
}
|
||
|
||
let logText = "WebSocket 客户端日志\n====================\n\n";
|
||
|
||
logHistory.forEach(entry => {
|
||
if (entry.isRawData) {
|
||
let content = entry.rawContent;
|
||
base64DataMap.forEach((data, marker) => {
|
||
content = content.replace(marker, data);
|
||
});
|
||
logText += `${content}\n\n`;
|
||
} else {
|
||
logText += `[${entry.time}] ${entry.rawContent}\n`;
|
||
}
|
||
});
|
||
|
||
// 添加结构化数据导出
|
||
if (dataRecords.length > 0) {
|
||
logText += "\n\n证件数据记录\n====================\n\n";
|
||
dataRecords.forEach((record, index) => {
|
||
logText += `记录 ${index + 1} [${record.timestamp}]:\n`;
|
||
logText += `原始数据: ${JSON.stringify(record.data, null, 2)}\n\n`;
|
||
});
|
||
}
|
||
|
||
const blob = new Blob([logText], { type: 'text/plain; charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `websocket-log-${new Date().getTime()}.txt`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
logMessage(`日志已导出,共 ${logHistory.length} 条记录`);
|
||
}
|
||
|
||
// ========== 页面初始化(延迟执行) ==========
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 懒加载DOM元素引用
|
||
setTimeout(() => {
|
||
DOM.statusElement = document.getElementById('connectionStatus');
|
||
DOM.sendButton = document.getElementById('sendButton');
|
||
DOM.messageLog = document.getElementById('messageLog');
|
||
DOM.serverUrlInput = document.getElementById('serverUrl');
|
||
DOM.commandSelect = document.getElementById('commandSelect');
|
||
DOM.avatarImage = document.getElementById('avatarImage');
|
||
DOM.tableView = document.getElementById('tableView');
|
||
DOM.dataTableContainer = document.getElementById('dataTableContainer');
|
||
DOM.logTableContainer = document.getElementById('logTableContainer');
|
||
DOM.viewToggleText = document.getElementById('viewToggleText');
|
||
|
||
// 初始化表格视图
|
||
renderDataTable();
|
||
// 初始化日志
|
||
logMessage('客户端初始化完成,等待连接...');
|
||
}, 100);
|
||
});
|
||
|
||
// 暴露全局函数
|
||
window.connectWebSocket = connectWebSocket;
|
||
window.disconnectWebSocket = disconnectWebSocket;
|
||
window.sendData = sendData;
|
||
window.clearLogs = clearLogs;
|
||
window.downloadLogs = downloadLogs;
|
||
</script>
|
||
</body>
|
||
</html> |