Fix report draft save and microphone startup
- Allow draft reports to be saved without patient name or hospital ID while keeping completed reports strictly validated. - Preserve completed-report identity validation when updating existing reports by checking merged old and new values. - Show real API save errors in the report editor and send expired sessions back to login instead of reporting a generic backend outage. - Guard speech startup for missing getUserMedia or AudioContext support and explain localhost/HTTPS microphone requirements. - Add report schema tests covering draft identity fields and completed-report validation. - Update AGENTS and docs for report editor behavior, feature status, progress, and testing coverage.
This commit is contained in:
@@ -14,6 +14,7 @@ import { printDocument } from '../utils/print';
|
||||
import { storage } from '../utils/storage';
|
||||
import { canEditReport, getUsableTemplates } from '../utils/permissions';
|
||||
import { getReport, saveReportToApi } from '../api/reports';
|
||||
import { ApiError } from '../api/client';
|
||||
import { createTemplate, listTemplates } from '../api/templates';
|
||||
import { getSystemSettings } from '../api/settings';
|
||||
import { createAiChatCompletion } from '../api/ai';
|
||||
@@ -23,6 +24,19 @@ import { listFiles, uploadFileResource } from '../api/files';
|
||||
import { isLocalFallbackEnabled } from '../config/runtime';
|
||||
import { diffChars } from 'diff';
|
||||
|
||||
type AudioWindow = Window & typeof globalThis & {
|
||||
webkitAudioContext?: typeof AudioContext;
|
||||
};
|
||||
|
||||
const getApiErrorMessage = (error: unknown, fallback: string) => {
|
||||
if (error instanceof ApiError) {
|
||||
if (error.status === 401) return '登录状态已失效,请重新登录后再保存。';
|
||||
return error.message || fallback;
|
||||
}
|
||||
if (error instanceof Error) return error.message || fallback;
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export default function ReportEditor() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -1104,15 +1118,28 @@ export default function ReportEditor() {
|
||||
}
|
||||
|
||||
try {
|
||||
const mediaDevices = navigator.mediaDevices;
|
||||
const AudioContextClass = window.AudioContext || (window as AudioWindow).webkitAudioContext;
|
||||
if (!mediaDevices?.getUserMedia) {
|
||||
alert(window.isSecureContext
|
||||
? '当前浏览器不支持麦克风采集,请更换新版 Chrome/Edge 后重试。'
|
||||
: '麦克风需要在 localhost 或 HTTPS 环境下使用。请通过 localhost 访问,或配置 HTTPS 后重试。');
|
||||
return;
|
||||
}
|
||||
if (!AudioContextClass) {
|
||||
alert('当前浏览器不支持音频采集处理,请更换新版 Chrome/Edge 后重试。');
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(getSpeechIatWebSocketUrl());
|
||||
xfWsRef.current = ws;
|
||||
let frameStatus = 0;
|
||||
|
||||
ws.onopen = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const stream = await mediaDevices.getUserMedia({ audio: true });
|
||||
xfMediaStreamRef.current = stream;
|
||||
const audioContext = new AudioContext({ sampleRate: 16000 });
|
||||
const audioContext = new AudioContextClass({ sampleRate: 16000 });
|
||||
xfAudioContextRef.current = audioContext;
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
const processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
@@ -1797,9 +1824,11 @@ export default function ReportEditor() {
|
||||
try {
|
||||
savedReport = await saveReportToApi(finalReport, reportId || undefined);
|
||||
apiSaved = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (!isLocalFallbackEnabled()) {
|
||||
alert('保存失败:后端服务不可用');
|
||||
const message = getApiErrorMessage(error, '后端服务不可用');
|
||||
alert(`保存失败:${message}`);
|
||||
if (error instanceof ApiError && error.status === 401) navigate('/');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1854,9 +1883,11 @@ export default function ReportEditor() {
|
||||
try {
|
||||
savedTemplate = await createTemplate(newTemplate);
|
||||
apiSaved = true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (!isLocalFallbackEnabled()) {
|
||||
alert('保存个人模板失败:后端服务不可用');
|
||||
const message = getApiErrorMessage(error, '后端服务不可用');
|
||||
alert(`保存个人模板失败:${message}`);
|
||||
if (error instanceof ApiError && error.status === 401) navigate('/');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user