创建第一个版本的工程,基本逻辑和显示界面已经完成
This commit is contained in:
369
remo_disp_ui.html
Normal file
369
remo_disp_ui.html
Normal file
@@ -0,0 +1,369 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user