Files
DTU-RemtoeLCD/remo_disp_ui.html

478 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<!-- 文档类型声明HTML5 -->
<html lang="zh-CN">
<!-- 根元素,语言设为简体中文 -->
<head>
<!-- 头部:元数据与资源 -->
<meta charset="UTF-8">
<!-- 字符编码UTF-8 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 视口:适配移动端,初始缩放 1 -->
<title>远程显示</title>
<!-- 页面标题 -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<!-- 网站图标 -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<!-- Socket.IO 客户端库,用于 WebSocket 通信 -->
<style>
/* ========== 全局样式 ========== */
* { box-sizing: border-box; margin: 0; padding: 0; }
/* 通配符:盒模型为 border-box去除默认边距 */
body {
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
/* 字体:微软雅黑、苹方,无衬线回退 */
background: linear-gradient(135deg, #e8f4fc 0%, #f0f8ff 50%, #e6f2ff 100%);
/* 背景135 度渐变,浅蓝到淡蓝 */
min-height: 100vh;
/* 最小高度为视口高度 */
padding: 16px;
/* 内边距 */
}
/* ========== 主容器 ========== */
.app {
max-width: 900px;
/* 最大宽度限制 */
margin: 0 auto;
/* 水平居中 */
background: #fff;
/* 白色背景 */
border-radius: 12px;
/* 圆角 */
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
/* 阴影 */
overflow: hidden;
/* 子元素超出则裁剪 */
}
/* ========== 顶部菜单栏 ========== */
.menu-bar {
display: flex;
/* 弹性布局 */
align-items: center;
/* 垂直居中 */
justify-content: center;
/* 水平居中 */
gap: 8px;
/* 子元素间距 */
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #e8e8e8;
/* 底部分割线 */
}
.menu-item {
display: flex;
flex-direction: column;
/* 纵向排列图标和文字 */
align-items: center;
gap: 4px;
padding: 8px 14px;
border: none;
background: transparent;
cursor: pointer;
/* 鼠标指针为手型 */
border-radius: 8px;
transition: all 0.2s;
/* 过渡动画 */
color: #333;
font-size: 12px;
}
.menu-item:hover {
background: #f5f5f5;
/* 悬停时浅灰背景 */
}
.menu-item .icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
/* 图标区域 */
}
.menu-item.connect .icon { color: #22c55e; }
/* 连接按钮图标:绿色 */
.menu-item.settings .icon { color: #3b82f6; }
/* 设置按钮图标:蓝色 */
.menu-item.exit .icon { color: #ef4444; }
/* 断开按钮图标:红色 */
.menu-item.about .icon { color: #64748b; }
/* 关于按钮图标:灰色 */
.menu-item.connected .icon { color: #22c55e; }
.menu-item.connected { color: #22c55e; }
/* 已连接状态:绿色 */
/* ========== 主内容区 ========== */
.main {
display: flex;
padding: 16px;
gap: 20px;
/* 左右分栏间距 */
}
/* ========== 显示区域 ========== */
.display-area {
flex: 1;
/* 占据剩余空间 */
min-width: 0;
/* 允许 flex 子项收缩 */
aspect-ratio: 1;
/* 宽高比 1:1 */
max-height: 420px;
background: #1e3a5f;
/* 深蓝背景,模拟 LCD 边框 */
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: inset 0 2px 8px rgba(0,0,0,0.2);
/* 内阴影 */
}
.display-area canvas {
max-width: 100%;
max-height: 100%;
/* canvas 自适应容器 */
image-rendering: pixelated;
image-rendering: crisp-edges;
/* 像素风格渲染,避免模糊 */
}
.display-placeholder {
color: rgba(255,255,255,0.5);
/* 半透明白色提示文字 */
font-size: 14px;
}
/* ========== 右侧控制面板 ========== */
.control-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 8px;
}
/* ========== D-pad 方向键 ========== */
.dpad {
display: grid;
grid-template-columns: 50px 50px 50px;
grid-template-rows: 50px 50px 50px;
gap: 4px;
/* 3x3 网格布局 */
}
.dpad .btn {
width: 50px;
height: 50px;
border: none;
border-radius: 8px;
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
/* 渐变背景 */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #22c55e;
transition: all 0.15s;
}
.dpad .btn:hover {
background: linear-gradient(180deg, #fff 0%, #f1f3f5 100%);
box-shadow: 0 3px 6px rgba(0,0,0,0.15);
transform: translateY(-1px);
/* 悬停上浮效果 */
}
.dpad .btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
/* 按下时恢复 */
}
.dpad .btn-up { grid-column: 2; grid-row: 1; }
/* 上键:中上 */
.dpad .btn-down { grid-column: 2; grid-row: 3; }
/* 下键:中下 */
.dpad .btn-left { grid-column: 1; grid-row: 2; }
/* 左键:左中 */
.dpad .btn-right { grid-column: 3; grid-row: 2; }
/* 右键:右中 */
.dpad .btn-ok { grid-column: 2; grid-row: 2; }
/* 确认键:正中 */
/* ========== 动作按钮 ========== */
.action-btns {
display: flex;
gap: 12px;
}
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 18px;
border: none;
border-radius: 8px;
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: pointer;
font-size: 14px;
color: #333;
transition: all 0.15s;
}
.action-btn:hover {
background: linear-gradient(180deg, #fff 0%, #f1f3f5 100%);
box-shadow: 0 3px 6px rgba(0,0,0,0.15);
transform: translateY(-1px);
}
.action-btn .icon { color: #22c55e; font-size: 16px; }
/* ========== 连接设置弹窗 ========== */
.modal {
display: none;
/* 默认隐藏 */
position: fixed;
inset: 0;
/* 全屏覆盖 */
background: rgba(0,0,0,0.4);
/* 半透明黑色遮罩 */
align-items: center;
justify-content: center;
z-index: 100;
}
.modal.show { display: flex; }
/* 显示时用 flex 居中 */
.modal-content {
background: #fff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
min-width: 320px;
}
.modal-content h3 { margin-bottom: 16px; font-size: 16px; }
.modal-content input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
margin-bottom: 12px;
font-size: 14px;
}
.modal-content .btn-row {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 16px;
}
.modal-content button {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.modal-content .btn-primary {
background: #22c55e;
color: #fff;
/* 主按钮:绿色 */
}
.modal-content .btn-secondary {
background: #e5e7eb;
color: #333;
/* 次要按钮:灰色 */
}
</style>
</head>
<body>
<!-- 页面主体 -->
<div class="app">
<!-- 主应用容器 -->
<div class="menu-bar">
<!-- 顶部菜单栏 -->
<button class="menu-item connect" id="btnConnect" title="连接">
<span class="icon">📶</span>
<span>连接</span>
</button>
<button class="menu-item settings" title="设置">
<span class="icon"></span>
<span>设置</span>
</button>
<button class="menu-item exit" title="断开">
<span class="icon"></span>
<span>断开</span>
</button>
<button class="menu-item about" id="btnAbout" title="关于">
<span class="icon"></span>
<span>关于</span>
</button>
</div>
<div class="main">
<!-- 主内容区:显示 + 控制 -->
<div class="display-area">
<!-- 屏幕显示区域 -->
<canvas id="screen" style="display:none;"></canvas>
<!-- canvas 初始隐藏,有画面后显示 -->
<span class="display-placeholder" id="placeholder">点击「连接」连接装置</span>
<!-- 未连接时的提示文字 -->
</div>
<div class="control-panel">
<!-- 右侧控制面板 -->
<div class="dpad">
<!-- 方向键 + 确认 -->
<button class="btn btn-up" data-key="U"></button>
<button class="btn btn-left" data-key="L"></button>
<button class="btn btn-ok" data-key="ENT"></button>
<button class="btn btn-right" data-key="R"></button>
<button class="btn btn-down" data-key="D"></button>
</div>
<div class="action-btns">
<!-- 复归、返回按钮 -->
<button class="action-btn" data-key="RESET">
<span class="icon"></span>
<span>复归</span>
</button>
<button class="action-btn" data-key="ESC">
<span class="icon"></span>
<span>返回</span>
</button>
</div>
</div>
</div>
</div>
<!-- 连接弹窗 -->
<div class="modal" id="connectModal">
<div class="modal-content">
<h3>连接装置</h3>
<input type="text" id="hostInput" placeholder="装置 IP 地址" value="192.168.253.3">
<input type="number" id="portInput" placeholder="端口" value="7003">
<div class="btn-row">
<button class="btn-secondary" id="modalCancel">取消</button>
<button class="btn-primary" id="modalConnect">连接</button>
</div>
</div>
</div>
<script>
/**
* 前端逻辑:通过 Socket.IO 与后端 WebSocket 通信。
* - connect_device: 连接装置,成功后后端持续推送 screen 事件
* - disconnect_device: 断开装置
* - key: 发送按键码
*/
// ========== 常量与 DOM 引用 ==========
// 按键名 -> RemoDispBus 协议码U/D/L/R 方向ENT 确认ESC 返回RESET 复归
const KEY_MAP = { U: 0x02, D: 0x40, L: 0x10, R: 0x08, ENT: 0x20, ESC: 0x01, RESET: 0x04 };
const canvas = document.getElementById('screen');
// canvas 元素,用于绘制远程屏幕
const placeholder = document.getElementById('placeholder');
// 占位提示文字
const connectBtn = document.getElementById('btnConnect');
// 连接按钮
const connectModal = document.getElementById('connectModal');
// 连接弹窗
const hostInput = document.getElementById('hostInput');
// IP 输入框
const portInput = document.getElementById('portInput');
// 端口输入框
const modalConnect = document.getElementById('modalConnect');
// 弹窗内「连接」按钮
const modalCancel = document.getElementById('modalCancel');
// 弹窗内「取消」按钮
// ========== WebSocket 连接 ==========
const socket = io();
// 连接当前页面的 origin建立 Socket.IO 连接
socket.on('connect', () => {});
// 连接成功时(可留空)
socket.on('connect_result', (data) => {
// 收到后端连接结果
if (data.success) {
hideConnectModal();
// 关闭弹窗
connectBtn.classList.add('connected');
// 标记为已连接状态
connectBtn.querySelector('span:last-child').textContent = '已连接';
// 更新按钮文字
} else {
alert('连接失败: ' + (data.error || '未知错误'));
// 弹出错误提示
}
});
// 服务端推送屏幕data.png 为 base64 编码的 PNG解码后绘制到 canvas
socket.on('screen', (data) => {
if (!data.png) return;
// 无 base64 数据则跳过
const binary = atob(data.png);
// base64 解码为二进制字符串
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
// 转为 Uint8Array
const blob = new Blob([bytes], { type: 'image/png' });
// 创建 PNG Blob
const url = URL.createObjectURL(blob);
// 创建临时 URL
const img = new Image();
img.onload = () => {
// 图片加载完成后放大一倍显示160x160 -> 320x320
const scale = 2;
canvas.width = img.width * scale;
canvas.height = img.height * scale;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// 关闭平滑,像素清晰放大
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
// 绘制到 canvas2 倍尺寸)
canvas.style.display = 'block';
placeholder.style.display = 'none';
// 显示 canvas隐藏占位符
URL.revokeObjectURL(url);
// 释放临时 URL
};
img.src = url;
// 触发加载
});
socket.on('disconnect_result', () => {});
// 收到断开结果(可留空)
function showConnectModal() { connectModal.classList.add('show'); }
// 显示连接弹窗
function hideConnectModal() { connectModal.classList.remove('show'); }
// 隐藏连接弹窗
connectBtn.onclick = showConnectModal;
// 点击连接按钮 -> 显示弹窗
modalCancel.onclick = hideConnectModal;
// 点击取消 -> 隐藏弹窗
modalConnect.onclick = () => {
// 点击弹窗内「连接」按钮
const host = hostInput.value.trim();
// 获取 IP去空格
const port = portInput.value || '7003';
// 获取端口,缺省 7003
socket.emit('connect_device', { host, port });
// 向后端发送连接请求
};
function sendKey(key) {
// 发送按键到后端
if (!connectBtn.classList.contains('connected')) return;
// 未连接则不发送
socket.emit('key', { code: KEY_MAP[key] ?? 0 });
// 根据按键名查协议码并发送
}
function doDisconnect() {
// 执行断开
socket.emit('disconnect_device');
// 通知后端断开设备
connectBtn.classList.remove('connected');
connectBtn.querySelector('span:last-child').textContent = '连接';
// 恢复按钮状态和文字
canvas.style.display = 'none';
placeholder.style.display = '';
placeholder.textContent = '点击「连接」连接装置';
// 隐藏 canvas显示占位符
}
document.querySelectorAll('.dpad .btn, .action-btn').forEach(btn => {
// 为所有方向键和动作按钮绑定点击
btn.onclick = () => sendKey(btn.dataset.key);
// 点击时发送对应按键码
});
document.querySelector('.menu-item.exit').onclick = doDisconnect;
// 断开按钮 -> 执行断开
document.getElementById('btnAbout').onclick = () => alert('阜阳师范大学物理与电子工程学院\nDTU 远程液晶屏幕通信工具');
// 关于按钮 -> 弹出说明
</script>
</body>
</html>