feat: 模板AI区域化+默认配置优化+API密钥安全(20260419_2316)

- 默认模板: 手术步骤段落包裹进 .ai-region AI可编辑区域
- API密钥: DEFAULT_AI_PROVIDERS.kimi.apiKey 预设默认值,
  输入框增加onCopy/onCut防复制, storage.ts增加XOR+Base64透明加密
- 默认模型: kimi modelName改为 moonshot-v1-auto
- 抽帧配置: 12个位置改为指定百分比[7.9,9.3,46.2,49.1,63.9,64.8,
  68.8,73.7,80.2,85.0,96.3,98.6], 默认模式从uniform改为keep
This commit is contained in:
2026-04-19 23:24:36 +08:00
parent 3bec69986e
commit 0039b18a26
9 changed files with 329 additions and 34 deletions

View File

@@ -63,16 +63,11 @@ export default function Login() {
const settingsRaw = storage.get<SystemSettings>('systemSettings', {} as SystemSettings);
if (!settingsRaw.frameCount) {
const round1 = (n: number) => Math.round(n * 10) / 10;
const positions: number[] = [];
for (let i = 1; i <= 12; i++) {
positions.push(round1((100 / 13) * i));
}
const defaultSettings = {
frameCount: 12,
framePositions: positions,
framePositions: [7.9, 9.3, 46.2, 49.1, 63.9, 64.8, 68.8, 73.7, 80.2, 85.0, 96.3, 98.6],
defaultTemplate: savedTemplates[0]?.id || '',
frameMode: 'uniform',
frameMode: 'keep',
activeAiProvider: 'kimi',
aiProviders: { ...DEFAULT_AI_PROVIDERS },
autoInsertFrames: true,

View File

@@ -736,7 +736,7 @@ export default function ReportEditor() {
if (!videoRef.current || currentVideoIndex === -1) return;
const video = videoRef.current;
const settings = storage.get<SystemSettings>('systemSettings', {} as SystemSettings);
const positions = settings.framePositions || [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60];
const positions = settings.framePositions || [7.9, 9.3, 46.2, 49.1, 63.9, 64.8, 68.8, 73.7, 80.2, 85.0, 96.3, 98.6];
const dur = video.duration || 1;
const canvas = canvasRef.current;
@@ -898,7 +898,7 @@ export default function ReportEditor() {
const provider = settings.aiProviders?.[settings.activeAiProvider || 'kimi'];
const apiKey = provider?.apiKey || '';
const apiEndpoint = (provider?.endpoint || 'https://api.moonshot.cn/v1').replace(/\/+$/, '');
const modelName = provider?.modelName || 'kimi-k2-5';
const modelName = provider?.modelName || 'moonshot-v1-auto';
if (!apiKey) {
setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: '【系统提示】尚未配置 AI API Key请前往系统设置填写。' }]);
setIsGenerating(false);

View File

@@ -10,9 +10,9 @@ export default function SystemSettings() {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [settings, setSettings] = useState<ISystemSettings & { frameMode?: 'uniform' | 'keep' }>({
frameCount: 12,
framePositions: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60],
framePositions: [7.9, 9.3, 46.2, 49.1, 63.9, 64.8, 68.8, 73.7, 80.2, 85.0, 96.3, 98.6],
defaultTemplate: '',
frameMode: 'uniform',
frameMode: 'keep',
activeAiProvider: 'kimi',
aiProviders: { ...DEFAULT_AI_PROVIDERS }
});
@@ -38,7 +38,7 @@ export default function SystemSettings() {
providers.kimi = {
endpoint: (savedSettings as any).kimiApiEndpoint || providers.kimi.endpoint,
apiKey: (savedSettings as any).kimiApiKey || '',
modelName: 'kimi-k2-5'
modelName: 'moonshot-v1-auto'
};
}
savedSettings.aiProviders = providers;
@@ -50,12 +50,12 @@ export default function SystemSettings() {
if (!savedSettings.defaultTemplate && savedTemplates.length > 0) {
savedSettings.defaultTemplate = savedTemplates[0].id;
}
if (!savedSettings.frameMode) savedSettings.frameMode = 'uniform';
if (!savedSettings.frameMode) savedSettings.frameMode = 'keep';
if (typeof savedSettings.autoInsertFrames !== 'boolean') savedSettings.autoInsertFrames = false;
if (typeof savedSettings.autoInsertDelay !== 'number') savedSettings.autoInsertDelay = 0;
setSettings(savedSettings);
} else if (savedTemplates.length > 0) {
setSettings(prev => ({ ...prev, defaultTemplate: savedTemplates[0].id, frameMode: prev.frameMode || 'uniform', autoInsertFrames: typeof prev.autoInsertFrames === 'boolean' ? prev.autoInsertFrames : false, autoInsertDelay: typeof prev.autoInsertDelay === 'number' ? prev.autoInsertDelay : 0 }));
setSettings(prev => ({ ...prev, defaultTemplate: savedTemplates[0].id, frameMode: prev.frameMode || 'keep', autoInsertFrames: typeof prev.autoInsertFrames === 'boolean' ? prev.autoInsertFrames : false, autoInsertDelay: typeof prev.autoInsertDelay === 'number' ? prev.autoInsertDelay : 0 }));
}
setTemplates(savedTemplates);
}, [navigate]);
@@ -128,9 +128,9 @@ export default function SystemSettings() {
if (window.confirm('确定要恢复系统设置出厂设置吗?所有自定义配置将被清除。')) {
const defaultSettings: ISystemSettings & { frameMode?: 'uniform' | 'keep' } = {
frameCount: 12,
framePositions: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60],
framePositions: [7.9, 9.3, 46.2, 49.1, 63.9, 64.8, 68.8, 73.7, 80.2, 85.0, 96.3, 98.6],
defaultTemplate: templates[0]?.id || '',
frameMode: 'uniform',
frameMode: 'keep',
activeAiProvider: 'kimi',
aiProviders: { ...DEFAULT_AI_PROVIDERS },
autoInsertFrames: true,
@@ -364,6 +364,8 @@ export default function SystemSettings() {
next[settings.activeAiProvider] = { ...next[settings.activeAiProvider], apiKey: e.target.value };
setSettings({ ...settings, aiProviders: next });
}}
onCopy={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
placeholder="sk-xxxxxxxxxxxxxxxx"
className="input-minimal"
/>

View File

@@ -89,7 +89,7 @@ export interface SystemSettings {
}
export const DEFAULT_AI_PROVIDERS: Record<string, AiProviderConfig> = {
kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: '', modelName: 'kimi-k2-5' },
kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: 'sk-2IAFn8ORoSdUcCxYX6DmXJWbH7BxftSSA8kN88mD1KUDTmkv', modelName: 'moonshot-v1-auto' },
deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: '', modelName: 'deepseek-chat' },
openai: { endpoint: 'https://api.openai.com/v1', apiKey: '', modelName: 'gpt-4o' },
custom: { endpoint: '', apiKey: '', modelName: '' }

View File

@@ -55,25 +55,30 @@ export const defaultReportContent = `
<strong>手术步骤、术中出现的情况及处理:</strong>
</p>
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
1患者仰卧位麻醉成功后常规消毒术野、铺无菌巾于脐下穿刺建立CO2气腹气腹压力为12mmHg进镜探查无穿刺损伤分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。
</p>
<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; 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;">
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;">
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;">
4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。
</p>
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml术中输血成分输血量是否有输血不良反应。
</p>
<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">
5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml术中输血成分输血量是否有输血不良反应。
</p>
</div>
</div>
<!-- 手术图片说明表格 -->
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; table-layout: fixed;">

View File

@@ -1,8 +1,35 @@
const CRYPTO_KEY = 'MedicalReportSys2024';
function xorEncrypt(text: string, key: string): string {
let result = '';
for (let i = 0; i < text.length; i++) {
result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
}
return btoa(result);
}
function xorDecrypt(encrypted: string, key: string): string {
const text = atob(encrypted);
let result = '';
for (let i = 0; i < text.length; i++) {
result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
}
return result;
}
export const storage = {
get<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : fallback;
if (!raw) return fallback;
if (key === 'systemSettings') {
try {
return JSON.parse(raw) as T;
} catch {
return JSON.parse(xorDecrypt(raw, CRYPTO_KEY)) as T;
}
}
return JSON.parse(raw) as T;
} catch {
return fallback;
}
@@ -10,7 +37,11 @@ export const storage = {
set<T>(key: string, value: T): void {
try {
localStorage.setItem(key, JSON.stringify(value));
let data = JSON.stringify(value);
if (key === 'systemSettings') {
data = xorEncrypt(data, CRYPTO_KEY);
}
localStorage.setItem(key, data);
} catch (e) {
console.error('Storage save failed (possibly quota exceeded):', e);
}