Files
operating-room-monitor-server/clients/voice-confirmation/index.html

360 lines
15 KiB
HTML
Raw Normal View History

2026-05-21 15:48:03 +08:00
<!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: {
clinical: {
fg: "#0f172a",
muted: "#475569",
border: "#e2e8f0",
accent: "#1d4ed8",
},
},
},
},
};
</script>
<style>
@keyframes activityBarWave {
0%,
100% {
transform: scaleY(0.22);
}
50% {
transform: scaleY(1);
}
}
@keyframes activityDotPulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.activity-banner-active .activity-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: activityBarWave 0.52s ease-in-out infinite;
}
.activity-banner-active .activity-wave-bar:nth-child(1) {
animation-delay: 0ms;
}
.activity-banner-active .activity-wave-bar:nth-child(2) {
animation-delay: 0.1s;
}
.activity-banner-active .activity-wave-bar:nth-child(3) {
animation-delay: 0.2s;
}
.activity-banner-active .activity-wave-bar:nth-child(4) {
animation-delay: 0.05s;
}
.activity-banner-active .activity-wave-bar:nth-child(5) {
animation-delay: 0.28s;
}
.activity-banner-active .activity-wave-bar:nth-child(6) {
animation-delay: 0.15s;
}
.activity-banner-active .activity-wave-bar:nth-child(7) {
animation-delay: 0.08s;
}
.activity-dot {
animation: activityDotPulse 0.9s ease-in-out infinite;
}
#advancedPanel summary {
list-style: none;
}
#advancedPanel summary::-webkit-details-marker {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.activity-banner-active .activity-wave-bar {
animation: none;
transform: scaleY(0.65);
}
.activity-dot {
animation: none;
opacity: 1;
}
}
</style>
</head>
<body class="min-h-screen bg-slate-50 text-slate-900 antialiased">
<div class="mx-auto max-w-xl md:max-w-2xl px-4 py-6 md:px-8 md:py-10">
<header class="mb-8 border-b border-slate-200 pb-6">
<h1 id="pageTitle" class="text-xl font-semibold text-slate-900">语音确认</h1>
<p id="surgeryIdLine" class="mt-2 hidden text-base text-slate-600">
当前手术号:<span id="surgeryIdValue" class="font-medium text-slate-900"></span>
</p>
</header>
<main class="space-y-6">
<section class="space-y-4" aria-labelledby="status-heading">
<h2 id="status-heading" class="sr-only">当前状态</h2>
<p
id="statusClinical"
class="text-2xl font-semibold leading-snug text-slate-900 md:text-3xl"
role="status"
aria-live="polite"
>
等待下一个确认
</p>
<p id="queueClinical" class="text-sm leading-relaxed text-slate-600">暂无排队信息</p>
<div
id="recBanner"
class="activity-banner mb-3 hidden overflow-hidden rounded-xl border border-red-200 bg-red-50 px-4 py-4 text-center"
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"
aria-hidden="true"
>
<span class="activity-wave-bar bg-red-400/90"></span>
<span class="activity-wave-bar bg-red-400"></span>
<span class="activity-wave-bar bg-red-300"></span>
<span class="activity-wave-bar bg-red-200"></span>
<span class="activity-wave-bar bg-red-300"></span>
<span class="activity-wave-bar bg-red-400"></span>
<span class="activity-wave-bar bg-red-400/90"></span>
</div>
<p id="activityTitle" class="text-sm font-semibold text-red-900">
<span id="activityDot" class="activity-dot mr-1 inline-block text-red-600" aria-hidden="true"></span>
正在采集语音
</p>
<p id="activityDesc" class="mt-1 text-xs leading-relaxed text-red-800">
请对着麦克风清楚回答
</p>
</div>
<div
id="resultClinical"
class="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm leading-relaxed text-slate-700 shadow-sm"
role="region"
aria-label="最近一次确认结果"
>
尚无确认结果
</div>
<div class="flex flex-wrap gap-3 pt-1">
<button
type="button"
id="btnRetry"
class="min-h-11 rounded-lg bg-blue-700 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
>
重试
</button>
<button
type="button"
id="btnReplay"
class="min-h-11 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-800 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
>
重播提示
</button>
</div>
</section>
<details
id="advancedPanel"
class="rounded-xl border border-slate-200 bg-white shadow-sm"
>
<summary
class="cursor-pointer select-none px-4 py-3 text-sm font-medium text-slate-800 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 rounded-xl"
>
高级设置(运维 / 调试)
</summary>
<div class="space-y-5 border-t border-slate-200 px-4 py-4">
<p class="text-sm leading-relaxed text-slate-600">
以下供信息科或现场调试使用。术间日常使用请保持折叠。页面需用
<code class="text-xs text-slate-700">http://</code>
<code class="text-xs text-slate-700">https://</code> 打开(勿用本地文件协议);语音接口端口常与页面不同(例如页面
8080、接口 38080
</p>
<div>
<span class="text-xs font-medium uppercase tracking-wide text-slate-500"
>原始状态(调试)</span
>
<p
id="statusRaw"
class="mt-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800"
>
待机
</p>
</div>
<section>
<h3 class="mb-3 text-sm font-medium text-slate-800">连接</h3>
<div class="space-y-3">
<div>
<label class="mb-1 block text-xs font-medium text-slate-600" for="baseUrl"
>服务端 Base URL</label
>
<input
id="baseUrl"
type="url"
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder-slate-400 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600"
placeholder="http://192.168.1.100:38080"
autocomplete="off"
/>
<p class="mt-1 text-xs leading-relaxed text-slate-500">
填监控 API 的局域网地址;若本页是 :8080Base URL 通常应为同一主机的 :38080。
</p>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-slate-600" for="terminalId"
>本机语音终端 ID</label
>
<input
id="terminalId"
type="text"
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
placeholder="与站点配置中 voice_terminal_id 一致"
autocomplete="off"
/>
</div>
<label class="flex cursor-pointer items-start gap-2 text-sm text-slate-700">
<input
id="autoAssign"
type="checkbox"
class="mt-1 rounded border-slate-400 text-blue-700"
/>
<span
>启用服务端自动指派(录制开始后通过长连接接收指派,
<code class="text-xs text-slate-600">voice_assignment</code></span
>
</label>
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">
<label class="mb-1 block text-xs font-medium text-slate-600" for="manualSurgeryId"
>手动手术号6 位,未开录或未指派时)</label
>
<div class="flex flex-wrap gap-2">
<input
id="manualSurgeryId"
type="text"
inputmode="numeric"
maxlength="6"
class="w-36 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm tabular-nums text-slate-900"
placeholder="123456"
autocomplete="off"
/>
<button
type="button"
id="btnStartManual"
class="rounded-lg bg-blue-700 px-3 py-2 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
>
开始监控(手动)
</button>
</div>
<p class="mt-2 text-xs text-slate-500">
可与自动指派同时使用;若长连接暂无数据,会定时请求队首作为补充。
</p>
</div>
<div class="flex flex-wrap items-end gap-4">
<div>
<label class="mb-1 block text-xs font-medium text-slate-600" for="recordSec"
>TTS 播报结束后录制回答的秒数(默认 8</label
>
<input
id="recordSec"
type="number"
min="0"
max="60"
step="0.5"
value="8"
class="w-32 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm"
/>
</div>
<label class="flex cursor-pointer items-center gap-2 text-sm text-slate-600">
<input id="dryRun" type="checkbox" class="rounded border-slate-400" />
调试:只录音,不上传
</label>
</div>
</div>
</section>
<section>
<h3 class="mb-3 text-sm font-medium text-slate-800">本机操作</h3>
<div class="flex flex-wrap gap-2">
<button
type="button"
id="btnStop"
disabled
class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-800 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
>
停止监控(本机)
</button>
</div>
</section>
<div class="grid gap-4 sm:grid-cols-2">
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3">
<h3 class="text-xs font-medium text-slate-500">排队(原始字段)</h3>
<p id="queuePositionHint" class="mt-1 text-sm font-medium text-slate-800">无待确认</p>
<h3 class="mt-3 text-xs font-medium text-slate-500">本场累积序号</h3>
<p id="cumulativeHint" class="mt-1 text-sm font-medium text-slate-800">无待确认</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>
</p>
</div>
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3">
<h3 class="text-xs font-medium text-slate-500">最近接口响应JSON</h3>
<pre
id="resolveResult"
class="mt-2 max-h-36 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-700"
>—</pre>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<section class="min-h-[180px] rounded-lg border border-slate-200 bg-slate-50 p-3">
<h3 class="mb-2 text-xs font-medium text-slate-500">队首待确认JSON</h3>
<pre
id="pendingJson"
class="max-h-64 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-700"
>
</pre>
</section>
<section class="min-h-[180px] rounded-lg border border-slate-200 bg-slate-50 p-3">
<div class="mb-2 flex items-center justify-between gap-2">
<h3 class="text-xs font-medium text-slate-500">详细日志</h3>
<button
type="button"
id="btnClearLog"
class="rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-700 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-600"
>
清空
</button>
</div>
<pre
id="log"
class="max-h-64 overflow-auto whitespace-pre-wrap font-mono text-xs text-slate-600"
></pre>
</section>
</div>
</div>
</details>
</main>
</div>
<script src="voice_app.js" defer></script>
</body>
</html>