@bit-sun/business-component 4.2.0-alpha.28 → 4.2.0-alpha.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.fatherrc.ts +1 -1
  2. package/dist/components/Business/ImportExport/ExportButton/ModalTitle.d.ts +11 -0
  3. package/dist/components/Business/ImportExport/ExportButton/TemplatePicker.d.ts +13 -0
  4. package/dist/components/Business/ImportExport/ExportButton/index.d.ts +15 -0
  5. package/dist/components/Business/ImportExport/ImportWizard/Preview.d.ts +11 -0
  6. package/dist/components/Business/ImportExport/ImportWizard/index.d.ts +15 -0
  7. package/dist/components/Business/ImportExport/TaskDrawer/index.d.ts +15 -0
  8. package/dist/components/Business/ImportExport/TaskProgress/index.d.ts +15 -0
  9. package/dist/components/Business/ImportExport/businessFunction.d.ts +15 -0
  10. package/dist/components/Business/ImportExport/index.d.ts +14 -0
  11. package/dist/components/Business/ImportExport/sceneCapability.d.ts +20 -0
  12. package/dist/components/Business/ImportExport/services/importExport.d.ts +22 -0
  13. package/dist/components/Business/ImportExport/services/routing.d.ts +8 -0
  14. package/dist/components/Business/ImportExport/types.d.ts +134 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.esm.js +2465 -10
  17. package/dist/index.js +2484 -7
  18. package/dist/utils/auth.d.ts +4 -0
  19. package/docs/error-record.md +52 -0
  20. package/docs/task-records/2026-07-31-import-export-extraction.md +73 -0
  21. package/package.json +1 -1
  22. package/src/components/Business/AddSelectBusiness/index.tsx +1 -1
  23. package/src/components/Business/ImportExport/ExportButton/ModalTitle.tsx +27 -0
  24. package/src/components/Business/ImportExport/ExportButton/TemplatePicker.tsx +424 -0
  25. package/src/components/Business/ImportExport/ExportButton/index.module.less +382 -0
  26. package/src/components/Business/ImportExport/ExportButton/index.tsx +273 -0
  27. package/src/components/Business/ImportExport/ImportWizard/Preview.tsx +129 -0
  28. package/src/components/Business/ImportExport/ImportWizard/index.module.less +360 -0
  29. package/src/components/Business/ImportExport/ImportWizard/index.tsx +679 -0
  30. package/src/components/Business/ImportExport/TaskDrawer/index.tsx +285 -0
  31. package/src/components/Business/ImportExport/TaskProgress/index.tsx +267 -0
  32. package/src/components/Business/ImportExport/businessFunction.ts +29 -0
  33. package/src/components/Business/ImportExport/index.md +104 -0
  34. package/src/components/Business/ImportExport/index.tsx +170 -0
  35. package/src/components/Business/ImportExport/sceneCapability.ts +28 -0
  36. package/src/components/Business/ImportExport/services/importExport.ts +208 -0
  37. package/src/components/Business/ImportExport/services/routing.ts +21 -0
  38. package/src/components/Business/ImportExport/types.ts +180 -0
  39. package/src/components/Business/SearchSelect/BusinessUtils.tsx +1 -1
  40. package/src/components/Functional/SearchSelect/utils.tsx +0 -1
  41. package/src/index.ts +2 -1
  42. package/src/typings/umi.d.ts +9 -0
  43. package/src/utils/LocalstorageUtils.ts +1 -1
  44. package/src/utils/auth.ts +39 -2
@@ -1,3 +1,7 @@
1
+ /**
2
+ * 根据上游权限编码获取关联的导入导出业务功能编码。
3
+ */
4
+ export declare const getRelationTemplateCode: (authCode: string) => string;
1
5
  export declare const authFunc: (code: string) => any;
2
6
  export declare const handleJudgeAuthButtons: (buttonCodeArray: any[]) => true | boolean[];
3
7
  export declare const shouldUseAuth: () => boolean;
@@ -0,0 +1,52 @@
1
+ # 错误记录
2
+
3
+ ## 2026-07-31 导入导出组件抽取
4
+
5
+ ### 复制后直接按应用项目类型环境判断
6
+
7
+ - 现象:组件库严格构建发现可空 `serviceContext` 和对象展开重复字段错误。
8
+ - 原因:原业务项目的类型检查强度低于组件库,复制完成不等于迁移完成。
9
+ - 修正:把服务上下文改为显式必填参数;把任务默认字段放到服务响应展开之后。
10
+ - 后续规则:跨仓库迁移必须执行目标仓库完整构建,不能只依赖源仓库结果。
11
+
12
+ ### 文档整站构建不可用
13
+
14
+ - 现象:Dumi 无法解析旧组件中的 `@/` 别名、`umi` 和 `react-dom`。
15
+ - 原因:现有文档工程依赖与别名配置不完整,与本次新增模块无直接关系。
16
+ - 处理:保留失败证据并停止扩大修改范围;组件本体使用 Father 构建验证。
17
+
18
+ ### 业务项目独立类型校验不可用
19
+
20
+ - 现象:临时消费端文件在业务项目中执行独立 `tsc` 时,大量第三方声明文件出现语法错误。
21
+ - 原因:业务项目内置 TypeScript 版本无法解析当前安装的新版 `@types/node`、`@types/lodash` 等声明语法,错误未进入本次适配代码。
22
+ - 处理:删除临时验证文件,不以该命令作为完成证据;业务项目正式切换应在组件包发布并升级依赖后使用项目原构建链验证。
23
+
24
+ ## 2026-08-01 导入导出入口解析测试
25
+
26
+ ### Jest 未解析源码路径别名
27
+
28
+ - 现象:入口解析单元测试加载 `@/utils` 和 `@/utils/enumConfig` 失败,业务断言尚未执行。
29
+ - 原因:现有 `umi-test` 配置没有为 Jest 注册 `@/` 路径映射。
30
+ - 修正:本次被测工具链内部改用等价的相对路径引用,不扩大范围修改全局测试配置。
31
+ - 后续规则:新增可独立测试的底层工具时,优先使用相对路径引用同目录或固定层级依赖,避免测试结果依赖构建器专属别名。
32
+
33
+ ### 默认服务测试路径预期不完整
34
+
35
+ - 现象:默认快速导出服务测试预期缺少 `/import-export` 路径段,实际请求路径与路由工具输出不一致。
36
+ - 原因:测试断言凭接口后缀手写,没有先对照 `buildSdkUrl` 的统一前缀规则。
37
+ - 修正:按路由工具的实际输出更新断言。
38
+ - 后续规则:服务路径测试必须以现有路由构造规则为依据,禁止省略中间路径段。
39
+
40
+ ### Umi peer dependency 缺少本地类型声明
41
+
42
+ - 现象:导入宿主 `umi.request` 后,Father 构建提示找不到 `umi` 模块及类型声明。
43
+ - 原因:`umi` 仅声明为 peer dependency,本仓库安装环境没有对应运行模块和类型。
44
+ - 修正:保持 `umi` 为宿主依赖,补充最小请求类型声明,并在 Father 中显式标记为 external。
45
+ - 后续规则:组件库直接使用 peer dependency 时,必须同时验证本地类型可解析且构建产物不会打包重复运行时。
46
+
47
+ ### Distribution-front 测试命令不可用
48
+
49
+ - 现象:执行销售单导入导出目标测试时提示 `umi-test: command not found`。
50
+ - 原因:当前 `distribution-front` 本地依赖环境没有生成 `umi-test` 可执行文件。
51
+ - 处理:不下载或改动依赖,改为执行与目标测试一致的源码静态断言,并确认旧服务适配引用已清零。
52
+ - 后续规则:消费端依赖未完整安装时,不宣称测试套件通过;应明确区分正式测试结果和等价静态验证结果。
@@ -0,0 +1,73 @@
1
+ # 导入导出组件抽取记录
2
+
3
+ 日期:2026-07-31
4
+
5
+ ## 完成标准
6
+
7
+ - 完整导入、快速导出、按模板导出、任务进度和最近任务进入 `business-component`。
8
+ - 组件库不依赖业务项目的 `@/utils`、Umi 请求实例或微前端路由实现。
9
+ - 业务项目可以注入自己的请求函数、查询条件、权限开关和任务中心跳转。
10
+ - 组件库 CJS、ESM 构建通过。
11
+
12
+ ## 抽取结果
13
+
14
+ - 新增业务组件:`src/components/Business/ImportExport`。
15
+ - 新增公开入口:`ImportExportActions`。
16
+ - 新增请求适配工厂:`createImportExportServices(request, overrides)`。
17
+ - 新增高级组件导出:`ImportWizard`、`ExportButton`、`TaskProgress`、`TaskDrawer`。
18
+ - 新增类型、路由安全校验、任务归一化和场景能力工具。
19
+ - 新增 Dumi 使用文档和业务项目接入示例。
20
+ - 样式改为 CSS Modules,避免业务页面样式污染。
21
+
22
+ ## 依赖边界
23
+
24
+ - 组件库负责:界面、弹窗、上传与校验流程、导出流程、任务轮询、最近任务。
25
+ - 业务项目负责:按钮权限、业务功能编码、当前列表条件、结果数量、请求拦截器、微前端跳转。
26
+ - 接口差异通过服务覆盖处理,不在组件内部增加项目名称判断。
27
+
28
+ ## 验证结果
29
+
30
+ - `npm run build`:CJS 和 ESM 构建通过。
31
+ - 生成的声明文件包含 `ImportExportActions` 和 `createImportExportServices`。
32
+ - 组件源码和声明文件未残留业务项目路径、`@/utils` 或 `jumpRoutePath` 依赖。
33
+
34
+ ## 发布与业务项目切换
35
+
36
+ 当前组件库版本为 `4.2.0-alpha.25`,本次未修改版本号、未发布包,也未写入不可复现的本地绝对路径依赖。销售单源码已按发布包模式切换为从 `@bit-sun/business-component` 引用,并通过 `src/services/importExport.ts` 注入业务请求实例;业务项目当前安装的 `4.2.0-alpha.26` 尚不包含新导出,需要先手动替换本地产物或升级正式发布版本。
37
+
38
+ 发布新版本后,业务项目只需:
39
+
40
+ 1. 升级 `@bit-sun/business-component`。
41
+ 2. 用业务项目已有的 `request` 创建请求适配器并传给 `createImportExportServices`。
42
+ 3. 从组件库导入 `ImportExportActions`,传入权限开关、查询条件和跳转回调。
43
+ 4. 验证完成后删除业务项目原有的 `src/components/ImportExport` 副本。
44
+
45
+ ## 受限项
46
+
47
+ `npm run docs:build` 被仓库原有文档工程问题阻断:旧组件存在 `@/` 别名、`umi`、`react-dom` 等解析失败。本次组件库产物构建不受影响;未扩大范围修改旧文档工程。
48
+
49
+ ## 2026-08-01 任务中心路由修正
50
+
51
+ - 完成标准:上游未传 `taskCenterPath` 时,任务进度和最近任务入口都使用组件默认路由;上游显式传入时,两处同时使用传入值。
52
+ - 根因:`ImportExportActions` 已声明顶层默认值,但子组件仍读取 `config.taskCenterPath`,导致默认值被绕过。
53
+ - 修正:以 `ImportExportActionsProps.taskCenterPath` 为唯一来源,统一传给 `TaskProgress` 和 `TaskDrawer`,并移除 `ImportExportConfig` 中的重复字段。
54
+ - 验证:`npm run build` 执行成功,CJS 和 ESM 构建均通过。
55
+
56
+ ## 2026-08-01 导入导出业务入口拆分
57
+
58
+ - 完成标准:导入通过 `importCode` 获取独立业务功能编码;快速导出和模板导出通过两个权限编码关联结果的“或”值共用一个导出业务功能编码。
59
+ - 数据来源:从权限菜单按钮的 `relationExcelTemplateCode` 读取业务功能编码,不再要求上游通过 `config.businessFunctionCode` 重复传入。
60
+ - 调用链:入口解析后同时用于业务功能服务解析、模板查询、导入导出任务创建和最近任务查询。
61
+ - 单元测试:入口解析测试 3 项通过,覆盖导入独立入口、快速导出入口存在、仅模板导出入口存在。
62
+ - 构建验证:`npm run build` 执行成功,CJS 和 ESM 构建均通过。
63
+
64
+ ## 2026-08-01 默认请求服务内置
65
+
66
+ - 完成标准:Umi 消费项目不传 `services` 也能使用完整导入导出服务,同时允许通过可选属性覆盖个别差异接口。
67
+ - 修正:组件库使用宿主 `umi.request` 创建默认服务;`services` 改为可选的 `Partial<ImportExportServices>`,并保留请求工厂作为扩展入口。
68
+ - 构建边界:`umi` 保持 peer dependency 并显式 external,组件包不打包重复 Umi 运行时。
69
+ - 消费端:销售单移除 `importExportServices` import、`services` 属性和无业务逻辑的服务适配文件。
70
+ - 单元测试:默认服务与局部覆盖测试 2 项、业务入口解析测试 3 项,共 5 项通过。
71
+ - 构建验证:`npm run build` 执行成功,CJS 和 ESM 构建均通过;声明文件中 `services` 为可选属性。
72
+ - 消费端验证:销售单等价源码静态断言通过;正式目标测试因本地缺少 `umi-test` 命令未执行。
73
+ - 后续处理:按用户要求删除本次新增的两个组件库测试文件,保留上述已执行结果作为历史验证记录。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bit-sun/business-component",
3
- "version": "4.2.0-alpha.28",
3
+ "version": "4.2.0-alpha.29",
4
4
  "scripts": {
5
5
  "start": "dumi dev",
6
6
  "docs:build": "dumi build",
@@ -654,7 +654,7 @@ export const AddSkcSelect = (parProps: any) => {
654
654
  ]
655
655
  const mTpTableColumn = handleTableColumn(initialTableColumn, parProps);
656
656
  const initialTableSearchForm = [
657
- { name: 'qp-code-in', label: 'SKC编码', type: 'multipleQueryInput' },
657
+ { name: 'qp-code-like', label: 'SKC编码' },
658
658
  { name: 'qp-skcName-like', label: 'SKC名称' },
659
659
  { name: 'qp-itemName-like', label: '商品名称' },
660
660
  { name: 'qp-colorName-in', type: 'select', label: '颜色', field: {
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import styles from './index.module.less';
3
+
4
+ interface ModalTitleProps {
5
+ icon: React.ReactNode;
6
+ title: React.ReactNode;
7
+ description: React.ReactNode;
8
+ }
9
+
10
+ /**
11
+ * 统一导出弹窗标题区的图标、主标题和辅助说明。
12
+ */
13
+ const ModalTitle: React.FC<ModalTitleProps> = ({
14
+ icon,
15
+ title,
16
+ description,
17
+ }) => (
18
+ <div className={styles.modalTitle}>
19
+ <span className={styles.modalTitleIcon}>{icon}</span>
20
+ <span>
21
+ <span className={styles.modalTitleText}>{title}</span>
22
+ <span className={styles.modalTitleDescription}>{description}</span>
23
+ </span>
24
+ </div>
25
+ );
26
+
27
+ export default ModalTitle;
@@ -0,0 +1,424 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import {
9
+ Alert,
10
+ Button,
11
+ Descriptions,
12
+ Empty,
13
+ Form,
14
+ Modal,
15
+ Radio,
16
+ Select,
17
+ Space,
18
+ Spin,
19
+ Tag,
20
+ message,
21
+ } from 'antd';
22
+ import {
23
+ DownloadOutlined,
24
+ FileExcelOutlined,
25
+ FilterOutlined,
26
+ } from '@ant-design/icons';
27
+ import { getItems } from '../services/importExport';
28
+ import type {
29
+ ImportExportRequestContext,
30
+ ImportExportServices,
31
+ ImportExportTemplate,
32
+ ImportExportTemplateField,
33
+ } from '../types';
34
+ import ModalTitle from './ModalTitle';
35
+ import styles from './index.module.less';
36
+
37
+ interface TemplatePickerProps {
38
+ visible: boolean;
39
+ context: ImportExportRequestContext;
40
+ services: ImportExportServices;
41
+ onCancel: () => void;
42
+ onConfirm: (templateCode: string, fileType: string) => Promise<void>;
43
+ documentName?: string;
44
+ resultCount?: number;
45
+ }
46
+
47
+ const TemplatePicker: React.FC<TemplatePickerProps> = ({
48
+ visible,
49
+ context,
50
+ services,
51
+ onCancel,
52
+ onConfirm,
53
+ documentName,
54
+ resultCount,
55
+ }) => {
56
+ const [items, setItems] = useState<ImportExportTemplate[]>([]);
57
+ const [fields, setFields] = useState<ImportExportTemplateField[]>([]);
58
+ const [templateCode, setTemplateCode] = useState<string>();
59
+ const [loading, setLoading] = useState(false);
60
+ const [fieldLoading, setFieldLoading] = useState(false);
61
+ const [confirming, setConfirming] = useState(false);
62
+ const [downloading, setDownloading] = useState(false);
63
+ const [fileType, setFileType] = useState('XLSX');
64
+ const searchTimer = useRef<number>();
65
+
66
+ const loadTemplates = useCallback(
67
+ async (keyword?: string) => {
68
+ setLoading(true);
69
+ try {
70
+ const response = await services.queryTemplates({
71
+ operationType: context.operationType,
72
+ businessFunctionCode: context.businessFunctionCode,
73
+ keyword,
74
+ enabled: true,
75
+ currentPage: 1,
76
+ pageSize: 100,
77
+ });
78
+ const loaded = getItems<ImportExportTemplate>(response);
79
+ setItems(loaded);
80
+ if (!keyword) {
81
+ const defaultTemplate = loaded.find(
82
+ (template) => template.defaultFlag && template.enabled !== false,
83
+ );
84
+ if (!defaultTemplate) {
85
+ message.error('当前业务功能未配置启用的默认导出模板');
86
+ } else {
87
+ setTemplateCode(defaultTemplate.templateCode);
88
+ }
89
+ }
90
+ } catch (error) {
91
+ message.error('导出模板加载失败,请稍后重试');
92
+ } finally {
93
+ setLoading(false);
94
+ }
95
+ },
96
+ [context.businessFunctionCode, context.operationType, services],
97
+ );
98
+
99
+ useEffect(() => {
100
+ if (!visible) return;
101
+ setTemplateCode(undefined);
102
+ setFields([]);
103
+ setFileType('XLSX');
104
+ loadTemplates();
105
+ }, [visible, loadTemplates]);
106
+
107
+ useEffect(
108
+ () => () => {
109
+ if (searchTimer.current) {
110
+ window.clearTimeout(searchTimer.current);
111
+ }
112
+ },
113
+ [],
114
+ );
115
+
116
+ const selectedTemplate = useMemo(
117
+ () => items.find((item) => item.templateCode === templateCode),
118
+ [items, templateCode],
119
+ );
120
+ const personalTemplates = items.filter(
121
+ (item) => item.visibility === 'PERSONAL',
122
+ );
123
+ const publicTemplates = items.filter(
124
+ (item) => item.visibility !== 'PERSONAL',
125
+ );
126
+ const previewFieldNames =
127
+ fields.length > 0
128
+ ? fields.map((field) => field.displayName || field.fieldCode)
129
+ : selectedTemplate?.fieldNames || [];
130
+ const previewFieldCount =
131
+ fields.length || selectedTemplate?.fieldCount || previewFieldNames.length;
132
+
133
+ const searchTemplates = (keyword: string) => {
134
+ if (searchTimer.current) {
135
+ window.clearTimeout(searchTimer.current);
136
+ }
137
+ searchTimer.current = window.setTimeout(
138
+ () => loadTemplates(keyword.trim() || undefined),
139
+ 300,
140
+ );
141
+ };
142
+
143
+ useEffect(() => {
144
+ if (!selectedTemplate) {
145
+ setFields([]);
146
+ return;
147
+ }
148
+ setFieldLoading(true);
149
+ services
150
+ .queryTemplateFields({
151
+ templateCode: selectedTemplate.templateCode,
152
+ metadataEntityCode: selectedTemplate.metadataEntityCode,
153
+ onlyUnmapped: false,
154
+ currentPage: 1,
155
+ pageSize: 500,
156
+ })
157
+ .then((response) => {
158
+ setFields(
159
+ getItems<ImportExportTemplateField>(response).filter(
160
+ (field) => field.mapped,
161
+ ),
162
+ );
163
+ })
164
+ .catch(() => {
165
+ setFields([]);
166
+ message.error('模板字段预览加载失败,请稍后重试');
167
+ })
168
+ .finally(() => setFieldLoading(false));
169
+ }, [selectedTemplate, services]);
170
+
171
+ const confirm = async () => {
172
+ if (!templateCode) return;
173
+ if (resultCount === 0) {
174
+ message.warning('当前查询结果为空,不创建导出任务');
175
+ return;
176
+ }
177
+ setConfirming(true);
178
+ try {
179
+ await onConfirm(templateCode, fileType);
180
+ } finally {
181
+ setConfirming(false);
182
+ }
183
+ };
184
+
185
+ const downloadTemplate = async () => {
186
+ if (!templateCode) {
187
+ message.warning('请先选择模板');
188
+ return;
189
+ }
190
+ setDownloading(true);
191
+ try {
192
+ const file = await services.downloadTemplate(templateCode, context);
193
+ const url = URL.createObjectURL(
194
+ file instanceof Blob ? file : new Blob([file]),
195
+ );
196
+ const link = document.createElement('a');
197
+ link.href = url;
198
+ link.download = `${selectedTemplate?.templateName || templateCode}.xlsx`;
199
+ document.body.appendChild(link);
200
+ link.click();
201
+ document.body.removeChild(link);
202
+ URL.revokeObjectURL(url);
203
+ message.success('模板下载成功');
204
+ } catch (error) {
205
+ message.error('模板下载失败,请稍后重试');
206
+ } finally {
207
+ setDownloading(false);
208
+ }
209
+ };
210
+
211
+ return (
212
+ <Modal
213
+ width={760}
214
+ title={
215
+ <ModalTitle
216
+ icon={<FileExcelOutlined />}
217
+ title={`${documentName || '业务单据'}按模板导出`}
218
+ description="按已配置的字段模板生成当前筛选结果"
219
+ />
220
+ }
221
+ visible={visible}
222
+ onCancel={onCancel}
223
+ onOk={confirm}
224
+ confirmLoading={confirming}
225
+ okButtonProps={{ disabled: !templateCode || resultCount === 0 }}
226
+ okText="创建导出任务"
227
+ cancelText="取消"
228
+ wrapClassName={styles.exportModal}
229
+ destroyOnClose
230
+ >
231
+ <div className={styles.dialogBody}>
232
+ <Form layout="vertical" className={styles.templateForm}>
233
+ <section className={styles.section}>
234
+ <div className={styles.sectionHeader}>
235
+ <span className={styles.sectionIndex}>1</span>
236
+ <span>
237
+ <span className={styles.sectionTitle}>选择导出模板</span>
238
+ <span className={styles.sectionDescription}>
239
+ 模板决定导出文件包含的字段及字段顺序
240
+ </span>
241
+ </span>
242
+ </div>
243
+ <Form.Item
244
+ label="导出模板"
245
+ required
246
+ className={styles.templateSelectItem}
247
+ >
248
+ <Spin spinning={loading}>
249
+ <Select
250
+ size="large"
251
+ showSearch
252
+ allowClear
253
+ filterOption={false}
254
+ onSearch={searchTemplates}
255
+ onClear={() => loadTemplates()}
256
+ placeholder="输入模板名称实时查询"
257
+ value={templateCode}
258
+ onChange={setTemplateCode}
259
+ style={{ width: '100%' }}
260
+ notFoundContent={
261
+ <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
262
+ }
263
+ >
264
+ {personalTemplates.length > 0 && (
265
+ <Select.OptGroup label="个人模板">
266
+ {personalTemplates.map((item) => (
267
+ <Select.Option
268
+ key={item.templateCode}
269
+ value={item.templateCode}
270
+ >
271
+ {item.templateName}
272
+ </Select.Option>
273
+ ))}
274
+ </Select.OptGroup>
275
+ )}
276
+ {publicTemplates.length > 0 && (
277
+ <Select.OptGroup label="公共模板">
278
+ {publicTemplates.map((item) => (
279
+ <Select.Option
280
+ key={item.templateCode}
281
+ value={item.templateCode}
282
+ >
283
+ {item.templateName}
284
+ </Select.Option>
285
+ ))}
286
+ </Select.OptGroup>
287
+ )}
288
+ </Select>
289
+ </Spin>
290
+ </Form.Item>
291
+
292
+ {selectedTemplate && (
293
+ <div className={styles.templatePreview}>
294
+ <div className={styles.templatePreviewHeader}>
295
+ <span className={styles.sectionTitle}>
296
+ 模板信息与字段预览
297
+ </span>
298
+ <Button
299
+ className={styles.downloadTemplateButton}
300
+ type="link"
301
+ icon={<DownloadOutlined />}
302
+ loading={downloading}
303
+ onClick={downloadTemplate}
304
+ >
305
+ 下载空白模板
306
+ </Button>
307
+ </div>
308
+ <Descriptions
309
+ bordered
310
+ size="small"
311
+ column={{ xs: 1, sm: 3 }}
312
+ className={styles.templateSummary}
313
+ >
314
+ <Descriptions.Item label="模板名称">
315
+ {selectedTemplate.templateName}
316
+ </Descriptions.Item>
317
+ <Descriptions.Item label="模板类型">
318
+ {selectedTemplate.visibility === 'PERSONAL'
319
+ ? '个人模板'
320
+ : '公共模板'}
321
+ </Descriptions.Item>
322
+ <Descriptions.Item label="业务功能">
323
+ {documentName ||
324
+ selectedTemplate.businessFunctionCode ||
325
+ '-'}
326
+ </Descriptions.Item>
327
+ </Descriptions>
328
+ <div className={styles.fieldPreview}>
329
+ <div className={styles.fieldPreviewHeader}>
330
+ <span className={styles.fieldPreviewLabel}>字段预览</span>
331
+ <span className={styles.fieldCount}>
332
+ {previewFieldCount} 个字段
333
+ </span>
334
+ </div>
335
+ <div className={styles.fieldTags}>
336
+ <Spin spinning={fieldLoading}>
337
+ <Space size={[4, 0]} wrap>
338
+ {previewFieldNames.map((name) => (
339
+ <Tag className={styles.fieldTag} key={name}>
340
+ {name}
341
+ </Tag>
342
+ ))}
343
+ {!fieldLoading && previewFieldNames.length === 0 && (
344
+ <span className={styles.emptyFields}>
345
+ 暂无已映射字段
346
+ </span>
347
+ )}
348
+ </Space>
349
+ </Spin>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ )}
354
+ </section>
355
+
356
+ <section className={styles.section}>
357
+ <div className={styles.sectionHeader}>
358
+ <span className={styles.sectionIndex}>2</span>
359
+ <span>
360
+ <span className={styles.sectionTitle}>设置导出参数</span>
361
+ <span className={styles.sectionDescription}>
362
+ 确认当前导出范围并选择文件格式
363
+ </span>
364
+ </span>
365
+ </div>
366
+ <div className={styles.scopeCard}>
367
+ <span className={styles.scopeIcon}>
368
+ <FilterOutlined />
369
+ </span>
370
+ <span className={styles.scopeContent}>
371
+ <span className={styles.scopeTitle}>当前筛选结果</span>
372
+ <span className={styles.scopeDescription}>
373
+ 系统将按列表当前查询条件和所选模板字段导出
374
+ </span>
375
+ </span>
376
+ <span className={styles.resultCount}>
377
+ {resultCount == null ? '以查询结果为准' : `${resultCount} 条`}
378
+ </span>
379
+ </div>
380
+ <Form.Item
381
+ label="文件格式"
382
+ style={{ marginTop: 16, marginBottom: 0 }}
383
+ >
384
+ <Radio.Group
385
+ className={styles.fileTypeGrid}
386
+ value={fileType}
387
+ onChange={(event) => setFileType(event.target.value)}
388
+ >
389
+ <Radio className={styles.fileTypeOption} value="XLSX">
390
+ <span className={styles.fileTypeContent}>
391
+ <span className={styles.fileTypeName}>XLSX</span>
392
+ <span className={styles.fileTypeDescription}>
393
+ 保留表格格式,适合查看与编辑
394
+ </span>
395
+ </span>
396
+ </Radio>
397
+ <Radio className={styles.fileTypeOption} value="CSV">
398
+ <span className={styles.fileTypeContent}>
399
+ <span className={styles.fileTypeName}>CSV</span>
400
+ <span className={styles.fileTypeDescription}>
401
+ 文件更轻量,适合系统间交换
402
+ </span>
403
+ </span>
404
+ </Radio>
405
+ </Radio.Group>
406
+ </Form.Item>
407
+ <Alert
408
+ className={styles.exportAlert}
409
+ type={resultCount === 0 ? 'warning' : 'info'}
410
+ showIcon
411
+ message={
412
+ resultCount === 0
413
+ ? '当前查询结果为空,暂时无法创建导出任务。'
414
+ : '创建后可在任务中心查看处理进度并下载导出文件。'
415
+ }
416
+ />
417
+ </section>
418
+ </Form>
419
+ </div>
420
+ </Modal>
421
+ );
422
+ };
423
+
424
+ export default TemplatePicker;