feat(admin,rvw,asl,frontend): Batch import redesign + RVW parallel skills + UI improvements
Backend: - Redesign batch user import: add autoInheritModules param, users auto-inherit tenant modules when true - Add module validation: reject modules not subscribed by the tenant - Soften department validation: skip instead of fail when department name not found - Fix RVW skill status semantics: review findings (ERROR issues) no longer mark skill as error - Add parallel execution support to SkillExecutor via parallelGroup - Configure Editorial + Methodology skills to run in parallel (~240s -> ~130s) - Update legacy bridge error message to user-friendly text Frontend: - Redesign ImportUserModal: 4-step flow (select tenant -> upload -> preview -> result) - Simplify import template: remove tenant code and module columns - Show tenant subscribed modules before import with auto-inherit option - Fix isLegacyEmbed modules bypassing RouteGuard and TopNavigation permission checks - Hide ASL fulltext screening (step 3), renumber subsequent nav items - Add ExtractionWorkbenchGuide page when no taskId provided - Update legacy system error message to network-friendly text Docs: - Update deployment changelog with BE-9, FE-11 entries Made-with: Cursor
This commit is contained in:
@@ -91,10 +91,15 @@ export async function updateUserModules(userId: string, data: UpdateUserModulesR
|
||||
/**
|
||||
* 批量导入用户
|
||||
*/
|
||||
export async function importUsers(users: ImportUserRow[], defaultTenantId?: string): Promise<ImportResult> {
|
||||
export async function importUsers(
|
||||
users: ImportUserRow[],
|
||||
defaultTenantId: string,
|
||||
autoInheritModules: boolean = true
|
||||
): Promise<ImportResult> {
|
||||
const response = await apiClient.post<{ code: number; data: ImportResult }>(`${BASE_URL}/import`, {
|
||||
users,
|
||||
defaultTenantId,
|
||||
autoInheritModules,
|
||||
});
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* 批量导入用户弹窗
|
||||
* 4步流程:选择租户 → 下载模板/上传 → 预览确认 → 导入结果
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Upload,
|
||||
@@ -14,15 +15,39 @@ import {
|
||||
Typography,
|
||||
message,
|
||||
Divider,
|
||||
Steps,
|
||||
Tag,
|
||||
Checkbox,
|
||||
Descriptions,
|
||||
Spin,
|
||||
} from 'antd';
|
||||
import { InboxOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
InboxOutlined,
|
||||
DownloadOutlined,
|
||||
TeamOutlined,
|
||||
AppstoreOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as userApi from '../api/userApi';
|
||||
import type { TenantOption, ImportUserRow, ImportResult } from '../types/user';
|
||||
import type {
|
||||
TenantOption,
|
||||
ImportUserRow,
|
||||
ImportResult,
|
||||
ModuleOption,
|
||||
} from '../types/user';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
const { Text } = Typography;
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
type StepKey = 'tenant' | 'upload' | 'preview' | 'result';
|
||||
|
||||
const STEPS: { key: StepKey; title: string }[] = [
|
||||
{ key: 'tenant', title: '选择租户' },
|
||||
{ key: 'upload', title: '上传文件' },
|
||||
{ key: 'preview', title: '预览确认' },
|
||||
{ key: 'result', title: '导入结果' },
|
||||
];
|
||||
|
||||
interface ImportUserModalProps {
|
||||
visible: boolean;
|
||||
@@ -37,29 +62,47 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
onSuccess,
|
||||
tenantOptions,
|
||||
}) => {
|
||||
const [step, setStep] = useState<'upload' | 'preview' | 'result'>('upload');
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [selectedTenantId, setSelectedTenantId] = useState<string>();
|
||||
const [tenantModules, setTenantModules] = useState<ModuleOption[]>([]);
|
||||
const [loadingModules, setLoadingModules] = useState(false);
|
||||
const [autoInherit, setAutoInherit] = useState(true);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [parsedData, setParsedData] = useState<ImportUserRow[]>([]);
|
||||
const [defaultTenantId, setDefaultTenantId] = useState<string>();
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
||||
|
||||
// 重置状态
|
||||
const currentStepKey = STEPS[currentStep].key;
|
||||
const selectedTenant = tenantOptions.find((t) => t.id === selectedTenantId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTenantId) {
|
||||
setTenantModules([]);
|
||||
return;
|
||||
}
|
||||
setLoadingModules(true);
|
||||
userApi
|
||||
.getModuleOptions(selectedTenantId)
|
||||
.then((modules) => setTenantModules(modules))
|
||||
.catch(() => message.error('获取租户模块配置失败'))
|
||||
.finally(() => setLoadingModules(false));
|
||||
}, [selectedTenantId]);
|
||||
|
||||
const resetState = () => {
|
||||
setStep('upload');
|
||||
setCurrentStep(0);
|
||||
setSelectedTenantId(undefined);
|
||||
setTenantModules([]);
|
||||
setAutoInherit(true);
|
||||
setFileList([]);
|
||||
setParsedData([]);
|
||||
setDefaultTenantId(undefined);
|
||||
setImportResult(null);
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
resetState();
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 解析Excel文件
|
||||
const parseExcel = (file: File): Promise<ImportUserRow[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -69,17 +112,17 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
const workbook = XLSX.read(data, { type: 'binary' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json<any>(worksheet);
|
||||
|
||||
// 映射列名
|
||||
const jsonData = XLSX.utils.sheet_to_json<Record<string, unknown>>(worksheet);
|
||||
|
||||
const rows: ImportUserRow[] = jsonData.map((row) => ({
|
||||
phone: String(row['手机号'] || row['phone'] || '').trim(),
|
||||
name: String(row['姓名'] || row['name'] || '').trim(),
|
||||
email: row['邮箱'] || row['email'] || undefined,
|
||||
role: row['角色'] || row['role'] || undefined,
|
||||
tenantCode: row['租户代码'] || row['tenantCode'] || undefined,
|
||||
departmentName: row['科室'] || row['departmentName'] || undefined,
|
||||
modules: row['模块'] || row['modules'] || undefined,
|
||||
email: (row['邮箱'] || row['email'] || undefined) as string | undefined,
|
||||
role: (row['角色'] || row['role'] || undefined) as string | undefined,
|
||||
departmentName: (row['科室'] || row['departmentName'] || undefined) as string | undefined,
|
||||
modules: autoInherit
|
||||
? undefined
|
||||
: ((row['模块'] || row['modules'] || undefined) as string | undefined),
|
||||
}));
|
||||
|
||||
resolve(rows);
|
||||
@@ -92,7 +135,6 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 处理文件上传
|
||||
const handleUpload = async (file: File) => {
|
||||
try {
|
||||
const rows = await parseExcel(file);
|
||||
@@ -101,91 +143,209 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
return false;
|
||||
}
|
||||
setParsedData(rows);
|
||||
setStep('preview');
|
||||
} catch (error) {
|
||||
message.error('解析文件失败');
|
||||
setCurrentStep(2);
|
||||
} catch {
|
||||
message.error('解析文件失败,请检查文件格式');
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 执行导入
|
||||
const handleImport = async () => {
|
||||
if (!defaultTenantId) {
|
||||
message.warning('请选择默认租户');
|
||||
return;
|
||||
}
|
||||
if (!selectedTenantId) return;
|
||||
|
||||
setImporting(true);
|
||||
try {
|
||||
const result = await userApi.importUsers(parsedData, defaultTenantId);
|
||||
const result = await userApi.importUsers(parsedData, selectedTenantId, autoInherit);
|
||||
setImportResult(result);
|
||||
setStep('result');
|
||||
setCurrentStep(3);
|
||||
if (result.success > 0) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
message.error('导入失败');
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = () => {
|
||||
const template = [
|
||||
const baseTemplate = [
|
||||
{
|
||||
'手机号': '13800138000',
|
||||
'姓名': '张三',
|
||||
'邮箱': 'zhangsan@example.com',
|
||||
'角色': 'USER',
|
||||
'租户代码': '',
|
||||
'科室': '',
|
||||
'模块': 'AIA,PKB',
|
||||
},
|
||||
];
|
||||
|
||||
const template = autoInherit
|
||||
? baseTemplate
|
||||
: baseTemplate.map((row) => ({
|
||||
...row,
|
||||
'模块': subscribedModuleCodes,
|
||||
}));
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(template);
|
||||
|
||||
// 设置列宽
|
||||
ws['!cols'] = [
|
||||
{ wch: 15 },
|
||||
{ wch: 10 },
|
||||
{ wch: 25 },
|
||||
{ wch: 10 },
|
||||
{ wch: 15 },
|
||||
...(autoInherit ? [] : [{ wch: 20 }]),
|
||||
];
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, '用户导入模板');
|
||||
XLSX.writeFile(wb, '用户导入模板.xlsx');
|
||||
XLSX.writeFile(wb, `用户导入模板_${selectedTenant?.name || ''}.xlsx`);
|
||||
};
|
||||
|
||||
// 预览表格列
|
||||
const subscribedModules = tenantModules.filter((m) => m.isSubscribed);
|
||||
const subscribedModuleCodes = subscribedModules.map((m) => m.code).join(',');
|
||||
|
||||
const previewColumns = [
|
||||
{ title: '行号', key: 'rowNum', width: 60, render: (_: unknown, __: unknown, index: number) => index + 2 },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email', width: 180 },
|
||||
{ title: '角色', dataIndex: 'role', key: 'role', width: 100 },
|
||||
{ title: '租户代码', dataIndex: 'tenantCode', key: 'tenantCode', width: 100 },
|
||||
{ title: '角色', dataIndex: 'role', key: 'role', width: 100, render: (v: string) => v || 'USER' },
|
||||
{ title: '科室', dataIndex: 'departmentName', key: 'departmentName', width: 100 },
|
||||
{ title: '模块', dataIndex: 'modules', key: 'modules', width: 120 },
|
||||
...(!autoInherit
|
||||
? [{ title: '模块', dataIndex: 'modules', key: 'modules', width: 120 }]
|
||||
: []),
|
||||
];
|
||||
|
||||
// 错误表格列
|
||||
const errorColumns = [
|
||||
{ title: '行号', dataIndex: 'row', key: 'row', width: 80 },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
||||
{ title: '错误原因', dataIndex: 'error', key: 'error' },
|
||||
];
|
||||
|
||||
const canGoNext = () => {
|
||||
if (currentStepKey === 'tenant') return !!selectedTenantId;
|
||||
if (currentStepKey === 'upload') return parsedData.length > 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="批量导入用户"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
width={800}
|
||||
width={860}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
>
|
||||
{step === 'upload' && (
|
||||
<Steps
|
||||
current={currentStep}
|
||||
size="small"
|
||||
style={{ marginBottom: 24 }}
|
||||
items={STEPS.map((s) => ({ title: s.title }))}
|
||||
/>
|
||||
|
||||
{/* Step 1: 选择租户 */}
|
||||
{currentStepKey === 'tenant' && (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>选择导入目标租户:</Text>
|
||||
<Select
|
||||
placeholder="请选择租户"
|
||||
value={selectedTenantId}
|
||||
onChange={setSelectedTenantId}
|
||||
style={{ width: 360, marginLeft: 12 }}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={tenantOptions.map((t) => ({
|
||||
value: t.id,
|
||||
label: `${t.name} (${t.code})`,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedTenantId && (
|
||||
<Spin spinning={loadingModules}>
|
||||
<Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
column={1}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<Space>
|
||||
<TeamOutlined />
|
||||
<span>租户名称</span>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{selectedTenant?.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<Space>
|
||||
<AppstoreOutlined />
|
||||
<span>已开通模块</span>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{subscribedModules.length > 0 ? (
|
||||
<Space wrap>
|
||||
{subscribedModules.map((m) => (
|
||||
<Tag color="blue" key={m.code}>
|
||||
{m.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">该租户暂无已开通模块</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Alert
|
||||
message="模块权限说明"
|
||||
description="导入的用户将自动获得该租户已开通的全部模块权限。如需为每个用户单独配置模块,可在下一步取消「自动继承」选项。"
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
</Spin>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={handleClose}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canGoNext()}
|
||||
onClick={() => setCurrentStep(1)}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 2: 下载模板与上传 */}
|
||||
{currentStepKey === 'upload' && (
|
||||
<>
|
||||
<Alert
|
||||
message="导入说明"
|
||||
description={
|
||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||
<li>支持 .xlsx 或 .xls 格式的Excel文件</li>
|
||||
<li>
|
||||
目标租户:<Tag color="blue">{selectedTenant?.name}</Tag>
|
||||
</li>
|
||||
<li>支持 .xlsx 或 .xls 格式的 Excel 文件</li>
|
||||
<li>必填字段:手机号、姓名</li>
|
||||
<li>角色可选值:SUPER_ADMIN、HOSPITAL_ADMIN、PHARMA_ADMIN、DEPARTMENT_ADMIN、USER</li>
|
||||
<li>模块字段使用逗号分隔,如:AIA,PKB,RVW</li>
|
||||
<li>角色可选值:SUPER_ADMIN、HOSPITAL_ADMIN、PHARMA_ADMIN、DEPARTMENT_ADMIN、USER(默认 USER)</li>
|
||||
{!autoInherit && (
|
||||
<li>模块字段使用逗号分隔,如:{subscribedModuleCodes}</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
type="info"
|
||||
@@ -193,6 +353,20 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Checkbox
|
||||
checked={autoInherit}
|
||||
onChange={(e) => setAutoInherit(e.target.checked)}
|
||||
>
|
||||
自动继承租户模块配置(推荐)
|
||||
</Checkbox>
|
||||
{!autoInherit && (
|
||||
<Text type="warning" style={{ display: 'block', marginTop: 4, marginLeft: 24 }}>
|
||||
取消勾选后,需在 Excel 中为每个用户填写「模块」列(仅限租户已开通的模块)
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button icon={<DownloadOutlined />} onClick={downloadTemplate}>
|
||||
下载导入模板
|
||||
@@ -203,7 +377,7 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
accept=".xlsx,.xls"
|
||||
fileList={fileList}
|
||||
beforeUpload={handleUpload}
|
||||
onChange={({ fileList }) => setFileList(fileList)}
|
||||
onChange={({ fileList: fl }) => setFileList(fl)}
|
||||
maxCount={1}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
@@ -212,46 +386,59 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||
<p className="ant-upload-hint">支持 .xlsx 或 .xls 格式</p>
|
||||
</Dragger>
|
||||
|
||||
<Divider />
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => { setCurrentStep(0); setFileList([]); setParsedData([]); }}>
|
||||
上一步
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 'preview' && (
|
||||
{/* Step 3: 预览确认 */}
|
||||
{currentStepKey === 'preview' && (
|
||||
<>
|
||||
<Alert
|
||||
message={`共解析 ${parsedData.length} 条数据,请确认后导入`}
|
||||
message={
|
||||
<Space>
|
||||
<span>共解析 <Text strong>{parsedData.length}</Text> 条数据</span>
|
||||
<Divider type="vertical" />
|
||||
<span>
|
||||
目标租户:<Tag color="blue">{selectedTenant?.name}</Tag>
|
||||
</span>
|
||||
<Divider type="vertical" />
|
||||
<span>
|
||||
模块权限:
|
||||
{autoInherit ? (
|
||||
<Tag color="green">自动继承租户配置</Tag>
|
||||
) : (
|
||||
<Tag color="orange">按 Excel 指定</Tag>
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Text>默认租户:</Text>
|
||||
<Select
|
||||
placeholder="请选择默认租户"
|
||||
value={defaultTenantId}
|
||||
onChange={setDefaultTenantId}
|
||||
style={{ width: 250 }}
|
||||
options={tenantOptions.map((t) => ({
|
||||
value: t.id,
|
||||
label: `${t.name} (${t.code})`,
|
||||
}))}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={previewColumns}
|
||||
dataSource={parsedData.map((row, i) => ({ ...row, key: i }))}
|
||||
size="small"
|
||||
scroll={{ x: 800, y: 300 }}
|
||||
scroll={{ x: 700, y: 300 }}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => setStep('upload')}>上一步</Button>
|
||||
<Button onClick={() => { setCurrentStep(1); setFileList([]); setParsedData([]); }}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button type="primary" loading={importing} onClick={handleImport}>
|
||||
开始导入
|
||||
</Button>
|
||||
@@ -259,7 +446,8 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 'result' && importResult && (
|
||||
{/* Step 4: 导入结果 */}
|
||||
{currentStepKey === 'result' && importResult && (
|
||||
<>
|
||||
<Alert
|
||||
message={`导入完成:成功 ${importResult.success} 条,失败 ${importResult.failed} 条`}
|
||||
@@ -268,11 +456,18 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
{autoInherit && importResult.success > 0 && (
|
||||
<Alert
|
||||
message={`已成功导入的 ${importResult.success} 位用户自动继承租户「${selectedTenant?.name}」的全部模块权限`}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{importResult.errors.length > 0 && (
|
||||
<>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>
|
||||
失败记录:
|
||||
</Text>
|
||||
<Title level={5}>失败记录:</Title>
|
||||
<Table
|
||||
columns={errorColumns}
|
||||
dataSource={importResult.errors.map((e, i) => ({ ...e, key: i }))}
|
||||
@@ -298,4 +493,3 @@ const ImportUserModal: React.FC<ImportUserModalProps> = ({
|
||||
};
|
||||
|
||||
export default ImportUserModal;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
FileSearchOutlined,
|
||||
DatabaseOutlined,
|
||||
BarChartOutlined,
|
||||
SettingOutlined,
|
||||
@@ -57,32 +56,10 @@ const ASLLayout = () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'fulltext-screening',
|
||||
icon: <FileSearchOutlined />,
|
||||
label: '3. 全文复筛',
|
||||
children: [
|
||||
{
|
||||
key: '/literature/screening/fulltext/settings',
|
||||
icon: <SettingOutlined />,
|
||||
label: '设置与启动',
|
||||
},
|
||||
{
|
||||
key: '/literature/screening/fulltext/workbench',
|
||||
icon: <CheckSquareOutlined />,
|
||||
label: '审核工作台',
|
||||
},
|
||||
{
|
||||
key: '/literature/screening/fulltext/results',
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: '复筛结果',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'extraction',
|
||||
icon: <DatabaseOutlined />,
|
||||
label: '4. 全文智能提取',
|
||||
label: '3. 全文智能提取',
|
||||
children: [
|
||||
{
|
||||
key: '/literature/extraction/setup',
|
||||
@@ -99,12 +76,12 @@ const ASLLayout = () => {
|
||||
{
|
||||
key: '/literature/charting',
|
||||
icon: <ApartmentOutlined />,
|
||||
label: '5. SR 图表生成器',
|
||||
label: '4. SR 图表生成器',
|
||||
},
|
||||
{
|
||||
key: '/literature/meta-analysis',
|
||||
icon: <BarChartOutlined />,
|
||||
label: '6. Meta 分析引擎',
|
||||
label: '5. Meta 分析引擎',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -122,7 +99,6 @@ const ASLLayout = () => {
|
||||
// 根据当前路径确定展开的菜单
|
||||
const getOpenKeys = () => {
|
||||
if (currentPath.includes('screening/title')) return ['title-screening'];
|
||||
if (currentPath.includes('screening/fulltext')) return ['fulltext-screening'];
|
||||
if (currentPath.includes('/extraction')) return ['extraction'];
|
||||
if (currentPath.includes('/charting')) return [];
|
||||
if (currentPath.includes('/meta-analysis')) return [];
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Spin } from 'antd';
|
||||
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
|
||||
import { Spin, Result, Button } from 'antd';
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
|
||||
// 懒加载组件
|
||||
const ASLLayout = lazy(() => import('./components/ASLLayout'));
|
||||
@@ -36,6 +37,22 @@ const SRChartGenerator = lazy(() => import('./pages/SRChartGenerator'));
|
||||
// 工具 5:Meta 分析引擎
|
||||
const MetaAnalysisEngine = lazy(() => import('./pages/MetaAnalysisEngine'));
|
||||
|
||||
const ExtractionWorkbenchGuide = () => {
|
||||
const nav = useNavigate();
|
||||
return (
|
||||
<Result
|
||||
icon={<SettingOutlined style={{ color: '#1890ff' }} />}
|
||||
title="请先配置提取任务"
|
||||
subTitle="审核工作台需要在「配置与启动」中创建并完成提取任务后才可使用。请先前往配置页面上传文献并启动提取。"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => nav('/literature/extraction/setup')}>
|
||||
前往配置与启动
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ASLModule = () => {
|
||||
return (
|
||||
<Suspense
|
||||
@@ -77,6 +94,7 @@ const ASLModule = () => {
|
||||
<Route index element={<Navigate to="setup" replace />} />
|
||||
<Route path="setup" element={<ExtractionSetup />} />
|
||||
<Route path="progress/:taskId" element={<ExtractionProgress />} />
|
||||
<Route path="workbench" element={<ExtractionWorkbenchGuide />} />
|
||||
<Route path="workbench/:taskId" element={<ExtractionWorkbench />} />
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -51,10 +51,9 @@ const LegacySystemPage: React.FC<LegacySystemPageProps> = ({ targetUrl }) => {
|
||||
authDoneRef.current = true
|
||||
setStatus('ready')
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || err?.message || '服务连接失败,请稍后重试'
|
||||
const msg = err?.response?.data?.message || '网络连接问题,请点击下方按钮重试'
|
||||
setErrorMsg(msg)
|
||||
setStatus('error')
|
||||
message.error(msg)
|
||||
}
|
||||
}, [targetUrl])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user