diff --git a/package.json b/package.json
index 34098d3..9aaaf43 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@kne-components/components-admin",
- "version": "1.1.45",
+ "version": "1.1.47",
"description": "用于实现一个后台管理系统的必要组件",
"scripts": {
"init": "husky",
@@ -64,7 +64,7 @@
}
},
"devDependencies": {
- "@kne/modules-dev": "^2.2.1",
+ "@kne/modules-dev": "^2.3.4",
"@kne/react-fetch": "^1.4.3",
"@kne/react-scripts": "^5.1.5",
"@kne/remote-loader": "^1.2.3",
@@ -88,6 +88,7 @@
"@kne/axios-fetch": "^1.2.0",
"@kne/column-split": "^1.0.5",
"@kne/count-down": "^0.2.2",
+ "@kne/ensure-slash": "^0.1.0",
"@kne/is-empty": "^1.0.1",
"@kne/json-view": "^0.1.1",
"@kne/react-box": "^0.1.9",
@@ -99,6 +100,7 @@
"@kne/use-control-value": "^0.1.9",
"@kne/use-ref-callback": "^0.1.2",
"@kne/use-refer-navigate": "^1.0.0",
+ "dayjs": "^1.11.21",
"md5": "^2.3.0",
"xlsx": "^0.18.5"
}
diff --git "a/prompts/prompts-remote-components/\345\233\275\351\231\205\345\214\226.md" "b/prompts/prompts-remote-components/\345\233\275\351\231\205\345\214\226.md"
index 30401db..b7fc66f 100644
--- "a/prompts/prompts-remote-components/\345\233\275\351\231\205\345\214\226.md"
+++ "b/prompts/prompts-remote-components/\345\233\275\351\231\205\345\214\226.md"
@@ -90,12 +90,139 @@ const columns = getColumns({formatMessage});
formatMessage({ id: 'KeyWithParam' }, { name: value })
```
+### 6. 两种可复用国际化模式
+
+#### 模式一:提供 `locale` 的 function(常用于枚举国际化)
+
+适用场景:枚举、静态配置、非 React 组件函数等不能直接使用 `useIntl` 的地方。参考 `EnumLoader`。
+
+核心做法是在 `withLocale.js` 中额外导出 `createFormatMessage(locale)`,由普通函数接收 `locale` 后创建当前语言的 `formatMessage`:
+
+```javascript
+import { createWithIntlProvider, createIntl } from '@kne/react-intl';
+import zhCN from './locale/zh-CN';
+import enUS from './locale/en-US';
+
+const withLocale = createWithIntlProvider({
+ defaultLocale: 'zh-CN',
+ messages: {
+ 'zh-CN': zhCN,
+ 'en-US': enUS
+ },
+ namespace: 'ai-talent-saas'
+});
+
+export const createFormatMessage = locale => {
+ const { formatMessage } = createIntl({
+ locale,
+ messages: {
+ 'zh-CN': zhCN,
+ 'en-US': enUS
+ },
+ namespace: 'ai-talent-saas'
+ });
+ return formatMessage;
+};
+
+export default withLocale;
+```
+
+枚举函数只负责接收 `locale` 并返回国际化后的枚举项,`value` 保持业务值稳定,只翻译 `description`:
+
+```javascript
+import { createFormatMessage } from './withLocale';
+
+const employeeStatus = ({ locale }) => {
+ const formatMessage = createFormatMessage(locale);
+ return [
+ { description: formatMessage({ id: 'enumLoader.employeeStatusActive' }), value: 'ACTIVE' },
+ { description: formatMessage({ id: 'enumLoader.employeeStatusResign' }), value: 'RESIGN' }
+ ];
+};
+
+export default employeeStatus;
+```
+
+使用时由调用方传入当前语言:
+
+```javascript
+const statusList = enums.employeeStatus({ locale });
+```
+
+#### 模式二:将国际化内容作为 JSX(常用于配置项、列、卡片内容)
+
+适用场景:`label`、`title`、`content` 等字段支持 `ReactNode`,并且这些配置可能在普通函数中返回,不能直接调用 Hook。参考 `TenantUserPlugin`。
+
+核心做法是在 `withLocale.js` 中额外导出一个被 `withLocale` 包裹的 `FormatMessage` 组件:
+
+```javascript
+import { createWithIntlProvider, useIntl } from '@kne/react-intl';
+import zhCN from './locale/zh-CN';
+import enUS from './locale/en-US';
+
+const withLocale = createWithIntlProvider({
+ defaultLocale: 'zh-CN',
+ messages: {
+ 'zh-CN': zhCN,
+ 'en-US': enUS
+ },
+ namespace: 'ai-talent-saas'
+});
+
+export const FormatMessage = withLocale(props => {
+ const { formatMessage } = useIntl();
+ return formatMessage(props);
+});
+
+export default withLocale;
+```
+
+普通配置函数中直接把翻译内容作为 JSX 返回:
+
+```javascript
+import withLocale, { FormatMessage } from './withLocale';
+
+export const personalCard = ({ moreInfo }) => {
+ return [
+ ...moreInfo,
+ {
+ key: 'position',
+ label: ,
+ content: 'xxx'
+ }
+ ];
+};
+
+export const getUserListColumns = ({ columns }) => {
+ return [
+ ...columns,
+ {
+ title: ,
+ name: 'options.position'
+ }
+ ];
+};
+```
+
+如果组件内部本身需要立即得到字符串(如表单 `label`、按钮文本、错误信息、modal 标题),仍然在被 `withLocale` 包裹的组件内使用 `useIntl`:
+
+```javascript
+const TenantUserPlugin = createWithRemoteLoader({...})(
+ withLocale(({ remoteModules, ...props }) => {
+ const { formatMessage } = useIntl();
+ return ;
+ })
+);
+```
+
## 四、注意事项
1. **所有使用 `useIntl` 的组件必须用 `withLocale` 包裹**
2. **`getColumns` 等工具函数通过参数接收 `formatMessage`,不使用 `useIntl`**
3. **语言包中避免重复的 key**,命名规则:`模块名 + 功能名`,如 `UserName`、`UserRole`
4. **`createWithRemoteLoader` 创建的组件内部使用 useIntl 时,外层需要重命名并用 withLocale 包裹**
+5. **枚举/普通函数国际化优先使用 `createFormatMessage(locale)` 模式**,函数入参显式接收 `locale`,返回值中只翻译展示字段
+6. **配置项需要返回 ReactNode 时使用 `` 模式**,不要在普通函数中直接调用 `useIntl`
---
@@ -177,6 +304,8 @@ const getColumns = ({formatMessage}) => {
- [ ] `getColumns` 等工具函数通过参数接收 `formatMessage`
- [ ] 语言包中无重复 key
- [ ] `createWithRemoteLoader` 组件必须使用 `createWithRemoteLoader({...})(withLocale(...))` 链式调用格式,**禁止**先定义中间变量再包裹
+- [ ] 枚举/普通函数如需国际化,使用 `createFormatMessage(locale)`,不要在函数中使用 Hook
+- [ ] `label`、`title`、`content` 等支持 JSX 的配置项可使用 ``
### 5. 最后检查
运行命令找到所有使用 useIntl 的文件,确保都已正确包裹:
diff --git a/src/components/Task/enums.js b/src/components/Task/enums.js
index 7e57ae0..8f6d3cd 100644
--- a/src/components/Task/enums.js
+++ b/src/components/Task/enums.js
@@ -1,20 +1,17 @@
-const TASK_STATUS_ENUM = [
- { value: 'pending', description: '等待执行', type: 'info' },
- {
- value: 'running',
- description: '执行中',
- type: 'progress'
- },
- { value: 'waiting', description: '等待操作', type: 'info' },
- {
- value: 'success',
- description: '成功',
- type: 'success'
- },
- { value: 'failed', description: '失败', type: 'danger' },
- { value: 'canceled', description: '取消' }
-];
+import { createFormatMessage } from './withLocale';
-const enums = { taskStatus: TASK_STATUS_ENUM };
+const taskStatus = ({ locale }) => {
+ const formatMessage = createFormatMessage(locale);
+ return [
+ { description: formatMessage({ id: 'Pending' }), value: 'pending', type: 'info' },
+ { description: formatMessage({ id: 'Running' }), value: 'running', type: 'progress' },
+ { description: formatMessage({ id: 'Waiting' }), value: 'waiting', type: 'info' },
+ { description: formatMessage({ id: 'Success' }), value: 'success', type: 'success' },
+ { description: formatMessage({ id: 'Failed' }), value: 'failed', type: 'danger' },
+ { description: formatMessage({ id: 'Canceled' }), value: 'canceled' }
+ ];
+};
+
+const enums = { taskStatus };
export default enums;
diff --git a/src/components/Task/withLocale.js b/src/components/Task/withLocale.js
index c932c37..6663bef 100644
--- a/src/components/Task/withLocale.js
+++ b/src/components/Task/withLocale.js
@@ -1,4 +1,4 @@
-import { createWithIntlProvider } from '@kne/react-intl';
+import { createWithIntlProvider, createIntl } from '@kne/react-intl';
import zhCN from './locale/zh-CN';
import enUS from './locale/en-US';
@@ -11,4 +11,16 @@ const withLocale = createWithIntlProvider({
namespace: 'components-admin:Task'
});
+export const createFormatMessage = locale => {
+ const { formatMessage } = createIntl({
+ locale,
+ messages: {
+ 'zh-CN': zhCN,
+ 'en-US': enUS
+ },
+ namespace: 'components-admin:Task'
+ });
+ return formatMessage;
+};
+
export default withLocale;
diff --git a/src/components/Tenant/CompanyInfo/DevelopmentHistory/index.js b/src/components/Tenant/CompanyInfo/DevelopmentHistory/index.js
index 79549e0..f58131c 100644
--- a/src/components/Tenant/CompanyInfo/DevelopmentHistory/index.js
+++ b/src/components/Tenant/CompanyInfo/DevelopmentHistory/index.js
@@ -2,6 +2,7 @@ import { createWithRemoteLoader } from '@kne/remote-loader';
import { Empty } from 'antd';
import dayjs from 'dayjs';
import Timeline from '@kne/timeline';
+import ensureSlash from '@kne/ensure-slash';
import withLocale from '../../withLocale';
import style from '../style.module.scss';
@@ -16,7 +17,7 @@ const resolveImageSrc = (id, origin) => {
if (typeof id === 'string' && /^https?:\/\//i.test(id)) {
return id;
}
- return `${origin}/api/v1/static/file-id/${id}`;
+ return `${ensureSlash(origin) || window.location.origin}/api/v1/static/file-id/${id}`;
};
const DevelopmentHistory = createWithRemoteLoader({
@@ -31,8 +32,6 @@ const DevelopmentHistory = createWithRemoteLoader({
return ;
}
- const origin = (typeof staticUrl === 'string' && staticUrl) || (typeof window !== 'undefined' ? window.location.origin : '');
-
const timelineData = list.map(item => {
const row = {
title: formatTime(item.time),
@@ -40,7 +39,7 @@ const DevelopmentHistory = createWithRemoteLoader({
};
if (item.images && item.images.length > 0) {
row.images = item.images.map(id => ({
- src: resolveImageSrc(id, origin)
+ src: resolveImageSrc(id, staticUrl)
}));
}
if (item.extra) {
diff --git a/src/components/Tenant/Tenant.js b/src/components/Tenant/Tenant.js
index 7bed20c..0812ee7 100644
--- a/src/components/Tenant/Tenant.js
+++ b/src/components/Tenant/Tenant.js
@@ -24,6 +24,7 @@ const Tenant = createWithRemoteLoader({
navigation={{
base: `${baseUrl}/tenant`,
showIndex: false,
+ defaultTitle: navigation.defaultTitle,
list: [
...(navigation.list || []),
{