refactor(admin): 按 aislop 质量门禁修复两个代码质量警告
- RoleTour: buildSteps 拆分为各角色的声明式步骤常量,函数从 91 行降至组装逻辑 - ImportWizardModal: 抽取 uploadRun 共享 helper,消除 handleUpload/handleReupload 之间重复的上传进度与 loading 状态处理 aislop scan --changes: 99/100 → 100/100 (5 引擎 0 issues)
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
|||||||
type ImportPreviewResult,
|
type ImportPreviewResult,
|
||||||
type ImportReceipt,
|
type ImportReceipt,
|
||||||
type ImportRunDetail,
|
type ImportRunDetail,
|
||||||
|
type ImportStageRequest,
|
||||||
type ImportStepKey,
|
type ImportStepKey,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
@@ -187,25 +188,41 @@ export const ImportWizardModal: React.FC<ImportWizardModalProps> = ({
|
|||||||
return [...headers];
|
return [...headers];
|
||||||
}, [run, activeStepKey, sheetSelection]);
|
}, [run, activeStepKey, sheetSelection]);
|
||||||
|
|
||||||
const handleUpload: UploadProps['customRequest'] = async (options) => {
|
/** 创建导入任务并加载详情:统一处理上传进度与 loading 状态。成功返回 run 详情,失败返回 null */
|
||||||
const file = options.file as File;
|
const uploadRun = async (
|
||||||
|
file: File,
|
||||||
|
options: {
|
||||||
|
source: 'ai' | 'manual';
|
||||||
|
conversationId?: number;
|
||||||
|
stages?: ImportStageRequest[];
|
||||||
|
mapping?: Record<string, Record<string, string>>;
|
||||||
|
},
|
||||||
|
errorMessage: string,
|
||||||
|
): Promise<ImportRunDetail | null> => {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
setUploadPercent(0);
|
setUploadPercent(0);
|
||||||
try {
|
try {
|
||||||
const detail = await createImportRun(file, {
|
const detail = await createImportRun(file, {
|
||||||
source: 'manual',
|
...options,
|
||||||
onProgress: (percent) => setUploadPercent(percent),
|
onProgress: (percent) => setUploadPercent(percent),
|
||||||
});
|
});
|
||||||
await loadRun(detail.id);
|
await loadRun(detail.id);
|
||||||
message.success(`已识别 ${detail.sheets.length} 个工作表`);
|
return detail;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '文件上传失败');
|
message.error(error instanceof Error ? error.message : errorMessage);
|
||||||
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
setUploadPercent(0);
|
setUploadPercent(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpload: UploadProps['customRequest'] = async (options) => {
|
||||||
|
const file = options.file as File;
|
||||||
|
const detail = await uploadRun(file, { source: 'manual' }, '文件上传失败');
|
||||||
|
if (detail) message.success(`已识别 ${detail.sheets.length} 个工作表`);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePreview = async () => {
|
const handlePreview = async () => {
|
||||||
if (!run || !activeStepKey || !activeStep) return;
|
if (!run || !activeStepKey || !activeStep) return;
|
||||||
const mapping = mappingDraft[activeStepKey] ?? {};
|
const mapping = mappingDraft[activeStepKey] ?? {};
|
||||||
@@ -262,23 +279,16 @@ export const ImportWizardModal: React.FC<ImportWizardModalProps> = ({
|
|||||||
|
|
||||||
const handleReupload = async (file: File) => {
|
const handleReupload = async (file: File) => {
|
||||||
if (!run || !activeStepKey) return;
|
if (!run || !activeStepKey) return;
|
||||||
setUploading(true);
|
const detail = await uploadRun(
|
||||||
setUploadPercent(0);
|
file,
|
||||||
try {
|
{
|
||||||
const detail = await createImportRun(file, {
|
|
||||||
source: 'manual',
|
source: 'manual',
|
||||||
stages: [{ stepKey: activeStepKey, sheets: sheetSelection[activeStepKey] ?? [] }],
|
stages: [{ stepKey: activeStepKey, sheets: sheetSelection[activeStepKey] ?? [] }],
|
||||||
mapping: { [activeStepKey]: mappingDraft[activeStepKey] ?? {} },
|
mapping: { [activeStepKey]: mappingDraft[activeStepKey] ?? {} },
|
||||||
onProgress: (percent) => setUploadPercent(percent),
|
},
|
||||||
});
|
'重新上传失败',
|
||||||
await loadRun(detail.id);
|
);
|
||||||
message.success('已重新上传,并保留原列映射');
|
if (detail) message.success('已重新上传,并保留原列映射');
|
||||||
} catch (error) {
|
|
||||||
message.error(error instanceof Error ? error.message : '重新上传失败');
|
|
||||||
} finally {
|
|
||||||
setUploading(false);
|
|
||||||
setUploadPercent(0);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const previewRows = useMemo(() => {
|
const previewRows = useMemo(() => {
|
||||||
|
|||||||
@@ -25,94 +25,100 @@ function menuTarget(label: string): () => HTMLElement {
|
|||||||
const aiTarget = (): HTMLElement =>
|
const aiTarget = (): HTMLElement =>
|
||||||
document.querySelector<HTMLElement>('button[aria-label="打开 AI 助理"]') as HTMLElement;
|
document.querySelector<HTMLElement>('button[aria-label="打开 AI 助理"]') as HTMLElement;
|
||||||
|
|
||||||
function buildSteps(domains: Set<string>): TourStep[] {
|
/** 各角色的引导步骤(声明式,按角色域组装) */
|
||||||
const steps: TourStep[] = [];
|
const TEACHER_STEPS: TourStep[] = [
|
||||||
const has = (key: string) => domains.has(key);
|
{
|
||||||
|
target: menuTarget('今日教学'),
|
||||||
|
title: '今日教学',
|
||||||
|
description: '在这里查看今天的课程安排。课程开始后可以拉取钉钉考勤并点名。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: menuTarget('课程考勤'),
|
||||||
|
title: '课程考勤',
|
||||||
|
description: '查看历史考勤记录,课程截止后系统会自动结算缺勤。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (has('teacher')) {
|
const ACADEMIC_STEPS: TourStep[] = [
|
||||||
steps.push(
|
{
|
||||||
{
|
target: menuTarget('学生管理'),
|
||||||
target: menuTarget('今日教学'),
|
title: '学生管理',
|
||||||
title: '今日教学',
|
description: '管理学生档案:可单个录入、Excel 批量导入,或用 AI 助手帮你录入。',
|
||||||
description: '在这里查看今天的课程安排。课程开始后可以拉取钉钉考勤并点名。',
|
},
|
||||||
},
|
{
|
||||||
{
|
target: menuTarget('班级管理'),
|
||||||
target: menuTarget('课程考勤'),
|
title: '班级管理',
|
||||||
title: '课程考勤',
|
description: '学生入学后先分班,再排课、考勤,形成完整教学闭环。',
|
||||||
description: '查看历史考勤记录,课程截止后系统会自动结算缺勤。',
|
},
|
||||||
},
|
];
|
||||||
);
|
|
||||||
}
|
|
||||||
if (has('academic')) {
|
|
||||||
steps.push(
|
|
||||||
{
|
|
||||||
target: menuTarget('学生管理'),
|
|
||||||
title: '学生管理',
|
|
||||||
description: '管理学生档案:可单个录入、Excel 批量导入,或用 AI 助手帮你录入。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: menuTarget('班级管理'),
|
|
||||||
title: '班级管理',
|
|
||||||
description: '学生入学后先分班,再排课、考勤,形成完整教学闭环。',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (has('accommodation')) {
|
|
||||||
steps.push(
|
|
||||||
{
|
|
||||||
target: menuTarget('住宿总览'),
|
|
||||||
title: '住宿总览',
|
|
||||||
description: '可视化查看各宿舍入住情况,支持历史日期回溯和查寝。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: menuTarget('入住管理'),
|
|
||||||
title: '入住管理',
|
|
||||||
description: '学生入住/退宿/换宿都在这里办理。入住后再录费用、生成账单。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: menuTarget('账单管理'),
|
|
||||||
title: '账单管理',
|
|
||||||
description: '每月生成账单、确认并标记已付,完成「住宿→计费」闭环。',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (has('classroom')) {
|
|
||||||
steps.push(
|
|
||||||
{
|
|
||||||
target: menuTarget('教室排期'),
|
|
||||||
title: '教室排期',
|
|
||||||
description: '查看教室占用与排期,避免时间冲突。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: menuTarget('租赁订单'),
|
|
||||||
title: '租赁订单',
|
|
||||||
description: '管理教室租赁订单与合同,跟踪租期与状态。',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (has('system') || has('super')) {
|
|
||||||
steps.push(
|
|
||||||
{
|
|
||||||
target: menuTarget('账号管理'),
|
|
||||||
title: '账号管理',
|
|
||||||
description: '管理系统账号、角色与权限,为不同岗位分配对应能力。',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (has('super')) {
|
|
||||||
steps.unshift({
|
|
||||||
target: menuTarget('数据面板'),
|
|
||||||
title: '数据面板',
|
|
||||||
description: '全局经营概览:入住率、收入、待办都在这里。',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
steps.push({
|
const ACCOMMODATION_STEPS: TourStep[] = [
|
||||||
|
{
|
||||||
|
target: menuTarget('住宿总览'),
|
||||||
|
title: '住宿总览',
|
||||||
|
description: '可视化查看各宿舍入住情况,支持历史日期回溯和查寝。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: menuTarget('入住管理'),
|
||||||
|
title: '入住管理',
|
||||||
|
description: '学生入住/退宿/换宿都在这里办理。入住后再录费用、生成账单。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: menuTarget('账单管理'),
|
||||||
|
title: '账单管理',
|
||||||
|
description: '每月生成账单、确认并标记已付,完成「住宿→计费」闭环。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CLASSROOM_STEPS: TourStep[] = [
|
||||||
|
{
|
||||||
|
target: menuTarget('教室排期'),
|
||||||
|
title: '教室排期',
|
||||||
|
description: '查看教室占用与排期,避免时间冲突。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: menuTarget('租赁订单'),
|
||||||
|
title: '租赁订单',
|
||||||
|
description: '管理教室租赁订单与合同,跟踪租期与状态。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SYSTEM_STEPS: TourStep[] = [
|
||||||
|
{
|
||||||
|
target: menuTarget('账号管理'),
|
||||||
|
title: '账号管理',
|
||||||
|
description: '管理系统账号、角色与权限,为不同岗位分配对应能力。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SUPER_STEPS: TourStep[] = [
|
||||||
|
{
|
||||||
|
target: menuTarget('数据面板'),
|
||||||
|
title: '数据面板',
|
||||||
|
description: '全局经营概览:入住率、收入、待办都在这里。',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const AI_STEPS: TourStep[] = [
|
||||||
|
{
|
||||||
target: aiTarget,
|
target: aiTarget,
|
||||||
title: 'AI 助手',
|
title: 'AI 助手',
|
||||||
description:
|
description:
|
||||||
'有任何问题都可以问我。我可以帮你查询数据、录入学生、生成批量导入预览——写操作都会先经你确认。',
|
'有任何问题都可以问我。我可以帮你查询数据、录入学生、生成批量导入预览——写操作都会先经你确认。',
|
||||||
});
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildSteps(domains: Set<string>): TourStep[] {
|
||||||
|
const steps: TourStep[] = [];
|
||||||
|
const has = (key: string) => domains.has(key);
|
||||||
|
|
||||||
|
if (has('super')) steps.unshift(...SUPER_STEPS);
|
||||||
|
if (has('teacher')) steps.push(...TEACHER_STEPS);
|
||||||
|
if (has('academic')) steps.push(...ACADEMIC_STEPS);
|
||||||
|
if (has('accommodation')) steps.push(...ACCOMMODATION_STEPS);
|
||||||
|
if (has('classroom')) steps.push(...CLASSROOM_STEPS);
|
||||||
|
if (has('system') || has('super')) steps.push(...SYSTEM_STEPS);
|
||||||
|
steps.push(...AI_STEPS);
|
||||||
|
|
||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user