Remove frontend JSON export actions
- Remove user-visible JSON export options from report editor, report management, template management, bulk template export, and AI debug logs. - Keep HTML template package and PDF/browser print exports as the supported frontend export formats. - Change per-template export to generate reusable HTML template packages. - Preserve legacy JSON template import compatibility without exposing new JSON export buttons. - Update README, AGENTS, feature, requirement, design, module, API contract, progress, and testing docs for the export policy change.
This commit is contained in:
@@ -125,14 +125,6 @@ export default function ReportEditor() {
|
||||
]);
|
||||
const [isEditingPrompts, setIsEditingPrompts] = useState(false);
|
||||
const [diffModal, setDiffModal] = useState<{isOpen: boolean, originalHtml: string, newHtml: string, targetId: string} | null>(null);
|
||||
const [lastExchangeLog, setLastExchangeLog] = useState<{
|
||||
startTime: string;
|
||||
modelConfig: { provider: string; endpoint: string; modelName: string };
|
||||
requestPayload: any;
|
||||
responsePayload: any | null;
|
||||
errorDetail: { status: number; statusText: string; responseText: string; message: string } | null;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
stateRef.current.chatMessages = chatMessages;
|
||||
}, [chatMessages]);
|
||||
@@ -1327,16 +1319,7 @@ export default function ReportEditor() {
|
||||
delete payload.presence_penalty;
|
||||
delete payload.frequency_penalty;
|
||||
}
|
||||
const logEntry = {
|
||||
startTime: new Date().toISOString(),
|
||||
modelConfig: { provider: settings.activeAiProvider || 'kimi', endpoint: '/api/ai/chat', modelName },
|
||||
requestPayload: JSON.parse(JSON.stringify(payload)),
|
||||
responsePayload: null as any | null,
|
||||
errorDetail: null as { status: number; statusText: string; responseText: string; message: string } | null
|
||||
};
|
||||
const data = await createAiChatCompletion(payload);
|
||||
logEntry.responsePayload = data;
|
||||
setLastExchangeLog(logEntry);
|
||||
const responseText = data.choices[0].message.content.trim();
|
||||
const cleanedText = responseText.replace(/```json\n?|```/g, '');
|
||||
let responseJson: any = {};
|
||||
@@ -2861,32 +2844,6 @@ export default function ReportEditor() {
|
||||
{isEditingPrompts ? '+ 添加' : '⚙️'}
|
||||
</button>
|
||||
{isEditingPrompts && <button onClick={() => setIsEditingPrompts(false)} className="px-2 py-1 bg-blue-100 text-blue-600 text-[11px] rounded-full">完成</button>}
|
||||
<button onClick={() => {
|
||||
const data = {
|
||||
exportAt: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
messages: chatMessages,
|
||||
lastExchange: lastExchangeLog,
|
||||
metadata: {
|
||||
user: currentUser?.username || 'anonymous',
|
||||
activeProvider: (() => { const s = storage.get<SystemSettings>('systemSettings', {} as SystemSettings); return s.activeAiProvider || 'kimi'; })(),
|
||||
targetRegion: aiTargetRegion,
|
||||
modifyEnabled: aiModifyEnabled,
|
||||
chatInput,
|
||||
uploadedImagesCount: aiUploadedImages.length,
|
||||
selectedFramesCount: aiSelectedEditorImages.length
|
||||
}
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `ai-logs-${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}} className="px-2 py-1 bg-slate-100 text-slate-500 text-[11px] rounded-full hover:bg-slate-200 ml-auto" title="导出 AI 日志(调试用)">
|
||||
导出 AI 日志
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 沉浸式输入框 */}
|
||||
@@ -3058,23 +3015,6 @@ export default function ReportEditor() {
|
||||
}}
|
||||
className="w-full py-2.5 bg-accent text-white rounded text-sm font-semibold hover:opacity-90 transition-colors"
|
||||
>导出 PDF</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const ts = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().replace(/[:.]/g, '-').slice(0, 16);
|
||||
const title = reportData.title || '无标题';
|
||||
const patient = reportData.patientName || '未知';
|
||||
const hid = reportData.hospitalId || '无号';
|
||||
const blob = new Blob([JSON.stringify(reportData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `图文报告-${title}-${patient}-${hid}-${ts}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setExportModalOpen(false);
|
||||
}}
|
||||
className="w-full py-2.5 bg-slate-100 text-slate-700 rounded text-sm font-semibold hover:bg-slate-200 transition-colors"
|
||||
>导出 JSON</button>
|
||||
<button
|
||||
onClick={() => setExportModalOpen(false)}
|
||||
className="w-full py-2.5 border border-border text-text-main rounded text-sm font-semibold hover:bg-slate-50 transition-colors"
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import { Search, Eye, Edit, Trash2, FileText, History, X, Download, Printer } from 'lucide-react';
|
||||
import { User, Report, DEFAULT_FORM_FIELDS } from '../types';
|
||||
import { User, Report } from '../types';
|
||||
import { storage } from '../utils/storage';
|
||||
import { printDocument } from '../utils/print';
|
||||
import { canDeleteReport, canEditReport, getAccessibleReports } from '../utils/permissions';
|
||||
@@ -161,45 +161,10 @@ export default function ReportManage() {
|
||||
setSelectedIds([]);
|
||||
};
|
||||
|
||||
const buildExportData = (report: Report) => {
|
||||
const fields: Record<string, any> = {};
|
||||
DEFAULT_FORM_FIELDS.forEach(f => {
|
||||
fields[f.key] = (report as any)[f.key];
|
||||
});
|
||||
return {
|
||||
meta: {
|
||||
id: report.id,
|
||||
title: report.title,
|
||||
createdAt: report.createdAt,
|
||||
updatedAt: report.updatedAt,
|
||||
author: report.author,
|
||||
authorName: report.authorName,
|
||||
status: report.status,
|
||||
revision: report.revision || 1
|
||||
},
|
||||
fields
|
||||
};
|
||||
};
|
||||
|
||||
const downloadJSON = (data: any, filename: string) => {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const exportSinglePDF = (report: Report) => {
|
||||
printDocument(report.content);
|
||||
};
|
||||
|
||||
const exportSingleJSON = (report: Report) => {
|
||||
const data = buildExportData(report);
|
||||
downloadJSON(data, `报告_${report.patientName || '未命名'}_${report.id}.json`);
|
||||
};
|
||||
|
||||
const exportBulkPDF = () => {
|
||||
const users = storage.get<User[]>('users', []);
|
||||
const selectedReports = getAccessibleReports(currentUser!, reports, users).filter(r => selectedIds.includes(r.id));
|
||||
@@ -207,14 +172,6 @@ export default function ReportManage() {
|
||||
printDocument(mergedHTML);
|
||||
};
|
||||
|
||||
const exportBulkJSON = () => {
|
||||
const users = storage.get<User[]>('users', []);
|
||||
const selectedReports = getAccessibleReports(currentUser!, reports, users).filter(r => selectedIds.includes(r.id));
|
||||
const data = selectedReports.map(r => buildExportData(r));
|
||||
const timestamp = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().replace(/[:.]/g, '-').slice(0, 16);
|
||||
downloadJSON(data, `reports_export_${timestamp}.json`);
|
||||
};
|
||||
|
||||
const openExportModal = (report: Report) => {
|
||||
setExportTarget(report);
|
||||
setExportModalOpen(true);
|
||||
@@ -280,12 +237,6 @@ export default function ReportManage() {
|
||||
>
|
||||
<Printer size={14} /> 批量导出 PDF
|
||||
</button>
|
||||
<button
|
||||
onClick={exportBulkJSON}
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-lg bg-white border border-border hover:bg-slate-100 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<Download size={14} /> 批量导出 JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBulkDelete}
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-lg bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
|
||||
@@ -480,7 +431,7 @@ export default function ReportManage() {
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mb-4">选择导出格式:</p>
|
||||
<p className="text-sm text-text-muted mb-4">将通过浏览器打印导出,可保存为 PDF。</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => { exportSinglePDF(exportTarget); setExportModalOpen(false); }}
|
||||
@@ -492,16 +443,6 @@ export default function ReportManage() {
|
||||
<div className="text-xs text-text-muted">调用浏览器打印并保存为 PDF</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { exportSingleJSON(exportTarget); setExportModalOpen(false); }}
|
||||
className="w-full px-4 py-3 rounded-lg bg-slate-50 hover:bg-slate-100 transition-colors flex items-center gap-3"
|
||||
>
|
||||
<Download size={18} className="text-text-muted" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-semibold text-text-main">导出 JSON</div>
|
||||
<div className="text-xs text-text-muted">下载结构化字段数据</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -763,24 +763,6 @@ export default function TemplateManage() {
|
||||
setSelectedIds([]);
|
||||
};
|
||||
|
||||
const handleBatchExport = () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
const targets = templates.filter(t => selectedIds.includes(t.id));
|
||||
const ts = getExportTimestamp();
|
||||
const exportData = {
|
||||
version: '1.0',
|
||||
type: 'surclaw_template_package_batch',
|
||||
templates: targets
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `模板批量导出-${ts}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
@@ -803,30 +785,29 @@ export default function TemplateManage() {
|
||||
|
||||
const handleExportTemplate = (template: Template) => {
|
||||
const exportData = createTemplatePackage(template, template.content, template.fields || formFields);
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const html = createTemplateHtmlDocument(exportData);
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const ts = getExportTimestamp();
|
||||
a.download = `模板导出-${safeFileName(template.name)}-${ts}.json`;
|
||||
a.download = `模板导出-${safeFileName(template.name)}-${ts}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadCurrentTemplatePackage = (format: 'json' | 'html') => {
|
||||
const downloadCurrentTemplateHtmlPackage = () => {
|
||||
const content = editorRef.current?.innerHTML || currentTemplate?.content || '';
|
||||
const name = currentTemplate?.name || '模板';
|
||||
const templatePackage = createTemplatePackage(currentTemplate, content, currentTemplate?.fields || formFields);
|
||||
const ts = getExportTimestamp();
|
||||
const safeName = safeFileName(name);
|
||||
const body = format === 'html'
|
||||
? createTemplateHtmlDocument(templatePackage)
|
||||
: JSON.stringify(templatePackage, null, 2);
|
||||
const blob = new Blob([body], { type: format === 'html' ? 'text/html' : 'application/json' });
|
||||
const body = createTemplateHtmlDocument(templatePackage);
|
||||
const blob = new Blob([body], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${safeName}-${ts}.${format}`;
|
||||
a.download = `${safeName}-${ts}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
@@ -952,12 +933,6 @@ export default function TemplateManage() {
|
||||
<div className="px-4 pt-3 pb-1 flex items-center justify-between bg-slate-50 border-b border-border">
|
||||
<span className="text-xs text-text-muted font-bold">已选中 {selectedIds.length} 项</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleBatchExport}
|
||||
className="px-2 py-1 rounded-md bg-blue-50 text-blue-600 text-[10px] font-bold hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
批量导出
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBatchDelete}
|
||||
className="px-2 py-1 rounded-md bg-red-50 text-red-600 text-[10px] font-bold hover:bg-red-100 transition-colors"
|
||||
@@ -1007,7 +982,7 @@ export default function TemplateManage() {
|
||||
onClick={(e) => { e.stopPropagation(); handleExportTemplate(tpl); }}
|
||||
className="px-2 py-1 rounded-md bg-blue-50 text-blue-600 text-[10px] font-bold hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
导出
|
||||
导出HTML
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteTemplate(tpl.id); }}
|
||||
@@ -1566,18 +1541,11 @@ export default function TemplateManage() {
|
||||
>导出 PDF</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
downloadCurrentTemplatePackage('html');
|
||||
downloadCurrentTemplateHtmlPackage();
|
||||
setExportModalOpen(false);
|
||||
}}
|
||||
className="w-full py-2.5 bg-slate-900 text-white rounded text-sm font-semibold hover:bg-slate-800 transition-colors"
|
||||
>导出 HTML 模板包</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
downloadCurrentTemplatePackage('json');
|
||||
setExportModalOpen(false);
|
||||
}}
|
||||
className="w-full py-2.5 bg-slate-100 text-slate-700 rounded text-sm font-semibold hover:bg-slate-200 transition-colors"
|
||||
>导出 JSON 模板包</button>
|
||||
<button
|
||||
onClick={() => setExportModalOpen(false)}
|
||||
className="w-full py-2.5 border border-border text-text-main rounded text-sm font-semibold hover:bg-slate-50 transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user