{formatMessage({ id: 'InviteLink' })}
diff --git a/src/components/Tenant/UserList/Actions/buildInvitePersonalCardProps.js b/src/components/Tenant/UserList/Actions/buildInvitePersonalCardProps.js
deleted file mode 100644
index f0d670b..0000000
--- a/src/components/Tenant/UserList/Actions/buildInvitePersonalCardProps.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import get from 'lodash/get';
-import { Typography } from 'antd';
-import getUserOrgDisplayItems from '../getUserOrgDisplayItems';
-import normalizeTenantUserForPersonalCard from '../normalizeTenantUserForPersonalCard';
-import { resolvePositionDisplayName } from '../buildUserListWithPositionList';
-import style from './personalCard.module.scss';
-
-/** 副标题单行省略,悬停展示完整内容 */
-export const renderPersonalCardEllipsisTitle = text => {
- if (!text) {
- return undefined;
- }
- return (
-
- {text}
-
- );
-};
-
-const buildRolesTitle = data => {
- const roles = Array.isArray(data?.roles) && data.roles.length ? data.roles : data?.roleDetails;
- if (!Array.isArray(roles) || !roles.length) {
- return '';
- }
- return roles
- .map(item => (typeof item === 'string' ? item : item?.name))
- .filter(Boolean)
- .join('、');
-};
-
-const buildOrgMoreInfo = data =>
- getUserOrgDisplayItems(data).map(org => {
- const path = org.fullPath || org.label;
- const showPath = path && org.label && path !== org.label;
- return {
- key: `org-${org.id}`,
- label: org.label,
- content: showPath ? path : org.label
- };
- });
-
-const insertAfterKey = (items, key, entry) => {
- const index = items.findIndex(item => item.key === key);
- const next = items.slice();
- next.splice(index >= 0 ? index + 1 : 0, 0, entry);
- return next;
-};
-
-const appendPositionMoreInfo = (moreInfo, data, { formatMessage, positionList }) => {
- if (moreInfo.some(item => item.key === 'position')) {
- return moreInfo;
- }
- const positionName = resolvePositionDisplayName(data, positionList);
- if (!positionName) {
- return moreInfo;
- }
- return insertAfterKey(moreInfo, 'roles', {
- key: 'position',
- label: formatMessage?.({ id: 'Position' }) || '岗位',
- content: renderPersonalCardEllipsisTitle(positionName)
- });
-};
-
-const buildRolesAndOrgMoreInfo = (data, { formatMessage }) => {
- const roles = buildRolesTitle(data);
- const roleContent =
- roles || formatMessage?.({ id: 'DefaultRole' }) || '默认角色';
-
- const items = [
- {
- key: 'roles',
- label: formatMessage?.({ id: 'UserRole' }) || '角色',
- content: renderPersonalCardEllipsisTitle(roleContent)
- }
- ];
-
- const orgMoreInfo = buildOrgMoreInfo(data);
- if (orgMoreInfo.length > 1) {
- return items.concat(orgMoreInfo);
- }
- if (orgMoreInfo.length === 1) {
- const org = getUserOrgDisplayItems(data)[0];
- const path = org?.fullPath || org?.label;
- if (path && org?.label && path !== org.label) {
- return items.concat([
- {
- key: 'org-primary',
- label: formatMessage?.({ id: 'Department' }) || '部门',
- content: renderPersonalCardEllipsisTitle(path)
- }
- ]);
- }
- return items.concat(orgMoreInfo);
- }
- return items;
-};
-
-const applyPersonalCardPlugins = (moreInfo, data, context) => {
- let next = moreInfo;
- const enhancer = get(context.plugins, 'tenantAdmin.personalCard');
- if (typeof enhancer === 'function') {
- next = enhancer({ moreInfo: next, data, ...context }) ?? next;
- }
- return appendPositionMoreInfo(next, data, context);
-};
-
-/** 将用户列表行 / 邀请数据映射为 PersonalCard 属性 */
-const buildInvitePersonalCardProps = (data, context = {}) => {
- const { Image, formatMessage, positionList, plugins } = context;
- const normalized = normalizeTenantUserForPersonalCard(data);
-
- let moreInfo = buildRolesAndOrgMoreInfo(normalized, { formatMessage });
- moreInfo = applyPersonalCardPlugins(moreInfo, normalized, {
- formatMessage,
- positionList,
- plugins
- });
-
- const positionName = resolvePositionDisplayName(normalized, positionList);
- const titleText = positionName || undefined;
-
- return {
- mode: 'horizontal',
- name: normalized?.name,
- title: renderPersonalCardEllipsisTitle(titleText),
- email: normalized?.email,
- phone: normalized?.phone,
- description: normalized?.description,
- moreInfo,
- avatar: ({ className }) =>
- };
-};
-
-export default buildInvitePersonalCardProps;
diff --git a/src/components/Tenant/UserList/Actions/buildInvitePersonalCardProps.test.js b/src/components/Tenant/UserList/Actions/buildInvitePersonalCardProps.test.js
deleted file mode 100644
index c399390..0000000
--- a/src/components/Tenant/UserList/Actions/buildInvitePersonalCardProps.test.js
+++ /dev/null
@@ -1,104 +0,0 @@
-jest.mock('antd', () => ({
- Typography: {
- Text: ({ children }) => children
- }
-}));
-
-import buildInvitePersonalCardProps from './buildInvitePersonalCardProps';
-
-const formatMessage = ({ id }) => id;
-const Image = { Avatar: () => null };
-
-const sampleUser = {
- name: '张三',
- email: 'zhang@example.com',
- phone: '13800000000',
- roles: [{ name: '管理员' }, { name: 'HR' }],
- options: { position: '产品经理' },
- tenantOrg: { name: '研发部', path: '公司/研发部' }
-};
-
-describe('buildInvitePersonalCardProps without plugin', () => {
- it('builds role and position moreInfo when plugins is undefined', () => {
- const props = buildInvitePersonalCardProps(sampleUser, {
- Image,
- formatMessage,
- positionList: []
- });
-
- expect(props.name).toBe('张三');
- expect(props.email).toBe('zhang@example.com');
- const keys = props.moreInfo.map(item => item.key);
- expect(keys).toContain('roles');
- expect(keys).toContain('position');
- });
-
- it('builds role and position when plugins is empty object', () => {
- const props = buildInvitePersonalCardProps(sampleUser, {
- Image,
- formatMessage,
- plugins: {},
- positionList: []
- });
-
- const keys = props.moreInfo.map(item => item.key);
- expect(keys).toContain('roles');
- expect(keys).toContain('position');
- });
-
- it('ignores non-function tenantAdmin.personalCard', () => {
- const props = buildInvitePersonalCardProps(sampleUser, {
- Image,
- formatMessage,
- plugins: { tenantAdmin: { personalCard: 'not-a-function' } },
- positionList: []
- });
-
- expect(props.moreInfo.some(item => item.key === 'position')).toBe(true);
- });
-
- it('still appends position after plugin enhancer when plugin omits position', () => {
- const props = buildInvitePersonalCardProps(sampleUser, {
- Image,
- formatMessage,
- plugins: {
- tenantAdmin: {
- personalCard: ({ moreInfo }) => moreInfo.filter(item => item.key !== 'position')
- }
- },
- positionList: []
- });
-
- expect(props.moreInfo.some(item => item.key === 'position')).toBe(true);
- });
-
- it('resolves position name from positionList by id', () => {
- const props = buildInvitePersonalCardProps(
- {
- ...sampleUser,
- options: { position: 'pos-1' }
- },
- {
- Image,
- formatMessage,
- positionList: [{ id: 'pos-1', name: '高级产品经理' }]
- }
- );
-
- const positionItem = props.moreInfo.find(item => item.key === 'position');
- expect(positionItem).toBeDefined();
- });
-
- it('maps roleDetails to roles via normalize', () => {
- const props = buildInvitePersonalCardProps(
- {
- name: '李四',
- roleDetails: [{ name: '财务' }]
- },
- { Image, formatMessage }
- );
-
- const rolesItem = props.moreInfo.find(item => item.key === 'roles');
- expect(rolesItem).toBeDefined();
- });
-});
diff --git a/src/components/Tenant/UserList/Actions/personalCardWrap.module.scss b/src/components/Tenant/UserList/Actions/personalCardWrap.module.scss
deleted file mode 100644
index 0f66197..0000000
--- a/src/components/Tenant/UserList/Actions/personalCardWrap.module.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-.user-card-wrap {
- width: 100%;
- min-width: 0;
-
- :global(.kne-react-box) {
- width: 100%;
- min-width: 0;
- }
-}
diff --git a/src/components/Tenant/UserList/DepartmentTreeFilterItem.js b/src/components/Tenant/UserList/DepartmentTreeFilterItem.js
index 9b67c93..fe6e3f1 100644
--- a/src/components/Tenant/UserList/DepartmentTreeFilterItem.js
+++ b/src/components/Tenant/UserList/DepartmentTreeFilterItem.js
@@ -1,37 +1,9 @@
import { createWithRemoteLoader } from '@kne/remote-loader';
-const orgFilterInterceptor = {
- input: value => {
- if (!value) {
- return value;
- }
- if (value.id != null) {
- return value;
- }
- if (value.value != null) {
- return { id: value.value, name: value.label };
- }
- return value;
- },
- output: selected => {
- if (!selected) {
- return selected;
- }
- const item = Array.isArray(selected) ? selected[0] : selected;
- if (!item) {
- return null;
- }
- return {
- label: item.name ?? item.label,
- value: item.id ?? item.value
- };
- }
-};
-
const DepartmentTreeFilterItem = createWithRemoteLoader({
- modules: ['components-core:Filter@withFieldItem', 'components-core:Common@SuperSelectTreeField']
+ modules: ['components-core:Filter@withFieldItem', 'components-core:Filter@singleSelectInterceptor', 'components-core:Common@SuperSelectTreeField']
})(({ remoteModules, ...props }) => {
- const [withFieldItem, SuperSelectTreeField] = remoteModules;
+ const [withFieldItem, singleSelectInterceptor, SuperSelectTreeField] = remoteModules;
const Item = withFieldItem(SuperSelectTreeField);
return (
);
});
diff --git a/src/components/Tenant/UserList/FormInner.js b/src/components/Tenant/UserList/FormInner.js
index 83fb480..350b340 100644
--- a/src/components/Tenant/UserList/FormInner.js
+++ b/src/components/Tenant/UserList/FormInner.js
@@ -1,11 +1,11 @@
import { createWithRemoteLoader } from '@kne/remote-loader';
import { useMemo } from 'react';
import { Flex } from 'antd';
-import merge from 'lodash/merge';
import get from 'lodash/get';
import withLocale from '../withLocale';
import { useIntl } from '@kne/react-intl';
import useRefCallback from '@kne/use-ref-callback';
+import getRoleListApi from '../Role/getRoleListApi';
const FormInnerInner = createWithRemoteLoader({
modules: ['components-core:FormInfo', 'components-core:Global@usePreset']
@@ -31,16 +31,7 @@ const FormInnerInner = createWithRemoteLoader({
{
- return Object.assign({}, data, {
- pageData: data.pageData.filter(item => !(item.type === 'system' && item.code === 'default'))
- });
- }
- })}
+ api={getRoleListApi(apis)}
valueKey="id"
labelKey="name"
interceptor="array-output-value"
diff --git a/src/components/Tenant/UserList/OrgTooltipContent.js b/src/components/Tenant/UserList/OrgTooltipContent.js
deleted file mode 100644
index 4af1750..0000000
--- a/src/components/Tenant/UserList/OrgTooltipContent.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import style from './UserOrgTags.module.scss';
-
-export const tooltipOverlayProps = {
- overlayClassName: style.tooltipOverlay,
- mouseEnterDelay: 0.2
-};
-
-/** 单部门:名称 + 完整路径(有差异时分行展示) */
-export const OrgPathTooltip = ({ org }) => {
- const path = org.fullPath || org.label;
- if (!path) {
- return null;
- }
- if (!org.label || path === org.label) {
- return {path}
;
- }
- return (
-
- );
-};
-
-/** 多部门列表(+N 悬停) */
-export const OrgListTooltip = ({ orgs, title }) => (
-
-
{title}
-
- {orgs.map(org => {
- const path = org.fullPath || org.label;
- const showPath = path && org.label && path !== org.label;
- return (
- -
- {org.label || path}
- {showPath ? {path} : null}
-
- );
- })}
-
-
-);
diff --git a/src/components/Tenant/UserList/TenantUserPersonalCard.js b/src/components/Tenant/UserList/TenantUserPersonalCard.js
deleted file mode 100644
index b72b9bd..0000000
--- a/src/components/Tenant/UserList/TenantUserPersonalCard.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import '@kne/react-box/dist/index.css';
-import classnames from 'classnames';
-import { createWithRemoteLoader } from '@kne/remote-loader';
-import { PersonalCard } from '@kne/react-box';
-import { useIntl } from '@kne/react-intl';
-import buildInvitePersonalCardProps from './Actions/buildInvitePersonalCardProps';
-import withLocale from '../withLocale';
-import style from './Actions/personalCardWrap.module.scss';
-
-/** 租户用户 PersonalCard(邀请弹窗、加入确认等场景共用) */
-const TenantUserPersonalCard = createWithRemoteLoader({
- modules: ['components-core:Image', 'components-core:Global@usePreset']
-})(({ remoteModules, data, positionList, className }) => {
- const [Image, usePreset] = remoteModules;
- const { plugins } = usePreset();
- const { formatMessage } = useIntl();
-
- if (!data) {
- return null;
- }
-
- return (
-
- );
-});
-
-export default withLocale(TenantUserPersonalCard);
diff --git a/src/components/Tenant/UserList/UserCard/index.js b/src/components/Tenant/UserList/UserCard/index.js
deleted file mode 100644
index 15c994d..0000000
--- a/src/components/Tenant/UserList/UserCard/index.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { createWithRemoteLoader } from '@kne/remote-loader';
-import { Card, Typography, Flex } from 'antd';
-import UserOrgTags from '../UserOrgTags';
-import getUserOrgDisplayItems from '../getUserOrgDisplayItems';
-import style from './style.module.scss';
-
-const { Title, Paragraph } = Typography;
-
-const UserCard = createWithRemoteLoader({
- modules: ['components-core:Image', 'components-core:InfoPage@SplitLine']
-})(({ remoteModules, data }) => {
- const [Image, SplitLine] = remoteModules;
-
- return (
-
-
-
-
- {data.name}
- {getUserOrgDisplayItems(data).length > 0 && (
-
-
-
- )}
- {
- return item.map(item => item.name).join(',') || '默认角色';
- }
- }
- ]}
- />
- {data.description}
-
- );
-});
-
-export default UserCard;
diff --git a/src/components/Tenant/UserList/UserCard/style.module.scss b/src/components/Tenant/UserList/UserCard/style.module.scss
deleted file mode 100644
index 67485ac..0000000
--- a/src/components/Tenant/UserList/UserCard/style.module.scss
+++ /dev/null
@@ -1,17 +0,0 @@
-.card {
- text-align: center;
- background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjgiPgo8cmVjdCB3aWR0aD0iOCIgaGVpZ2h0PSI4IiBmaWxsPSIjZmFmYWZhIj48L3JlY3Q+CjxwYXRoIGQ9Ik0wIDBMOCA4Wk04IDBMMCA4WiIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9IiNlZWVlZWUiPjwvcGF0aD4KPC9zdmc+');
-}
-
-.orgs {
- margin: 8px 0 16px;
- max-width: 100%;
-}
-
-.avatar {
- margin-bottom: 24px;
-
- :global(.ant-avatar) {
- border: 4px solid #f0f0f0;
- }
-}
diff --git a/src/components/Tenant/UserList/UserOrgTags.js b/src/components/Tenant/UserList/UserOrgTags.js
index c290e69..3a008e7 100644
--- a/src/components/Tenant/UserList/UserOrgTags.js
+++ b/src/components/Tenant/UserList/UserOrgTags.js
@@ -2,12 +2,47 @@ import { Flex, Tag, Tooltip } from 'antd';
import classnames from 'classnames';
import { useIntl } from '@kne/react-intl';
import getUserOrgDisplayItems from './getUserOrgDisplayItems';
-import { OrgListTooltip, OrgPathTooltip, tooltipOverlayProps } from './OrgTooltipContent';
import withLocale from '../withLocale';
import style from './UserOrgTags.module.scss';
const MAX_VISIBLE = 2;
+export const tooltipOverlayProps = {
+ overlayClassName: style.tooltipOverlay,
+ mouseEnterDelay: 0.2
+};
+
+/** 单部门:名称 + 完整路径(有差异时分行展示) */
+export const OrgPathTooltip = ({ org }) => {
+ if (!org.path) {
+ return null;
+ }
+ if (!org.showPath) {
+ return {org.path}
;
+ }
+ return (
+
+
{org.label}
+
{org.path}
+
+ );
+};
+
+/** 多部门列表(+N 悬停) */
+export const OrgListTooltip = ({ orgs, title }) => (
+
+
{title}
+
+ {orgs.map(org => (
+ -
+ {org.label || org.path}
+ {org.showPath ? {org.path} : null}
+
+ ))}
+
+
+);
+
const UserOrgTags = ({ item, maxVisible = MAX_VISIBLE }) => {
const { formatMessage } = useIntl();
const orgs = getUserOrgDisplayItems(item);
@@ -20,15 +55,13 @@ const UserOrgTags = ({ item, maxVisible = MAX_VISIBLE }) => {
const listTooltipTitle = formatMessage({ id: 'OrgTooltipListTitle' }, { count: orgs.length });
const renderTag = org => {
- const path = org.fullPath || org.label;
- const showTooltip = path && org.label && path !== org.label;
const tag = (
{org.label}
);
- if (!showTooltip) {
+ if (!org.showPath) {
return (
{tag}
diff --git a/src/components/Tenant/UserList/UserPersonalCard/index.js b/src/components/Tenant/UserList/UserPersonalCard/index.js
new file mode 100644
index 0000000..c24b0f9
--- /dev/null
+++ b/src/components/Tenant/UserList/UserPersonalCard/index.js
@@ -0,0 +1,111 @@
+import '@kne/react-box/dist/index.css';
+import classnames from 'classnames';
+import { createWithRemoteLoader } from '@kne/remote-loader';
+import { PersonalCard } from '@kne/react-box';
+import { Typography } from 'antd';
+import get from 'lodash/get';
+import { useIntl } from '@kne/react-intl';
+import withLocale from '../../withLocale';
+import getUserOrgDisplayItems from '../getUserOrgDisplayItems';
+import buildRolesTitle from '../../Role/buildRolesTitle';
+import style from './style.module.scss';
+
+/** 副标题单行省略,悬停展示完整内容 */
+const renderEllipsisTitle = text => {
+ if (!text) {
+ return undefined;
+ }
+ return (
+
+ {text}
+
+ );
+};
+
+const buildOrgMoreInfo = data =>
+ getUserOrgDisplayItems(data).map(org => ({
+ key: `org-${org.id}`,
+ label: org.label,
+ content: org.showPath ? org.path : org.label
+ }));
+
+const buildMoreInfo = (data, { formatMessage }) => {
+ const roles = buildRolesTitle(data);
+ const roleContent = roles || formatMessage?.({ id: 'DefaultRole' }) || '默认角色';
+
+ const items = [
+ {
+ key: 'roles',
+ label: formatMessage?.({ id: 'UserRole' }) || '角色',
+ content: renderEllipsisTitle(roleContent)
+ }
+ ];
+
+ const orgMoreInfo = buildOrgMoreInfo(data);
+ if (orgMoreInfo.length > 1) {
+ return items.concat(orgMoreInfo);
+ }
+ if (orgMoreInfo.length === 1) {
+ const org = getUserOrgDisplayItems(data)[0];
+ if (org.showPath) {
+ return items.concat([
+ {
+ key: 'org-primary',
+ label: formatMessage?.({ id: 'Department' }) || '部门',
+ content: renderEllipsisTitle(org.path)
+ }
+ ]);
+ }
+ return items.concat(orgMoreInfo);
+ }
+ return items;
+};
+
+const applyPlugins = (moreInfo, data, context) => {
+ const enhancer = get(context.plugins, 'tenantAdmin.personalCard');
+ if (typeof enhancer === 'function') {
+ return enhancer({ moreInfo, data, ...context }) ?? moreInfo;
+ }
+ return moreInfo;
+};
+
+/** 将用户数据映射为 PersonalCard 属性 */
+const buildPersonalCardProps = (data, context = {}) => {
+ const { Image, formatMessage, plugins } = context;
+
+ let moreInfo = buildMoreInfo(data, { formatMessage });
+ moreInfo = applyPlugins(moreInfo, data, { formatMessage, plugins });
+
+ return {
+ mode: 'horizontal',
+ name: data?.name,
+ email: data?.email,
+ phone: data?.phone,
+ description: data?.description,
+ moreInfo,
+ avatar: ({ className }) =>
+ };
+};
+
+/** 租户用户 PersonalCard(邀请弹窗、加入确认等场景共用) */
+const UserPersonalCard = createWithRemoteLoader({
+ modules: ['components-core:Image', 'components-core:Global@usePreset']
+})(({ remoteModules, data, className }) => {
+ const [Image, usePreset] = remoteModules;
+ const { plugins } = usePreset();
+ const { formatMessage } = useIntl();
+
+ if (!data) {
+ return null;
+ }
+
+ return (
+
+ );
+});
+
+export default withLocale(UserPersonalCard);
diff --git a/src/components/Tenant/UserList/UserPersonalCard/index.test.js b/src/components/Tenant/UserList/UserPersonalCard/index.test.js
new file mode 100644
index 0000000..d5b7d82
--- /dev/null
+++ b/src/components/Tenant/UserList/UserPersonalCard/index.test.js
@@ -0,0 +1,81 @@
+jest.mock('antd', () => ({
+ Typography: {
+ Text: ({ children }) => children
+ }
+}));
+
+import { buildPersonalCardProps } from '../UserPersonalCard';
+
+const formatMessage = ({ id }) => id;
+const Image = { Avatar: () => null };
+
+const sampleUser = {
+ name: '张三',
+ email: 'zhang@example.com',
+ phone: '13800000000',
+ roles: [{ name: '管理员' }, { name: 'HR' }],
+ tenantOrg: { name: '研发部', path: '公司/研发部' }
+};
+
+describe('buildPersonalCardProps', () => {
+ it('builds role and org moreInfo when plugins is undefined', () => {
+ const props = buildPersonalCardProps(sampleUser, {
+ Image,
+ formatMessage
+ });
+
+ expect(props.name).toBe('张三');
+ expect(props.email).toBe('zhang@example.com');
+ const keys = props.moreInfo.map(item => item.key);
+ expect(keys).toContain('roles');
+ });
+
+ it('builds role and org when plugins is empty object', () => {
+ const props = buildPersonalCardProps(sampleUser, {
+ Image,
+ formatMessage,
+ plugins: {}
+ });
+
+ const keys = props.moreInfo.map(item => item.key);
+ expect(keys).toContain('roles');
+ });
+
+ it('ignores non-function tenantAdmin.personalCard', () => {
+ const props = buildPersonalCardProps(sampleUser, {
+ Image,
+ formatMessage,
+ plugins: { tenantAdmin: { personalCard: 'not-a-function' } }
+ });
+
+ expect(props.moreInfo.some(item => item.key === 'roles')).toBe(true);
+ });
+
+ it('applies plugin personalCard enhancer', () => {
+ const props = buildPersonalCardProps(sampleUser, {
+ Image,
+ formatMessage,
+ plugins: {
+ tenantAdmin: {
+ personalCard: ({ moreInfo }) => [...moreInfo, { key: 'custom', label: 'Custom', content: 'test' }]
+ }
+ }
+ });
+
+ expect(props.moreInfo.some(item => item.key === 'custom')).toBe(true);
+ expect(props.moreInfo.some(item => item.key === 'roles')).toBe(true);
+ });
+
+ it('maps roleDetails to roles', () => {
+ const props = buildPersonalCardProps(
+ {
+ name: '李四',
+ roleDetails: [{ name: '财务' }]
+ },
+ { Image, formatMessage }
+ );
+
+ const rolesItem = props.moreInfo.find(item => item.key === 'roles');
+ expect(rolesItem).toBeDefined();
+ });
+});
diff --git a/src/components/Tenant/UserList/Actions/personalCard.module.scss b/src/components/Tenant/UserList/UserPersonalCard/style.module.scss
similarity index 52%
rename from src/components/Tenant/UserList/Actions/personalCard.module.scss
rename to src/components/Tenant/UserList/UserPersonalCard/style.module.scss
index 1bfb70d..21cee9d 100644
--- a/src/components/Tenant/UserList/Actions/personalCard.module.scss
+++ b/src/components/Tenant/UserList/UserPersonalCard/style.module.scss
@@ -1,4 +1,14 @@
-.personal-card-title {
+.wrap {
+ width: 100%;
+ min-width: 0;
+
+ :global(.kne-react-box) {
+ width: 100%;
+ min-width: 0;
+ }
+}
+
+.ellipsis-title {
display: block !important;
max-width: 100%;
margin-bottom: 0 !important;
diff --git a/src/components/Tenant/UserList/buildOrgDisplayPath.js b/src/components/Tenant/UserList/buildOrgDisplayPath.js
deleted file mode 100644
index 989d454..0000000
--- a/src/components/Tenant/UserList/buildOrgDisplayPath.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import getUserOrgDisplayItems from './getUserOrgDisplayItems';
-
-const buildOrgDisplayPath = item => {
- const items = getUserOrgDisplayItems(item);
- if (items.length) {
- return items.map(org => org.fullPath || org.label).join(';');
- }
- return '';
-};
-
-export default buildOrgDisplayPath;
diff --git a/src/components/Tenant/UserList/buildTenantUserPersonalCardProps.js b/src/components/Tenant/UserList/buildTenantUserPersonalCardProps.js
deleted file mode 100644
index 1234400..0000000
--- a/src/components/Tenant/UserList/buildTenantUserPersonalCardProps.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default, renderPersonalCardEllipsisTitle } from './Actions/buildInvitePersonalCardProps';
diff --git a/src/components/Tenant/UserList/buildUserListFilterFromSearch.js b/src/components/Tenant/UserList/buildUserListFilterFromSearch.js
deleted file mode 100644
index b946f88..0000000
--- a/src/components/Tenant/UserList/buildUserListFilterFromSearch.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * 从 URL 或外部参数构建用户列表初始筛选项(含默认「开启」状态)。
- */
-const buildUserListFilterFromSearch = ({ formatMessage, tenantOrgId, orgName, userId }) => {
- const items = [
- {
- name: 'status',
- label: formatMessage({ id: 'FilterStatus' }),
- value: { label: formatMessage({ id: 'Open' }), value: 'open' }
- }
- ];
-
- const filterUserId = userId != null ? String(userId).trim() : '';
- if (filterUserId) {
- items.push({
- name: 'id',
- label: formatMessage({ id: 'FilterUserId' }),
- value: { label: filterUserId, value: filterUserId }
- });
- }
-
- const orgId = tenantOrgId != null ? String(tenantOrgId).trim() : '';
- if (orgId) {
- const name = orgName != null ? String(orgName) : orgId;
- items.push({
- name: 'tenantOrgId',
- label: formatMessage({ id: 'Department' }),
- value: {
- label: name,
- value: orgId,
- id: orgId,
- name
- }
- });
- }
- return items;
-};
-
-export default buildUserListFilterFromSearch;
diff --git a/src/components/Tenant/UserList/buildUserListWithPositionList.js b/src/components/Tenant/UserList/buildUserListWithPositionList.js
deleted file mode 100644
index 6c7a3e9..0000000
--- a/src/components/Tenant/UserList/buildUserListWithPositionList.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import merge from 'lodash/merge';
-import { extractPositionPageData } from './mapUserOptionsToFormValue';
-
-export function extractListBody(response) {
- return response?.data?.results ?? response?.data?.data ?? response?.data;
-}
-
-const collectPositionIds = pageData => {
- const ids = new Set();
- (pageData || []).forEach(item => {
- const raw = item?.options?.position;
- if (raw == null || raw === '') {
- return;
- }
- if (typeof raw === 'object' && raw.id != null) {
- ids.add(String(raw.id));
- return;
- }
- ids.add(String(raw));
- });
- return [...ids];
-};
-
-/** 列表接口响应是否已附带 positionList(如 tenant-extra/user-list) */
-export function listResponseHasPositionList(listApi) {
- const url = listApi?.url || '';
- return url.includes('tenant-extra') && url.includes('user-list');
-}
-
-/**
- * 为租户用户列表附加 positionList,供岗位列展示。
- * 若 list 已是 tenant-extra 富集接口则不再包装。
- */
-export function buildUserListWithPositionList(listApi, positionListApi, ajax) {
- if (!listApi || !ajax || !positionListApi?.url || listResponseHasPositionList(listApi)) {
- return listApi;
- }
-
- return Object.assign({}, listApi, {
- loader: async () => {
- const userRes = await ajax(merge({}, listApi));
- const userBody = extractListBody(userRes) || {};
- const ids = collectPositionIds(userBody.pageData);
-
- let positionList = [];
- if (ids.length) {
- const posRes = await ajax(
- merge({}, positionListApi, {
- params: merge({}, positionListApi.params || {}, {
- filter: Object.assign({}, positionListApi.params?.filter || {}, { ids }),
- perPage: Math.max(ids.length, 1),
- currentPage: 1
- })
- })
- );
- positionList = extractPositionPageData(posRes);
- }
-
- return Object.assign({}, userBody, { positionList });
- }
- });
-}
-
-/** 根据 positionList 解析岗位展示名称(支持 options.position、顶层 position 字段) */
-export function resolvePositionDisplayName(item, positionList) {
- const raw = item?.options?.position ?? item?.position;
- if (raw == null || raw === '') {
- return '';
- }
- if (typeof raw === 'object' && raw.name) {
- return String(raw.name).trim();
- }
- if (typeof raw === 'string' && Number.isNaN(Number(raw))) {
- return raw.trim();
- }
- const id = String(typeof raw === 'object' ? raw.id : raw);
- const hit = (positionList || []).find(p => String(p.id) === id);
- return hit?.name ? String(hit.name) : '';
-}
diff --git a/src/components/Tenant/UserList/doc/api.md b/src/components/Tenant/UserList/doc/api.md
new file mode 100644
index 0000000..9554fd6
--- /dev/null
+++ b/src/components/Tenant/UserList/doc/api.md
@@ -0,0 +1,102 @@
+### UserList
+
+租户用户列表组件,集成筛选、分页、操作按钮等功能,支持通过 `plugins.tenantAdmin` 插件扩展列表列、表单字段和 PersonalCard 展示。
+
+|| 属性名 | 说明 | 类型 | 默认值 ||
+|| --- | --- | --- | --- ||
+|| apis | API 接口配置对象 | object | - ||
+|| apis.list | 用户列表接口 | object | - ||
+|| apis.create | 创建用户接口,为 falsy 时隐藏创建按钮 | object | - ||
+|| apis.save | 保存用户接口,为 falsy 时隐藏编辑/状态切换 | object | - ||
+|| apis.remove | 删除用户接口,为 falsy 时隐藏删除按钮 | object | - ||
+|| apis.setStatus | 切换用户状态接口 | object | - ||
+|| apis.inviteToken | 获取邀请链接接口 | object | - ||
+|| apis.userInviteMessage | 发送邀请邮件接口 | object | - ||
+|| apis.roleList | 角色列表接口 | object | - ||
+|| apis.orgList | 组织列表接口 | object | - ||
+|| topOptionsSize | 顶部操作按钮尺寸 | string | - ||
+|| onMount | 组件挂载回调,接收 `{ filter, filterList, topOptions, tableOptions }` | function | - ||
+|| children | 自定义渲染函数,接收与 onMount 相同的参数 | function | - ||
+|| initialTenantOrgId | 初始选中的组织 ID | string | - ||
+|| initialOrgName | 初始选中的组织名称 | string | - ||
+|| initialUserId | 初始筛选的用户 ID | string | - ||
+|| allowQueryIdForUserFilter | 是否允许通过 URL `id` 参数筛选用户 | boolean | false |
+
+### UserCard
+
+用户信息卡片组件,用于展示用户基本信息。
+
+|| 属性名 | 说明 | 类型 | 默认值 ||
+|| --- | --- | --- | --- ||
+|| data | 用户数据对象 | object | - ||
+
+### TenantUserPersonalCard
+
+用户个人卡片组件,用于邀请弹窗、加入确认等场景。
+
+|| 属性名 | 说明 | 类型 | 默认值 ||
+|| --- | --- | --- | --- ||
+|| data | 用户数据对象 | object | - ||
+|| className | 自定义样式类名 | string | - |
+
+#### 插件扩展点
+
+通过 `plugins.tenantAdmin` 在 preset 中注册插件,可扩展用户属性展示。
+
+##### UserFormInner
+
+替换用户创建/编辑表单组件,接收 `{ list, apis, column }` 属性,`list` 为默认表单字段列表,可插入自定义字段。
+
+```javascript
+// 注册方式:在 preset 的 plugins.tenantAdmin.UserFormInner 中配置
+plugins: {
+ tenantAdmin: {
+ UserFormInner: YourFormComponent
+ }
+}
+```
+
+##### getUserListColumns
+
+扩展用户列表表格列,接收 `{ columns, apis }`,返回新的列配置数组。
+
+```javascript
+plugins: {
+ tenantAdmin: {
+ getUserListColumns: ({ columns, apis }) => {
+ const newColumns = columns.slice(0);
+ newColumns.splice(7, 0, { title: '岗位', name: 'options.position', type: 'other' });
+ return newColumns;
+ }
+ }
+}
+```
+
+##### personalCard
+
+增强 PersonalCard 的 moreInfo 展示,接收 `{ moreInfo, data, formatMessage, plugins }`,返回增强后的 moreInfo 数组。
+
+```javascript
+plugins: {
+ tenantAdmin: {
+ personalCard: ({ moreInfo, data, formatMessage }) => {
+ return [...moreInfo, { key: 'position', label: '岗位', content: data.options?.position }];
+ }
+ }
+}
+```
+
+##### getUserApis
+
+提供额外的 API 端点(如岗位列表),接收 `{ tenantId, apis }`,返回需要合并到 UserList apis 中的对象。
+
+```javascript
+plugins: {
+ tenantAdmin: {
+ getUserApis: ({ tenantId, apis }) => ({
+ positionList: apis.talentSaas?.tenantAdmin?.position?.list,
+ list: apis.talentSaas?.tenantAdmin?.userList
+ })
+ }
+}
+```
diff --git a/src/components/Tenant/UserList/doc/base.js b/src/components/Tenant/UserList/doc/base.js
new file mode 100644
index 0000000..8f97be3
--- /dev/null
+++ b/src/components/Tenant/UserList/doc/base.js
@@ -0,0 +1,27 @@
+const { UserList } = _Tenant;
+const { default: mockPreset } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+
+const BaseExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ return (
+
+
+
+
+
+ );
+});
+
+render();
diff --git a/src/components/Tenant/UserList/doc/custom-render.js b/src/components/Tenant/UserList/doc/custom-render.js
new file mode 100644
index 0000000..c12736a
--- /dev/null
+++ b/src/components/Tenant/UserList/doc/custom-render.js
@@ -0,0 +1,39 @@
+const { UserList } = _Tenant;
+const { default: mockPreset } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+const { Flex } = antd;
+
+/**
+ * 自定义渲染示例:
+ * 通过 children 函数获取 UserList 内部的 filter、tableOptions 等,
+ * 自行组合布局,例如将筛选和表格嵌入到自定义页面结构中。
+ */
+const CustomRenderExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout', 'components-core:Table@TablePage']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout, TablePage] = remoteModules;
+ return (
+
+
+
+ {({ tableOptions }) => (
+
+
+ 自定义区域:可以在此放置统计面板、快捷操作等内容
+
+
+
+ )}
+
+
+
+ );
+});
+
+render();
diff --git a/src/components/Tenant/UserList/doc/example.json b/src/components/Tenant/UserList/doc/example.json
new file mode 100644
index 0000000..6de6360
--- /dev/null
+++ b/src/components/Tenant/UserList/doc/example.json
@@ -0,0 +1,62 @@
+{
+ "isFull": true,
+ "list": [
+ {
+ "title": "基础用法",
+ "description": "UserList 组件提供完整的用户列表管理功能,包含搜索筛选、分页、创建/编辑/删除用户等操作",
+ "code": "./base.js",
+ "scope": [
+ {
+ "name": "_Tenant",
+ "packageName": "@components/Tenant"
+ },
+ {
+ "name": "_mockPreset",
+ "packageName": "@root/mockPreset"
+ },
+ {
+ "name": "remoteLoader",
+ "packageName": "@kne/remote-loader"
+ }
+ ]
+ },
+ {
+ "title": "插件扩展",
+ "description": "通过 plugins.tenantAdmin 插件扩展用户属性,如增加岗位、入职时间等字段,扩展列表列和 PersonalCard 展示",
+ "code": "./plugin-extension.js",
+ "scope": [
+ {
+ "name": "_Tenant",
+ "packageName": "@components/Tenant"
+ },
+ {
+ "name": "_mockPreset",
+ "packageName": "@root/mockPreset"
+ },
+ {
+ "name": "remoteLoader",
+ "packageName": "@kne/remote-loader"
+ }
+ ]
+ },
+ {
+ "title": "自定义渲染",
+ "description": "通过 children 函数自定义 UserList 的渲染方式,获取 filter、filterList、topOptions、tableOptions 自行组合布局",
+ "code": "./custom-render.js",
+ "scope": [
+ {
+ "name": "_Tenant",
+ "packageName": "@components/Tenant"
+ },
+ {
+ "name": "_mockPreset",
+ "packageName": "@root/mockPreset"
+ },
+ {
+ "name": "remoteLoader",
+ "packageName": "@kne/remote-loader"
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/components/Tenant/UserList/doc/plugin-extension.js b/src/components/Tenant/UserList/doc/plugin-extension.js
new file mode 100644
index 0000000..83271d5
--- /dev/null
+++ b/src/components/Tenant/UserList/doc/plugin-extension.js
@@ -0,0 +1,130 @@
+const { UserList } = _Tenant;
+const { default: mockPreset, ...rest } = _mockPreset;
+const { createWithRemoteLoader } = remoteLoader;
+
+/**
+ * 插件扩展示例:
+ * 通过 plugins.tenantAdmin 注册扩展点,为用户列表增加自定义字段。
+ *
+ * 四个扩展点:
+ * 1. UserFormInner - 替换用户创建/编辑表单,在默认字段基础上插入自定义字段
+ * 2. getUserListColumns - 扩展用户列表表格列,插入自定义列
+ * 3. personalCard - 增强 PersonalCard 的 moreInfo 展示,插入自定义信息项
+ * 4. getUserApis - 提供额外的 API 端点(如岗位列表接口)
+ */
+
+// 模拟岗位列表数据
+const mockPositionList = [
+ { id: 'pos-1', name: '前端工程师' },
+ { id: 'pos-2', name: '产品经理' },
+ { id: 'pos-3', name: 'UI设计师' }
+];
+
+// 扩展点 1:自定义用户表单(在默认字段基础上插入岗位选择器)
+const UserFormInnerPlugin = createWithRemoteLoader({
+ modules: ['components-core:FormInfo']
+})(({ remoteModules, list, apis, ...props }) => {
+ const [FormInfo] = remoteModules;
+ const { SuperSelect } = FormInfo.fields;
+ const newList = list.slice(0);
+ // 在第 3 个位置插入岗位选择字段
+ newList.splice(2, 0, (
+ ({
+ pageData: mockPositionList
+ })
+ }}
+ />
+ ));
+ return ;
+});
+
+// 扩展点 2:扩展用户列表列(插入岗位列)
+const getUserListColumns = ({ columns }) => {
+ const newColumns = columns.slice(0);
+ newColumns.splice(7, 0, {
+ title: '岗位',
+ name: 'options.position',
+ type: 'other',
+ valueOf: item => {
+ const raw = item?.options?.position;
+ if (!raw) return '-';
+ if (typeof raw === 'object' && raw.name) return raw.name;
+ const hit = mockPositionList.find(p => String(p.id) === String(raw));
+ return hit ? hit.name : raw;
+ }
+ });
+ return newColumns;
+};
+
+// 扩展点 3:增强 PersonalCard 展示(插入岗位信息)
+const personalCard = ({ moreInfo, data }) => {
+ if (moreInfo.some(item => item.key === 'position')) {
+ return moreInfo;
+ }
+ const raw = data?.options?.position;
+ if (!raw) return moreInfo;
+ const positionName = typeof raw === 'object' && raw.name ? raw.name : (
+ mockPositionList.find(p => String(p.id) === String(raw))?.name || raw
+ );
+ if (!positionName) return moreInfo;
+ const rolesIndex = moreInfo.findIndex(item => item.key === 'roles');
+ const next = moreInfo.slice();
+ next.splice(rolesIndex + 1, 0, {
+ key: 'position',
+ label: '岗位',
+ content: positionName
+ });
+ return next;
+};
+
+// 扩展点 4:提供额外 API
+const getUserApis = () => ({
+ positionList: {
+ loader: () => ({ pageData: mockPositionList })
+ }
+});
+
+// 构造带插件的 preset
+const presetWithPlugins = Object.assign({}, mockPreset, {
+ plugins: Object.assign({}, mockPreset.plugins || {}, {
+ tenantAdmin: Object.assign({}, mockPreset.plugins?.tenantAdmin || {}, {
+ UserFormInner: UserFormInnerPlugin,
+ getUserListColumns,
+ personalCard,
+ getUserApis
+ })
+ })
+});
+
+const PluginExtensionExample = createWithRemoteLoader({
+ modules: ['components-core:Global@PureGlobal', 'components-core:Layout']
+})(({ remoteModules }) => {
+ const [PureGlobal, Layout] = remoteModules;
+ return (
+
+
+
+
+
+ );
+});
+
+render();
diff --git a/src/components/Tenant/UserList/doc/summary.md b/src/components/Tenant/UserList/doc/summary.md
new file mode 100644
index 0000000..be00c1a
--- /dev/null
+++ b/src/components/Tenant/UserList/doc/summary.md
@@ -0,0 +1 @@
+租户用户列表组件,提供完整的用户管理功能,包括用户搜索、创建、编辑、状态切换、邀请和删除。支持通过插件机制扩展用户属性,如岗位、入职时间等业务特定字段。
diff --git a/src/components/Tenant/UserList/getUserListFilterValue.js b/src/components/Tenant/UserList/getUserListFilterValue.js
deleted file mode 100644
index efa4243..0000000
--- a/src/components/Tenant/UserList/getUserListFilterValue.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { normalizeTenantUserStatus } from './normalizeTenantUserStatus';
-
-const pickSelectValues = value => {
- if (value == null || value === '') {
- return [];
- }
- const list = Array.isArray(value) ? value : [value];
- return list
- .map(item => {
- if (item == null) {
- return null;
- }
- if (typeof item !== 'object') {
- return String(item);
- }
- if (item.value != null && item.value !== '') {
- return String(item.value);
- }
- if (item.id != null && item.id !== '') {
- return String(item.id);
- }
- return null;
- })
- .filter(Boolean);
-};
-
-/**
- * SuperSelectFilterItem 使用 valueKey=id 时筛选项为 { id, name },
- * Filter.getFilterValue 默认只读取 { value },需在此补全 roles 等字段。
- */
-const getUserListFilterValue = (filter, getFilterValue) => {
- const value = getFilterValue(filter);
-
- if (value.id != null && value.id !== '') {
- value.id = String(value.id);
- }
-
- const rolesEntry = filter.find(item => item.name === 'roles');
- if (rolesEntry) {
- const roles = pickSelectValues(rolesEntry.value);
- if (roles.length) {
- value.roles = roles;
- } else {
- delete value.roles;
- }
- }
-
- const orgEntry = filter.find(item => item.name === 'tenantOrgId');
- if (orgEntry?.value != null && orgEntry.value !== '') {
- const orgValues = pickSelectValues(orgEntry.value);
- if (orgValues[0]) {
- value.tenantOrgId = orgValues[0];
- }
- }
-
- if (value.status != null && value.status !== '') {
- const raw =
- typeof value.status === 'object' && value.status !== null && 'value' in value.status
- ? value.status.value
- : value.status;
- const normalized = normalizeTenantUserStatus(raw);
- if (normalized) {
- value.status = normalized;
- } else {
- delete value.status;
- }
- }
-
- return value;
-};
-
-export default getUserListFilterValue;
diff --git a/src/components/Tenant/UserList/getUserOrgDisplayItems.js b/src/components/Tenant/UserList/getUserOrgDisplayItems.js
index 5377033..a7d449c 100644
--- a/src/components/Tenant/UserList/getUserOrgDisplayItems.js
+++ b/src/components/Tenant/UserList/getUserOrgDisplayItems.js
@@ -19,52 +19,41 @@ const getOrgFullPath = org => {
return getOrgLabel(org);
};
-/** @returns {{ id: string, label: string, fullPath: string }[]} */
+const buildOrgDisplayItem = (id, org) => {
+ const label = getOrgLabel(org);
+ if (!label) {
+ return null;
+ }
+ const fullPath = getOrgFullPath(org);
+ const path = fullPath || label;
+ const showPath = path && label && path !== label;
+ return { id, label, fullPath, path, showPath };
+};
+
+/** @returns {{ id: string, label: string, fullPath: string, path: string, showPath: boolean }[]} */
const getUserOrgDisplayItems = item => {
if (Array.isArray(item?.tenantOrgs) && item.tenantOrgs.length) {
return item.tenantOrgs
- .map((org, index) => {
- const id = org.id != null ? String(org.id) : `org-${index}`;
- const label = getOrgLabel(org);
- if (!label) {
- return null;
- }
- return {
- id,
- label,
- fullPath: getOrgFullPath(org)
- };
- })
+ .map((org, index) => buildOrgDisplayItem(org.id != null ? String(org.id) : `org-${index}`, org))
.filter(Boolean);
}
if (item?.tenantOrgPath) {
- const path = String(item.tenantOrgPath).trim();
- const segments = path.split(/[;;]/).map(s => s.trim()).filter(Boolean);
+ const rawPath = String(item.tenantOrgPath).trim();
+ const segments = rawPath.split(/[;;]/).map(s => s.trim()).filter(Boolean);
if (segments.length > 1) {
- return segments.map((segment, index) => ({
- id: `path-${index}`,
- label: getOrgLabel({ path: segment }),
- fullPath: segment
- }));
+ return segments
+ .map((segment, index) => buildOrgDisplayItem(`path-${index}`, { path: segment }))
+ .filter(Boolean);
}
- return [
- {
- id: item.tenantOrg?.id != null ? String(item.tenantOrg.id) : 'primary',
- label: item.tenantOrg?.name || getOrgLabel({ path }),
- fullPath: path
- }
- ];
+ return [buildOrgDisplayItem(
+ item.tenantOrg?.id != null ? String(item.tenantOrg.id) : 'primary',
+ { name: item.tenantOrg?.name, path: rawPath }
+ )].filter(Boolean);
}
if (item?.tenantOrg?.name) {
- return [
- {
- id: String(item.tenantOrg.id ?? 'primary'),
- label: String(item.tenantOrg.name).trim(),
- fullPath: getOrgFullPath(item.tenantOrg)
- }
- ];
+ return [buildOrgDisplayItem(String(item.tenantOrg.id ?? 'primary'), item.tenantOrg)].filter(Boolean);
}
return [];
diff --git a/src/components/Tenant/UserList/index.js b/src/components/Tenant/UserList/index.js
index b378cc0..f6f444a 100644
--- a/src/components/Tenant/UserList/index.js
+++ b/src/components/Tenant/UserList/index.js
@@ -1,20 +1,15 @@
import { createWithRemoteLoader } from '@kne/remote-loader';
-import { useState, useRef, useEffect, useMemo } from 'react';
-import { useSearchParams } from 'react-router-dom';
-import merge from 'lodash/merge';
-import get from 'lodash/get';
+import { useRef, useEffect, useMemo } from 'react';
import { Flex } from 'antd';
-import getColumns from './getColumns';
import Actions from './Actions';
import Create from './Actions/Create';
import withLocale from '../withLocale';
import { useIntl } from '@kne/react-intl';
import useRefCallback from '@kne/use-ref-callback';
-import DepartmentTreeFilterItem from './DepartmentTreeFilterItem';
-import getUserListFilterValue from './getUserListFilterValue';
-import buildUserListFilterFromSearch from './buildUserListFilterFromSearch';
-import { readUserListUrlFilters, stripUserListUrlFilterKeys } from './userListUrlFilters';
-import { buildUserListWithPositionList, extractListBody } from './buildUserListWithPositionList';
+
+import useFilterList from './useFilterList';
+import useColumns from './useColumns';
+import useListApi from './useListApi';
const UserList = createWithRemoteLoader({
modules: ['components-core:Table@TablePage', 'components-core:Filter', 'components-core:Global@usePreset']
@@ -33,153 +28,91 @@ const UserList = createWithRemoteLoader({
}) => {
const [TablePage, Filter, usePreset] = remoteModules;
const { ajax } = usePreset();
- const ref = useRef();
+ const tableRef = useRef();
const { formatMessage } = useIntl();
- const { SearchInput, FilterProvider, getFilterValue, fields: filterFields } = Filter;
+ const {
+ SearchInput, getFilterValue, createFilterValueMapper,
+ useUrlFilter, createUrlFilterReader, multiSelectInterceptor, fields: filterFields
+ } = Filter;
const { InputFilterItem, AdvancedSelectFilterItem, SuperSelectFilterItem } = filterFields;
const { plugins } = usePreset();
- const [positionList, setPositionList] = useState([]);
-
- const [searchParams, setSearchParams] = useSearchParams();
- const urlFilterSnapshotRef = useRef(null);
- if (urlFilterSnapshotRef.current === null) {
- urlFilterSnapshotRef.current = readUserListUrlFilters(searchParams, {
- initialTenantOrgId,
- initialOrgName,
- initialUserId,
- allowQueryIdForUserFilter
- });
- }
-
- const [filter, setFilter] = useState(() =>
- buildUserListFilterFromSearch({
- formatMessage,
- ...urlFilterSnapshotRef.current
- })
- );
- const urlStrippedRef = useRef(false);
- useEffect(() => {
- if (urlStrippedRef.current) {
- return;
- }
- urlStrippedRef.current = true;
- const nextParams = stripUserListUrlFilterKeys(searchParams, urlFilterSnapshotRef.current?.consumedKeys);
- if (nextParams) {
- setSearchParams(nextParams, { replace: true });
+ const mapFilterValue = useMemo(() => createFilterValueMapper({
+ id: 'string',
+ roles: 'multi',
+ tenantOrgId: 'single'
+ }), [createFilterValueMapper]);
+
+ const [filter, setFilter] = useUrlFilter({
+ readUrlParams: (searchParams) => {
+ const reader = createUrlFilterReader(searchParams);
+ const tenantOrgEntry = reader.takeFilterEntry('tenantOrgId');
+ let userEntry = reader.takeFilterEntry('userId');
+ const filterUserEntry = reader.takeFilterEntry('filterUserId');
+ if (!userEntry && filterUserEntry) {
+ userEntry = filterUserEntry;
+ }
+ let idEntry = null;
+ if (allowQueryIdForUserFilter) {
+ idEntry = reader.takeFilterEntry('id');
+ }
+ const tenantOrgId = tenantOrgEntry?.value || initialTenantOrgId || null;
+ const orgName = tenantOrgEntry?.label || initialOrgName || '';
+ const userId = userEntry?.value || idEntry?.value || initialUserId || null;
+ return {
+ consumedKeys: reader.getConsumedKeys(),
+ tenantOrgId,
+ orgName,
+ userId,
+ tenantOrgEntry,
+ userEntry: userId ? (userEntry || { label: String(userId), value: String(userId) }) : null
+ };
+ },
+ buildFilter: ({ tenantOrgId, orgName, tenantOrgEntry, userEntry }) => {
+ const items = [
+ { name: 'status', label: formatMessage({ id: 'FilterStatus' }), value: { label: formatMessage({ id: 'Open' }), value: 'open' } }
+ ];
+ if (userEntry) {
+ items.push({ name: 'id', label: formatMessage({ id: 'FilterUserId' }), value: userEntry });
+ }
+ if (tenantOrgEntry) {
+ items.push({
+ name: 'tenantOrgId',
+ label: formatMessage({ id: 'Department' }),
+ value: { ...tenantOrgEntry, id: tenantOrgEntry.value, name: tenantOrgEntry.label }
+ });
+ } else if (tenantOrgId) {
+ const orgId = String(tenantOrgId).trim();
+ const name = orgName != null ? String(orgName) : orgId;
+ items.push({
+ name: 'tenantOrgId',
+ label: formatMessage({ id: 'Department' }),
+ value: { label: name, value: orgId, id: orgId, name }
+ });
+ }
+ return items;
}
- }, [searchParams, setSearchParams]);
-
- const filterValue = useMemo(() => getUserListFilterValue(filter, getFilterValue), [filter, getFilterValue]);
+ });
- const filterList = useMemo(
- () => [
- [
- ,
-
- Object.assign({}, data, {
- pageData: (data.pageData || []).filter(
- item => !(item.type === 'system' && item.code === 'default')
- )
- })
- })}
- />,
- ,
- ({
- pageData: [
- { label: formatMessage({ id: 'Open' }), value: 'open' },
- { label: formatMessage({ id: 'Close' }), value: 'closed' }
- ]
- })
- }}
- />
- ]
- ],
- [
- formatMessage,
- apis.roleList,
- apis.orgList,
- InputFilterItem,
- AdvancedSelectFilterItem,
- SuperSelectFilterItem,
- DepartmentTreeFilterItem
- ]
- );
+ const filterValue = useMemo(() => mapFilterValue(filter, getFilterValue), [filter, getFilterValue, mapFilterValue]);
+ const filterList = useFilterList({ formatMessage, apis, InputFilterItem, AdvancedSelectFilterItem, SuperSelectFilterItem, multiSelectInterceptor });
+ const columns = useColumns({ formatMessage, apis, plugins });
+ const listApi = useListApi({ apis, filterValue, ajax });
const topOptions = (
{apis.create && (
- {
- ref.current.reload();
- }}>
+ tableRef.current.reload()}>
{formatMessage({ id: 'Add' })}
)}
);
- const columns = useMemo(() => {
- const getUserListColumns = get(plugins, 'tenantAdmin.getUserListColumns');
- const cols = getColumns({ formatMessage });
- if (typeof getUserListColumns === 'function') {
- return getUserListColumns({ columns: cols, apis });
- }
- return cols;
- }, [plugins, formatMessage, apis]);
-
- const listApi = useMemo(() => {
- const baseApi = buildUserListWithPositionList(
- merge({}, apis.list, {
- params: {
- filter: filterValue,
- ...(apis.list?.params || {})
- }
- }),
- apis.positionList,
- ajax
- );
- const loadList = baseApi.loader
- ? (...args) => baseApi.loader(...args)
- : async () => extractListBody(await ajax(merge({}, baseApi)));
- return Object.assign({}, baseApi, {
- loader: async (...args) => {
- const body = (await loadList(...args)) || {};
- setPositionList(body.positionList || []);
- return body;
- }
- });
- }, [apis.list, apis.positionList, filterValue, ajax]);
-
const tableOptions = {
...listApi,
- ref,
+ ref: tableRef,
columns: [
...columns,
{
@@ -187,22 +120,17 @@ const UserList = createWithRemoteLoader({
type: 'options',
title: formatMessage({ id: 'Operation' }),
fixed: 'right',
- valueOf: item => {
- return {
- children: (
- {
- ref.current.reload();
- }}
- />
- )
- };
- }
+ valueOf: item => ({
+ children: (
+ tableRef.current.reload()}
+ />
+ )
+ })
}
],
name: 'tenant-user-list',
@@ -210,26 +138,15 @@ const UserList = createWithRemoteLoader({
};
const handlerMount = useRefCallback(() => {
- onMount &&
- onMount({
- filter: { value: filter, onChange: setFilter },
- filterList,
- topOptions,
- tableOptions
- });
+ onMount?.({ filter: { value: filter, onChange: setFilter }, filterList, topOptions, tableOptions });
});
useEffect(() => {
handlerMount();
- }, [handlerMount]);
+ }, [handlerMount, filter]);
if (typeof children === 'function') {
- return children({
- filter: { value: filter, onChange: setFilter },
- filterList,
- topOptions,
- tableOptions
- });
+ return children({ filter: { value: filter, onChange: setFilter }, filterList, topOptions, tableOptions });
}
return (
@@ -244,7 +161,4 @@ const UserList = createWithRemoteLoader({
export default UserList;
-export { default as UserCard } from './UserCard';
-export { default as TenantUserPersonalCard } from './TenantUserPersonalCard';
-export { default as buildTenantUserPersonalCardProps } from './buildTenantUserPersonalCardProps';
-export { default as buildInvitePersonalCardProps } from './Actions/buildInvitePersonalCardProps';
+export { default as TenantUserPersonalCard } from './UserPersonalCard';
diff --git a/src/components/Tenant/UserList/mapUserOptionsToFormValue.js b/src/components/Tenant/UserList/mapUserOptionsToFormValue.js
deleted file mode 100644
index 88ea7c6..0000000
--- a/src/components/Tenant/UserList/mapUserOptionsToFormValue.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/** 从岗位列表接口响应中取出 pageData */
-export function extractPositionPageData(response) {
- const body = response?.data?.results ?? response?.data?.data ?? response?.data;
- if (Array.isArray(body?.pageData)) {
- return body.pageData;
- }
- if (Array.isArray(body)) {
- return body;
- }
- return [];
-}
-
-/** SuperSelect object-output-value 回显需 { id, name } */
-export function resolvePositionSelectValue(position, positionList = []) {
- if (position == null || position === '') {
- return null;
- }
- if (typeof position === 'object' && position.id != null) {
- const id = String(position.id);
- const name = position.name != null ? String(position.name).trim() : '';
- if (name) {
- return { id: position.id, name };
- }
- const hit = positionList.find(item => String(item.id) === id);
- return hit ? { id: hit.id, name: hit.name } : { id: position.id };
- }
- const id = String(position);
- const hit = positionList.find(item => String(item.id) === id);
- return hit ? { id: hit.id, name: hit.name } : { id: position };
-}
-
-/**
- * @param {Record | null | undefined} options
- * @param {{ positionList?: { id: string, name: string }[] }} [context]
- */
-const normalizeOptions = options => {
- if (options == null) {
- return options;
- }
- if (typeof options === 'string') {
- try {
- return JSON.parse(options);
- } catch {
- return options;
- }
- }
- return options;
-};
-
-export function mapUserOptionsToFormValue(options, { positionList = [] } = {}) {
- const normalized = normalizeOptions(options);
- if (!normalized || normalized.position == null) {
- return normalized;
- }
- const resolved = resolvePositionSelectValue(normalized.position, positionList);
- if (!resolved) {
- return normalized;
- }
- return Object.assign({}, normalized, { position: resolved });
-}
diff --git a/src/components/Tenant/UserList/normalizeTenantUserForPersonalCard.js b/src/components/Tenant/UserList/normalizeTenantUserForPersonalCard.js
deleted file mode 100644
index 102fecb..0000000
--- a/src/components/Tenant/UserList/normalizeTenantUserForPersonalCard.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/** 统一租户用户结构,供 PersonalCard 映射(含 parseJoinToken / 列表行) */
-const normalizeTenantUserForPersonalCard = tenantUser => {
- if (!tenantUser) {
- return tenantUser;
- }
- const data = typeof tenantUser.toJSON === 'function' ? tenantUser.toJSON() : Object.assign({}, tenantUser);
-
- if (Array.isArray(data.roleDetails) && data.roleDetails.length) {
- data.roles = data.roleDetails;
- }
-
- if (data.options == null && (data.position != null || data.joinDate != null)) {
- data.options = Object.assign(
- {},
- data.position != null ? { position: data.position } : null,
- data.joinDate != null ? { joinDate: data.joinDate } : null
- );
- }
-
- return data;
-};
-
-export default normalizeTenantUserForPersonalCard;
diff --git a/src/components/Tenant/UserList/normalizeTenantUserStatus.js b/src/components/Tenant/UserList/normalizeTenantUserStatus.js
deleted file mode 100644
index 81f348f..0000000
--- a/src/components/Tenant/UserList/normalizeTenantUserStatus.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * 租户用户 status 为 open / closed;兼容 active / inactive 别名。
- *
- * @param {unknown} status
- * @returns {'open' | 'closed' | undefined}
- */
-export function normalizeTenantUserStatus(status) {
- if (status == null || status === '') {
- return undefined;
- }
- const s = String(status).trim();
- if (s === 'active') {
- return 'open';
- }
- if (s === 'inactive') {
- return 'closed';
- }
- if (s === 'open' || s === 'closed') {
- return s;
- }
- return undefined;
-}
diff --git a/src/components/Tenant/UserList/pickTenantOrgIdsFromForm.js b/src/components/Tenant/UserList/transformUserFormData.js
similarity index 75%
rename from src/components/Tenant/UserList/pickTenantOrgIdsFromForm.js
rename to src/components/Tenant/UserList/transformUserFormData.js
index d8d758a..ff1e2a5 100644
--- a/src/components/Tenant/UserList/pickTenantOrgIdsFromForm.js
+++ b/src/components/Tenant/UserList/transformUserFormData.js
@@ -41,3 +41,13 @@ export function mapUserOrgIdsToFormValue(data) {
}
return [];
}
+
+/** 将表单数据转换为 API 提交格式(处理组织 ID 映射) */
+const transformUserFormData = (formData, existingData) => {
+ const tenantOrgIds = pickTenantOrgIdsFromForm(formData.tenantOrgIds);
+ return Object.assign({}, formData, existingData ? { id: existingData.id } : null, {
+ tenantOrgIds
+ });
+};
+
+export default transformUserFormData;
diff --git a/src/components/Tenant/UserList/getColumns.js b/src/components/Tenant/UserList/useColumns.js
similarity index 70%
rename from src/components/Tenant/UserList/getColumns.js
rename to src/components/Tenant/UserList/useColumns.js
index f7222cb..5528aff 100644
--- a/src/components/Tenant/UserList/getColumns.js
+++ b/src/components/Tenant/UserList/useColumns.js
@@ -1,4 +1,7 @@
+import { useMemo } from 'react';
+import get from 'lodash/get';
import UserOrgTags from './UserOrgTags';
+import buildRolesTitle from '../Role/buildRolesTitle';
const getColumns = ({ formatMessage }) => {
return [
@@ -35,9 +38,7 @@ const getColumns = ({ formatMessage }) => {
{
name: 'roles',
title: formatMessage({ id: 'UserRole' }),
- valueOf: item => {
- return item.roles.map(item => item.name).join(',') || formatMessage({ id: 'DefaultRole' });
- }
+ valueOf: item => buildRolesTitle(item) || formatMessage({ id: 'DefaultRole' })
},
{
name: 'tenantOrg',
@@ -65,4 +66,15 @@ const getColumns = ({ formatMessage }) => {
];
};
-export default getColumns;
+const useColumns = ({ formatMessage, apis, plugins }) => {
+ return useMemo(() => {
+ const getUserListColumns = get(plugins, 'tenantAdmin.getUserListColumns');
+ const cols = getColumns({ formatMessage });
+ if (typeof getUserListColumns === 'function') {
+ return getUserListColumns({ columns: cols, apis });
+ }
+ return cols;
+ }, [plugins, formatMessage, apis]);
+};
+
+export default useColumns;
diff --git a/src/components/Tenant/UserList/useFilterList.js b/src/components/Tenant/UserList/useFilterList.js
new file mode 100644
index 0000000..75a6b10
--- /dev/null
+++ b/src/components/Tenant/UserList/useFilterList.js
@@ -0,0 +1,46 @@
+import { useMemo } from 'react';
+import merge from 'lodash/merge';
+import DepartmentTreeFilterItem from './DepartmentTreeFilterItem';
+import getRoleListApi from '../Role/getRoleListApi';
+
+const useFilterList = ({ formatMessage, apis, InputFilterItem, AdvancedSelectFilterItem, SuperSelectFilterItem, multiSelectInterceptor }) => {
+ return useMemo(
+ () => [
+ [
+ ,
+ ,
+ ,
+ ({
+ pageData: [
+ { label: formatMessage({ id: 'Open' }), value: 'open' },
+ { label: formatMessage({ id: 'Close' }), value: 'closed' }
+ ]
+ })
+ }}
+ />
+ ]
+ ],
+ [formatMessage, apis.roleList, apis.orgList, InputFilterItem, AdvancedSelectFilterItem, SuperSelectFilterItem]
+ );
+};
+
+export default useFilterList;
diff --git a/src/components/Tenant/UserList/useListApi.js b/src/components/Tenant/UserList/useListApi.js
new file mode 100644
index 0000000..a365268
--- /dev/null
+++ b/src/components/Tenant/UserList/useListApi.js
@@ -0,0 +1,23 @@
+import { useMemo } from 'react';
+import merge from 'lodash/merge';
+
+const extractListBody = response => response?.data?.results ?? response?.data?.data ?? response?.data;
+
+const useListApi = ({ apis, filterValue, ajax }) => {
+ return useMemo(() => {
+ const api = merge({}, apis.list, {
+ params: {
+ filter: filterValue,
+ ...(apis.list?.params || {})
+ }
+ });
+ if (api.loader) {
+ return api;
+ }
+ return Object.assign({}, api, {
+ loader: async () => extractListBody(await ajax(merge({}, api)))
+ });
+ }, [apis.list, filterValue, ajax]);
+};
+
+export default useListApi;
diff --git a/src/components/Tenant/UserList/userListUrlFilters.js b/src/components/Tenant/UserList/userListUrlFilters.js
deleted file mode 100644
index e1eec2c..0000000
--- a/src/components/Tenant/UserList/userListUrlFilters.js
+++ /dev/null
@@ -1,61 +0,0 @@
-const decodeQueryValue = value => {
- if (value == null || value === '') {
- return '';
- }
- try {
- return decodeURIComponent(value);
- } catch {
- return value;
- }
-};
-
-/**
- * 读取用户列表 URL 筛参(仅应在首屏调用一次),并记录需从地址栏移除的 query key。
- */
-export const readUserListUrlFilters = (
- searchParams,
- { initialTenantOrgId, initialOrgName, initialUserId, allowQueryIdForUserFilter } = {}
-) => {
- const consumedKeys = [];
-
- const take = key => {
- if (!searchParams.has(key)) {
- return null;
- }
- consumedKeys.push(key);
- return searchParams.get(key);
- };
-
- let tenantOrgId = take('tenantOrgId') || initialTenantOrgId || null;
- let orgName = take('orgName') || initialOrgName || '';
- let userId = take('userId') || take('filterUserId') || initialUserId || null;
-
- if (!userId && allowQueryIdForUserFilter) {
- const idFromQuery = take('id');
- if (idFromQuery) {
- userId = idFromQuery;
- }
- }
-
- return {
- tenantOrgId,
- orgName: decodeQueryValue(orgName),
- userId,
- consumedKeys
- };
-};
-
-export const stripUserListUrlFilterKeys = (searchParams, consumedKeys) => {
- if (!consumedKeys?.length) {
- return null;
- }
- const next = new URLSearchParams(searchParams);
- let changed = false;
- consumedKeys.forEach(key => {
- if (next.has(key)) {
- next.delete(key);
- changed = true;
- }
- });
- return changed ? next : null;
-};
diff --git a/src/components/Tenant/locale/en-US.js b/src/components/Tenant/locale/en-US.js
index bc0cb3d..9f1d24c 100644
--- a/src/components/Tenant/locale/en-US.js
+++ b/src/components/Tenant/locale/en-US.js
@@ -216,7 +216,29 @@ const locale = {
CompanyInfoPage: 'Company Info',
CompanyInfoSaveSuccess: 'Company info saved successfully',
OrgStructure: 'Organization Structure',
- PermissionManagement: 'Permission Management'
+ PermissionManagement: 'Permission Management',
+
+ // TenantPermission
+ SelectAll: 'Select All',
+
+ // OrgLink
+ OrgLinkTitle: 'Linked Organization',
+ OrgLinkHint: 'After enabling, you can sync organization data from WeCom or DingTalk. Synced organizations cannot be edited or deleted, only the department leader can be modified.',
+ OrgLinkEnable: 'Enable Link',
+ OrgLinkSource: 'Source',
+ OrgLinkSyncInterval: 'Auto Sync Interval',
+ OrgLinkTargetId: 'Target ID',
+ OrgLinkTargetIdPlaceholder: 'Select a TARGET_LINKED_ environment variable',
+ OrgLinkTargetIdDesc: 'Please add environment variables in "Settings" with a KEY starting with TARGET_LINKED_ to make them available here.',
+ OrgLinkManualSync: 'Manual Sync',
+ OrgLinkCancel: 'Cancel Link',
+ OrgLinkCancelConfirm: 'Are you sure you want to cancel the link? Synced organizations will be kept but will no longer be auto-updated.',
+ OrgLinkLastSyncTime: 'Last Sync Time',
+ OrgLinkSaveSuccess: 'Link configuration saved successfully',
+ OrgLinkCancelSuccess: 'Link cancelled successfully',
+ OrgLinkSyncSuccess: 'Sync completed successfully',
+ OrgSourceFrom: 'Source: {source}',
+ EditOrgLeader: 'Edit Leader',
};
export default locale;
diff --git a/src/components/Tenant/locale/zh-CN.js b/src/components/Tenant/locale/zh-CN.js
index 7fd4591..999f104 100644
--- a/src/components/Tenant/locale/zh-CN.js
+++ b/src/components/Tenant/locale/zh-CN.js
@@ -212,7 +212,29 @@ const locale = {
CompanyInfoPage: '公司信息',
CompanyInfoSaveSuccess: '公司信息保存成功',
OrgStructure: '组织架构',
- PermissionManagement: '权限管理'
+ PermissionManagement: '权限管理',
+
+ // TenantPermission
+ SelectAll: '全选',
+
+ // OrgLink
+ OrgLinkTitle: '关联组织架构',
+ OrgLinkHint: '开启关联后,可从企业微信或钉钉同步组织架构数据,同步的组织不可修改和删除,仅可修改部门负责人。',
+ OrgLinkEnable: '开启关联',
+ OrgLinkSource: '来源',
+ OrgLinkSyncInterval: '自动同步间隔',
+ OrgLinkTargetId: '关联目标ID',
+ OrgLinkTargetIdPlaceholder: '请选择 TARGET_LINKED_ 开头的环境变量',
+ OrgLinkTargetIdDesc: '请在「设置」中添加环境变量,且 KEY 需以 TARGET_LINKED_ 开头方可在此选择。',
+ OrgLinkManualSync: '手动同步',
+ OrgLinkCancel: '取消关联',
+ OrgLinkCancelConfirm: '确定要取消关联吗?取消后已同步的组织将保留,但不再自动同步更新。',
+ OrgLinkLastSyncTime: '上次同步时间',
+ OrgLinkSaveSuccess: '关联配置保存成功',
+ OrgLinkCancelSuccess: '已取消关联',
+ OrgLinkSyncSuccess: '同步成功',
+ OrgSourceFrom: '来源:{source}',
+ EditOrgLeader: '修改负责人',
};
export default locale;
diff --git a/src/components/TenantAdmin/TabDetail/Company/index.js b/src/components/TenantAdmin/TabDetail/Company/index.js
index 7722095..f7be50a 100644
--- a/src/components/TenantAdmin/TabDetail/Company/index.js
+++ b/src/components/TenantAdmin/TabDetail/Company/index.js
@@ -2,13 +2,16 @@ import { createWithRemoteLoader } from '@kne/remote-loader';
import { CompanyInfo } from '@components/Tenant';
import Fetch from '@kne/react-fetch';
import { App } from 'antd';
+import { useIntl } from '@kne/react-intl';
+import withLocale from '../../withLocale';
const Company = createWithRemoteLoader({
modules: ['components-core:Global@usePreset']
-})(({ remoteModules, tenant }) => {
+})(withLocale(({ remoteModules, tenant }) => {
const [usePreset] = remoteModules;
const { apis, ajax } = usePreset();
const { message } = App.useApp();
+ const { formatMessage } = useIntl();
return (
@@ -37,6 +40,6 @@ const Company = createWithRemoteLoader({
}}
/>
);
-});
+}));
export default Company;
diff --git a/src/components/TenantAdmin/TabDetail/Org/index.js b/src/components/TenantAdmin/TabDetail/Org/index.js
index 2ff1a0a..70e8d65 100644
--- a/src/components/TenantAdmin/TabDetail/Org/index.js
+++ b/src/components/TenantAdmin/TabDetail/Org/index.js
@@ -2,58 +2,78 @@ import { createWithRemoteLoader } from '@kne/remote-loader';
import { OrgInfo } from '@components/Tenant';
import Fetch from '@kne/react-fetch';
import { useSearchParams } from 'react-router-dom';
+import { useIntl } from '@kne/react-intl';
+import withLocale from '../../withLocale';
+import { Flex } from 'antd';
const Org = createWithRemoteLoader({
- modules: ['components-core:Global@usePreset']
-})(({ remoteModules, tenant }) => {
- const [usePreset] = remoteModules;
+ modules: ['components-core:Filter@filterToUrlParams', 'components-core:Global@usePreset']
+})(withLocale(({ remoteModules, tenant }) => {
+ const [filterToUrlParams, usePreset] = remoteModules;
const { apis } = usePreset();
+ const { formatMessage } = useIntl();
const [searchParams, setSearchParams] = useSearchParams();
return (
{
+ render={({ data: linkConfigData, reload: reloadLinkConfig }) => {
+ const linkedSource = linkConfigData?.enabled ? linkConfigData.source : null;
return (
- {
- const next = new URLSearchParams(searchParams);
- next.set('tab', 'user');
- next.set('tenantOrgId', String(org.id));
- if (org.name) {
- next.set('orgName', org.name);
- } else {
- next.delete('orgName');
+ {
+ return (
+ {
+ const next = new URLSearchParams(searchParams);
+ next.set('tab', 'user');
+ const filterParams = filterToUrlParams([
+ { name: 'tenantOrgId', label: formatMessage({ id: 'Department' }), value: { label: org.name || String(org.id), value: String(org.id) } }
+ ]);
+ filterParams.forEach((value, key) => {
+ next.set(key, value);
+ });
+ setSearchParams(next);
+ }}
+ apis={{
+ create: Object.assign({}, apis.tenantAdmin.orgCreate, {
+ data: { tenantId: tenant.id }
+ }),
+ save: Object.assign({}, apis.tenantAdmin.orgSave, {
+ data: { tenantId: tenant.id }
+ }),
+ remove: Object.assign({}, apis.tenantAdmin.orgRemove, {
+ data: { tenantId: tenant.id }
+ }),
+ userList: Object.assign({}, apis.tenantAdmin.userList, {
+ params: { tenantId: tenant.id }
+ }),
+ import: apis.tenantAdmin.orgBatchImport
+ }}
+ />
+ );
}}
/>
);
}}
/>
);
-});
+}));
export default Org;
diff --git a/src/components/TenantAdmin/locale/en-US.js b/src/components/TenantAdmin/locale/en-US.js
index 7f2acd9..bb3893c 100644
--- a/src/components/TenantAdmin/locale/en-US.js
+++ b/src/components/TenantAdmin/locale/en-US.js
@@ -58,7 +58,31 @@ const locale = {
AccountManagement: 'Account Management',
DetailInfo: 'Detail Info',
ServiceTimeRange: 'Service Time',
- AccountCountTag: 'Account Count'
+ AccountCountTag: 'Account Count',
+
+ // TabDetail
+ CompanyInfoSaveSuccess: 'Company info saved successfully',
+ Department: 'Department',
+
+ // OrgLink
+ OrgLinkTitle: 'Linked Organization',
+ OrgLinkHint:
+ 'After enabling, you can sync organization data from WeCom or DingTalk. Synced organizations cannot be edited or deleted, only the department leader can be modified.',
+ OrgLinkEnable: 'Enable Link',
+ OrgLinkSource: 'Source',
+ OrgLinkSyncInterval: 'Auto Sync Interval',
+ OrgLinkTargetId: 'Target ID',
+ OrgLinkTargetIdPlaceholder: 'Select a TARGET_LINKED_ environment variable',
+ OrgLinkTargetIdDesc: 'Please add environment variables in "Settings" with a KEY starting with TARGET_LINKED_ to make them available here.',
+ OrgLinkManualSync: 'Manual Sync',
+ OrgLinkCancel: 'Cancel Link',
+ OrgLinkCancelConfirm: 'Are you sure you want to cancel the link? Synced organizations will be kept but will no longer be auto-updated.',
+ OrgLinkLastSyncTime: 'Last Sync Time',
+ OrgLinkSaveSuccess: 'Link configuration saved successfully',
+ OrgLinkCancelSuccess: 'Link cancelled successfully',
+ OrgLinkSyncSuccess: 'Sync completed successfully',
+ OrgSourceFrom: 'Source: {source}',
+ EditOrgLeader: 'Edit Leader'
};
export default locale;
diff --git a/src/components/TenantAdmin/locale/zh-CN.js b/src/components/TenantAdmin/locale/zh-CN.js
index d818dff..0c68cec 100644
--- a/src/components/TenantAdmin/locale/zh-CN.js
+++ b/src/components/TenantAdmin/locale/zh-CN.js
@@ -58,7 +58,30 @@ const locale = {
AccountManagement: '账号管理',
DetailInfo: '详情信息',
ServiceTimeRange: '服务时间',
- AccountCountTag: '开通账号数'
+ AccountCountTag: '开通账号数',
+
+ // TabDetail
+ CompanyInfoSaveSuccess: '公司信息保存成功',
+ Department: '部门',
+
+ // OrgLink
+ OrgLinkTitle: '关联组织架构',
+ OrgLinkHint: '开启关联后,可从企业微信或钉钉同步组织架构数据,同步的组织不可修改和删除,仅可修改部门负责人。',
+ OrgLinkEnable: '开启关联',
+ OrgLinkSource: '来源',
+ OrgLinkSyncInterval: '自动同步间隔',
+ OrgLinkTargetId: '关联目标ID',
+ OrgLinkTargetIdPlaceholder: '请选择 TARGET_LINKED_ 开头的环境变量',
+ OrgLinkTargetIdDesc: '请在「设置」中添加环境变量,且 KEY 需以 TARGET_LINKED_ 开头方可在此选择。',
+ OrgLinkManualSync: '手动同步',
+ OrgLinkCancel: '取消关联',
+ OrgLinkCancelConfirm: '确定要取消关联吗?取消后已同步的组织将保留,但不再自动同步更新。',
+ OrgLinkLastSyncTime: '上次同步时间',
+ OrgLinkSaveSuccess: '关联配置保存成功',
+ OrgLinkCancelSuccess: '已取消关联',
+ OrgLinkSyncSuccess: '同步成功',
+ OrgSourceFrom: '来源:{source}',
+ EditOrgLeader: '修改负责人',
};
export default locale;
diff --git a/src/preset.js b/src/preset.js
index 6927b81..b622091 100644
--- a/src/preset.js
+++ b/src/preset.js
@@ -81,7 +81,7 @@ export const globalInit = async () => {
const componentsCoreRemote = {
...registry,
remote: 'components-core',
- defaultVersion: '0.4.64'
+ defaultVersion: '0.4.73'
};
remoteLoaderPreset({
remotes: {
diff --git a/src/utils/useManagedEventSource.js b/src/utils/useManagedEventSource.js
new file mode 100644
index 0000000..ee19c18
--- /dev/null
+++ b/src/utils/useManagedEventSource.js
@@ -0,0 +1,57 @@
+import { useEffect, useRef } from 'react';
+
+/**
+ * EventSource with explicit teardown: no browser auto-reconnect after errors,
+ * and close on unmount / page hide (tab close or SPA navigation).
+ */
+const useManagedEventSource = (url, { enabled = true, onMessage, onOpen, onError } = {}) => {
+ const handlersRef = useRef({ onMessage, onOpen, onError });
+ handlersRef.current = { onMessage, onOpen, onError };
+
+ useEffect(() => {
+ if (!enabled || !url || typeof window === 'undefined' || typeof window.EventSource !== 'function') {
+ return undefined;
+ }
+
+ const source = new EventSource(url);
+
+ const closeSource = () => {
+ source.onopen = null;
+ source.onmessage = null;
+ source.onerror = null;
+ if (source.readyState !== EventSource.CLOSED) {
+ source.close();
+ }
+ };
+
+ source.onopen = event => {
+ handlersRef.current.onOpen?.(event, source);
+ };
+
+ source.onmessage = event => {
+ handlersRef.current.onMessage?.(event, source);
+ };
+
+ source.onerror = event => {
+ handlersRef.current.onError?.(event, source);
+ // Prevent default auto-reconnect (otherwise server sees a new SSE loop after disconnect)
+ closeSource();
+ };
+
+ const onPageHide = () => closeSource();
+ const onVisibilityChange = () => {
+ if (document.visibilityState === 'hidden') closeSource();
+ };
+
+ window.addEventListener('pagehide', onPageHide);
+ document.addEventListener('visibilitychange', onVisibilityChange);
+
+ return () => {
+ window.removeEventListener('pagehide', onPageHide);
+ document.removeEventListener('visibilitychange', onVisibilityChange);
+ closeSource();
+ };
+ }, [url, enabled]);
+};
+
+export default useManagedEventSource;