Compare commits
9 Commits
ea789cee26
...
3827d09ad3
| Author | SHA1 | Date | |
|---|---|---|---|
| 3827d09ad3 | |||
| 49886e5080 | |||
| 5f68f4b820 | |||
| c544483705 | |||
| 9aec836e93 | |||
| 75e4e56cb3 | |||
| b07bcfaad2 | |||
| 4f27edcc92 | |||
| d235ced187 |
14
package-lock.json
generated
14
package-lock.json
generated
@@ -10,7 +10,9 @@
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"crypto-js": "^4.2.0",
|
||||
"diff": "^9.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
@@ -1492,6 +1494,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/crypto-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1904,6 +1912,12 @@
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"crypto-js": "^4.2.0",
|
||||
"diff": "^9.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { defaultReportContent } from '../utils/defaultContent';
|
||||
import { printDocument } from '../utils/print';
|
||||
import { storage, getDefaultApiKey } from '../utils/storage';
|
||||
import { diffChars } from 'diff';
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
export default function ReportEditor() {
|
||||
const navigate = useNavigate();
|
||||
@@ -896,14 +897,11 @@ export default function ReportEditor() {
|
||||
async function getXfAuthUrl(apiKey: string, apiSecret: string): Promise<string> {
|
||||
const host = 'iat-api.xfyun.cn';
|
||||
const date = new Date().toUTCString();
|
||||
const signatureOrigin = `host: "${host}"\ndate: "${date}"\nGET /v2/iat HTTP/1.1`;
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey('raw', encoder.encode(apiSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(signatureOrigin));
|
||||
const signatureBase64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
|
||||
const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signatureBase64}"`;
|
||||
const authorization = btoa(authorizationOrigin);
|
||||
return `wss://iat-api.xfyun.cn/v2/iat?authorization=${encodeURIComponent(authorization)}&date=${encodeURIComponent(date)}&host=${encodeURIComponent(host)}`;
|
||||
const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
|
||||
const signature = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(signatureOrigin, apiSecret));
|
||||
const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
|
||||
const authorization = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin));
|
||||
return `wss://iat-api.xfyun.cn/v2/iat?authorization=${authorization}&date=${date}&host=${host}`;
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(input: Float32Array): ArrayBuffer {
|
||||
@@ -925,24 +923,43 @@ export default function ReportEditor() {
|
||||
}
|
||||
|
||||
const toggleListening = async () => {
|
||||
// 专门提取一个彻底关闭物理麦克风的函数
|
||||
const stopMicrophone = () => {
|
||||
if (xfAudioContextRef.current) {
|
||||
try { xfAudioContextRef.current.close(); } catch {}
|
||||
xfAudioContextRef.current = null;
|
||||
}
|
||||
if (xfMediaStreamRef.current) {
|
||||
xfMediaStreamRef.current.getTracks().forEach(t => t.stop());
|
||||
xfMediaStreamRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (isListening) {
|
||||
setIsListening(false);
|
||||
if (xfWsRef.current) { try { xfWsRef.current.close(); } catch {} xfWsRef.current = null; }
|
||||
if (xfAudioContextRef.current) { try { xfAudioContextRef.current.close(); } catch {} xfAudioContextRef.current = null; }
|
||||
if (xfMediaStreamRef.current) { xfMediaStreamRef.current.getTracks().forEach(t => t.stop()); xfMediaStreamRef.current = null; }
|
||||
stopMicrophone();
|
||||
|
||||
if (xfWsRef.current && xfWsRef.current.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
const endFrame = { data: { status: 2, format: 'audio/L16;rate=16000', encoding: 'raw', audio: '' } };
|
||||
xfWsRef.current.send(JSON.stringify(endFrame));
|
||||
} catch {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const xfConfig = storage.get<SystemSettings>('systemSettings', {} as SystemSettings).xfSpeechConfig || DEFAULT_XF_SPEECH;
|
||||
if (!xfConfig?.appId || !xfConfig?.apiKey || !xfConfig?.apiSecret) {
|
||||
alert('请先在系统设置中配置讯飞语音 APPID/APIKey/APISecret');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const authUrl = await getXfAuthUrl(xfConfig.apiKey, xfConfig.apiSecret);
|
||||
const ws = new WebSocket(authUrl);
|
||||
xfWsRef.current = ws;
|
||||
let frameStatus = 0;
|
||||
let transcript = chatInput;
|
||||
|
||||
ws.onopen = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
@@ -951,16 +968,21 @@ export default function ReportEditor() {
|
||||
xfAudioContextRef.current = audioContext;
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
const processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
|
||||
processor.onaudioprocess = (e) => {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
if (ws.readyState !== WebSocket.OPEN || !xfAudioContextRef.current) return;
|
||||
const inputData = e.inputBuffer.getChannelData(0);
|
||||
const pcmBuffer = floatTo16BitPCM(inputData);
|
||||
const base64Audio = arrayBufferToBase64(pcmBuffer);
|
||||
const frame: any = { data: { status: frameStatus, format: 'audio/L16;rate=16000', encoding: 'raw', audio: base64Audio } };
|
||||
if (frameStatus === 0) { frame.common = { app_id: xfConfig.appId }; frame.business = { language: 'zh_cn', domain: 'iat', accent: 'mandarin' }; }
|
||||
if (frameStatus === 0) {
|
||||
frame.common = { app_id: xfConfig.appId };
|
||||
frame.business = { language: 'zh_cn', domain: 'iat', accent: 'mandarin' };
|
||||
}
|
||||
ws.send(JSON.stringify(frame));
|
||||
frameStatus = 1;
|
||||
};
|
||||
|
||||
source.connect(processor);
|
||||
processor.connect(audioContext.destination);
|
||||
setIsListening(true);
|
||||
@@ -970,19 +992,35 @@ export default function ReportEditor() {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const jsonData = JSON.parse(event.data);
|
||||
if (jsonData.code !== 0 && jsonData.code !== undefined) {
|
||||
alert(`讯飞语音报错: ${jsonData.message || '未知错误'} (错误码: ${jsonData.code})`);
|
||||
setIsListening(false);
|
||||
stopMicrophone();
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
if (jsonData.data?.result?.ws) {
|
||||
let seg = '';
|
||||
for (const w of jsonData.data.result.ws) { if (w.cw?.[0]?.w) seg += w.cw[0].w; }
|
||||
if (jsonData.data.result.ls) { transcript += seg; setChatInput(transcript); }
|
||||
else { setChatInput(transcript + seg); }
|
||||
if (seg) {
|
||||
setChatInput(prev => prev + seg);
|
||||
}
|
||||
}
|
||||
if (jsonData.data?.status === 2) {
|
||||
ws.close();
|
||||
xfWsRef.current = null;
|
||||
setIsListening(false);
|
||||
stopMicrophone();
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
ws.onerror = () => { alert('讯飞语音连接失败'); setIsListening(false); };
|
||||
ws.onclose = () => { setIsListening(false); };
|
||||
|
||||
ws.onerror = () => { alert('讯飞语音连接失败'); setIsListening(false); stopMicrophone(); };
|
||||
ws.onclose = () => { setIsListening(false); stopMicrophone(); };
|
||||
} catch (e: any) {
|
||||
alert('讯飞语音初始化失败: ' + e.message);
|
||||
}
|
||||
@@ -1054,7 +1092,7 @@ export default function ReportEditor() {
|
||||
}
|
||||
const systemPrompt = aiModifyEnabled
|
||||
? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据用户的【医生指令】,直接重写并输出目标区域的 HTML。\n\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复)和 "updatedHtml"(修改后的完整 HTML 代码)两个字段\n3. 【内容边界】:全局参考内容仅供你理解上下文。你的 updatedHtml 只能包含目标区域本身的内容(例如:如果目标区域是"手术步骤",你就只写步骤)。严禁输出签名、落款、术后总结等属于报告其他部分的结构!\n4. 段落必须使用 <p> 标签包裹,段落间绝对不要使用 <br> 标签,也不要使用换行符 (\\n)\n5. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n6. 绝对不要包含任何 Markdown 标记(如 ```json)'
|
||||
: '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记';
|
||||
: '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记\n5. 无论图片内容是什么,请严格以纯 JSON 格式输出,禁止任何多余的开头解释';
|
||||
const payload: any = {
|
||||
model: modelName,
|
||||
messages: [
|
||||
@@ -1106,8 +1144,17 @@ export default function ReportEditor() {
|
||||
responseJson = JSON.parse(cleanedText);
|
||||
} catch {
|
||||
const jsonMatch = cleanedText.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) responseJson = JSON.parse(jsonMatch[0]);
|
||||
else throw new Error('AI 返回格式异常,无法解析 JSON');
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
responseJson = JSON.parse(jsonMatch[0]);
|
||||
} catch {
|
||||
if (!aiModifyEnabled) responseJson = { reply: cleanedText };
|
||||
else throw new Error('AI 返回格式异常,无法解析 JSON');
|
||||
}
|
||||
} else {
|
||||
if (!aiModifyEnabled) responseJson = { reply: cleanedText };
|
||||
else throw new Error('AI 返回格式异常,无法解析 JSON');
|
||||
}
|
||||
}
|
||||
if (responseJson.reply) {
|
||||
setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: responseJson.reply }]);
|
||||
@@ -1121,7 +1168,7 @@ export default function ReportEditor() {
|
||||
cleanHtml = cleanHtml.replace(/<\/p>\s*<p>/gi, '</p><p>');
|
||||
cleanHtml = cleanHtml.trim();
|
||||
cleanHtml = cleanHtml.replace(/>(\s+)</g, '><');
|
||||
cleanHtml = cleanHtml.replace(/<p>/gi, '<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">');
|
||||
cleanHtml = cleanHtml.replace(/<p>/gi, '<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">');
|
||||
if (targetRegionEl) {
|
||||
setDiffModal({
|
||||
isOpen: true,
|
||||
@@ -1924,10 +1971,11 @@ export default function ReportEditor() {
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => { setActiveTab(tab); stateRef.current = { ...stateRef.current, activeTab: tab }; saveDraftToStorage(); }}
|
||||
className={`flex-1 py-4 text-xs font-bold transition-all border-b-2 uppercase tracking-wider ${
|
||||
className={`flex-1 py-4 text-xs font-bold transition-all border-b-2 uppercase tracking-wider flex items-center justify-center gap-1.5 ${
|
||||
activeTab === tab ? 'text-accent border-accent' : 'text-text-muted border-transparent hover:text-text-main'
|
||||
}`}
|
||||
>
|
||||
{tab === 'ai' && <Bot size={16} className={activeTab === 'ai' ? 'text-accent' : 'text-text-muted'} />}
|
||||
{tab === 'info' ? '基本信息' : tab === 'video' ? '视频分析' : 'AI撰写'}
|
||||
</button>
|
||||
))}
|
||||
@@ -2840,7 +2888,9 @@ export default function ReportEditor() {
|
||||
<div className="flex-1 flex flex-col bg-white border border-red-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="bg-red-50 px-3 py-2 text-xs font-bold text-red-600 border-b border-red-100 uppercase tracking-wider">原始版本</div>
|
||||
<div className="p-4 flex-1 overflow-y-auto opacity-70 cursor-not-allowed custom-scrollbar"
|
||||
dangerouslySetInnerHTML={{ __html: leftDiffHtml }}></div>
|
||||
dangerouslySetInnerHTML={{ __html: leftDiffHtml }}
|
||||
style={{ fontFamily: 'SimSun, "Microsoft YaHei", serif', fontSize: '12pt', lineHeight: '1.5' }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col bg-white border border-green-400 rounded-xl overflow-hidden shadow-md relative">
|
||||
<div className="bg-green-50 px-3 py-2 text-xs font-bold text-green-700 border-b border-green-200 uppercase tracking-wider flex justify-between">
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function SystemSettings() {
|
||||
frameMode: 'keep',
|
||||
activeAiProvider: 'kimi',
|
||||
aiProviders: { ...DEFAULT_AI_PROVIDERS },
|
||||
xfIatConfig: { appId: 'e0fe23e3', apiKey: '7fd08be316718c2280e85af4fe126306', apiSecret: 'ZGI5MjAzZDA0YzYwNDhjMWZiNTM2NDE0' }
|
||||
xfSpeechConfig: { appId: 'e0fe23e3', apiKey: '7fd08be316718c2280e85af4fe126306', apiSecret: 'ZGI5MjAzZDA0YzYwNDhjMWZiNTM2NDE0' }
|
||||
});
|
||||
const [templates, setTemplates] = useState<Template[]>([]);
|
||||
const [isSaved, setIsSaved] = useState(false);
|
||||
@@ -23,6 +23,9 @@ export default function SystemSettings() {
|
||||
const [modeModalOpen, setModeModalOpen] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const apiKeyInputRef = useRef<HTMLInputElement>(null);
|
||||
const xfAppIdRef = useRef<HTMLInputElement>(null);
|
||||
const xfApiKeyRef = useRef<HTMLInputElement>(null);
|
||||
const xfApiSecretRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (apiKeyInputRef.current) {
|
||||
@@ -33,6 +36,21 @@ export default function SystemSettings() {
|
||||
}
|
||||
}, [settings.aiProviders[settings.activeAiProvider]?.apiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (xfAppIdRef.current) {
|
||||
const target = settings.xfSpeechConfig?.appId || '';
|
||||
if (xfAppIdRef.current.value !== target) xfAppIdRef.current.value = target;
|
||||
}
|
||||
if (xfApiKeyRef.current) {
|
||||
const target = settings.xfSpeechConfig?.apiKey || '';
|
||||
if (xfApiKeyRef.current.value !== target) xfApiKeyRef.current.value = target;
|
||||
}
|
||||
if (xfApiSecretRef.current) {
|
||||
const target = settings.xfSpeechConfig?.apiSecret || '';
|
||||
if (xfApiSecretRef.current.value !== target) xfApiSecretRef.current.value = target;
|
||||
}
|
||||
}, [settings.xfSpeechConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const user = storage.get<User | null>('currentUser', null);
|
||||
if (!user) {
|
||||
@@ -429,46 +447,55 @@ export default function SystemSettings() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-bold text-text-main flex items-center gap-2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
|
||||
讯飞语音配置
|
||||
讯飞语音听写Websocket接口配置
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">APPID</label>
|
||||
<input
|
||||
ref={xfAppIdRef}
|
||||
type="password"
|
||||
value={settings.xfIatConfig?.appId || ''}
|
||||
onChange={(e) => {
|
||||
const next = { ...(settings.xfIatConfig || { appId: '', apiKey: '', apiSecret: '' }), appId: e.target.value };
|
||||
setSettings({ ...settings, xfIatConfig: next });
|
||||
const next = { ...(settings.xfSpeechConfig || { appId: '', apiKey: '', apiSecret: '' }), appId: e.target.value };
|
||||
setSettings({ ...settings, xfSpeechConfig: next });
|
||||
}}
|
||||
placeholder="e0fe23e3"
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
autoComplete="new-password"
|
||||
placeholder="********"
|
||||
className="input-minimal"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">APIKey</label>
|
||||
<input
|
||||
ref={xfApiKeyRef}
|
||||
type="password"
|
||||
value={settings.xfIatConfig?.apiKey || ''}
|
||||
onChange={(e) => {
|
||||
const next = { ...(settings.xfIatConfig || { appId: '', apiKey: '', apiSecret: '' }), apiKey: e.target.value };
|
||||
setSettings({ ...settings, xfIatConfig: next });
|
||||
const next = { ...(settings.xfSpeechConfig || { appId: '', apiKey: '', apiSecret: '' }), apiKey: e.target.value };
|
||||
setSettings({ ...settings, xfSpeechConfig: next });
|
||||
}}
|
||||
placeholder="7fd08be316718c2280e85af4fe126306"
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
autoComplete="new-password"
|
||||
placeholder="********************************"
|
||||
className="input-minimal"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">APISecret</label>
|
||||
<input
|
||||
ref={xfApiSecretRef}
|
||||
type="password"
|
||||
value={settings.xfIatConfig?.apiSecret || ''}
|
||||
onChange={(e) => {
|
||||
const next = { ...(settings.xfIatConfig || { appId: '', apiKey: '', apiSecret: '' }), apiSecret: e.target.value };
|
||||
setSettings({ ...settings, xfIatConfig: next });
|
||||
const next = { ...(settings.xfSpeechConfig || { appId: '', apiKey: '', apiSecret: '' }), apiSecret: e.target.value };
|
||||
setSettings({ ...settings, xfSpeechConfig: next });
|
||||
}}
|
||||
placeholder="ZGI5MjAzZDA0YzYwNDhjMWZiNTM2NDE0"
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
autoComplete="new-password"
|
||||
placeholder="********************************"
|
||||
className="input-minimal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ export const defaultReportContent = `
|
||||
|
||||
<div class="ai-region" data-ai-id="手术步骤" data-ai-title="手术步骤、术中出现的情况及处理" style="border: 1px dashed #3b82f6; padding: 16px 12px 12px; margin: 8px 0; position: relative; min-height: 60px; background: #f8fafc; border-radius: 6px;">
|
||||
<div contenteditable="false" style="position: absolute; top: -10px; right: 10px; background: #3b82f6; color: white; font-size: 10px; padding: 2px 8px; border-radius: 12px; z-index: 10; user-select: none;">手术步骤、术中出现的情况及处理-AI可编辑区域</div>
|
||||
<div class="ai-content" style="min-height: 20px;"><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">1.患者仰卧位,麻醉成功后,常规消毒术野、铺无菌巾,于脐下穿刺建立CO2气腹,气腹压力为12mmHg,进镜探查无穿刺损伤,分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">2.腹腔镜探查:腹腔内无腹水形成,无明显粘连,肝脏色红质软,无明显结节硬化改变,胆囊大小约 cm× cm× cm,壁轻度水肿,张力可,胆囊三角解剖关系清楚,胆囊管及胆总管无明显扩张。胃、十二指肠、小肠、结肠、脾脏及盆腔未见明显异常。术中诊断:胆囊结石伴慢性胆囊炎。遂行腹腔镜胆囊切除术。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">3.切除胆囊:钳夹胆囊颈部并解剖胆囊三角,游离出胆囊动脉及胆囊管,明确胆囊与胆总管的关系,距胆总管0.3cm处近端以一枚可吸收夹,远端夹一枚钛夹夹闭胆囊管,两夹间以剪刀剪断胆囊管,另用一枚可吸收夹夹闭胆囊动脉后离断。顺行游离胆囊浆膜,完整切除胆囊后装入标本袋取出。胆囊床严密止血并覆盖止血材料。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml,术中输血成分,输血量,是否有输血不良反应。</p></div>
|
||||
<div class="ai-content" style="min-height: 20px;"><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">1.患者仰卧位,麻醉成功后,常规消毒术野、铺无菌巾,于脐下穿刺建立CO2气腹,气腹压力为12mmHg,进镜探查无穿刺损伤,分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">2.腹腔镜探查:腹腔内无腹水形成,无明显粘连,肝脏色红质软,无明显结节硬化改变,胆囊大小约 cm× cm× cm,壁轻度水肿,张力可,胆囊三角解剖关系清楚,胆囊管及胆总管无明显扩张。胃、十二指肠、小肠、结肠、脾脏及盆腔未见明显异常。术中诊断:胆囊结石伴慢性胆囊炎。遂行腹腔镜胆囊切除术。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">3.切除胆囊:钳夹胆囊颈部并解剖胆囊三角,游离出胆囊动脉及胆囊管,明确胆囊与胆总管的关系,距胆总管0.3cm处近端以一枚可吸收夹,远端夹一枚钛夹夹闭胆囊管,两夹间以剪刀剪断胆囊管,另用一枚可吸收夹夹闭胆囊动脉后离断。顺行游离胆囊浆膜,完整切除胆囊后装入标本袋取出。胆囊床严密止血并覆盖止血材料。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml,术中输血成分,输血量,是否有输血不良反应。</p></div>
|
||||
</div>
|
||||
|
||||
<!-- 手术图片说明表格 -->
|
||||
|
||||
Reference in New Issue
Block a user