AML/docs/KYC/Scanner/DKePassport-release-1.3.0/智能读证终端网页.html

1049 lines
46 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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>智能读证网页终端 &copy; 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>