feat: enhance Druid integration and improve user management features

This commit is contained in:
2026-07-16 09:59:27 +08:00
parent 2e88885a27
commit f9f5ceac9d
10 changed files with 253 additions and 72 deletions

View File

@@ -1,4 +1,7 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { message } from 'antd';
import { getDruidIndexUrl } from '@/utils/publicUrl';
import { establishDruidSession } from '@/services/monitor/druid';
/* *
*
@@ -8,6 +11,32 @@ import React, { useEffect } from 'react';
* */
const DruidInfo: React.FC = () => {
const [druidLoginUrl, setDruidLoginUrl] = useState<string>();
useEffect(() => {
let active = true;
establishDruidSession()
.then((res) => {
if (!active) {
return;
}
if (res.code !== 200) {
message.error(res.msg || 'Druid 自动登录失败');
return;
}
setDruidLoginUrl(getDruidIndexUrl());
})
.catch(() => {
if (active) {
message.error('Druid 自动登录失败');
}
});
return () => {
active = false;
};
}, []);
useEffect(() => {
const frame = document.getElementById('bdIframe');
if (frame) {
@@ -16,14 +45,14 @@ const DruidInfo: React.FC = () => {
frame.style.width = `${Number(deviceWidth) - 220}px`;
frame.style.height = `${Number(deviceHeight) - 120}px`;
}
});
}, [druidLoginUrl]);
return (
<iframe
style={{ width: '100%', border: '0px', height: '100%' }}
src={process.env.NODE_ENV === 'development' ? '/api/druid/login.html' : '/api/ks/druid/login.html'}
src={druidLoginUrl}
id="bdIframe"
/>
/>
// </WrapContent>
);
};

View File

@@ -23,10 +23,11 @@ const handleAdd = async (fields: API.System.Dept) => {
hide();
if (resp.code === 200) {
message.success('添加成功');
return true;
} else {
message.error(resp.msg);
message.error(resp.msg || '添加失败');
return false;
}
return true;
} catch (error) {
hide();
message.error('添加失败请重试!');
@@ -46,10 +47,11 @@ const handleUpdate = async (fields: API.System.Dept) => {
hide();
if (resp.code === 200) {
message.success('更新成功');
return true;
} else {
message.error(resp.msg);
message.error(resp.msg || '更新失败');
return false;
}
return true;
} catch (error) {
hide();
message.error('配置失败请重试!');
@@ -70,10 +72,11 @@ const handleRemove = async (selectedRows: API.System.Dept[]) => {
hide();
if (resp.code === 200) {
message.success('删除成功,即将刷新');
return true;
} else {
message.error(resp.msg);
message.error(resp.msg || '删除失败');
return false;
}
return true;
} catch (error) {
hide();
message.error('删除失败,请重试');
@@ -90,10 +93,11 @@ const handleRemoveOne = async (selectedRow: API.System.Dept) => {
hide();
if (resp.code === 200) {
message.success('删除成功,即将刷新');
return true;
} else {
message.error(resp.msg);
message.error(resp.msg || '删除失败');
return false;
}
return true;
} catch (error) {
hide();
message.error('删除失败,请重试');
@@ -101,6 +105,16 @@ const handleRemoveOne = async (selectedRow: API.System.Dept) => {
}
};
/**
* 获取树表格中所有有下级部门的节点,确保异步加载后能够展开到任意层级。
*/
const getExpandableRowKeys = (nodes: any[]): React.Key[] =>
nodes.flatMap((node) => {
if (!Array.isArray(node.children) || node.children.length === 0) {
return [];
}
return [node.deptId, ...getExpandableRowKeys(node.children)];
});
const DeptTableList: React.FC = () => {
const formTableRef = useRef<FormInstance>();
@@ -110,6 +124,7 @@ const DeptTableList: React.FC = () => {
const actionRef = useRef<ActionType>();
const [currentRow, setCurrentRow] = useState<API.System.Dept>();
const [selectedRows, setSelectedRows] = useState<API.System.Dept[]>([]);
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
const [deptTree, setDeptTree] = useState<any>([]);
const [statusOptions, setStatusOptions] = useState<any>([]);
@@ -125,6 +140,22 @@ const DeptTableList: React.FC = () => {
});
}, []);
const openAddModal = async (parentId?: number) => {
const res = await getDeptList();
if (res.code !== 200) {
message.warning(res.msg || '部门列表加载失败');
return;
}
setDeptTree(buildTreeData(res.data, 'deptId', 'deptName', '', '', ''));
setCurrentRow(
parentId === undefined
? undefined
: ({ parentId, orderNum: 1, status: '0' } as API.System.Dept),
);
setModalVisible(true);
};
const columns: ProColumns<API.System.Dept>[] = [
{
title: <FormattedMessage id="system.dept.dept_name" defaultMessage="部门名称" />,
@@ -148,14 +179,14 @@ const DeptTableList: React.FC = () => {
{
title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
dataIndex: 'option',
width: '220px',
width: '300px',
valueType: 'option',
render: (_, record) => [
<Button
type="link"
size="small"
key="edit"
hidden={!access.hasPerms('system:dept:edit.tsx')}
hidden={!access.hasPerms('system:dept:edit')}
onClick={() => {
getDeptListExcludeChild(record.deptId).then((res) => {
if (res.code === 200) {
@@ -174,6 +205,15 @@ const DeptTableList: React.FC = () => {
>
</Button>,
<Button
type="link"
size="small"
key="addChild"
hidden={!access.hasPerms('system:dept:add')}
onClick={() => openAddModal(record.deptId)}
>
<PlusOutlined />
</Button>,
<Button
type="link"
size="small"
@@ -218,22 +258,16 @@ const DeptTableList: React.FC = () => {
search={{
labelWidth: 120,
}}
expandable={{
expandedRowKeys,
onExpandedRowsChange: (keys) => setExpandedRowKeys(keys),
}}
toolBarRender={() => [
<Button
type="primary"
key="add"
hidden={!access.hasPerms('system:dept:add')}
onClick={async () => {
getDeptList().then((res) => {
if (res.code === 200) {
setDeptTree(buildTreeData(res.data, 'deptId', 'deptName', '', '', ''));
setCurrentRow(undefined);
setModalVisible(true);
} else {
message.warning(res.msg);
}
});
}}
onClick={() => openAddModal()}
>
<PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
</Button>,
@@ -264,8 +298,10 @@ const DeptTableList: React.FC = () => {
]}
request={(params) =>
getDeptList({ ...params } as API.System.DeptListParams).then((res) => {
const treeData = buildTreeData(res.data, 'deptId', 'deptName', '', '', '');
setExpandedRowKeys(getExpandableRowKeys(treeData));
const result = {
data: buildTreeData(res.data, 'deptId', '', '', '', ''),
data: treeData,
total: res.data.length,
success: true,
};
@@ -293,7 +329,7 @@ const DeptTableList: React.FC = () => {
<Button
key="remove"
danger
hidden={!access.hasPerms('system:dept:del')}
hidden={!access.hasPerms('system:dept:remove')}
onClick={async () => {
Modal.confirm({
title: '删除',

View File

@@ -11,27 +11,52 @@ const { DirectoryTree } = Tree;
*
* */
export type TreeProps = {
onSelect: (values: any) => Promise<void>;
defaultDeptId?: number;
};
const getAllNodeKeys = (nodes: any[]): React.Key[] =>
nodes.flatMap((node) => [node.key, ...(node.children ? getAllNodeKeys(node.children) : [])]);
const DeptTree: React.FC<TreeProps> = (props) => {
const [treeData, setTreeData] = useState<any>([]);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
const findNodeById = (nodes: any[], deptId?: number): any => {
if (!deptId) {
return undefined;
}
for (const node of nodes) {
if (Number(node.id) === deptId) {
return node;
}
if (node.children) {
const matched = findNodeById(node.children, deptId);
if (matched) {
return matched;
}
}
}
return undefined;
};
const fetchDeptList = async () => {
const hide = message.loading('正在查询');
try {
await getDeptTree({}).then((res: any) => {
const exKeys = [];
exKeys.push('1');
setTreeData(res);
exKeys.push(res[0].children[0].id);
setExpandedKeys(exKeys);
props.onSelect(res[0].children[0]);
});
const res = await getDeptTree({});
setTreeData(res);
setExpandedKeys(getAllNodeKeys(res));
const defaultNode = props.defaultDeptId
? findNodeById(res, props.defaultDeptId)
: res[0]?.children?.[0] || res[0];
if (defaultNode) {
setSelectedKeys([String(defaultNode.key ?? defaultNode.id)]);
props.onSelect(defaultNode);
}
hide();
return true;
} catch (error) {
@@ -45,6 +70,7 @@ const DeptTree: React.FC<TreeProps> = (props) => {
}, []);
const onSelect = (keys: React.Key[], info: any) => {
setSelectedKeys(keys);
props.onSelect(info.node);
};
@@ -59,6 +85,7 @@ const DeptTree: React.FC<TreeProps> = (props) => {
defaultExpandAll
onExpand={onExpand}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
autoExpandParent={autoExpandParent}
onSelect={onSelect}
treeData={treeData}

View File

@@ -92,6 +92,9 @@ const UserForm: React.FC<UserFormProps> = (props) => {
layout="horizontal"
submitter={false}
onFinish={handleFinish}>
<Form.Item name="userId" hidden>
<input type="hidden" />
</Form.Item>
<ProFormText
name="nickName"
label={intl.formatMessage({

View File

@@ -1,11 +1,32 @@
import React, { useState, useRef, useEffect } from 'react';
import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
import { Card, Col, Dropdown, FormInstance, Row, Space, Switch } from 'antd';
import { useIntl, FormattedMessage, useAccess, useModel } from '@umijs/max';
import { Card, Col, Dropdown, FormInstance, Radio, Row, Space, Switch } from 'antd';
import { Button, message, Modal } from 'antd';
import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, DownOutlined, EditOutlined } from '@ant-design/icons';
import { getUserList, removeUser, addUser, updateUser, exportUser, getUser, changeUserStatus, updateAuthRole, resetUserPwd } from '@/services/system/user';
import {
ActionType,
FooterToolbar,
PageContainer,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import {
PlusOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
DownOutlined,
EditOutlined,
} from '@ant-design/icons';
import {
getUserList,
removeUser,
addUser,
updateUser,
exportUser,
getUser,
changeUserStatus,
updateAuthRole,
resetUserPwd,
} from '@/services/system/user';
import UpdateForm from './edit';
import { getDictValueEnum } from '@/services/system/dict';
import { DataNode } from 'antd/es/tree';
@@ -104,10 +125,10 @@ const handleRemoveOne = async (selectedRow: API.System.User) => {
*
*
*/
const handleExport = async () => {
const handleExport = async (params?: API.System.UserListParams) => {
const hide = message.loading('正在导出');
try {
await exportUser();
await exportUser(params);
hide();
message.success('导出成功');
return true;
@@ -120,6 +141,8 @@ const handleExport = async () => {
const UserTableList: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const { initialState } = useModel('@@initialState');
const currentDeptId = Number((initialState?.currentUser as any)?.dept?.deptId || 0);
const formTableRef = useRef<FormInstance>();
@@ -131,7 +154,8 @@ const UserTableList: React.FC = () => {
const [currentRow, setCurrentRow] = useState<API.System.User>();
const [selectedRows, setSelectedRows] = useState<API.System.User[]>([]);
const [selectDept, setSelectDept] = useState<any>({ id: 0 });
const [selectDept, setSelectDept] = useState<any>({ id: currentDeptId });
const [deptScope, setDeptScope] = useState<'SELF' | 'CHILDREN'>('SELF');
const [sexOptions, setSexOptions] = useState<any>([]);
const [statusOptions, setStatusOptions] = useState<any>([]);
@@ -143,6 +167,18 @@ const UserTableList: React.FC = () => {
const access = useAccess();
useEffect(() => {
if (currentDeptId && !selectDept.id) {
setSelectDept({ id: currentDeptId });
}
}, [currentDeptId, selectDept.id]);
useEffect(() => {
if (selectDept.id) {
actionRef.current?.reload();
}
}, [selectDept.id, deptScope]);
/** 国际化配置 */
const intl = useIntl();
@@ -156,12 +192,12 @@ const UserTableList: React.FC = () => {
}, []);
const showChangeStatusConfirm = (record: API.System.User) => {
let text = record.status === "1" ? "启用" : "停用";
let text = record.status === '1' ? '启用' : '停用';
const newStatus = record.status === '0' ? '1' : '0';
confirm({
title: `确认要${text}${record.userName}用户吗?`,
onOk() {
changeUserStatus(record.userId, newStatus).then(resp => {
changeUserStatus(record.userId, newStatus).then((resp) => {
if (resp.code === 200) {
messageApi.open({
type: 'success',
@@ -221,7 +257,7 @@ const UserTableList: React.FC = () => {
title: <FormattedMessage id="system.user.dept_name" defaultMessage="部门" />,
dataIndex: ['dept', 'deptName'],
valueType: 'text',
hideInSearch: true
hideInSearch: true,
},
{
title: <FormattedMessage id="system.user.phonenumber" defaultMessage="手机号码" />,
@@ -241,7 +277,8 @@ const UserTableList: React.FC = () => {
unCheckedChildren="停用"
defaultChecked
onClick={() => showChangeStatusConfirm(record)}
/>)
/>
);
},
},
{
@@ -297,7 +334,9 @@ const UserTableList: React.FC = () => {
menu={{
items: [
{
label: <FormattedMessage id="system.user.reset.password" defaultMessage="密码重置" />,
label: (
<FormattedMessage id="system.user.reset.password" defaultMessage="密码重置" />
),
key: 'reset',
disabled: !access.hasPerms('system:user:edit.tsx'),
},
@@ -311,13 +350,12 @@ const UserTableList: React.FC = () => {
if (key === 'reset') {
setResetPwdModalVisible(true);
setCurrentRow(record);
}
else if (key === 'authRole') {
} else if (key === 'authRole') {
fetchUserInfo(record.userId);
setAuthRoleModalVisible(true);
setCurrentRow(record);
}
}
},
}}
>
<a onClick={(e) => e.preventDefault()}>
@@ -338,11 +376,9 @@ const UserTableList: React.FC = () => {
<Col lg={6} md={24}>
<Card>
<DeptTree
defaultDeptId={currentDeptId}
onSelect={async (value: any) => {
setSelectDept(value);
if (actionRef.current) {
formTableRef?.current?.submit();
}
}}
/>
</Card>
@@ -361,6 +397,18 @@ const UserTableList: React.FC = () => {
labelWidth: 120,
}}
toolBarRender={() => [
<Radio.Group
key="deptScope"
optionType="button"
buttonStyle="solid"
value={deptScope}
onChange={(event) => {
setDeptScope(event.target.value);
}}
>
<Radio.Button value="SELF"></Radio.Button>
<Radio.Button value="CHILDREN"></Radio.Button>
</Radio.Group>,
<Button
type="primary"
key="add"
@@ -369,7 +417,7 @@ const UserTableList: React.FC = () => {
const treeData = await getDeptTree({});
setDeptTree(treeData);
const postResp = await getPostList()
const postResp = await getPostList();
if (postResp.code === 200) {
setPostList(
postResp.rows.map((item: any) => {
@@ -381,7 +429,7 @@ const UserTableList: React.FC = () => {
);
}
const roleResp = await getRoleList()
const roleResp = await getRoleList();
if (roleResp.code === 200) {
setRoleList(
roleResp.rows.map((item: any) => {
@@ -396,7 +444,8 @@ const UserTableList: React.FC = () => {
setModalVisible(true);
}}
>
<PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
<PlusOutlined />{' '}
<FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
</Button>,
<Button
type="primary"
@@ -415,7 +464,7 @@ const UserTableList: React.FC = () => {
actionRef.current?.reloadAndRest?.();
}
},
onCancel() { },
onCancel() {},
});
}}
>
@@ -427,7 +476,7 @@ const UserTableList: React.FC = () => {
key="export"
hidden={!access.hasPerms('system:user:export')}
onClick={async () => {
handleExport();
handleExport({ deptId: selectDept.id, deptScope });
}}
>
<PlusOutlined />
@@ -435,7 +484,11 @@ const UserTableList: React.FC = () => {
</Button>,
]}
request={(params) =>
getUserList({ ...params, deptId: selectDept.id } as API.System.UserListParams).then((res) => {
getUserList({
...params,
deptId: selectDept.id,
deptScope,
} as API.System.UserListParams).then((res) => {
const result = {
data: res.rows,
total: res.total,

View File

@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import { getProductionApiBaseUrl } from '@/utils/publicUrl';
/**
*
@@ -7,6 +8,11 @@ import React, { useEffect } from 'react';
* */
const CacheInfo: React.FC = () => {
const swaggerUrl =
process.env.NODE_ENV === 'development'
? '/api/swagger-ui/index.html'
: `${getProductionApiBaseUrl()}swagger-ui/index.html`;
useEffect(() => {
const frame = document.getElementById('bdIframe');
if (frame) {
@@ -21,7 +27,7 @@ const CacheInfo: React.FC = () => {
<div style={{}}>
<iframe
style={{ width: '100%', border: '0px', height: '100%' }}
src={process.env.NODE_ENV === 'development' ? '/api/swagger-ui/index.html' : '/api/ks/swagger-ui/index.html'}
src={swaggerUrl}
id="bdIframe"
/>
</div>

View File

@@ -0,0 +1,12 @@
import { request } from '@umijs/max';
export interface DruidSsoResponse {
code: number;
msg?: string;
}
export async function establishDruidSession() {
return request<DruidSsoResponse>('/api/monitor/druid/sso', {
method: 'GET',
});
}

View File

@@ -1,6 +1,4 @@
declare namespace API.System {
interface User {
userId: number;
deptId: number;
@@ -26,6 +24,7 @@ declare namespace API.System {
export interface UserListParams {
userId?: string;
deptId?: string;
deptScope?: 'SELF' | 'CHILDREN';
userName?: string;
nickName?: string;
userType?: string;
@@ -61,11 +60,10 @@ declare namespace API.System {
roles: [];
}
export interface UserPageResult {
export interface UserPageResult {
code: number;
msg: string;
total: number;
rows: Array<User>;
}
}

View File

@@ -12,6 +12,17 @@ export function getProductionApiBaseUrl(origin = getBrowserOrigin()) {
return `${normalizedOrigin || ''}/api/shihezi/`;
}
/**
* 返回已经建立登录态后的 Druid 首页地址。
*/
export function getDruidIndexUrl(origin = getBrowserOrigin()) {
const druidIndexPath = 'druid/index.html';
if (process.env.NODE_ENV === 'development') {
return `/api/${druidIndexPath}`;
}
return `${getProductionApiBaseUrl(origin)}${druidIndexPath}`;
}
/**
* 后端可能因为反向代理配置返回内网 H5 地址,前端展示和打开二维码时使用当前公开 origin。
*/

View File

@@ -1,4 +1,4 @@
import { getProductionApiBaseUrl, normalizePublicUrl } from '@/utils/publicUrl';
import { getDruidIndexUrl, getProductionApiBaseUrl, normalizePublicUrl } from '@/utils/publicUrl';
describe('public URL helpers', () => {
it('builds the production API base URL from the browser origin', () => {
@@ -7,6 +7,12 @@ describe('public URL helpers', () => {
);
});
it('builds the Druid index URL from the public API base URL', () => {
expect(getDruidIndexUrl('http://47.111.103.66')).toBe(
'http://47.111.103.66/api/shihezi/druid/index.html',
);
});
it('replaces an internal backend origin with the browser origin', () => {
expect(
normalizePublicUrl(