From bcbf3c8b2e48f49943350f250f26a086b88799c4 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Fri, 7 Aug 2026 17:43:00 +0800 Subject: [PATCH] =?UTF-8?q?refactor(admin):=20=E6=8C=89=20aislop=20?= =?UTF-8?q?=E8=B4=A8=E9=87=8F=E9=97=A8=E7=A6=81=E4=BF=AE=E5=A4=8D=E4=B8=A4?= =?UTF-8?q?=E4=B8=AA=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RoleTour: buildSteps 拆分为各角色的声明式步骤常量,函数从 91 行降至组装逻辑 - ImportWizardModal: 抽取 uploadRun 共享 helper,消除 handleUpload/handleReupload 之间重复的上传进度与 loading 状态处理 aislop scan --changes: 99/100 → 100/100 (5 引擎 0 issues) --- .../ImportWizard/ImportWizardModal.tsx | 48 +++-- .../src/components/Onboarding/RoleTour.tsx | 170 +++++++++--------- 2 files changed, 117 insertions(+), 101 deletions(-) diff --git a/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx b/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx index 66d8957..eb6b2e3 100644 --- a/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx +++ b/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx @@ -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 = ({ 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>; + }, + errorMessage: string, + ): Promise => { 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 = ({ 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(() => { diff --git a/apps/admin/src/components/Onboarding/RoleTour.tsx b/apps/admin/src/components/Onboarding/RoleTour.tsx index cbbf197..9a8a394 100644 --- a/apps/admin/src/components/Onboarding/RoleTour.tsx +++ b/apps/admin/src/components/Onboarding/RoleTour.tsx @@ -25,94 +25,100 @@ function menuTarget(label: string): () => HTMLElement { const aiTarget = (): HTMLElement => document.querySelector('button[aria-label="打开 AI 助理"]') as HTMLElement; -function buildSteps(domains: Set): 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): 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; }