@alicloud/appflow-chat 0.0.1-beta.2 → 0.0.1-beta.4
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.
- package/dist/appflow-chat.cjs.js +252 -133
- package/dist/appflow-chat.esm.js +11261 -10184
- package/dist/types/index.d.ts +86 -51
- package/package.json +1 -1
- package/src/components/HumanVerify/CustomParamsRenderer/ArrayField.tsx +162 -7
- package/src/components/HumanVerify/CustomParamsRenderer/EnumField.tsx +199 -0
- package/src/components/HumanVerify/CustomParamsRenderer/FieldRenderer.tsx +152 -8
- package/src/components/HumanVerify/CustomParamsRenderer/FileField.tsx +509 -0
- package/src/components/HumanVerify/CustomParamsRenderer/ObjectField.tsx +10 -3
- package/src/components/HumanVerify/CustomParamsRenderer/TimeField.tsx +104 -0
- package/src/components/HumanVerify/CustomParamsRenderer/index.tsx +16 -2
- package/src/components/HumanVerify/CustomParamsRenderer/types.ts +231 -5
- package/src/components/HumanVerify/HistoryCard.tsx +39 -21
- package/src/components/HumanVerify/HumanVerify.tsx +23 -45
- package/src/components/MessageBubble.tsx +4 -10
- package/src/components/RichMessageBubble.tsx +2 -2
- package/src/index.ts +1 -0
- package/src/markdown/components/Mermaid.tsx +265 -0
- package/src/markdown/index.tsx +7 -0
- package/src/utils/loadMermaid.ts +40 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { default as default_2 } from 'react';
|
|
2
2
|
import { Provider } from 'react';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* AssociationPropertyMetadata 类型定义
|
|
6
|
+
* 包含枚举、时间、文件等扩展属性的元数据
|
|
7
|
+
*/
|
|
8
|
+
declare interface AssociationPropertyMetadata {
|
|
9
|
+
/** 子类型(用于 time 和 file 类型),现在是数组 */
|
|
10
|
+
SubType?: (TimeSubType | FileSubType)[];
|
|
11
|
+
/** 枚举值 */
|
|
12
|
+
EnumValues?: (string | number | boolean)[];
|
|
13
|
+
/** 枚举展示样式 */
|
|
14
|
+
EnumDisplayStyle?: EnumDisplayStyle;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare type BasicParamType = 'string' | 'number' | 'boolean';
|
|
18
|
+
|
|
4
19
|
/**
|
|
5
20
|
* BubbleContent - 消息气泡核心展示组件
|
|
6
21
|
*
|
|
@@ -213,6 +228,8 @@ export declare interface ChatStreamCallbacks {
|
|
|
213
228
|
onError?: (error: Error) => void;
|
|
214
229
|
}
|
|
215
230
|
|
|
231
|
+
declare type ComplexParamType = 'array' | 'object';
|
|
232
|
+
|
|
216
233
|
/**
|
|
217
234
|
* 将后端返回的小写 schema 转换为组件需要的大写格式
|
|
218
235
|
* @param schema 后端返回的 schema(小写字段名)
|
|
@@ -230,6 +247,18 @@ export declare interface CustomParamSchema {
|
|
|
230
247
|
Required?: string[];
|
|
231
248
|
Properties?: Record<string, CustomParamSchema>;
|
|
232
249
|
Items?: CustomParamSchema;
|
|
250
|
+
order?: number;
|
|
251
|
+
Order?: string | number;
|
|
252
|
+
AssociationPropertyMetadata?: AssociationPropertyMetadata;
|
|
253
|
+
AssociationProperty?: string;
|
|
254
|
+
/** @deprecated 使用 AssociationPropertyMetadata.SubType 代替 */
|
|
255
|
+
TimeSubType?: TimeSubType;
|
|
256
|
+
/** @deprecated 使用 AssociationPropertyMetadata.SubType 代替 */
|
|
257
|
+
FileSubType?: FileSubType;
|
|
258
|
+
/** @deprecated 使用 AssociationPropertyMetadata.EnumValues 代替 */
|
|
259
|
+
EnumValues?: string[];
|
|
260
|
+
/** @deprecated 使用 AssociationPropertyMetadata.EnumDisplayStyle 代替 */
|
|
261
|
+
EnumDisplayStyle?: EnumDisplayStyle;
|
|
233
262
|
}
|
|
234
263
|
|
|
235
264
|
/**
|
|
@@ -244,6 +273,9 @@ export declare interface CustomParamSchema {
|
|
|
244
273
|
* name: { Type: 'string', Title: '名称', Description: '请输入名称' },
|
|
245
274
|
* age: { Type: 'number', Title: '年龄' },
|
|
246
275
|
* enabled: { Type: 'boolean', Title: '是否启用' },
|
|
276
|
+
* date: { Type: 'time', Title: '日期', TimeSubType: 'year-month-day' },
|
|
277
|
+
* file: { Type: 'file', Title: '文件', FileSubType: 'image' },
|
|
278
|
+
* status: { Type: 'string', Title: '状态', EnumValues: ['active', 'inactive'], EnumDisplayStyle: 'radio' },
|
|
247
279
|
* tags: {
|
|
248
280
|
* Type: 'array',
|
|
249
281
|
* Title: '标签',
|
|
@@ -273,7 +305,7 @@ export declare const CustomParamsRenderer: default_2.FC<CustomParamsRendererProp
|
|
|
273
305
|
/**
|
|
274
306
|
* CustomParamsRenderer 组件的 Props
|
|
275
307
|
*/
|
|
276
|
-
export declare interface CustomParamsRendererProps {
|
|
308
|
+
export declare interface CustomParamsRendererProps extends UploadConfig {
|
|
277
309
|
/** CustomParams 的 schema */
|
|
278
310
|
schema: CustomParamSchema;
|
|
279
311
|
/** 表单值 */
|
|
@@ -323,18 +355,21 @@ export declare interface DocReferencesProps {
|
|
|
323
355
|
style?: default_2.CSSProperties;
|
|
324
356
|
}
|
|
325
357
|
|
|
358
|
+
declare type EnumDisplayStyle = 'select' | 'checkbox' | 'radio' | 'multi-select';
|
|
359
|
+
|
|
360
|
+
declare type ExtendedParamType = 'time' | 'file';
|
|
361
|
+
|
|
362
|
+
declare type FileSubType = 'default' | 'jpg' | 'png' | 'svg' | 'doc' | 'ppt' | 'excel' | 'txt' | 'markdown' | 'zip';
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 文件上传方法类型
|
|
366
|
+
* 用于实际执行文件上传到 OSS
|
|
367
|
+
*/
|
|
368
|
+
declare type FileUploader = (file: Blob, uploadUrl: string) => Promise<void>;
|
|
369
|
+
|
|
326
370
|
/**
|
|
327
371
|
* HistoryCard 历史卡片组件 (SDK 版本)
|
|
328
372
|
* 用于展示历史对话中的 card 类型消息(只读模式)
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* ```tsx
|
|
332
|
-
* <HistoryCard
|
|
333
|
-
* approvalStatus="approved"
|
|
334
|
-
* formValues={{ name: 'test', age: 18 }}
|
|
335
|
-
* formSchema={schema}
|
|
336
|
-
* />
|
|
337
|
-
* ```
|
|
338
373
|
*/
|
|
339
374
|
export declare const HistoryCard: default_2.FC<HistoryCardProps>;
|
|
340
375
|
|
|
@@ -346,12 +381,8 @@ export declare interface HistoryCardData {
|
|
|
346
381
|
}
|
|
347
382
|
|
|
348
383
|
export declare interface HistoryCardProps {
|
|
349
|
-
/**
|
|
350
|
-
|
|
351
|
-
/** 表单值 */
|
|
352
|
-
formValues?: Record<string, any>;
|
|
353
|
-
/** 表单 Schema */
|
|
354
|
-
formSchema?: CustomParamSchema;
|
|
384
|
+
/** 消息数据对象 */
|
|
385
|
+
data: any;
|
|
355
386
|
}
|
|
356
387
|
|
|
357
388
|
export declare interface HistoryMessage {
|
|
@@ -371,30 +402,6 @@ export declare interface HistoryMessage {
|
|
|
371
402
|
/**
|
|
372
403
|
* HumanVerify 人工审核组件 (SDK 版本)
|
|
373
404
|
* 用于展示需要人工审核的表单,并处理提交逻辑
|
|
374
|
-
*
|
|
375
|
-
* @example
|
|
376
|
-
* ```tsx
|
|
377
|
-
* <HumanVerify
|
|
378
|
-
* verifyId="xxx"
|
|
379
|
-
* sessionWebhook="https://..."
|
|
380
|
-
* approved={false}
|
|
381
|
-
* customParamsSchema={schema}
|
|
382
|
-
* customParamsKey="customParams"
|
|
383
|
-
* onSubmit={(data) => {
|
|
384
|
-
* bot.postMessage({
|
|
385
|
-
* msgType: 'cardCallBack',
|
|
386
|
-
* data: {
|
|
387
|
-
* sessionWebhook: data.sessionWebhook,
|
|
388
|
-
* content: JSON.stringify({
|
|
389
|
-
* verifyId: data.verifyId,
|
|
390
|
-
* status: data.status,
|
|
391
|
-
* [data.customParamsKey]: data.customParamsValue,
|
|
392
|
-
* }),
|
|
393
|
-
* },
|
|
394
|
-
* });
|
|
395
|
-
* }}
|
|
396
|
-
* />
|
|
397
|
-
* ```
|
|
398
405
|
*/
|
|
399
406
|
export declare const HumanVerify: default_2.FC<HumanVerifyProps>;
|
|
400
407
|
|
|
@@ -408,16 +415,12 @@ export declare interface HumanVerifyData {
|
|
|
408
415
|
}
|
|
409
416
|
|
|
410
417
|
export declare interface HumanVerifyProps {
|
|
411
|
-
/**
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
|
|
417
|
-
/** CustomParams 的 schema */
|
|
418
|
-
customParamsSchema?: CustomParamSchema;
|
|
419
|
-
/** CustomParams 的 key(用于提交时的字段名) */
|
|
420
|
-
customParamsKey?: string;
|
|
418
|
+
/** 消息数据对象 */
|
|
419
|
+
data: any;
|
|
420
|
+
/** 上传发送方法 */
|
|
421
|
+
uploadSender?: UploadSender;
|
|
422
|
+
/** 文件上传方法 */
|
|
423
|
+
fileUploader?: FileUploader;
|
|
421
424
|
/** 提交回调函数 */
|
|
422
425
|
onSubmit?: (data: {
|
|
423
426
|
verifyId: string;
|
|
@@ -443,6 +446,8 @@ export declare interface HumanVerifySubmitData {
|
|
|
443
446
|
*/
|
|
444
447
|
export declare const loadEchartsScript: () => Promise<void>;
|
|
445
448
|
|
|
449
|
+
export declare const loadMermaidScript: () => Promise<void>;
|
|
450
|
+
|
|
446
451
|
/**
|
|
447
452
|
* MarkdownRenderer - Markdown渲染组件
|
|
448
453
|
*
|
|
@@ -558,7 +563,7 @@ export declare interface ModelInfo {
|
|
|
558
563
|
};
|
|
559
564
|
}
|
|
560
565
|
|
|
561
|
-
declare type ParamType =
|
|
566
|
+
declare type ParamType = BasicParamType | ComplexParamType | ExtendedParamType;
|
|
562
567
|
|
|
563
568
|
/**
|
|
564
569
|
* RichBubbleContent - 富文本消息气泡核心展示组件
|
|
@@ -737,6 +742,36 @@ export declare interface SourceItem {
|
|
|
737
742
|
index_id?: string;
|
|
738
743
|
}
|
|
739
744
|
|
|
745
|
+
declare type TimeSubType = 'year-month' | 'year-month-day' | 'datetime';
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* 上传配置
|
|
749
|
+
*/
|
|
750
|
+
declare interface UploadConfig {
|
|
751
|
+
/** 上传发送方法 */
|
|
752
|
+
uploadSender?: UploadSender;
|
|
753
|
+
/** 文件上传方法 */
|
|
754
|
+
fileUploader?: FileUploader;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* 上传请求参数
|
|
759
|
+
*/
|
|
760
|
+
declare interface UploadRequestParams {
|
|
761
|
+
/** 事件类型:uploadToken-获取上传凭证,uploadFile-获取文件ID */
|
|
762
|
+
eventType: 'uploadToken' | 'uploadFile';
|
|
763
|
+
/** 文件名 */
|
|
764
|
+
fileName: string;
|
|
765
|
+
/** 文件下载 URL(uploadFile 时需要) */
|
|
766
|
+
content?: string;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* 上传发送方法类型
|
|
771
|
+
* 用于发送上传相关的事件请求
|
|
772
|
+
*/
|
|
773
|
+
declare type UploadSender = (params: UploadRequestParams) => Promise<string | null>;
|
|
774
|
+
|
|
740
775
|
/**
|
|
741
776
|
* 使用 CustomParamsRenderer 的 Hook
|
|
742
777
|
* 提供表单值管理和校验功能
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import React, { useCallback } from 'react';
|
|
1
|
+
import React, { useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import { Button, Input, InputNumber, Switch, Space } from 'antd';
|
|
3
3
|
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
|
|
4
|
+
import { ArrayFieldProps, CustomParamSchema, FileSubType, sortPropertiesByOrder } from './types';
|
|
4
5
|
import styled from 'styled-components';
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
// 图片类型列表
|
|
8
|
+
const IMAGE_TYPES: FileSubType[] = ['jpg', 'png', 'svg'];
|
|
6
9
|
|
|
7
10
|
// 前向声明,避免循环依赖
|
|
8
11
|
const FieldRenderer = React.lazy(() => import('./FieldRenderer'));
|
|
9
12
|
|
|
13
|
+
// 全局计数器,用于生成稳定的数组项 key
|
|
14
|
+
let globalItemCounter = 0;
|
|
15
|
+
|
|
10
16
|
// ==================== Styled Components ====================
|
|
11
17
|
|
|
12
18
|
const ArrayFieldContainer = styled.div`
|
|
@@ -104,6 +110,37 @@ const ArrayActions = styled.div`
|
|
|
104
110
|
gap: 8px;
|
|
105
111
|
`;
|
|
106
112
|
|
|
113
|
+
// 复杂类型变体
|
|
114
|
+
type ComplexItemVariant = 'fileImage' | 'fileDoc' | 'time' | 'enum';
|
|
115
|
+
|
|
116
|
+
// 复杂类型数组项行
|
|
117
|
+
const ArrayItemRowComplex = styled.div`
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: flex-start;
|
|
120
|
+
gap: 8px;
|
|
121
|
+
margin-bottom: 8px;
|
|
122
|
+
|
|
123
|
+
&:last-child {
|
|
124
|
+
margin-bottom: 0;
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
// 复杂类型的 Actions 容器,根据变体调整 padding-top
|
|
129
|
+
const ArrayItemActionsComplex = styled.div<{ $variant?: ComplexItemVariant }>`
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
padding-top: ${({ $variant }) => {
|
|
132
|
+
switch ($variant) {
|
|
133
|
+
case 'fileDoc':
|
|
134
|
+
case 'time':
|
|
135
|
+
case 'enum':
|
|
136
|
+
return '4px';
|
|
137
|
+
case 'fileImage':
|
|
138
|
+
default:
|
|
139
|
+
return '38px';
|
|
140
|
+
}
|
|
141
|
+
}};
|
|
142
|
+
`;
|
|
143
|
+
|
|
107
144
|
/**
|
|
108
145
|
* 获取数组项的默认值
|
|
109
146
|
*/
|
|
@@ -149,6 +186,8 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
149
186
|
level = 0,
|
|
150
187
|
errors = {},
|
|
151
188
|
fieldPath = '',
|
|
189
|
+
uploadSender,
|
|
190
|
+
fileUploader,
|
|
152
191
|
}) => {
|
|
153
192
|
const { Title, Description, Items } = schema;
|
|
154
193
|
const displayTitle = Title || name;
|
|
@@ -156,11 +195,39 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
156
195
|
// 计算当前数组的完整路径
|
|
157
196
|
const currentPath = fieldPath ? `${fieldPath}.${name}` : name;
|
|
158
197
|
|
|
198
|
+
// 使用 ref 来存储每个数组项的唯一 key,避免在数据中添加额外字段
|
|
199
|
+
const itemKeysRef = useRef<Map<number, string>>(new Map());
|
|
200
|
+
|
|
201
|
+
// 获取或创建数组项的唯一 key
|
|
202
|
+
const getItemKey = useCallback((index: number, item: any): string => {
|
|
203
|
+
// 如果 item 有 url 或 uid,优先使用(已上传的文件)
|
|
204
|
+
if (item?.url) return item.url;
|
|
205
|
+
if (item?.uid) return item.uid;
|
|
206
|
+
if (item?.__arrayItemId) return item.__arrayItemId;
|
|
207
|
+
|
|
208
|
+
// 否则使用 ref 中存储的 key
|
|
209
|
+
if (!itemKeysRef.current.has(index)) {
|
|
210
|
+
itemKeysRef.current.set(index, `item-${++globalItemCounter}`);
|
|
211
|
+
}
|
|
212
|
+
return itemKeysRef.current.get(index)!;
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
159
215
|
// 添加数组项
|
|
160
216
|
const handleAdd = useCallback(() => {
|
|
161
217
|
if (!Items) return;
|
|
162
218
|
const newItem = getDefaultValue(Items);
|
|
163
|
-
|
|
219
|
+
const newIndex = value.length;
|
|
220
|
+
|
|
221
|
+
// 为新项预先生成 key
|
|
222
|
+
itemKeysRef.current.set(newIndex, `item-${++globalItemCounter}`);
|
|
223
|
+
|
|
224
|
+
// 对于 object 类型,添加 __arrayItemId;对于其他类型,保持原值
|
|
225
|
+
if (typeof newItem === 'object' && newItem !== null) {
|
|
226
|
+
onChange?.([...value, { ...newItem, __arrayItemId: itemKeysRef.current.get(newIndex) }]);
|
|
227
|
+
} else {
|
|
228
|
+
// 对于 file、time 等类型,保持 undefined,不要包装成对象
|
|
229
|
+
onChange?.([...value, newItem]);
|
|
230
|
+
}
|
|
164
231
|
}, [Items, value, onChange]);
|
|
165
232
|
|
|
166
233
|
// 删除数组项
|
|
@@ -196,9 +263,35 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
196
263
|
[value, onChange]
|
|
197
264
|
);
|
|
198
265
|
|
|
266
|
+
// 判断数组项是否有枚举值(从 AssociationPropertyMetadata 或旧字段读取)
|
|
267
|
+
const hasItemEnumValues = (Items?.AssociationPropertyMetadata?.EnumValues?.length ?? 0) > 0 ||
|
|
268
|
+
(Array.isArray(Items?.EnumValues) && Items.EnumValues.length > 0);
|
|
269
|
+
|
|
199
270
|
// 判断数组项类型(必须在 early return 之前)
|
|
200
271
|
const isObjectItems = Items?.Type === 'object' && Items?.Properties;
|
|
201
272
|
const isArrayItems = Items?.Type === 'array';
|
|
273
|
+
const isFileItems = Items?.Type === 'file';
|
|
274
|
+
const isTimeItems = Items?.Type === 'time';
|
|
275
|
+
// 需要使用 FieldRenderer 渲染的复杂类型
|
|
276
|
+
const needsFieldRenderer = isObjectItems || isArrayItems || isFileItems || isTimeItems || hasItemEnumValues;
|
|
277
|
+
|
|
278
|
+
// 判断文件类型是否为图片类型
|
|
279
|
+
const isImageFileType = useMemo((): boolean => {
|
|
280
|
+
if (!isFileItems) return false;
|
|
281
|
+
|
|
282
|
+
// 优先从 AssociationPropertyMetadata.SubType 读取
|
|
283
|
+
const subTypes = Items?.AssociationPropertyMetadata?.SubType;
|
|
284
|
+
if (Array.isArray(subTypes) && subTypes.length > 0) {
|
|
285
|
+
return subTypes.some(subType => IMAGE_TYPES.includes(subType as FileSubType));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 兼容旧的 FileSubType 字段
|
|
289
|
+
if (Items?.FileSubType) {
|
|
290
|
+
return IMAGE_TYPES.includes(Items.FileSubType);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false;
|
|
294
|
+
}, [isFileItems, Items]);
|
|
202
295
|
|
|
203
296
|
if (!Items) {
|
|
204
297
|
return null;
|
|
@@ -271,7 +364,10 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
271
364
|
const renderObjectItemContent = (item: any, index: number) => {
|
|
272
365
|
if (!Items.Properties) return null;
|
|
273
366
|
|
|
274
|
-
|
|
367
|
+
// 使用排序后的属性列表进行渲染
|
|
368
|
+
const sortedProperties = sortPropertiesByOrder(Items.Properties);
|
|
369
|
+
|
|
370
|
+
return sortedProperties.map(([propertyName, propertySchema]) => {
|
|
275
371
|
const isRequired = itemRequired.includes(propertyName);
|
|
276
372
|
return (
|
|
277
373
|
<React.Suspense key={propertyName} fallback={<div>加载中...</div>}>
|
|
@@ -285,6 +381,8 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
285
381
|
level={level + 1}
|
|
286
382
|
errors={errors}
|
|
287
383
|
fieldPath={`${currentPath}[${index}]`}
|
|
384
|
+
uploadSender={uploadSender}
|
|
385
|
+
fileUploader={fileUploader}
|
|
288
386
|
/>
|
|
289
387
|
</React.Suspense>
|
|
290
388
|
);
|
|
@@ -304,6 +402,29 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
304
402
|
level={level + 1}
|
|
305
403
|
errors={errors}
|
|
306
404
|
fieldPath={currentPath}
|
|
405
|
+
uploadSender={uploadSender}
|
|
406
|
+
fileUploader={fileUploader}
|
|
407
|
+
/>
|
|
408
|
+
</React.Suspense>
|
|
409
|
+
);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// 渲染复杂类型的数组项(file、time、带枚举的类型)
|
|
413
|
+
const renderComplexItemContent = (item: any, index: number) => {
|
|
414
|
+
return (
|
|
415
|
+
<React.Suspense fallback={<div>加载中...</div>}>
|
|
416
|
+
<FieldRenderer
|
|
417
|
+
name={`[${index}]`}
|
|
418
|
+
schema={Items}
|
|
419
|
+
value={item}
|
|
420
|
+
onChange={(newValue) => handleItemChange(index, newValue)}
|
|
421
|
+
disabled={disabled}
|
|
422
|
+
level={level + 1}
|
|
423
|
+
errors={errors}
|
|
424
|
+
fieldPath={currentPath}
|
|
425
|
+
hideLabel={true}
|
|
426
|
+
uploadSender={uploadSender}
|
|
427
|
+
fileUploader={fileUploader}
|
|
307
428
|
/>
|
|
308
429
|
</React.Suspense>
|
|
309
430
|
);
|
|
@@ -322,6 +443,9 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
322
443
|
|
|
323
444
|
<ArrayItems>
|
|
324
445
|
{value.map((item, index) => {
|
|
446
|
+
// 获取数组项的唯一key
|
|
447
|
+
const itemKey = getItemKey(index, item);
|
|
448
|
+
|
|
325
449
|
// 渲染数组项内容
|
|
326
450
|
const renderItemContent = () => {
|
|
327
451
|
if (isObjectItems) {
|
|
@@ -340,6 +464,37 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
340
464
|
</ArrayItemContent>
|
|
341
465
|
);
|
|
342
466
|
}
|
|
467
|
+
if (isFileItems || isTimeItems || hasItemEnumValues) {
|
|
468
|
+
// 复杂类型(file、time、带枚举):使用 FieldRenderer 渲染
|
|
469
|
+
// 确定变体类型
|
|
470
|
+
let variant: ComplexItemVariant | undefined;
|
|
471
|
+
if (isFileItems) {
|
|
472
|
+
// 区分图片类型和非图片类型的文件上传
|
|
473
|
+
variant = isImageFileType ? 'fileImage' : 'fileDoc';
|
|
474
|
+
} else if (isTimeItems) {
|
|
475
|
+
// 时间类型
|
|
476
|
+
variant = 'time';
|
|
477
|
+
} else if (hasItemEnumValues) {
|
|
478
|
+
variant = 'enum';
|
|
479
|
+
}
|
|
480
|
+
return (
|
|
481
|
+
<ArrayItemRowComplex>
|
|
482
|
+
<ArrayItemInput>
|
|
483
|
+
{renderComplexItemContent(item, index)}
|
|
484
|
+
</ArrayItemInput>
|
|
485
|
+
<ArrayItemActionsComplex $variant={variant}>
|
|
486
|
+
<Button
|
|
487
|
+
type="text"
|
|
488
|
+
danger
|
|
489
|
+
icon={<MinusOutlined />}
|
|
490
|
+
onClick={() => handleRemove(index)}
|
|
491
|
+
disabled={disabled}
|
|
492
|
+
size="small"
|
|
493
|
+
/>
|
|
494
|
+
</ArrayItemActionsComplex>
|
|
495
|
+
</ArrayItemRowComplex>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
343
498
|
// 基础类型:渲染输入框和删除按钮
|
|
344
499
|
return (
|
|
345
500
|
<ArrayItemRow>
|
|
@@ -361,7 +516,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
361
516
|
};
|
|
362
517
|
|
|
363
518
|
return (
|
|
364
|
-
<ArrayItem key={
|
|
519
|
+
<ArrayItem key={itemKey}>
|
|
365
520
|
{renderItemContent()}
|
|
366
521
|
</ArrayItem>
|
|
367
522
|
);
|
|
@@ -376,7 +531,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
376
531
|
shape="circle"
|
|
377
532
|
icon={<PlusOutlined />}
|
|
378
533
|
/>
|
|
379
|
-
{
|
|
534
|
+
{needsFieldRenderer && value.length > 0 && (
|
|
380
535
|
<Button
|
|
381
536
|
onClick={() => handleRemove(value.length - 1)}
|
|
382
537
|
shape="circle"
|
|
@@ -391,4 +546,4 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
391
546
|
);
|
|
392
547
|
};
|
|
393
548
|
|
|
394
|
-
export default ArrayField;
|
|
549
|
+
export default ArrayField;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { Select, Checkbox, Radio } from 'antd';
|
|
3
|
+
import type { RadioChangeEvent } from 'antd';
|
|
4
|
+
import { EnumFieldProps, EnumDisplayStyle } from './types';
|
|
5
|
+
import styled from 'styled-components';
|
|
6
|
+
|
|
7
|
+
const { Option } = Select;
|
|
8
|
+
|
|
9
|
+
// ==================== Styled Components ====================
|
|
10
|
+
|
|
11
|
+
// 枚举选择器容器
|
|
12
|
+
const EnumSelect = styled.div`
|
|
13
|
+
width: 100%;
|
|
14
|
+
|
|
15
|
+
.ant-select {
|
|
16
|
+
width: 100%;
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
// 枚举复选框容器
|
|
21
|
+
const EnumCheckbox = styled.div`
|
|
22
|
+
width: 100%;
|
|
23
|
+
|
|
24
|
+
.ant-checkbox-group {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
gap: 8px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ant-checkbox-wrapper {
|
|
31
|
+
margin-right: 0;
|
|
32
|
+
|
|
33
|
+
&:hover .ant-checkbox-inner {
|
|
34
|
+
border-color: #1890ff;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
// 枚举单选按钮容器
|
|
40
|
+
const EnumRadio = styled.div`
|
|
41
|
+
width: 100%;
|
|
42
|
+
|
|
43
|
+
.ant-radio-group {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ant-radio-wrapper {
|
|
50
|
+
margin-right: 0;
|
|
51
|
+
|
|
52
|
+
&:hover .ant-radio-inner {
|
|
53
|
+
border-color: #1890ff;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 枚举字段组件
|
|
60
|
+
* 根据 EnumDisplayStyle 渲染不同的选择器
|
|
61
|
+
* - select: 下拉选择框
|
|
62
|
+
* - multi-select: 多选下拉框
|
|
63
|
+
* - checkbox: 复选框组(多选)
|
|
64
|
+
* - radio: 单选按钮组
|
|
65
|
+
*/
|
|
66
|
+
export const EnumField: React.FC<EnumFieldProps> = ({
|
|
67
|
+
schema,
|
|
68
|
+
value,
|
|
69
|
+
onChange,
|
|
70
|
+
disabled = false,
|
|
71
|
+
}) => {
|
|
72
|
+
const { Type } = schema;
|
|
73
|
+
|
|
74
|
+
// 优先从 AssociationPropertyMetadata 中读取,兼容旧的字段
|
|
75
|
+
const enumValues = useMemo(() => {
|
|
76
|
+
return schema.AssociationPropertyMetadata?.EnumValues || schema.EnumValues || [];
|
|
77
|
+
}, [schema]);
|
|
78
|
+
|
|
79
|
+
const displayStyle: EnumDisplayStyle = useMemo(() => {
|
|
80
|
+
return schema.AssociationPropertyMetadata?.EnumDisplayStyle || schema.EnumDisplayStyle || 'select';
|
|
81
|
+
}, [schema]);
|
|
82
|
+
|
|
83
|
+
// 处理 Select 变化
|
|
84
|
+
const handleSelectChange = useCallback(
|
|
85
|
+
(newValue: string | string[]) => {
|
|
86
|
+
onChange?.(newValue);
|
|
87
|
+
},
|
|
88
|
+
[onChange]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// 处理 Checkbox 变化
|
|
92
|
+
const handleCheckboxChange = useCallback(
|
|
93
|
+
(checkedValues: (string | number | boolean)[]) => {
|
|
94
|
+
onChange?.(checkedValues as string[]);
|
|
95
|
+
},
|
|
96
|
+
[onChange]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// 处理 Radio 变化
|
|
100
|
+
const handleRadioChange = useCallback(
|
|
101
|
+
(e: RadioChangeEvent) => {
|
|
102
|
+
onChange?.(e.target.value);
|
|
103
|
+
},
|
|
104
|
+
[onChange]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// 根据展示样式渲染不同的组件
|
|
108
|
+
const renderByDisplayStyle = () => {
|
|
109
|
+
switch (displayStyle) {
|
|
110
|
+
case 'checkbox':
|
|
111
|
+
// 复选框组 - 多选
|
|
112
|
+
return (
|
|
113
|
+
<EnumCheckbox>
|
|
114
|
+
<Checkbox.Group
|
|
115
|
+
value={Array.isArray(value) ? value : value ? [value] : []}
|
|
116
|
+
onChange={handleCheckboxChange}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
>
|
|
119
|
+
{enumValues.map((item) => (
|
|
120
|
+
<Checkbox key={String(item)} value={item}>
|
|
121
|
+
{String(item)}
|
|
122
|
+
</Checkbox>
|
|
123
|
+
))}
|
|
124
|
+
</Checkbox.Group>
|
|
125
|
+
</EnumCheckbox>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
case 'radio':
|
|
129
|
+
// 单选按钮组
|
|
130
|
+
return (
|
|
131
|
+
<EnumRadio>
|
|
132
|
+
<Radio.Group
|
|
133
|
+
value={value}
|
|
134
|
+
onChange={handleRadioChange}
|
|
135
|
+
disabled={disabled}
|
|
136
|
+
>
|
|
137
|
+
{enumValues.map((item) => (
|
|
138
|
+
<Radio key={String(item)} value={item}>
|
|
139
|
+
{String(item)}
|
|
140
|
+
</Radio>
|
|
141
|
+
))}
|
|
142
|
+
</Radio.Group>
|
|
143
|
+
</EnumRadio>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
case 'multi-select':
|
|
147
|
+
// 多选下拉框
|
|
148
|
+
return (
|
|
149
|
+
<EnumSelect>
|
|
150
|
+
<Select
|
|
151
|
+
value={Array.isArray(value) ? value : value ? [value] : []}
|
|
152
|
+
onChange={handleSelectChange}
|
|
153
|
+
disabled={disabled}
|
|
154
|
+
mode="multiple"
|
|
155
|
+
placeholder={`请选择`}
|
|
156
|
+
style={{ width: '100%' }}
|
|
157
|
+
allowClear
|
|
158
|
+
>
|
|
159
|
+
{enumValues.map((item) => (
|
|
160
|
+
<Option key={String(item)} value={item}>
|
|
161
|
+
{String(item)}
|
|
162
|
+
</Option>
|
|
163
|
+
))}
|
|
164
|
+
</Select>
|
|
165
|
+
</EnumSelect>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
case 'select':
|
|
169
|
+
default: {
|
|
170
|
+
// 下拉选择框
|
|
171
|
+
// 根据原始类型决定是否支持多选
|
|
172
|
+
const isMultiple = Type === 'array';
|
|
173
|
+
return (
|
|
174
|
+
<EnumSelect>
|
|
175
|
+
<Select
|
|
176
|
+
value={value}
|
|
177
|
+
onChange={handleSelectChange}
|
|
178
|
+
disabled={disabled}
|
|
179
|
+
mode={isMultiple ? 'multiple' : undefined}
|
|
180
|
+
placeholder={`请选择`}
|
|
181
|
+
style={{ width: '100%' }}
|
|
182
|
+
allowClear
|
|
183
|
+
>
|
|
184
|
+
{enumValues.map((item) => (
|
|
185
|
+
<Option key={String(item)} value={item}>
|
|
186
|
+
{String(item)}
|
|
187
|
+
</Option>
|
|
188
|
+
))}
|
|
189
|
+
</Select>
|
|
190
|
+
</EnumSelect>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return renderByDisplayStyle();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export default EnumField;
|