Files
DTU-RemtoeLCD/remo_disp_ui.html

370 lines
11 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>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>远程显示</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
background: linear-gradient(135deg, #e8f4fc 0%, #f0f8ff 50%, #e6f2ff 100%);
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;
aspect-ratio: 1;
max-height: 420px;
background: #1e3a5f;
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%;
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;
}
.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; }
.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>
<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.1.100">
<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>
const KEY_MAP = { U: 0x02, D: 0x40, L: 0x10, R: 0x08, ENT: 0x20, ESC: 0x01, RESET: 0x04 };
let apiBase = '';
let refreshTimer = null;
const canvas = document.getElementById('screen');
const placeholder = document.getElementById('placeholder');
const connectBtn = document.getElementById('btnConnect');
const connectModal = document.getElementById('connectModal');
const hostInput = document.getElementById('hostInput');
const portInput = document.getElementById('portInput');
const modalConnect = document.getElementById('modalConnect');
const modalCancel = document.getElementById('modalCancel');
function showConnectModal() {
connectModal.classList.add('show');
}
function hideConnectModal() {
connectModal.classList.remove('show');
}
connectBtn.onclick = showConnectModal;
modalCancel.onclick = hideConnectModal;
modalConnect.onclick = async () => {
const host = hostInput.value.trim();
const port = portInput.value || '7003';
try {
const r = await fetch(`/connect?host=${encodeURIComponent(host)}&port=${port}`);
const ok = await r.json();
if (ok.success) {
hideConnectModal();
connectBtn.classList.add('connected');
connectBtn.querySelector('span:last-child').textContent = '已连接';
apiBase = 'ok';
startRefresh();
} else {
alert('连接失败: ' + (ok.error || '未知错误'));
}
} catch (e) {
alert('连接失败,请先启动: python remo_disp_server.py');
}
};
async function sendKey(key) {
if (!connectBtn.classList.contains('connected')) return;
const code = KEY_MAP[key] ?? 0;
await fetch(`/key?code=${code}`);
}
async function fetchScreen() {
if (!connectBtn.classList.contains('connected')) return;
try {
const r = await fetch('/screen');
const ct = r.headers.get('Content-Type') || '';
const blob = await r.blob();
if (ct.includes('image/png')) {
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.style.display = 'block';
placeholder.style.display = 'none';
URL.revokeObjectURL(url);
};
img.src = url;
} else {
const buf = await blob.arrayBuffer();
const data = new Uint8Array(buf);
const w = parseInt(r.headers.get('X-Width') || '160', 10);
const h = parseInt(r.headers.get('X-Height') || '160', 10);
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
const id = ctx.createImageData(w, h);
for (let y = 0; y < h; y++)
for (let x = 0; x < w; x++) {
const bi = (y * w + x) >> 3, bit = 7 - (x & 7);
const v = (bi < data.length && (data[bi] >> bit) & 1) ? 255 : 0;
const i = (y * w + x) * 4;
id.data[i] = id.data[i+1] = id.data[i+2] = v;
id.data[i+3] = 255;
}
ctx.putImageData(id, 0, 0);
canvas.style.display = 'block';
placeholder.style.display = 'none';
}
} catch (e) {}
}
function startRefresh() {
fetchScreen();
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(fetchScreen, 500);
}
document.querySelectorAll('.dpad .btn, .action-btn').forEach(btn => {
btn.onclick = () => sendKey(btn.dataset.key);
});
document.querySelector('.menu-item.exit').onclick = () => sendKey('ESC');
document.getElementById('btnAbout').onclick = () => alert('远程显示工具\n与 RemoDispBus 协议兼容\n端口 7003');
</script>
</body>
</html>