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:
2026-08-07 17:43:00 +08:00
parent ff3b6cdfde
commit bcbf3c8b2e
2 changed files with 117 additions and 101 deletions

View File

@@ -40,6 +40,7 @@ import {
type ImportPreviewResult,
type ImportReceipt,
type ImportRunDetail,
type ImportStageRequest,
type ImportStepKey,
} from './types';
@@ -187,25 +188,41 @@ export const ImportWizardModal: React.FC<ImportWizardModalProps> = ({
return [...headers];
}, [run, activeStepKey, sheetSelection]);
const handleUpload: UploadProps['customRequest'] = async (options) => {
const file = options.file as File;
/** 创建导入任务并加载详情:统一处理上传进度与 loading 状态。成功返回 run 详情,失败返回 null */
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);
setUploadPercent(0);
try {
const detail = await createImportRun(file, {
source: 'manual',
...options,
onProgress: (percent) => setUploadPercent(percent),
});
await loadRun(detail.id);
message.success(`已识别 ${detail.sheets.length} 个工作表`);
return detail;
} catch (error) {
message.error(error instanceof Error ? error.message : '文件上传失败');
message.error(error instanceof Error ? error.message : errorMessage);
return null;
} finally {
setUploading(false);
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 () => {
if (!run || !activeStepKey || !activeStep) return;
const mapping = mappingDraft[activeStepKey] ?? {};
@@ -262,23 +279,16 @@ export const ImportWizardModal: React.FC<ImportWizardModalProps> = ({
const handleReupload = async (file: File) => {
if (!run || !activeStepKey) return;
setUploading(true);
setUploadPercent(0);
try {
const detail = await createImportRun(file, {
const detail = await uploadRun(
file,
{
source: 'manual',
stages: [{ stepKey: activeStepKey, sheets: sheetSelection[activeStepKey] ?? [] }],
mapping: { [activeStepKey]: mappingDraft[activeStepKey] ?? {} },
onProgress: (percent) => setUploadPercent(percent),
});
await loadRun(detail.id);
message.success('已重新上传,并保留原列映射');
} catch (error) {
message.error(error instanceof Error ? error.message : '重新上传失败');
} finally {
setUploading(false);
setUploadPercent(0);
}
},
'重新上传失败',
);
if (detail) message.success('已重新上传,并保留原列映射');
};
const previewRows = useMemo(() => {

View File

@@ -25,94 +25,100 @@ function menuTarget(label: string): () => HTMLElement {
const aiTarget = (): HTMLElement =>
document.querySelector<HTMLElement>('button[aria-label="打开 AI 助理"]') as HTMLElement;
function buildSteps(domains: Set<string>): TourStep[] {
const steps: TourStep[] = [];
const has = (key: string) => domains.has(key);
/** 各角色的引导步骤(声明式,按角色域组装) */
const TEACHER_STEPS: TourStep[] = [
{
target: menuTarget('今日教学'),
title: '今日教学',
description: '在这里查看今天的课程安排。课程开始后可以拉取钉钉考勤并点名。',
},
{
target: menuTarget('课程考勤'),
title: '课程考勤',
description: '查看历史考勤记录,课程截止后系统会自动结算缺勤。',
},
];
if (has('teacher')) {
steps.push(
{
target: menuTarget('今日教学'),
title: '今日教学',
description: '在这里查看今天的课程安排。课程开始后可以拉取钉钉考勤并点名。',
},
{
target: menuTarget('课程考勤'),
title: '课程考勤',
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: '全局经营概览:入住率、收入、待办都在这里。',
});
}
const ACADEMIC_STEPS: TourStep[] = [
{
target: menuTarget('学生管理'),
title: '学生管理',
description: '管理学生档案可单个录入、Excel 批量导入,或用 AI 助手帮你录入。',
},
{
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,
title: 'AI 助手',
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;
}