@alicloud/appflow-chat 0.0.4-beta.5 → 0.0.4-beta.6
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 +157 -161
- package/dist/appflow-chat.esm.js +11980 -11526
- package/dist/types/index.d.ts +169 -0
- package/package.json +3 -15
- package/src/components/HumanVerify/CustomParamsRenderer/ArrayField.tsx +6 -4
- package/src/components/HumanVerify/CustomParamsRenderer/EnumField.tsx +5 -3
- package/src/components/HumanVerify/CustomParamsRenderer/FieldRenderer.tsx +6 -4
- package/src/components/HumanVerify/CustomParamsRenderer/FileField.tsx +38 -30
- package/src/components/HumanVerify/CustomParamsRenderer/ObjectField.tsx +4 -2
- package/src/components/HumanVerify/CustomParamsRenderer/TimeField.tsx +38 -101
- package/src/components/HumanVerify/HistoryCard.tsx +6 -4
- package/src/components/HumanVerify/HumanVerify.tsx +5 -3
- package/src/components/MessageBubble.tsx +4 -2
- package/src/components/RichMessageBubble.tsx +4 -2
- package/src/core/RichBubbleContent.tsx +4 -2
- package/src/core/SourceContent.tsx +5 -3
- package/src/core/WebSearchContent.tsx +3 -1
- package/src/i18n/LocaleContext.tsx +70 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/translate.ts +42 -0
- package/src/i18n/useTranslation.ts +39 -0
- package/src/i18n/utils.ts +67 -0
- package/src/index.ts +25 -0
- package/src/locales/en-US.ts +77 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/types.ts +35 -0
- package/src/locales/zh-CN.ts +78 -0
- package/src/markdown/components/Chart.tsx +3 -1
- package/src/markdown/components/Mermaid.tsx +8 -6
- package/src/markdown/index.tsx +11 -8
package/dist/types/index.d.ts
CHANGED
|
@@ -320,6 +320,21 @@ export declare interface CustomParamsRendererProps extends UploadConfig {
|
|
|
320
320
|
errors?: Record<string, string>;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/** 递归生成所有叶子节点的扁平路径联合类型 */
|
|
324
|
+
declare type DeepKey<T, P extends string = ''> = {
|
|
325
|
+
[K in keyof T & string]: T[K] extends object ? DeepKey<T[K], `${P}${P extends '' ? '' : '.'}${K}`> : `${P}${P extends '' ? '' : '.'}${K}`;
|
|
326
|
+
}[keyof T & string];
|
|
327
|
+
|
|
328
|
+
/** 深度可选,用于 LocaleProvider 的 overrides 字段 */
|
|
329
|
+
export declare type DeepPartial<T> = {
|
|
330
|
+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/** 把所有叶子节点的值类型统一为 string,并去掉 readonly */
|
|
334
|
+
declare type DeepStringify<T> = {
|
|
335
|
+
-readonly [K in keyof T]: T[K] extends object ? DeepStringify<T[K]> : string;
|
|
336
|
+
};
|
|
337
|
+
|
|
323
338
|
export declare type DocReferenceItem = SourceItem;
|
|
324
339
|
|
|
325
340
|
/**
|
|
@@ -357,6 +372,8 @@ export declare interface DocReferencesProps {
|
|
|
357
372
|
|
|
358
373
|
declare type EnumDisplayStyle = 'select' | 'checkbox' | 'radio' | 'multi-select';
|
|
359
374
|
|
|
375
|
+
export declare const enUS: Locale;
|
|
376
|
+
|
|
360
377
|
declare type ExtendedParamType = 'time' | 'file';
|
|
361
378
|
|
|
362
379
|
declare type FileSubType = 'default' | 'jpg' | 'png' | 'svg' | 'doc' | 'ppt' | 'excel' | 'txt' | 'markdown' | 'zip';
|
|
@@ -367,6 +384,12 @@ declare type FileSubType = 'default' | 'jpg' | 'png' | 'svg' | 'doc' | 'ppt' | '
|
|
|
367
384
|
*/
|
|
368
385
|
declare type FileUploader = (file: Blob, uploadUrl: string) => Promise<void>;
|
|
369
386
|
|
|
387
|
+
/** 获取当前全局 locale */
|
|
388
|
+
export declare function getGlobalLocale(): Locale;
|
|
389
|
+
|
|
390
|
+
/** 获取当前全局 locale 名称 */
|
|
391
|
+
export declare function getGlobalLocaleName(): string;
|
|
392
|
+
|
|
370
393
|
/**
|
|
371
394
|
* HistoryCard 历史卡片组件 (SDK 版本)
|
|
372
395
|
* 用于展示历史对话中的 card 类型消息(只读模式)
|
|
@@ -448,6 +471,40 @@ export declare const loadEchartsScript: () => Promise<void>;
|
|
|
448
471
|
|
|
449
472
|
export declare const loadMermaidScript: () => Promise<void>;
|
|
450
473
|
|
|
474
|
+
/** 词条对象类型:保留 zhCN 的结构,但叶子节点统一为 string */
|
|
475
|
+
export declare type Locale = DeepStringify<typeof zhCN>;
|
|
476
|
+
|
|
477
|
+
export declare const LocaleContext: default_2.Context<LocaleContextValue>;
|
|
478
|
+
|
|
479
|
+
export declare interface LocaleContextValue {
|
|
480
|
+
locale: Locale;
|
|
481
|
+
localeName: string;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* LocaleProvider
|
|
486
|
+
*
|
|
487
|
+
* 用法示例:
|
|
488
|
+
* ```tsx
|
|
489
|
+
* import { LocaleProvider, enUS } from '@alicloud/appflow-chat';
|
|
490
|
+
*
|
|
491
|
+
* <LocaleProvider locale={enUS} localeName="en-US">
|
|
492
|
+
* <App />
|
|
493
|
+
* </LocaleProvider>
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
export declare const LocaleProvider: default_2.FC<LocaleProviderProps>;
|
|
497
|
+
|
|
498
|
+
export declare interface LocaleProviderProps {
|
|
499
|
+
/** 完整 locale 对象,默认使用内置 zhCN */
|
|
500
|
+
locale?: Locale;
|
|
501
|
+
/** 语言标识,如 'zh-CN' / 'en-US' */
|
|
502
|
+
localeName?: string;
|
|
503
|
+
/** 部分覆盖:在选定 locale 基础上 deep merge 自定义文案 */
|
|
504
|
+
overrides?: DeepPartial<Locale>;
|
|
505
|
+
children: default_2.ReactNode;
|
|
506
|
+
}
|
|
507
|
+
|
|
451
508
|
/**
|
|
452
509
|
* MarkdownRenderer - Markdown渲染组件
|
|
453
510
|
*
|
|
@@ -672,6 +729,9 @@ export declare interface RichMessageBubbleProps {
|
|
|
672
729
|
onWebSearchClick?: (items: DocReferenceItem[]) => void;
|
|
673
730
|
}
|
|
674
731
|
|
|
732
|
+
/** 设置全局 locale,由 LocaleProvider 在挂载/更新时调用 */
|
|
733
|
+
export declare function setGlobalLocale(locale: Locale, localeName?: string): void;
|
|
734
|
+
|
|
675
735
|
/**
|
|
676
736
|
* ChatService - 独立的聊天服务类
|
|
677
737
|
* 用于自定义UI场景,不依赖React
|
|
@@ -744,6 +804,21 @@ export declare interface SourceItem {
|
|
|
744
804
|
|
|
745
805
|
declare type TimeSubType = 'year-month' | 'year-month-day' | 'datetime';
|
|
746
806
|
|
|
807
|
+
/**
|
|
808
|
+
* 全局翻译函数(非 Hook 版本)
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* translate('common.loading') // '加载中...'
|
|
812
|
+
* translate('humanVerify.placeholder.input', { title: '名称' }) // '请输入名称'
|
|
813
|
+
*/
|
|
814
|
+
export declare function translate(key: TranslationKey, params?: TranslationParams): string;
|
|
815
|
+
|
|
816
|
+
/** 所有合法的扁平 key 路径,如 'common.loading' | 'humanVerify.placeholder.input' */
|
|
817
|
+
export declare type TranslationKey = DeepKey<typeof zhCN>;
|
|
818
|
+
|
|
819
|
+
/** 插值参数 */
|
|
820
|
+
export declare type TranslationParams = Record<string, string | number>;
|
|
821
|
+
|
|
747
822
|
/**
|
|
748
823
|
* 上传配置
|
|
749
824
|
*/
|
|
@@ -787,6 +862,24 @@ export declare const useCustomParamsRenderer: (schema: CustomParamSchema) => {
|
|
|
787
862
|
|
|
788
863
|
export declare const useRichBubbleContext: () => RichBubbleContextValue;
|
|
789
864
|
|
|
865
|
+
/**
|
|
866
|
+
* 在 React 组件内消费国际化文案
|
|
867
|
+
*
|
|
868
|
+
* @example
|
|
869
|
+
* const { t } = useTranslation();
|
|
870
|
+
* <Input placeholder={t('humanVerify.placeholder.input', { title: '名称' })} />
|
|
871
|
+
*/
|
|
872
|
+
export declare function useTranslation(): UseTranslationResult;
|
|
873
|
+
|
|
874
|
+
export declare interface UseTranslationResult {
|
|
875
|
+
/** 翻译函数 */
|
|
876
|
+
t: (key: TranslationKey, params?: TranslationParams) => string;
|
|
877
|
+
/** 当前 locale 对象 */
|
|
878
|
+
locale: Locale;
|
|
879
|
+
/** 当前语言标识 */
|
|
880
|
+
localeName: string;
|
|
881
|
+
}
|
|
882
|
+
|
|
790
883
|
/**
|
|
791
884
|
* 校验 CustomParams 的值
|
|
792
885
|
* @param schema Schema 定义
|
|
@@ -894,4 +987,80 @@ export declare interface WebSearchPanelProps {
|
|
|
894
987
|
style?: default_2.CSSProperties;
|
|
895
988
|
}
|
|
896
989
|
|
|
990
|
+
/**
|
|
991
|
+
* 中文词条(默认语言)
|
|
992
|
+
*
|
|
993
|
+
* 该文件作为 Locale 类型的基准结构,所有其他语言文件必须实现相同的 key 结构。
|
|
994
|
+
* 新增文案时优先在此处添加,再补齐其他语言文件。
|
|
995
|
+
*/
|
|
996
|
+
export declare const zhCN: {
|
|
997
|
+
readonly common: {
|
|
998
|
+
readonly loading: "加载中...";
|
|
999
|
+
readonly confirm: "确定";
|
|
1000
|
+
readonly cancel: "取消";
|
|
1001
|
+
readonly submit: "提交";
|
|
1002
|
+
readonly retry: "重试";
|
|
1003
|
+
readonly copy: "复制";
|
|
1004
|
+
readonly copied: "已复制";
|
|
1005
|
+
readonly placeholderSelect: "请选择";
|
|
1006
|
+
};
|
|
1007
|
+
readonly humanVerify: {
|
|
1008
|
+
readonly requiredAll: "请填写所有必填项";
|
|
1009
|
+
readonly submitted: "已提交";
|
|
1010
|
+
readonly pending: "待提交";
|
|
1011
|
+
readonly placeholder: {
|
|
1012
|
+
readonly input: "请输入{title}";
|
|
1013
|
+
readonly select: "请选择";
|
|
1014
|
+
};
|
|
1015
|
+
readonly file: {
|
|
1016
|
+
readonly supportAll: "支持所有文件格式";
|
|
1017
|
+
readonly supportFormats: "支持 {formats} 格式";
|
|
1018
|
+
readonly uploadButton: "选择文件";
|
|
1019
|
+
readonly uploading: "上传中";
|
|
1020
|
+
readonly upload: "上传";
|
|
1021
|
+
readonly defaultFileName: "文件";
|
|
1022
|
+
readonly maxSizeError: "文件大小不能超过 {size}";
|
|
1023
|
+
readonly uploadFailed: "文件上传失败";
|
|
1024
|
+
readonly tokenFailed: "获取上传凭证失败";
|
|
1025
|
+
readonly uploaderNotConfigured: "上传功能未配置";
|
|
1026
|
+
readonly uploadMethodNotConfigured: "文件上传方法未配置";
|
|
1027
|
+
readonly getFileIdFailed: "获取文件ID失败";
|
|
1028
|
+
};
|
|
1029
|
+
};
|
|
1030
|
+
readonly webSearch: {
|
|
1031
|
+
readonly title: "搜索结果";
|
|
1032
|
+
readonly foundPages: "已搜索到{count}个网页";
|
|
1033
|
+
};
|
|
1034
|
+
readonly source: {
|
|
1035
|
+
readonly title: "参考资料";
|
|
1036
|
+
readonly answerFrom: "回答来源:";
|
|
1037
|
+
readonly imageFrom: "图片来源:";
|
|
1038
|
+
};
|
|
1039
|
+
readonly rich: {
|
|
1040
|
+
readonly stepLabel: "步骤{index}:";
|
|
1041
|
+
readonly emptyContent: "暂无内容";
|
|
1042
|
+
};
|
|
1043
|
+
readonly markdown: {
|
|
1044
|
+
readonly copyCode: "复制代码";
|
|
1045
|
+
readonly copied: "已复制!";
|
|
1046
|
+
readonly copy: "复制";
|
|
1047
|
+
readonly copiedShort: "已复制";
|
|
1048
|
+
readonly deepThinking: "深度思考";
|
|
1049
|
+
readonly chartLoading: "图表加载中...";
|
|
1050
|
+
readonly tableLoading: "表格加载中...";
|
|
1051
|
+
readonly tableLoadFailed: "表格数据加载失败,请检查数据格式";
|
|
1052
|
+
readonly chartLoadFailed: "图表数据加载失败,请检查数据格式";
|
|
1053
|
+
readonly mermaidLoadFailed: "Mermaid 库加载失败";
|
|
1054
|
+
readonly mermaidNotLoaded: "Mermaid 未正确加载";
|
|
1055
|
+
readonly mermaidRenderFailed: "Mermaid 图表渲染失败";
|
|
1056
|
+
};
|
|
1057
|
+
readonly message: {
|
|
1058
|
+
readonly like: "点赞";
|
|
1059
|
+
readonly dislike: "点踩";
|
|
1060
|
+
readonly regenerate: "重新生成";
|
|
1061
|
+
readonly copy: "复制";
|
|
1062
|
+
readonly copied: "已复制";
|
|
1063
|
+
};
|
|
1064
|
+
};
|
|
1065
|
+
|
|
897
1066
|
export { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alicloud/appflow-chat",
|
|
3
|
-
"version": "0.0.4-beta.
|
|
3
|
+
"version": "0.0.4-beta.6",
|
|
4
4
|
"description": "Appflow-Chat AI聊天机器人组件库,提供聊天服务和UI组件",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/appflow-chat.cjs.js",
|
|
@@ -48,31 +48,19 @@
|
|
|
48
48
|
"@types/react-dom": "^18.3.1",
|
|
49
49
|
"@vitejs/plugin-react": "^4.3.3",
|
|
50
50
|
"antd": "^5.24.8",
|
|
51
|
-
"dayjs": "^1.11.0",
|
|
52
51
|
"eslint": "^9.13.0",
|
|
53
52
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
54
53
|
"eslint-plugin-react-refresh": "^0.4.14",
|
|
55
54
|
"less": "^4.2.0",
|
|
56
|
-
"moment": "^2.29.0",
|
|
57
55
|
"typescript": "~5.6.2",
|
|
58
56
|
"typescript-eslint": "^8.11.0",
|
|
59
57
|
"vite": "^5.4.10",
|
|
60
58
|
"vite-plugin-dts": "^4.3.0"
|
|
61
59
|
},
|
|
62
60
|
"peerDependencies": {
|
|
63
|
-
"antd": "^
|
|
61
|
+
"antd": "^5.0.0",
|
|
64
62
|
"react": "^18.0.0",
|
|
65
|
-
"react-dom": "^18.0.0"
|
|
66
|
-
"dayjs": "^1.11.0",
|
|
67
|
-
"moment": "^2.29.0"
|
|
68
|
-
},
|
|
69
|
-
"peerDependenciesMeta": {
|
|
70
|
-
"dayjs": {
|
|
71
|
-
"optional": true
|
|
72
|
-
},
|
|
73
|
-
"moment": {
|
|
74
|
-
"optional": true
|
|
75
|
-
}
|
|
63
|
+
"react-dom": "^18.0.0"
|
|
76
64
|
},
|
|
77
65
|
"files": [
|
|
78
66
|
"dist",
|
|
@@ -3,6 +3,7 @@ import { Button, Input, InputNumber, Switch, Space } from 'antd';
|
|
|
3
3
|
import { PlusOutlined, MinusOutlined } from '@ant-design/icons';
|
|
4
4
|
import { ArrayFieldProps, CustomParamSchema, FileSubType, sortPropertiesByOrder } from './types';
|
|
5
5
|
import styled from 'styled-components';
|
|
6
|
+
import { useTranslation } from '../../../i18n';
|
|
6
7
|
|
|
7
8
|
// 图片类型列表
|
|
8
9
|
const IMAGE_TYPES: FileSubType[] = ['jpg', 'png', 'svg'];
|
|
@@ -191,7 +192,8 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
191
192
|
}) => {
|
|
192
193
|
const { Title, Description, Items } = schema;
|
|
193
194
|
const displayTitle = Title || name;
|
|
194
|
-
|
|
195
|
+
const { t } = useTranslation();
|
|
196
|
+
|
|
195
197
|
// 计算当前数组的完整路径
|
|
196
198
|
const currentPath = fieldPath ? `${fieldPath}.${name}` : name;
|
|
197
199
|
|
|
@@ -370,7 +372,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
370
372
|
return sortedProperties.map(([propertyName, propertySchema]) => {
|
|
371
373
|
const isRequired = itemRequired.includes(propertyName);
|
|
372
374
|
return (
|
|
373
|
-
<React.Suspense key={propertyName} fallback={<div
|
|
375
|
+
<React.Suspense key={propertyName} fallback={<div>{t('common.loading')}</div>}>
|
|
374
376
|
<FieldRenderer
|
|
375
377
|
name={propertyName}
|
|
376
378
|
schema={propertySchema}
|
|
@@ -392,7 +394,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
392
394
|
// 渲染嵌套数组类型的数组项
|
|
393
395
|
const renderArrayItemContent = (item: any, index: number) => {
|
|
394
396
|
return (
|
|
395
|
-
<React.Suspense fallback={<div
|
|
397
|
+
<React.Suspense fallback={<div>{t('common.loading')}</div>}>
|
|
396
398
|
<FieldRenderer
|
|
397
399
|
name={`${name}[${index}]`}
|
|
398
400
|
schema={Items}
|
|
@@ -412,7 +414,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
412
414
|
// 渲染复杂类型的数组项(file、time、带枚举的类型)
|
|
413
415
|
const renderComplexItemContent = (item: any, index: number) => {
|
|
414
416
|
return (
|
|
415
|
-
<React.Suspense fallback={<div
|
|
417
|
+
<React.Suspense fallback={<div>{t('common.loading')}</div>}>
|
|
416
418
|
<FieldRenderer
|
|
417
419
|
name={`[${index}]`}
|
|
418
420
|
schema={Items}
|
|
@@ -3,6 +3,7 @@ import { Select, Checkbox, Radio } from 'antd';
|
|
|
3
3
|
import type { RadioChangeEvent } from 'antd';
|
|
4
4
|
import { EnumFieldProps, EnumDisplayStyle } from './types';
|
|
5
5
|
import styled from 'styled-components';
|
|
6
|
+
import { useTranslation } from '../../../i18n';
|
|
6
7
|
|
|
7
8
|
const { Option } = Select;
|
|
8
9
|
|
|
@@ -70,7 +71,8 @@ export const EnumField: React.FC<EnumFieldProps> = ({
|
|
|
70
71
|
disabled = false,
|
|
71
72
|
}) => {
|
|
72
73
|
const { Type } = schema;
|
|
73
|
-
|
|
74
|
+
const { t } = useTranslation();
|
|
75
|
+
|
|
74
76
|
// 优先从 AssociationPropertyMetadata 中读取,兼容旧的字段
|
|
75
77
|
const enumValues = useMemo(() => {
|
|
76
78
|
return schema.AssociationPropertyMetadata?.EnumValues || schema.EnumValues || [];
|
|
@@ -152,7 +154,7 @@ export const EnumField: React.FC<EnumFieldProps> = ({
|
|
|
152
154
|
onChange={handleSelectChange}
|
|
153
155
|
disabled={disabled}
|
|
154
156
|
mode="multiple"
|
|
155
|
-
placeholder={
|
|
157
|
+
placeholder={t('common.placeholderSelect')}
|
|
156
158
|
style={{ width: '100%' }}
|
|
157
159
|
allowClear
|
|
158
160
|
>
|
|
@@ -177,7 +179,7 @@ export const EnumField: React.FC<EnumFieldProps> = ({
|
|
|
177
179
|
onChange={handleSelectChange}
|
|
178
180
|
disabled={disabled}
|
|
179
181
|
mode={isMultiple ? 'multiple' : undefined}
|
|
180
|
-
placeholder={
|
|
182
|
+
placeholder={t('common.placeholderSelect')}
|
|
181
183
|
style={{ width: '100%' }}
|
|
182
184
|
allowClear
|
|
183
185
|
>
|
|
@@ -7,6 +7,7 @@ import { EnumField } from './EnumField';
|
|
|
7
7
|
import styled from 'styled-components';
|
|
8
8
|
import TimeField from './TimeField';
|
|
9
9
|
import FileField from './FileField';
|
|
10
|
+
import { useTranslation } from '../../../i18n';
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
// ==================== Styled Components ====================
|
|
@@ -130,7 +131,8 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
130
131
|
}) => {
|
|
131
132
|
const { Type, Title, Description } = schema;
|
|
132
133
|
const displayTitle = Title || name;
|
|
133
|
-
|
|
134
|
+
const { t } = useTranslation();
|
|
135
|
+
|
|
134
136
|
// 获取 Ant Design 的 prefixCls 配置,自动继承用户项目的 ConfigProvider 设置
|
|
135
137
|
// 使用 ConfigProvider.ConfigContext 获取完整配置
|
|
136
138
|
const configContext = useContext(ConfigProvider.ConfigContext);
|
|
@@ -176,7 +178,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
176
178
|
value={value}
|
|
177
179
|
onChange={(e) => handleChange(e.target.value)}
|
|
178
180
|
disabled={disabled}
|
|
179
|
-
placeholder={
|
|
181
|
+
placeholder={t('humanVerify.placeholder.input', { title: displayTitle })}
|
|
180
182
|
/>
|
|
181
183
|
</InputWrapper>
|
|
182
184
|
);
|
|
@@ -189,7 +191,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
189
191
|
onChange={handleChange}
|
|
190
192
|
disabled={disabled}
|
|
191
193
|
style={{ width: '100%' }}
|
|
192
|
-
placeholder={
|
|
194
|
+
placeholder={t('humanVerify.placeholder.input', { title: displayTitle })}
|
|
193
195
|
/>
|
|
194
196
|
</InputWrapper>
|
|
195
197
|
);
|
|
@@ -307,7 +309,7 @@ export const FieldRenderer: React.FC<FieldRendererProps> = ({
|
|
|
307
309
|
value={value}
|
|
308
310
|
onChange={(e) => handleChange(e.target.value)}
|
|
309
311
|
disabled={disabled}
|
|
310
|
-
placeholder={
|
|
312
|
+
placeholder={t('humanVerify.placeholder.input', { title: displayTitle })}
|
|
311
313
|
/>
|
|
312
314
|
</InputWrapper>
|
|
313
315
|
);
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
UploadFileResponse
|
|
11
11
|
} from './types';
|
|
12
12
|
import styled from 'styled-components';
|
|
13
|
+
import { useTranslation } from '../../../i18n';
|
|
14
|
+
import { translate } from '../../../i18n';
|
|
13
15
|
|
|
14
16
|
// ==================== Styled Components ====================
|
|
15
17
|
|
|
@@ -114,16 +116,17 @@ const getAcceptBySubTypes = (subTypes?: FileSubType[]): string => {
|
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
118
|
* 根据文件子类型数组获取上传提示文本
|
|
119
|
+
* 使用全局 translate(非 hook 版本)以便在组件外调用
|
|
117
120
|
*/
|
|
118
121
|
const getUploadHintBySubTypes = (subTypes?: FileSubType[]): string => {
|
|
119
122
|
if (!subTypes || subTypes.length === 0) {
|
|
120
|
-
return '
|
|
123
|
+
return translate('humanVerify.file.supportAll');
|
|
121
124
|
}
|
|
122
|
-
|
|
125
|
+
|
|
123
126
|
if (subTypes.includes('default')) {
|
|
124
|
-
return '
|
|
127
|
+
return translate('humanVerify.file.supportAll');
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
const hintMap: Record<FileSubType, string> = {
|
|
128
131
|
'jpg': 'JPG',
|
|
129
132
|
'png': 'PNG',
|
|
@@ -134,11 +137,13 @@ const getUploadHintBySubTypes = (subTypes?: FileSubType[]): string => {
|
|
|
134
137
|
'txt': 'TXT',
|
|
135
138
|
'markdown': 'Markdown',
|
|
136
139
|
'zip': 'ZIP/RAR/7Z',
|
|
137
|
-
'default': '
|
|
140
|
+
'default': translate('humanVerify.file.supportAll'),
|
|
138
141
|
};
|
|
139
|
-
|
|
142
|
+
|
|
140
143
|
const hints = subTypes.map(subType => hintMap[subType] || subType).filter(Boolean);
|
|
141
|
-
return hints.length > 0
|
|
144
|
+
return hints.length > 0
|
|
145
|
+
? translate('humanVerify.file.supportFormats', { formats: hints.join('、') })
|
|
146
|
+
: translate('humanVerify.file.supportAll');
|
|
142
147
|
};
|
|
143
148
|
|
|
144
149
|
/**
|
|
@@ -164,6 +169,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
164
169
|
uploadSender,
|
|
165
170
|
fileUploader,
|
|
166
171
|
}) => {
|
|
172
|
+
const { t } = useTranslation();
|
|
167
173
|
// 优先从 AssociationPropertyMetadata.SubType 读取,兼容旧的 FileSubType 字段
|
|
168
174
|
const subTypes = useMemo((): FileSubType[] => {
|
|
169
175
|
const subTypeArray = schema.AssociationPropertyMetadata?.SubType;
|
|
@@ -190,7 +196,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
190
196
|
if (Array.isArray(value)) {
|
|
191
197
|
return value.map((file: any, index: number) => ({
|
|
192
198
|
uid: file.uid || `${index}`,
|
|
193
|
-
name: file.name ||
|
|
199
|
+
name: file.name || `${t('humanVerify.file.defaultFileName')}${index + 1}`,
|
|
194
200
|
status: 'done' as const,
|
|
195
201
|
url: file.url,
|
|
196
202
|
...file,
|
|
@@ -199,7 +205,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
199
205
|
if (typeof value === 'object') {
|
|
200
206
|
return [{
|
|
201
207
|
uid: value.uid || '0',
|
|
202
|
-
name: value.name || '
|
|
208
|
+
name: value.name || t('humanVerify.file.defaultFileName'),
|
|
203
209
|
status: 'done' as const,
|
|
204
210
|
url: value.url,
|
|
205
211
|
...value,
|
|
@@ -227,7 +233,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
227
233
|
*/
|
|
228
234
|
const getUploadToken = useCallback(async (fileName: string): Promise<UploadTokenResponse | null> => {
|
|
229
235
|
if (!uploadSender) {
|
|
230
|
-
message.error('
|
|
236
|
+
message.error(t('humanVerify.file.uploaderNotConfigured'));
|
|
231
237
|
return null;
|
|
232
238
|
}
|
|
233
239
|
|
|
@@ -238,7 +244,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
238
244
|
});
|
|
239
245
|
|
|
240
246
|
if (!response) {
|
|
241
|
-
message.error('
|
|
247
|
+
message.error(t('humanVerify.file.tokenFailed'));
|
|
242
248
|
return null;
|
|
243
249
|
}
|
|
244
250
|
|
|
@@ -255,14 +261,14 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
255
261
|
return parsedResponse as UploadTokenResponse;
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
message.error('
|
|
264
|
+
message.error(t('humanVerify.file.tokenFailed'));
|
|
259
265
|
return null;
|
|
260
266
|
} catch (error) {
|
|
261
|
-
console.error('
|
|
262
|
-
message.error('
|
|
267
|
+
console.error(t('humanVerify.file.tokenFailed'), error);
|
|
268
|
+
message.error(t('humanVerify.file.tokenFailed'));
|
|
263
269
|
return null;
|
|
264
270
|
}
|
|
265
|
-
}, [uploadSender]);
|
|
271
|
+
}, [uploadSender, t]);
|
|
266
272
|
|
|
267
273
|
/**
|
|
268
274
|
* 获取文件 ID(文件上传专用)
|
|
@@ -298,10 +304,10 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
298
304
|
|
|
299
305
|
return null;
|
|
300
306
|
} catch (error) {
|
|
301
|
-
console.error('
|
|
307
|
+
console.error(t('humanVerify.file.getFileIdFailed'), error);
|
|
302
308
|
return null;
|
|
303
309
|
}
|
|
304
|
-
}, [uploadSender]);
|
|
310
|
+
}, [uploadSender, t]);
|
|
305
311
|
|
|
306
312
|
/**
|
|
307
313
|
* 图片上传处理
|
|
@@ -311,12 +317,12 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
311
317
|
// 1. 获取预签名 URL
|
|
312
318
|
const tokenResponse = await getUploadToken(file.name);
|
|
313
319
|
if (!tokenResponse) {
|
|
314
|
-
throw new Error('
|
|
320
|
+
throw new Error(t('humanVerify.file.tokenFailed'));
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
// 2. 上传文件到 OSS
|
|
318
324
|
if (!fileUploader) {
|
|
319
|
-
throw new Error('
|
|
325
|
+
throw new Error(t('humanVerify.file.uploadMethodNotConfigured'));
|
|
320
326
|
}
|
|
321
327
|
const blob = new Blob([file]);
|
|
322
328
|
await fileUploader(blob, tokenResponse.uploadUrl);
|
|
@@ -329,7 +335,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
329
335
|
type: file.type,
|
|
330
336
|
size: file.size,
|
|
331
337
|
};
|
|
332
|
-
}, [getUploadToken, fileUploader]);
|
|
338
|
+
}, [getUploadToken, fileUploader, t]);
|
|
333
339
|
|
|
334
340
|
/**
|
|
335
341
|
* 文件上传处理
|
|
@@ -339,12 +345,12 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
339
345
|
// 1. 获取预签名 URL
|
|
340
346
|
const tokenResponse = await getUploadToken(file.name);
|
|
341
347
|
if (!tokenResponse) {
|
|
342
|
-
throw new Error('
|
|
348
|
+
throw new Error(t('humanVerify.file.tokenFailed'));
|
|
343
349
|
}
|
|
344
350
|
|
|
345
351
|
// 2. 上传文件到 OSS
|
|
346
352
|
if (!fileUploader) {
|
|
347
|
-
throw new Error('
|
|
353
|
+
throw new Error(t('humanVerify.file.uploadMethodNotConfigured'));
|
|
348
354
|
}
|
|
349
355
|
const blob = new Blob([file]);
|
|
350
356
|
await fileUploader(blob, tokenResponse.uploadUrl);
|
|
@@ -362,7 +368,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
362
368
|
size: file.size,
|
|
363
369
|
fileType: file.name.split('.').pop(),
|
|
364
370
|
};
|
|
365
|
-
}, [getUploadToken, fileUploader, getFileId]);
|
|
371
|
+
}, [getUploadToken, fileUploader, getFileId, t]);
|
|
366
372
|
|
|
367
373
|
/**
|
|
368
374
|
* 自定义上传逻辑
|
|
@@ -379,7 +385,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
379
385
|
onSuccess?.({ url }, new XMLHttpRequest());
|
|
380
386
|
} catch (error) {
|
|
381
387
|
onError?.(error as Error);
|
|
382
|
-
message.error('
|
|
388
|
+
message.error(t('humanVerify.file.uploadFailed'));
|
|
383
389
|
}
|
|
384
390
|
return;
|
|
385
391
|
}
|
|
@@ -399,12 +405,12 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
399
405
|
onSuccess?.(result, new XMLHttpRequest());
|
|
400
406
|
} catch (error) {
|
|
401
407
|
onError?.(error as Error);
|
|
402
|
-
message.error('
|
|
408
|
+
message.error(t('humanVerify.file.uploadFailed'));
|
|
403
409
|
} finally {
|
|
404
410
|
setUploading(false);
|
|
405
411
|
}
|
|
406
412
|
},
|
|
407
|
-
[uploadSender, fileUploader, isImage, handleImageUpload, handleFileUpload]
|
|
413
|
+
[uploadSender, fileUploader, isImage, handleImageUpload, handleFileUpload, t]
|
|
408
414
|
);
|
|
409
415
|
|
|
410
416
|
// 处理文件变化
|
|
@@ -440,19 +446,21 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
440
446
|
// 文件大小限制(默认 10MB)
|
|
441
447
|
const maxSize = 10 * 1024 * 1024;
|
|
442
448
|
if (file.size > maxSize) {
|
|
443
|
-
message.error('
|
|
449
|
+
message.error(t('humanVerify.file.maxSizeError', { size: '10MB' }));
|
|
444
450
|
return Upload.LIST_IGNORE;
|
|
445
451
|
}
|
|
446
452
|
return true;
|
|
447
453
|
},
|
|
448
|
-
[]
|
|
454
|
+
[t]
|
|
449
455
|
);
|
|
450
456
|
|
|
451
457
|
// 上传按钮的加载指示器
|
|
452
458
|
const uploadButton = (
|
|
453
459
|
<div>
|
|
454
460
|
{uploading ? <LoadingOutlined /> : <PlusOutlined />}
|
|
455
|
-
<div style={{ marginTop: 8 }}>
|
|
461
|
+
<div style={{ marginTop: 8 }}>
|
|
462
|
+
{uploading ? t('humanVerify.file.uploading') : t('humanVerify.file.upload')}
|
|
463
|
+
</div>
|
|
456
464
|
</div>
|
|
457
465
|
);
|
|
458
466
|
|
|
@@ -498,7 +506,7 @@ export const FileField: React.FC<FileFieldProps> = ({
|
|
|
498
506
|
disabled={disabled || uploading}
|
|
499
507
|
loading={uploading}
|
|
500
508
|
>
|
|
501
|
-
{uploading ? '
|
|
509
|
+
{uploading ? t('humanVerify.file.uploading') : t('humanVerify.file.uploadButton')}
|
|
502
510
|
</Button>
|
|
503
511
|
</Upload>
|
|
504
512
|
<FileHint>{hint}</FileHint>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { ObjectFieldProps, sortPropertiesByOrder } from './types';
|
|
4
|
+
import { useTranslation } from '../../../i18n';
|
|
4
5
|
|
|
5
6
|
// 前向声明,避免循环依赖
|
|
6
7
|
const FieldRenderer = React.lazy(() => import('./FieldRenderer'));
|
|
@@ -71,7 +72,8 @@ export const ObjectField: React.FC<ObjectFieldProps> = ({
|
|
|
71
72
|
}) => {
|
|
72
73
|
const { Title, Description, Properties, Required: RequiredFields = [] } = schema;
|
|
73
74
|
const displayTitle = Title || name;
|
|
74
|
-
|
|
75
|
+
const { t } = useTranslation();
|
|
76
|
+
|
|
75
77
|
// 计算当前对象的完整路径
|
|
76
78
|
const currentPath = fieldPath ? `${fieldPath}.${name}` : name;
|
|
77
79
|
|
|
@@ -108,7 +110,7 @@ export const ObjectField: React.FC<ObjectFieldProps> = ({
|
|
|
108
110
|
{sortedProperties.map(([propertyName, propertySchema]) => {
|
|
109
111
|
const isRequired = RequiredFields.includes(propertyName);
|
|
110
112
|
return (
|
|
111
|
-
<React.Suspense key={propertyName} fallback={<div
|
|
113
|
+
<React.Suspense key={propertyName} fallback={<div>{t('common.loading')}</div>}>
|
|
112
114
|
<FieldRenderer
|
|
113
115
|
name={propertyName}
|
|
114
116
|
schema={propertySchema}
|