715 lines
17 KiB
HTML
715 lines
17 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<title>NaviGlass 导盲系统可视化</title>
|
||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg: #0b0f14;
|
||
--card: #121821;
|
||
--text: #e6edf3;
|
||
--muted: #9fb0c3;
|
||
--ok: #7ee787;
|
||
--err: #ff8080;
|
||
--warn: #fbbf24;
|
||
--line: #1f2937;
|
||
--panel: rgba(18, 24, 33, .75);
|
||
--primary: #3b82f6;
|
||
--primary-glow: rgba(59, 130, 246, 0.4);
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background: linear-gradient(180deg, #0b0f14, #0b0f14 60%, #0e1621);
|
||
color: var(--text);
|
||
font: 16px/1.6 system-ui, -apple-system, Segoe UI, Roboto, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial;
|
||
}
|
||
|
||
/* 两栏布局 - Day 20 修复:防止滚动条 */
|
||
html,
|
||
body {
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.app {
|
||
display: grid;
|
||
grid-template-columns: 1fr 700px;
|
||
gap: 16px;
|
||
height: 100vh;
|
||
padding: 16px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stage {
|
||
position: relative;
|
||
border: 1px solid var(--line);
|
||
border-radius: 14px;
|
||
background: var(--card);
|
||
/* Day 20 修复: 统一使用 --card 背景色 */
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.canvas-wrap,
|
||
#canvas {
|
||
position: absolute;
|
||
inset: 1px;
|
||
/* Day 20 修复: 留出边框空间 */
|
||
width: calc(100% - 2px);
|
||
height: calc(100% - 2px);
|
||
display: block;
|
||
background: #0a1017;
|
||
/* 视频区域保持深色 */
|
||
border-radius: 13px;
|
||
/* 比外框小 1px */
|
||
z-index: 10
|
||
}
|
||
|
||
/* 右上角:参数 + 日志 */
|
||
.tri-panels {
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 12px;
|
||
z-index: 30;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
max-width: min(720px, 60vw);
|
||
}
|
||
|
||
.box {
|
||
border: 2px dashed rgba(255, 255, 255, .35);
|
||
border-radius: 12px;
|
||
padding: 10px;
|
||
background: var(--panel);
|
||
backdrop-filter: blur(8px) saturate(130%);
|
||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06);
|
||
}
|
||
|
||
.box h4 {
|
||
margin: 0 0 6px 0;
|
||
font-size: 12px;
|
||
color: #ffd769
|
||
}
|
||
|
||
.kv {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-top: 1px dashed rgba(255, 255, 255, .15);
|
||
padding-top: 6px;
|
||
margin-top: 6px;
|
||
color: var(--muted);
|
||
font-size: 12px
|
||
}
|
||
|
||
/* 左上角 IMU 浮窗 */
|
||
.imu-float {
|
||
position: absolute;
|
||
left: 12px;
|
||
top: 12px;
|
||
z-index: 35;
|
||
width: 600px;
|
||
/* 恢复合理尺寸 */
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-radius: 12px;
|
||
background: rgba(18, 24, 33, 0.9);
|
||
backdrop-filter: blur(12px) saturate(140%);
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||
padding: 12px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.imu-float:hover {
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
/* Day 20: 优化折叠状态 - 只显示标题和按钮 */
|
||
.imu-float.collapsed {
|
||
width: 180px;
|
||
height: 40px;
|
||
padding: 8px 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.imu-float.collapsed .imu-header {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.imu-float.collapsed .imu-row,
|
||
.imu-float.collapsed #imu_top_status {
|
||
display: none !important;
|
||
}
|
||
|
||
.imu-toggle {
|
||
position: absolute;
|
||
top: 4px;
|
||
right: 8px;
|
||
z-index: 10;
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, .1);
|
||
border: none;
|
||
cursor: pointer;
|
||
color: var(--muted);
|
||
font-size: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.imu-toggle:hover {
|
||
background: rgba(255, 255, 255, .2);
|
||
color: var(--text);
|
||
}
|
||
|
||
.imu-row {
|
||
display: grid;
|
||
grid-template-columns: 1.5fr 1fr;
|
||
/* 左侧 3D 模型稍大 */
|
||
gap: 12px;
|
||
align-items: stretch;
|
||
min-height: 200px;
|
||
}
|
||
|
||
#imu_view {
|
||
position: relative;
|
||
min-height: 200px;
|
||
background: #0a1017;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
#imu_hud {
|
||
border-radius: 10px;
|
||
background: rgba(18, 24, 33, 0.8);
|
||
border: none;
|
||
padding: 8px;
|
||
overflow: visible;
|
||
/* Day 20: 允许内容自然显示 */
|
||
min-width: 150px;
|
||
/* Day 20: 缩小最小宽度 */
|
||
max-height: 300px;
|
||
}
|
||
|
||
#imu_hud::-webkit-scrollbar {
|
||
width: 8px;
|
||
/* 滚动条的宽度 */
|
||
}
|
||
|
||
#imu_hud::-webkit-scrollbar-thumb {
|
||
background-color: #2a6df4;
|
||
/* 滑块的颜色 */
|
||
border-radius: 4px;
|
||
/* 滑块的圆角 */
|
||
}
|
||
|
||
#imu_hud::-webkit-scrollbar-track {
|
||
background-color: #111a2e;
|
||
/* 滚动条轨道的颜色 */
|
||
border-radius: 4px;
|
||
/* 滚动条轨道的圆角 */
|
||
}
|
||
|
||
#imu_top_status {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
z-index: 2
|
||
}
|
||
|
||
#imu_top_status .badge {
|
||
background: rgba(0, 0, 0, .35);
|
||
border-color: #2d3b50
|
||
}
|
||
|
||
/* 角标 + 当前指令(角标在浮窗下) */
|
||
.badge-tag {
|
||
position: absolute;
|
||
left: 12px;
|
||
top: calc(12px + 300px + 28px);
|
||
z-index: 20;
|
||
background: linear-gradient(120deg, rgba(47, 134, 255, .9), rgba(26, 88, 255, .9));
|
||
color: #fff;
|
||
padding: 6px 10px;
|
||
font-weight: 700;
|
||
font-size: 12px;
|
||
border-radius: 999px;
|
||
box-shadow: 0 6px 16px rgba(47, 134, 255, .35)
|
||
}
|
||
|
||
.command {
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 46px;
|
||
transform: translateX(-50%);
|
||
z-index: 25;
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
padding: 12px 18px;
|
||
border-radius: 999px;
|
||
background: rgba(18, 24, 33, .75);
|
||
border: 1px solid rgba(255, 255, 255, .14);
|
||
backdrop-filter: blur(8px) saturate(140%);
|
||
box-shadow: 0 12px 28px rgba(0, 0, 0, .6)
|
||
}
|
||
|
||
.dot {
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
background: #2f86ff;
|
||
box-shadow: 0 0 16px #2f86ff
|
||
}
|
||
|
||
/* 右侧聊天(左右气泡)- Day 20 修复:移除 height:100vh */
|
||
.chat {
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid var(--line);
|
||
border-radius: 14px;
|
||
background: var(--card);
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||
overflow: hidden;
|
||
/* 不再使用 height:100vh,让 grid 自动分配高度 */
|
||
}
|
||
|
||
.chat-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--line)
|
||
}
|
||
|
||
.badges {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap
|
||
}
|
||
|
||
/* Day 20: 美化状态 badge */
|
||
.badge {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
padding: 5px 10px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, .1);
|
||
color: var(--muted);
|
||
background: rgba(255, 255, 255, .05);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.badge::before {
|
||
content: '';
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.badge.ok {
|
||
color: var(--ok);
|
||
border-color: rgba(126, 231, 135, .3);
|
||
background: rgba(126, 231, 135, .1);
|
||
}
|
||
|
||
.badge.ok::before {
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.badge.err {
|
||
color: var(--err);
|
||
border-color: rgba(255, 128, 128, .3);
|
||
background: rgba(255, 128, 128, .1);
|
||
}
|
||
|
||
.badge.connecting {
|
||
color: var(--warn);
|
||
border-color: rgba(251, 191, 36, .3);
|
||
background: rgba(251, 191, 36, .1);
|
||
}
|
||
|
||
.badge.connecting::before {
|
||
animation: blink 1s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
|
||
50% {
|
||
opacity: .6;
|
||
transform: scale(1.2);
|
||
}
|
||
}
|
||
|
||
@keyframes blink {
|
||
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
|
||
50% {
|
||
opacity: .2;
|
||
}
|
||
}
|
||
|
||
.chat-list {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
min-height: 0;
|
||
/* 确保 .chat-list 不会被外部内容撑大 */
|
||
}
|
||
|
||
.live {
|
||
padding: 12px;
|
||
border: 1px dashed #2d3b50;
|
||
border-radius: 12px;
|
||
background: #0c121a
|
||
}
|
||
|
||
.live h2 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 14px;
|
||
color: var(--muted);
|
||
font-weight: 600
|
||
}
|
||
|
||
.partial {
|
||
font-size: 20px;
|
||
min-height: 2.2em;
|
||
letter-spacing: .2px
|
||
}
|
||
|
||
.finals {
|
||
padding: 12px;
|
||
border: 1px solid #1f2937;
|
||
border-radius: 12px;
|
||
background: #0c121a;
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
/* 使用 flex: 1 而不是 flex-grow: 1 */
|
||
min-height: 0;
|
||
/* 允许收缩到小于内容高度 */
|
||
}
|
||
|
||
.finals h2 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 14px;
|
||
color: var(--muted);
|
||
font-weight: 600
|
||
}
|
||
|
||
.finals ul {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px
|
||
}
|
||
|
||
.bubble {
|
||
max-width: 82%;
|
||
padding: 10px 12px;
|
||
border-radius: 14px;
|
||
border: 1px solid #1f2937
|
||
}
|
||
|
||
.from-bot {
|
||
align-self: flex-start;
|
||
background: #0d1729
|
||
}
|
||
|
||
.from-me {
|
||
align-self: flex-end;
|
||
background: #12263a
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 8px
|
||
}
|
||
|
||
button {
|
||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||
border: none;
|
||
color: #fff;
|
||
border-radius: 10px;
|
||
padding: 8px 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
box-shadow: 0 2px 8px rgba(37, 99, 235, .3);
|
||
}
|
||
|
||
button:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(37, 99, 235, .4);
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.ghost {
|
||
background: transparent;
|
||
border: 1px solid #304057;
|
||
color: #9fb0c3;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.ghost:hover {
|
||
background: rgba(255, 255, 255, .05);
|
||
border-color: #405570;
|
||
}
|
||
|
||
/* 隐藏但保留:校准/滑杆(供 main.js 使用) */
|
||
.hidden-controls {
|
||
display: none
|
||
}
|
||
|
||
/* Day 20: 改善移动端适配 */
|
||
@media (max-width:1100px) {
|
||
.app {
|
||
grid-template-columns: 1fr;
|
||
height: auto;
|
||
padding: 8px;
|
||
overflow: visible;
|
||
}
|
||
|
||
/* 关键修复:确保视频区域有足够高度 */
|
||
.stage {
|
||
min-height: 50vh;
|
||
height: 50vh;
|
||
position: relative;
|
||
}
|
||
|
||
.chat {
|
||
height: 50vh;
|
||
min-height: 400px;
|
||
}
|
||
|
||
/* 移动端 IMU 浮窗缩小并移到右下角 */
|
||
.imu-float {
|
||
width: 160px;
|
||
left: auto;
|
||
right: 8px;
|
||
top: auto;
|
||
bottom: 8px;
|
||
padding: 8px;
|
||
}
|
||
|
||
/* 移动端默认折叠状态 */
|
||
.imu-float:not(.expanded) {
|
||
width: 160px;
|
||
height: 40px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.imu-float:not(.expanded) .imu-row,
|
||
.imu-float:not(.expanded) #imu_top_status {
|
||
display: none !important;
|
||
}
|
||
|
||
.imu-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
#imu_view {
|
||
min-height: 200px;
|
||
}
|
||
}
|
||
|
||
@media (max-width:600px) {
|
||
.badges {
|
||
gap: 4px;
|
||
}
|
||
|
||
.badge {
|
||
font-size: 10px;
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
.controls {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 手机端视频区域更大 */
|
||
.stage {
|
||
min-height: 45vh;
|
||
height: 45vh;
|
||
}
|
||
|
||
.chat {
|
||
height: 45vh;
|
||
min-height: 300px;
|
||
}
|
||
|
||
/* 手机端 IMU 浮窗始终小巧 */
|
||
.imu-float {
|
||
width: 140px;
|
||
right: 6px;
|
||
bottom: 6px;
|
||
left: auto;
|
||
top: auto;
|
||
padding: 6px;
|
||
}
|
||
|
||
.imu-float.collapsed,
|
||
.imu-float:not(.expanded) {
|
||
width: 140px;
|
||
height: 36px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<div class="app">
|
||
<!-- 左侧主舞台 -->
|
||
<section class="stage">
|
||
<div class="canvas-wrap"><canvas id="canvas"></canvas></div>
|
||
|
||
<!-- 左上角:IMU 浮窗(横向)Day 20 优化 -->
|
||
<div class="imu-float" id="imuFloat">
|
||
<button class="imu-toggle" id="imuToggle" title="折叠/展开">−</button>
|
||
<div class="imu-header" style="font-size:12px;color:#61dafb;margin-bottom:8px;font-weight:600;">📊 IMU 姿态可视化
|
||
</div>
|
||
<div class="imu-row">
|
||
<div id="imu_view"></div>
|
||
<div id="imu_hud"><!-- JS 会把“IMU 实时数据面板”插到这里 --></div>
|
||
</div>
|
||
<div id="imu_top_status" style="display: none;">
|
||
<span class="badge">UDP: <code>12345</code></span>
|
||
<span class="badge">Browser WS: <code>/ws</code></span>
|
||
<span class="badge" id="imu_ws_state">connecting…</span>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
</section>
|
||
|
||
<!-- 右侧聊天 -->
|
||
<aside class="chat">
|
||
<div class="chat-head">
|
||
<div class="badges">
|
||
<span id="camStatus" class="badge">Camera: connecting…</span>
|
||
<span id="asrStatus" class="badge">ASR: connecting…</span>
|
||
<span id="fps" class="badge">FPS: --</span>
|
||
</div>
|
||
<div class="controls">
|
||
<button class="ghost" id="btnClear">清空 Final</button>
|
||
<button id="btnReconnect">重连</button>
|
||
</div>
|
||
</div>
|
||
<div class="chat-list">
|
||
<div class="live">
|
||
<h2>流式识别(Partial)</h2>
|
||
<div class="partial" id="partial">(等待音频…)</div>
|
||
</div>
|
||
<div class="finals">
|
||
<h2>最终文本(Final)</h2>
|
||
<ul id="finalList"></ul>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<!-- 隐藏但保留:校准/滑杆(供 main.js 使用) -->
|
||
<div class="hidden-controls">
|
||
<button id="btn_zero"></button><button id="btn_reset"></button><button id="btn_bias_now"></button>
|
||
<input id="auto_rezero" type="checkbox" checked /><input id="auto_bias" type="checkbox" checked />
|
||
<input id="use_proj" type="checkbox" checked /><input id="freeze_still" type="checkbox" checked />
|
||
<select id="medn">
|
||
<option>3</option>
|
||
<option selected>5</option>
|
||
<option>7</option>
|
||
</select>
|
||
<select id="ang_ema">
|
||
<option value="0">0</option>
|
||
<option value="0.15" selected>0.15</option>
|
||
<option value="0.30">0.30</option>
|
||
<option value="0.5">0.5</option>
|
||
</select>
|
||
<select id="grav_beta">
|
||
<option value="0.95">0.95</option>
|
||
<option value="0.97">0.97</option>
|
||
<option value="0.98" selected>0.98</option>
|
||
<option value="0.99">0.99</option>
|
||
</select>
|
||
<select id="yaw_db">
|
||
<option value="0.05">0.05</option>
|
||
<option value="0.08" selected>0.08</option>
|
||
<option value="0.15">0.15</option>
|
||
<option value="0.30">0.30</option>
|
||
</select>
|
||
<select id="still_w">
|
||
<option value="0.4" selected>0.4</option>
|
||
<option value="0.6">0.6</option>
|
||
<option value="1.0">1.0</option>
|
||
</select>
|
||
<select id="yaw_leak">
|
||
<option value="0">0</option>
|
||
<option value="0.1">0.1</option>
|
||
<option value="0.2" selected>0.2</option>
|
||
<option value="0.5">0.5</option>
|
||
</select>
|
||
<input id="roll_sl" type="range"><span id="roll_val"></span>
|
||
<input id="pitch_sl" type="range"><span id="pitch_val"></span>
|
||
<input id="yaw_sl" type="range"><span id="yaw_val"></span>
|
||
<input id="gx_sl" type="range"><span id="gx_val"></span>
|
||
<input id="gy_sl" type="range"><span id="gy_val"></span>
|
||
<input id="gz_sl" type="range"><span id="gz_val"></span>
|
||
<input id="ax_sl" type="range"><span id="ax_val"></span>
|
||
<input id="ay_sl" type="range"><span id="ay_val"></span>
|
||
<input id="az_sl" type="range"><span id="az_val"></span>
|
||
</div>
|
||
|
||
<!-- three.js importmap -->
|
||
<script type="importmap">{
|
||
"imports": { "three": "https://unpkg.com/three@0.155.0/build/three.module.js" }
|
||
}</script>
|
||
|
||
<!-- 主脚本 -->
|
||
<script type="module" src="/static/main.js"></script>
|
||
</body>
|
||
|
||
</html> |