From 2b2688270d101ed6ba27a288752257c0dba4ace0 Mon Sep 17 00:00:00 2001 From: Linzp Date: Tue, 2 Jun 2026 09:09:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/components/Apis/getApis.js | 20 ++ src/components/BizUnit/README.md | 3 + src/components/BizUnit/doc/api.md | 3 + src/components/BizUnit/index.js | 39 +--- src/components/Task/Actions/InputDetail.js | 35 +++ src/components/Task/Actions/index.js | 11 + src/components/Task/locale/en-US.js | 2 + src/components/Task/locale/zh-CN.js | 2 + .../Tenant/AfterTenantLoginLayout.js | 8 +- src/components/Tenant/JoinInvitation/index.js | 89 +++----- .../Tenant/OrgInfo/OrgLinkSetting.js | 212 +++++++++--------- src/components/Tenant/OrgInfo/index.js | 129 +++++------ src/components/Tenant/Setting/Org.js | 21 +- src/components/Tenant/Setting/User.js | 14 +- .../Tenant/UserList/Actions/Edit.js | 2 +- .../Tenant/UserList/Actions/SendMessage.js | 108 +++++++++ .../Tenant/UserList/Actions/index.js | 97 ++++---- src/components/Tenant/UserList/FormInner.js | 45 +++- .../Tenant/UserList/UserPersonalCard/index.js | 11 +- src/components/Tenant/UserList/index.js | 114 +++++++--- src/components/Tenant/UserList/useColumns.js | 39 +++- .../Tenant/UserList/useFilterList.js | 15 ++ src/components/Tenant/UserList/useListApi.js | 23 -- src/components/Tenant/constants.js | 18 ++ src/components/Tenant/locale/en-US.js | 5 + src/components/Tenant/locale/zh-CN.js | 21 ++ .../TenantAdmin/TabDetail/Org/index.js | 153 +++++++------ .../TenantAdmin/TabDetail/User/index.js | 3 +- src/components/TenantAdmin/locale/en-US.js | 3 + src/components/TenantAdmin/locale/zh-CN.js | 3 + src/components/UserSelect/index.js | 2 +- 32 files changed, 802 insertions(+), 450 deletions(-) create mode 100644 src/components/Task/Actions/InputDetail.js create mode 100644 src/components/Tenant/UserList/Actions/SendMessage.js delete mode 100644 src/components/Tenant/UserList/useListApi.js create mode 100644 src/components/Tenant/constants.js diff --git a/package.json b/package.json index 07b3d11..052a1eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne-components/components-admin", - "version": "1.1.42", + "version": "1.1.43", "description": "用于实现一个后台管理系统的必要组件", "scripts": { "init": "husky", diff --git a/src/components/Apis/getApis.js b/src/components/Apis/getApis.js index 08f795d..076f7cb 100644 --- a/src/components/Apis/getApis.js +++ b/src/components/Apis/getApis.js @@ -284,6 +284,10 @@ const getApis = options => { url: `${prefix}/tenant/admin/send-invite-message`, method: 'POST' }, + sendOrgMessage: { + url: `${prefix}/tenant/admin/send-org-message`, + method: 'POST' + }, appendArgs: { url: `${prefix}/tenant/admin/append-args`, method: 'POST' @@ -428,6 +432,18 @@ const getApis = options => { url: `${prefix}/tenant/org-link-config`, method: 'GET' }, + orgLinkSave: { + url: `${prefix}/tenant/org-link-save`, + method: 'POST' + }, + orgLinkCancel: { + url: `${prefix}/tenant/org-link-cancel`, + method: 'POST' + }, + orgLinkSync: { + url: `${prefix}/tenant/org-link-sync`, + method: 'POST' + }, userList: { url: `${prefix}/tenant/user-list`, method: 'GET' @@ -456,6 +472,10 @@ const getApis = options => { url: `${prefix}/tenant/send-invite-message`, method: 'POST' }, + sendOrgMessage: { + url: `${prefix}/tenant/send-org-message`, + method: 'POST' + }, customComponentDetail: { url: `${prefix}/tenant/custom-component-detail`, method: 'GET' diff --git a/src/components/BizUnit/README.md b/src/components/BizUnit/README.md index 2c28fc0..d3fea97 100644 --- a/src/components/BizUnit/README.md +++ b/src/components/BizUnit/README.md @@ -495,6 +495,9 @@ render(); | titleExtra | 标题额外内容 | ReactNode | null | | children | 自定义渲染函数 | Function | - | | onMount | 组件挂载回调 | Function | - | +| filter | 外部筛选状态(受控),传入后 BizUnit 使用该值代替内部 useState | Array | - | +| onFilterChange | 外部筛选变更回调,与 filter 配合使用 | Function | - | +| urlFilterValue | URL 筛选参数映射,传入后 BizUnit 自动从 URL 初始化 filter。支持数组 `['id', 'status']` 或对象 `{ id: true, status: { multi: true } }` 格式,详见 `Filter.useUrlFilterValue` | Array \| Object | - | ### apis 配置 diff --git a/src/components/BizUnit/doc/api.md b/src/components/BizUnit/doc/api.md index 639e6f0..525a271 100644 --- a/src/components/BizUnit/doc/api.md +++ b/src/components/BizUnit/doc/api.md @@ -20,6 +20,9 @@ | titleExtra | 标题额外内容 | ReactNode | null | | children | 自定义渲染函数 | Function | - | | onMount | 组件挂载回调 | Function | - | +| filter | 外部筛选状态(受控),传入后 BizUnit 使用该值代替内部 useState | Array | - | +| onFilterChange | 外部筛选变更回调,与 filter 配合使用 | Function | - | +| urlFilterValue | URL 筛选参数映射,传入后 BizUnit 自动从 URL 初始化 filter。支持数组 `['id', 'status']` 或对象 `{ id: true, status: { multi: true } }` 格式,详见 `Filter.useUrlFilterValue` | Array \| Object | - | ### apis 配置 diff --git a/src/components/BizUnit/index.js b/src/components/BizUnit/index.js index 17f3382..73d7fe1 100644 --- a/src/components/BizUnit/index.js +++ b/src/components/BizUnit/index.js @@ -1,7 +1,6 @@ import { createWithRemoteLoader } from '@kne/remote-loader'; import { useRef, useState, useEffect, useMemo } from 'react'; import { Flex } from 'antd'; -import useRefCallback from '@kne/use-ref-callback'; import merge from 'lodash/merge'; import Actions from './Actions'; import Create from './Actions/Create'; @@ -25,7 +24,10 @@ const BizUnit = createWithRemoteLoader({ getActionList, allowKeywordSearch = true, onMount, - options + options, + filter: outerFilter, + onFilterChange: outerOnFilterChange, + urlFilterValue }) => { const { formatMessage } = useIntl(); options = merge( @@ -51,12 +53,14 @@ const BizUnit = createWithRemoteLoader({ options ); const [TablePage, Filter] = remoteModules; - const ref = useRef(); - const { SearchInput, getFilterValue } = Filter; - const [filter, setFilter] = useState([]); + const { SearchInput, getFilterValue, useUrlFilterValue } = Filter; + const ref = useRef(null); + const [urlFilter] = useUrlFilterValue(urlFilterValue || []); + + const [filter, setFilter] = useState(urlFilter); + const filterValue = options.mapFilterValue ? options.mapFilterValue(filter, getFilterValue) : getFilterValue(filter); - const filterParamsKey = useMemo(() => JSON.stringify(filterValue), [filterValue]); - const isFirstFilterEffect = useRef(true); + const topOptions = ( {allowKeywordSearch && } @@ -106,27 +110,6 @@ const BizUnit = createWithRemoteLoader({ name }); - const handlerMount = useRefCallback(() => { - onMount && - onMount({ - filter: { value: filter, onChange: setFilter }, - filterList, - topOptions, - tableOptions - }); - }); - - useEffect(() => { - handlerMount(); - }, [handlerMount, filter]); - - useEffect(() => { - if (isFirstFilterEffect.current) { - isFirstFilterEffect.current = false; - return; - } - ref.current?.reload?.(); - }, [filterParamsKey]); if (typeof children === 'function') { return children({ filter: { value: filter, onChange: setFilter, list: filterList }, diff --git a/src/components/Task/Actions/InputDetail.js b/src/components/Task/Actions/InputDetail.js new file mode 100644 index 0000000..bd2d959 --- /dev/null +++ b/src/components/Task/Actions/InputDetail.js @@ -0,0 +1,35 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { Button } from 'antd'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; +import JsonView from '@kne/json-view'; +import '@kne/json-view/dist/index.css'; + +const InputDetail = createWithRemoteLoader({ + modules: ['components-core:Modal@useModal', 'components-core:InfoPage'] +})(withLocale(({ remoteModules, data, ...props }) => { + const [useModal, InfoPage] = remoteModules; + const { formatMessage } = useIntl(); + const modal = useModal(); + + return ( + - handleCancelLink(reload)}> + + + + handleCancelLink(reload)}> diff --git a/src/components/Tenant/OrgInfo/index.js b/src/components/Tenant/OrgInfo/index.js index cca5bb3..a8e3f9e 100644 --- a/src/components/Tenant/OrgInfo/index.js +++ b/src/components/Tenant/OrgInfo/index.js @@ -1,5 +1,5 @@ import { createWithRemoteLoader } from '@kne/remote-loader'; -import { Flex, Tree, App, Card, Button, Space, Modal, Upload, Typography, Table, Avatar, Divider, Checkbox, Tag, Badge, Tooltip, Drawer } from 'antd'; +import { Flex, Tree, App, Card, Button, Space, Modal, Upload, Typography, Table, Divider, Checkbox, Tag, Badge, Tooltip, Drawer } from 'antd'; import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import OrgChart from '@kne/react-org-chart'; import merge from 'lodash/merge'; @@ -13,7 +13,6 @@ import { DownloadOutlined, TeamOutlined, UserOutlined, - CloudOutlined, EditOutlined, LinkOutlined } from '@ant-design/icons'; @@ -29,10 +28,7 @@ import { useIntl } from '@kne/react-intl'; import style from './style.module.scss'; import '@kne/react-org-chart/dist/index.css'; -const SOURCE_LABEL_MAP = { - wecom: '企业微信', - dingtalk: '钉钉' -}; +import { getSourceIcon, SOURCE_LABEL_MAP } from '../constants'; /** SuperSelect object-output-value 回显需 { id, name },不能只传 id 字符串 */ const mapOrgToFormData = org => { @@ -56,7 +52,7 @@ const OrgSourceTag = ({ source }) => { const label = SOURCE_LABEL_MAP[source] || source; return ( - } color="processing"> + {label} @@ -71,7 +67,7 @@ const OrgOptions = createWithRemoteLoader({ const formModal = useFormModal(); const { formatMessage } = useIntl(); const { message } = App.useApp(); - const isExternalSource = linkedSource && data.source === linkedSource; + const isExternalSource = data.synced && data.syncSource; return ( { const { data: resData } = await ajax( merge({}, apis.save, { - data: Object.assign({}, { - id: data.id, - parentId: data.parentId, - name: data.name, - description: data.description, - leaderUserId: normalizeLeaderUserIdForSubmit(formData.leaderUserId) - }) + data: Object.assign( + {}, + { + id: data.id, + parentId: data.parentId, + name: data.name, + description: data.description, + leaderUserId: normalizeLeaderUserIdForSubmit(formData.leaderUserId) + } + ) }) ); if (resData.code !== 0) { @@ -227,7 +226,6 @@ const OrgLeaderMeta = createWithRemoteLoader({ const name = String(leader.name); const initial = name.trim().charAt(0); - const avatarSrc = leader.avatar || leader.avatarUrl || leader.leaderAvatar; return (
@@ -391,7 +389,7 @@ const GraphOrg = createWithRemoteLoader({ extra={}>
{node.name}
- +
{node.description &&
{node.description}
} @@ -484,7 +482,7 @@ const TreeOrg = ({ data, ids, apis, onSuccess, onViewUsers, linkedSource }) => { {nodeData.name} - + @@ -787,9 +785,7 @@ const OrgInfo = createWithRemoteLoader({ /> {linkSettingProps ? ( - ) : null} @@ -817,6 +813,7 @@ const OrgInfo = createWithRemoteLoader({ width={480} destroyOnClose> { @@ -936,56 +933,60 @@ const OrgInfo = createWithRemoteLoader({ {formatMessage({ id: 'ImportSelectedCount' }, { selected: importSelectedRowKeys.length, total: parsedRows.length })} -
- record.users.length > 0, - expandedRowRender: record => - record.users.length ? ( -
- - - - {formatMessage({ id: 'ImportNestedUsers' })} - - +
+
record.users.length > 0, + expandedRowRender: record => + record.users.length ? ( +
+ + + + {formatMessage({ id: 'ImportNestedUsers' })} -
- - ) : null - }} - /> - + +
+ + ) : null + }} + /> + ) : null}
- {activeKey === 'tree' && } - {activeKey === 'graph' && } + {activeKey === 'tree' && ( + + )} + {activeKey === 'graph' && ( + + )}
); diff --git a/src/components/Tenant/Setting/Org.js b/src/components/Tenant/Setting/Org.js index c9b06f7..4d596cf 100644 --- a/src/components/Tenant/Setting/Org.js +++ b/src/components/Tenant/Setting/Org.js @@ -33,9 +33,10 @@ const OrgInner = createWithRemoteLoader({ children: ( { + {...Object.assign({}, apis.tenant.orgLinkConfig)} + render={({ data: linkConfigData, reload: reloadLinkConfig }) => { const linkedSource = linkConfigData?.enabled ? linkConfigData.source : null; + const syncSupported = linkConfigData?.syncSupported; return ( { + reloadLinkConfig(); + reload(); + } + } : null} onViewUsers={ allowViewUsers ? org => { const query = filterToUrlParams([ - { name: 'tenantOrgId', label: 'tenantOrgId', value: { label: org.name || '', value: String(org.id) } }, + { name: 'tenantOrgId', label: 'tenantOrgId', value: { label: org.name || '', value: String(org.id) } } ]); navigate(`${baseUrl}/user?${query.toString()}`); } @@ -63,7 +72,11 @@ const OrgInner = createWithRemoteLoader({ save: allowSave && Object.assign({}, apis.tenant.orgSave), remove: allowRemove && Object.assign({}, apis.tenant.orgRemove), userList: Object.assign({}, apis.tenant.userList), - import: allowImport && Object.assign({}, apis.tenant.orgBatchImport) + import: allowImport && Object.assign({}, apis.tenant.orgBatchImport), + orgLinkConfig: Object.assign({}, apis.tenant.orgLinkConfig), + orgLinkSave: Object.assign({}, apis.tenant.orgLinkSave), + orgLinkSync: Object.assign({}, apis.tenant.orgLinkSync), + orgLinkCancel: Object.assign({}, apis.tenant.orgLinkCancel) }} /> diff --git a/src/components/Tenant/Setting/User.js b/src/components/Tenant/Setting/User.js index 65019a7..3bb7ce2 100644 --- a/src/components/Tenant/Setting/User.js +++ b/src/components/Tenant/Setting/User.js @@ -2,7 +2,8 @@ import { createWithRemoteLoader } from '@kne/remote-loader'; import UserList from '../UserList'; import withLocale from '../withLocale'; import { useIntl } from '@kne/react-intl'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; +import get from 'lodash/get'; const User = createWithRemoteLoader({ modules: [ @@ -16,7 +17,8 @@ const User = createWithRemoteLoader({ })(({ remoteModules, menu, children, pageProps: originPageProps, apis: extraApis = {} }) => { const [Page, usePreset, Permissions, usePermissionsPass, TablePage, FilterProvider] = remoteModules; const { formatMessage } = useIntl(); - const { apis } = usePreset(); + const { apis, plugins } = usePreset(); + const getActions = get(plugins, 'tenant.getUserListActions'); const [target, setTarget] = useState({}); const filter = Object.assign({}, { value: [] }, target.filter); const allowCreate = usePermissionsPass({ request: ['setting:user-manager:create'] }); @@ -24,6 +26,10 @@ const User = createWithRemoteLoader({ const allowRemove = usePermissionsPass({ request: ['setting:user-manager:remove'] }); const allowInvite = usePermissionsPass({ request: ['setting:user-manager:invite'] }); + const handleMount = useCallback(data => { + setTarget(data); + }, []); + const pageProps = Object.assign({}, originPageProps, { menu, title: formatMessage({ id: 'UserManagement' }), @@ -33,7 +39,8 @@ const User = createWithRemoteLoader({ + children: }); }} /> diff --git a/src/components/Tenant/UserList/Actions/SendMessage.js b/src/components/Tenant/UserList/Actions/SendMessage.js new file mode 100644 index 0000000..0e849ea --- /dev/null +++ b/src/components/Tenant/UserList/Actions/SendMessage.js @@ -0,0 +1,108 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import merge from 'lodash/merge'; +import { App, Button, Tag, Flex } from 'antd'; +import withLocale from '../../withLocale'; +import { useIntl } from '@kne/react-intl'; + +import { SOURCE_LABEL_MAP } from '../../constants'; + +const SendMessage = createWithRemoteLoader({ + modules: ['components-core:FormInfo@useFormModal', 'components-core:FormInfo', 'components-admin:Editor', 'components-core:Global@usePreset'] +})(({ remoteModules, apis, selectedRows, onSuccess, size }) => { + const [useFormModal, FormInfo, Editor, usePreset] = remoteModules; + const { ajax } = usePreset(); + const formModal = useFormModal(); + const { formatMessage } = useIntl(); + const { message } = App.useApp(); + + const externalUsers = selectedRows.filter(row => row.syncSource && row.sourceId); + const syncSource = externalUsers.length > 0 ? externalUsers[0].syncSource : null; + const sourceLabel = SOURCE_LABEL_MAP[syncSource] || syncSource || ''; + + const { TextArea, RadioGroup, Input } = FormInfo.fields; + + const handleClick = () => { + if (externalUsers.length === 0) { + return; + } + + formModal({ + title: formatMessage({ id: 'SendOrgMessageTitle' }, { type: sourceLabel }), + size: 'small', + formProps: { + onSubmit: async formData => { + const { data: resData } = await ajax( + merge({}, apis.sendOrgMessage, { + data: { + userIds: externalUsers.map(u => String(u.id)), + content: formData.content, + msgtype: formData.msgtype + } + }) + ); + if (resData.code !== 0) { + return false; + } + message.success(formatMessage({ id: 'SendMessageSuccess' }, { count: externalUsers.length })); + onSuccess?.(); + } + }, + children: ( + +
+
+ {formatMessage({ id: 'SendMessageTarget' })}({externalUsers.length}) +
+
+ {externalUsers.map(u => ( + {u.name} + ))} +
+
+ , +