refactor: 优化代码质量,遵循 KISS 原则
- 移除自签证书回退逻辑,简化为仅使用 AnyIP 证书 - 删除 internal/tls/generate.go(不再需要) - 重构 main.go:提取初始化逻辑,main() 从 156 行降至 13 行 - 重构 internal/ws/handler.go:提取消息处理,handleConn() 从 131 行降至 25 行 - 重构 internal/config/load.go:使用 map 驱动消除重复代码 - 优化前端 startRecording():使用标准 AbortController API - 优化前端 showToast():预定义 DOM 元素,代码减少 50% 代码行数减少 90 行,复杂度显著降低,所有构建通过
This commit is contained in:
75
web/app.ts
75
web/app.ts
@@ -29,7 +29,7 @@ interface AppState {
|
||||
connected: boolean;
|
||||
recording: boolean;
|
||||
pendingStart: boolean;
|
||||
startCancelled: boolean;
|
||||
abortController: AbortController | null;
|
||||
audioCtx: AudioContext | null;
|
||||
workletNode: AudioWorkletNode | null;
|
||||
stream: MediaStream | null;
|
||||
@@ -65,7 +65,7 @@ const state: AppState = {
|
||||
connected: false,
|
||||
recording: false,
|
||||
pendingStart: false,
|
||||
startCancelled: false,
|
||||
abortController: null,
|
||||
audioCtx: null,
|
||||
workletNode: null,
|
||||
stream: null,
|
||||
@@ -128,20 +128,19 @@ function connectWS(): void {
|
||||
micBtn.disabled = false;
|
||||
};
|
||||
ws.onmessage = (e: MessageEvent) => handleServerMsg(e.data);
|
||||
ws.onclose = () => {
|
||||
state.connected = false;
|
||||
state.ws = null;
|
||||
micBtn.disabled = true;
|
||||
if (state.recording) stopRecording();
|
||||
// Clean up pending async start on disconnect
|
||||
if (state.pendingStart) {
|
||||
state.pendingStart = false;
|
||||
state.startCancelled = true;
|
||||
micBtn.classList.remove("recording");
|
||||
}
|
||||
setStatus("disconnected", "已断开");
|
||||
scheduleReconnect();
|
||||
};
|
||||
ws.onclose = () => {
|
||||
state.connected = false;
|
||||
state.ws = null;
|
||||
micBtn.disabled = true;
|
||||
if (state.recording) stopRecording();
|
||||
if (state.pendingStart) {
|
||||
state.abortController?.abort();
|
||||
state.pendingStart = false;
|
||||
micBtn.classList.remove("recording");
|
||||
}
|
||||
setStatus("disconnected", "已断开");
|
||||
scheduleReconnect();
|
||||
};
|
||||
ws.onerror = () => ws.close();
|
||||
state.ws = ws;
|
||||
}
|
||||
@@ -199,25 +198,14 @@ function setPreview(text: string, isFinal: boolean): void {
|
||||
previewBox.classList.toggle("active", !isFinal);
|
||||
}
|
||||
function showToast(msg: string): void {
|
||||
let toast = document.getElementById("toast");
|
||||
if (!toast) {
|
||||
toast = document.createElement("div");
|
||||
toast.id = "toast";
|
||||
toast.style.cssText =
|
||||
"position:fixed;bottom:calc(100px + var(--safe-bottom,0px));left:50%;" +
|
||||
"transform:translateX(-50%);background:#222;color:#eee;padding:8px 18px;" +
|
||||
"border-radius:20px;font-size:14px;z-index:999;opacity:0;transition:opacity .3s;";
|
||||
document.body.appendChild(toast);
|
||||
}
|
||||
const toast = q("#toast");
|
||||
toast.textContent = msg;
|
||||
toast.style.opacity = "1";
|
||||
clearTimeout(
|
||||
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer,
|
||||
);
|
||||
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer =
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = "0";
|
||||
}, 2000);
|
||||
toast.classList.add("show");
|
||||
const timer = (toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer;
|
||||
if (timer) clearTimeout(timer);
|
||||
(toast as HTMLElement & { _timer?: ReturnType<typeof setTimeout> })._timer = setTimeout(() => {
|
||||
toast.classList.remove("show");
|
||||
}, 2000);
|
||||
}
|
||||
// ── Audio pipeline ──
|
||||
async function initAudio(): Promise<void> {
|
||||
@@ -234,19 +222,19 @@ async function initAudio(): Promise<void> {
|
||||
async function startRecording(): Promise<void> {
|
||||
if (state.recording || state.pendingStart) return;
|
||||
state.pendingStart = true;
|
||||
state.startCancelled = false;
|
||||
const abortController = new AbortController();
|
||||
state.abortController = abortController;
|
||||
try {
|
||||
await initAudio();
|
||||
if (state.startCancelled) {
|
||||
if (abortController.signal.aborted) {
|
||||
state.pendingStart = false;
|
||||
return;
|
||||
}
|
||||
const audioCtx = state.audioCtx as AudioContext;
|
||||
// Ensure AudioContext is running (may suspend between recordings)
|
||||
if (audioCtx.state === "suspended") {
|
||||
await audioCtx.resume();
|
||||
}
|
||||
if (state.startCancelled) {
|
||||
if (abortController.signal.aborted) {
|
||||
state.pendingStart = false;
|
||||
return;
|
||||
}
|
||||
@@ -257,7 +245,7 @@ async function startRecording(): Promise<void> {
|
||||
channelCount: 1,
|
||||
},
|
||||
});
|
||||
if (state.startCancelled) {
|
||||
if (abortController.signal.aborted) {
|
||||
stream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
});
|
||||
@@ -275,34 +263,33 @@ async function startRecording(): Promise<void> {
|
||||
};
|
||||
source.connect(worklet);
|
||||
worklet.port.postMessage({ command: "start" });
|
||||
// Don't connect worklet to destination (no playback)
|
||||
state.workletNode = worklet;
|
||||
state.pendingStart = false;
|
||||
state.abortController = null;
|
||||
state.recording = true;
|
||||
sendJSON({ type: "start" });
|
||||
micBtn.classList.add("recording");
|
||||
setPreview("", false);
|
||||
} catch (err) {
|
||||
state.pendingStart = false;
|
||||
state.abortController = null;
|
||||
showToast(`麦克风错误: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
function stopRecording(): void {
|
||||
// Cancel pending async start if still initializing
|
||||
if (state.pendingStart) {
|
||||
state.startCancelled = true;
|
||||
state.abortController?.abort();
|
||||
state.abortController = null;
|
||||
micBtn.classList.remove("recording");
|
||||
return;
|
||||
}
|
||||
if (!state.recording) return;
|
||||
state.recording = false;
|
||||
// Stop worklet
|
||||
if (state.workletNode) {
|
||||
state.workletNode.port.postMessage({ command: "stop" });
|
||||
state.workletNode.disconnect();
|
||||
state.workletNode = null;
|
||||
}
|
||||
// Stop mic stream
|
||||
if (state.stream) {
|
||||
state.stream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<p id="history-empty" class="placeholder">暂无记录</p>
|
||||
</section>
|
||||
</div>
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script type="module" src="app.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -272,3 +272,22 @@ header h1 {
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
/* Toast */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: calc(100px + var(--safe-bottom, 0px));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #222;
|
||||
color: #eee;
|
||||
padding: 8px 18px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user