260 lines
10 KiB
HTML
260 lines
10 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>手术室耗材 — 语音确认</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
panel: { DEFAULT: "#111827", muted: "#1f2937" },
|
||
},
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
<style>
|
||
@keyframes recBarWave {
|
||
0%,
|
||
100% {
|
||
transform: scaleY(0.22);
|
||
}
|
||
50% {
|
||
transform: scaleY(1);
|
||
}
|
||
}
|
||
@keyframes recPanelGlow {
|
||
0%,
|
||
100% {
|
||
box-shadow: 0 0 0 0 rgba(248, 113, 113, 0.2);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 20px 3px rgba(248, 113, 113, 0.35);
|
||
}
|
||
}
|
||
@keyframes recDotPulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.35;
|
||
}
|
||
}
|
||
.rec-banner-active {
|
||
animation: recPanelGlow 1.4s ease-in-out infinite;
|
||
}
|
||
.rec-banner-active .rec-wave-bar {
|
||
display: inline-block;
|
||
width: 0.45rem;
|
||
min-height: 2.5rem;
|
||
height: 2.5rem;
|
||
transform-origin: bottom center;
|
||
will-change: transform;
|
||
border-radius: 0.2rem;
|
||
animation: recBarWave 0.52s ease-in-out infinite;
|
||
}
|
||
.rec-banner-active .rec-wave-bar:nth-child(1) {
|
||
animation-delay: 0ms;
|
||
}
|
||
.rec-banner-active .rec-wave-bar:nth-child(2) {
|
||
animation-delay: 0.1s;
|
||
}
|
||
.rec-banner-active .rec-wave-bar:nth-child(3) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
.rec-banner-active .rec-wave-bar:nth-child(4) {
|
||
animation-delay: 0.05s;
|
||
}
|
||
.rec-banner-active .rec-wave-bar:nth-child(5) {
|
||
animation-delay: 0.28s;
|
||
}
|
||
.rec-banner-active .rec-wave-bar:nth-child(6) {
|
||
animation-delay: 0.15s;
|
||
}
|
||
.rec-banner-active .rec-wave-bar:nth-child(7) {
|
||
animation-delay: 0.08s;
|
||
}
|
||
.rec-dot {
|
||
animation: recDotPulse 0.9s ease-in-out infinite;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.rec-banner-active {
|
||
animation: none;
|
||
}
|
||
.rec-banner-active .rec-wave-bar {
|
||
animation: none;
|
||
transform: scaleY(0.65);
|
||
}
|
||
.rec-dot {
|
||
animation: none;
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen bg-slate-950 text-slate-100 antialiased">
|
||
<div class="mx-auto max-w-5xl p-4 md:p-6">
|
||
<header class="mb-6 border-b border-slate-800 pb-4">
|
||
<h1 id="pageTitle" class="text-xl font-semibold tracking-tight text-sky-300">
|
||
语音确认
|
||
</h1>
|
||
<p class="mt-1 text-sm text-slate-400">
|
||
独立网页客户端:仅 WebSocket 收队首、HTTP 上传答复。请通过 http(s) 服务打开本页(勿用
|
||
<code class="text-slate-500">file://</code>)。生产环境请为静态页与 API 配置 HTTPS / WSS。
|
||
</p>
|
||
</header>
|
||
|
||
<div class="grid gap-4 lg:grid-cols-2">
|
||
<section class="rounded-xl border border-slate-800 bg-slate-900/80 p-4 shadow-lg">
|
||
<h2 class="mb-3 text-sm font-medium uppercase tracking-wide text-sky-400">连接</h2>
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="mb-1 block text-xs text-slate-500" for="baseUrl">服务端 Base URL</label>
|
||
<input
|
||
id="baseUrl"
|
||
type="url"
|
||
class="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm text-slate-100 placeholder-slate-600 focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-500"
|
||
placeholder="http://127.0.0.1:38080"
|
||
autocomplete="off"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs text-slate-500" for="terminalId">本机语音终端 ID</label>
|
||
<input
|
||
id="terminalId"
|
||
type="text"
|
||
class="w-full rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm"
|
||
placeholder="与 OR_SITE_CONFIG 中 voice_terminal_id 一致"
|
||
autocomplete="off"
|
||
/>
|
||
</div>
|
||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||
<input id="autoAssign" type="checkbox" class="rounded border-slate-600 bg-slate-950 text-sky-500" />
|
||
启用服务端自动指派(开录后 WebSocket 连接并接收 <code class="text-xs">voice_assignment</code>)
|
||
</label>
|
||
<div class="flex flex-wrap items-end gap-3">
|
||
<div>
|
||
<label class="mb-1 block text-xs text-slate-500" for="recordSec"
|
||
>TTS 起同时开录;本值=播完后再多采的秒数</label
|
||
>
|
||
<input
|
||
id="recordSec"
|
||
type="number"
|
||
min="2"
|
||
max="60"
|
||
step="0.5"
|
||
value="5"
|
||
class="w-32 rounded-lg border border-slate-700 bg-slate-950 px-3 py-2 text-sm"
|
||
/>
|
||
</div>
|
||
<label class="flex cursor-pointer items-center gap-2 text-sm text-slate-400">
|
||
<input id="dryRun" type="checkbox" class="rounded border-slate-600 bg-slate-950" />
|
||
Dry-run:录音后不上传
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="rounded-xl border border-slate-800 bg-slate-900/80 p-4 shadow-lg">
|
||
<h2 class="mb-3 text-sm font-medium uppercase tracking-wide text-sky-400">操作</h2>
|
||
<p id="status" class="mb-3 rounded-lg bg-slate-950 px-3 py-2 font-mono text-sm text-amber-200/90">
|
||
待机
|
||
</p>
|
||
<div
|
||
id="recBanner"
|
||
class="rec-banner mb-3 hidden overflow-hidden rounded-lg border-2 border-red-500/80 bg-gradient-to-b from-red-950/95 to-red-900/80 px-3 py-4 text-center shadow-lg"
|
||
role="status"
|
||
aria-live="polite"
|
||
aria-label="正在录音"
|
||
>
|
||
<div
|
||
id="recWave"
|
||
class="mb-3 flex h-14 items-end justify-center gap-1.5 sm:gap-2.5"
|
||
aria-hidden="true"
|
||
>
|
||
<span class="rec-wave-bar bg-rose-300/95 shadow-sm"></span>
|
||
<span class="rec-wave-bar bg-rose-200 shadow-sm"></span>
|
||
<span class="rec-wave-bar bg-rose-100 shadow-sm"></span>
|
||
<span class="rec-wave-bar bg-rose-50/95 shadow-sm"></span>
|
||
<span class="rec-wave-bar bg-rose-100 shadow-sm"></span>
|
||
<span class="rec-wave-bar bg-rose-200 shadow-sm"></span>
|
||
<span class="rec-wave-bar bg-rose-300/95 shadow-sm"></span>
|
||
</div>
|
||
<p class="text-sm font-bold text-red-50">
|
||
<span class="rec-dot mr-0.5 inline-block text-rose-200" aria-hidden="true">●</span>
|
||
正在录音 — 请对着麦克风清晰作答
|
||
</p>
|
||
</div>
|
||
<div class="flex flex-wrap gap-2">
|
||
<button
|
||
type="button"
|
||
id="btnStop"
|
||
disabled
|
||
class="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-sm font-medium text-slate-200 hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||
>
|
||
停止监控(本机)
|
||
</button>
|
||
<button
|
||
type="button"
|
||
id="btnRetry"
|
||
class="rounded-lg bg-amber-600 px-3 py-2 text-sm font-medium text-slate-950 hover:bg-amber-500"
|
||
>
|
||
重试本轮
|
||
</button>
|
||
<button
|
||
type="button"
|
||
id="btnReplay"
|
||
class="rounded-lg bg-slate-700 px-3 py-2 text-sm text-slate-100 hover:bg-slate-600"
|
||
>
|
||
仅重播话术
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="mt-4 grid gap-3 lg:grid-cols-2">
|
||
<div class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-3">
|
||
<h3 class="text-xs font-medium text-slate-500">排队序号(当前 FIFO)</h3>
|
||
<p id="queuePositionHint" class="mt-1 text-sm font-medium text-emerald-300/95">—</p>
|
||
<h3 class="mt-3 text-xs font-medium text-slate-500">累积序号(本场入队)</h3>
|
||
<p id="cumulativeHint" class="mt-1 text-sm font-medium text-cyan-300/95">—</p>
|
||
<p class="mt-2 text-xs text-slate-500">
|
||
排队:<code class="text-slate-600">pending_queue_position</code> /
|
||
<code class="text-slate-600">pending_queue_length</code>;累积:<code class="text-slate-600">pending_cumulative_ordinal</code>(均由服务端在 <code class="text-slate-600">voice_pending</code> 与 GET 中下发)。
|
||
</p>
|
||
</div>
|
||
<div class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-3">
|
||
<h3 class="text-xs font-medium text-slate-500">服务端语音确认结果(最近一次 HTTP 响应)</h3>
|
||
<pre
|
||
id="resolveResult"
|
||
class="mt-2 max-h-36 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-300"
|
||
>—</pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-4 grid gap-4 lg:grid-cols-2">
|
||
<section class="min-h-[220px] rounded-xl border border-slate-800 bg-slate-900/50 p-3">
|
||
<h3 class="mb-2 text-xs font-medium text-slate-500">队首待确认(JSON)</h3>
|
||
<pre
|
||
id="pendingJson"
|
||
class="max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-300"
|
||
></pre>
|
||
</section>
|
||
<section class="min-h-[220px] rounded-xl border border-slate-800 bg-slate-900/50 p-3">
|
||
<h3 class="mb-2 text-xs font-medium text-slate-500">日志</h3>
|
||
<pre
|
||
id="log"
|
||
class="max-h-80 overflow-auto whitespace-pre-wrap font-mono text-xs text-slate-400"
|
||
></pre>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="voice_app.js" defer></script>
|
||
</body>
|
||
</html>
|