feat: Excel 导入预检与动态问答,AI 聊天/文件解析体验修复

- imports: 新增 preflight_import 预检报告(判定/分阶段统计/阻断归因/问题/下一步/错误示例),导入任务 settings 落库(映射/校区/更新/重复/未匹配策略),预览应用策略,向导提交写操作日志
- ai-chat: 新增 excel_analyze(ExcelJS)工具,移除附件/上下文截断,start_import_wizard 支持确认参数,ui.import_preflight SSE,预览确认写操作日志
- admin: ImportPreflightCard 渲染与持久化,聊天抽屉布局/侧边栏修复,考勤页 CSS 引入,费用/学生页接口 schema 校验修复
This commit is contained in:
2026-08-05 21:12:50 +08:00
parent b70f45fb04
commit ab4765cee5
44 changed files with 2449 additions and 166 deletions

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { TableOutlined } from '@ant-design/icons';
import { Card, Flex, Space, Tag, Typography } from 'antd';
import type {
AiImportPreflight,
AiImportPreflightVerdict,
} from './types';
const VERDICT_META: Record<
AiImportPreflightVerdict,
{ label: string; color: string }
> = {
ready: { label: '可导入', color: 'success' },
needs_input: { label: '需要确认', color: 'warning' },
blocked: { label: '暂无法导入', color: 'error' },
};
/**
* 上传 Excel 后的“可插入性预检报告”卡片:展示判定结论、
* 分阶段统计、阻断原因、待确认问题与下一步建议。
*/
export const ImportPreflightCard: React.FC<{ preflight: AiImportPreflight }> = ({
preflight,
}) => {
const verdict = VERDICT_META[preflight.verdict] ?? VERDICT_META.needs_input;
return (
<Card
size="small"
className="ai-chat-preflight-card"
title={
<Flex gap={8} align="center" wrap>
<TableOutlined />
<Typography.Text strong>Excel </Typography.Text>
<Tag color={verdict.color}>{verdict.label}</Tag>
</Flex>
}
>
<Space orientation="vertical" size={12} style={{ width: '100%' }}>
{preflight.stages.length > 0 ? (
<Space orientation="vertical" size={10} style={{ width: '100%' }}>
{preflight.stages.map((stage) => (
<Flex key={stage.stepKey} vertical gap={4}>
<Flex gap={8} align="center" wrap>
<Typography.Text strong>{stage.label}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{stage.sheetNames.join('、')}
</Typography.Text>
</Flex>
<Space size={4} wrap>
<Tag color="blue"> {stage.total} </Tag>
<Tag color="green"> {stage.create}</Tag>
<Tag color="orange"> {stage.update}</Tag>
<Tag color="red"> {stage.error}</Tag>
<Tag> {stage.skip}</Tag>
</Space>
</Flex>
))}
</Space>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
{preflight.blocks.length > 0 && (
<Space orientation="vertical" size={4} style={{ width: '100%' }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
{preflight.blocks.map((block) => (
<Flex key={block.code} gap={8} align="baseline" wrap>
<Tag color="red">{block.label}</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{block.message}{block.count}
</Typography.Text>
</Flex>
))}
</Space>
)}
{preflight.questions.length > 0 && (
<Space orientation="vertical" size={4} style={{ width: '100%' }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
{preflight.questions.map((question) => (
<Flex key={question.key} gap={8} align="baseline" wrap>
<Tag color="gold">{question.label}</Tag>
{question.description && (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{question.description}
</Typography.Text>
)}
</Flex>
))}
</Space>
)}
{preflight.nextSteps.length > 0 && (
<Space orientation="vertical" size={4} style={{ width: '100%' }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
</Typography.Text>
{preflight.nextSteps.map((step) => (
<Flex key={step.key} gap={8} align="baseline" wrap>
<Tag color="blue">{step.label}</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{step.description}
</Typography.Text>
</Flex>
))}
</Space>
)}
</Space>
</Card>
);
};