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:
2026-05-02 04:21:08 +08:00
parent 7631ae34ce
commit 03b54fe18b
14 changed files with 37 additions and 195 deletions

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"