feat(plugin): P1-P4 审计修复 — 第一批 (Excel/CSV导出 + 市场API + 对账扫描)
1.1 Excel/CSV 导出:
- 后端 export 支持 format 参数 (json/csv/xlsx)
- rust_xlsxwriter 生成带样式 Excel
- 前端导出按钮改为 Dropdown 格式选择 (JSON/CSV/Excel)
- blob 下载支持 CSV/XLSX 二进制格式
1.2 市场后端 API + 前端对接:
- SeaORM Entity: market_entry, market_review
- API: 浏览/详情/一键安装/评论列表/提交评分
- 一键安装: upload → install → enable 一条龙 + 依赖检查
- 前端 PluginMarket 对接真实 API (搜索/分类/安装/评分)
1.3 对账扫描:
- reconcile_references() 扫描跨插件引用悬空 UUID
- POST /plugins/{plugin_id}/reconcile 端点
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
Timeline,
|
||||
Upload,
|
||||
Alert,
|
||||
Dropdown,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
batchPluginData,
|
||||
resolveRefLabels,
|
||||
exportPluginData,
|
||||
exportPluginDataAsBlob,
|
||||
importPluginData,
|
||||
type PluginDataListOptions,
|
||||
type ImportResult,
|
||||
@@ -576,32 +578,54 @@ export default function PluginCRUDPage({
|
||||
刷新
|
||||
</Button>
|
||||
{entityDef?.exportable && (
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
loading={exporting}
|
||||
onClick={async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const rows = await exportPluginData(pluginId, entityName, {
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
});
|
||||
const blob = new Blob([JSON.stringify(rows, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${entityName}_export_${Date.now()}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
message.success(`导出 ${rows.length} 条记录`);
|
||||
} catch {
|
||||
message.error('导出失败');
|
||||
}
|
||||
setExporting(false);
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'json', label: 'JSON' },
|
||||
{ key: 'csv', label: 'CSV' },
|
||||
{ key: 'xlsx', label: 'Excel (.xlsx)' },
|
||||
],
|
||||
onClick: async ({ key }) => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const ts = Date.now();
|
||||
if (key === 'json') {
|
||||
const rows = await exportPluginData(pluginId, entityName, {
|
||||
sort_by: sortBy,
|
||||
sort_order: sortOrder,
|
||||
});
|
||||
const blob = new Blob([JSON.stringify(rows, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${entityName}_export_${ts}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
message.success(`导出 ${rows.length} 条记录`);
|
||||
} else {
|
||||
const blob = await exportPluginDataAsBlob(
|
||||
pluginId, entityName, key as 'csv' | 'xlsx',
|
||||
{ sort_by: sortBy, sort_order: sortOrder },
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${entityName}_export_${ts}.${key === 'csv' ? 'csv' : 'xlsx'}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
message.success('导出成功');
|
||||
}
|
||||
} catch {
|
||||
message.error('导出失败');
|
||||
}
|
||||
setExporting(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
导出
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} loading={exporting}>
|
||||
导出
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
{entityDef?.importable && (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user