Files
Kevin 6bc6801df9 统一 Docker Compose 部署,并将客户端拆分为独立子项目。
移除宿主机/conda 启动脚本与 dev 联调工具,后端仅通过 docker compose 部署并默认启用 GPU。模拟客户端与语音确认页迁入 clients/ 下自包含目录,切断对后端源码路径的依赖。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 15:56:53 +08:00

360 lines
15 KiB
HTML
Executable File
Raw Permalink 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" />
<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>