增加三方接口管理

This commit is contained in:
戚辰先生 2024-12-02 23:37:30 +08:00
parent 9d825f3354
commit 9b16146cd0
10 changed files with 909 additions and 628 deletions

View File

@ -1,4 +1,6 @@
import React from 'react';
import * as AntdIcons from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons';
import type { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon';
// 图标名称映射配置 // 图标名称映射配置
export const iconMap: Record<string, string> = { export const iconMap: Record<string, string> = {
@ -15,7 +17,7 @@ export const iconMap: Record<string, string> = {
}; };
// 获取图标组件的通用函数 // 获取图标组件的通用函数
export const getIconComponent = (iconName: string | undefined) => { export const getIconComponent = (iconName: string | undefined): React.ReactNode => {
if (!iconName) return null; if (!iconName) return null;
// 如果在映射中存在,使用映射的名称 // 如果在映射中存在,使用映射的名称
@ -26,8 +28,8 @@ export const getIconComponent = (iconName: string | undefined) => {
? mappedName.charAt(0).toUpperCase() + mappedName.slice(1) ? mappedName.charAt(0).toUpperCase() + mappedName.slice(1)
: `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`; : `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`;
const Icon = (AntdIcons as any)[iconKey]; const Icon = (AntdIcons as Record<string, React.FC<AntdIconProps>>)[iconKey];
return Icon ? <Icon/> : null; return Icon ? React.createElement(Icon) : null;
}; };
// 获取所有可用的图标列表 // 获取所有可用的图标列表
@ -36,6 +38,6 @@ export const getAvailableIcons = () => {
.filter(key => key.endsWith('Outlined')) .filter(key => key.endsWith('Outlined'))
.map(key => ({ .map(key => ({
name: key, name: key,
component: (AntdIcons as any)[key] component: (AntdIcons as Record<string, React.FC<AntdIconProps>>)[key]
})); }));
}; };

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import * as AntdIcons from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons';
import type { ReactNode } from 'react';
// 图标名称映射配置 // 图标名称映射配置
export const iconMap: Record<string, string> = { export const iconMap: Record<string, string> = {
@ -12,12 +13,11 @@ export const iconMap: Record<string, string> = {
'department': 'TeamOutlined', 'department': 'TeamOutlined',
'role': 'UserSwitchOutlined', 'role': 'UserSwitchOutlined',
'external': 'ApiOutlined', 'external': 'ApiOutlined',
'system': 'SettingOutlined', 'system': 'SettingOutlined'
'dashboard': 'DashboardOutlined'
}; };
// 获取图标组件的通用函数 // 获取图标组件的通用函数
export const getIconComponent = (iconName: string | undefined) => { export const getIconComponent = (iconName: string | undefined): ReactNode => {
if (!iconName) return null; if (!iconName) return null;
// 如果在映射中存在,使用映射的名称 // 如果在映射中存在,使用映射的名称
@ -29,7 +29,7 @@ export const getIconComponent = (iconName: string | undefined) => {
: `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`; : `${mappedName.charAt(0).toUpperCase() + mappedName.slice(1)}Outlined`;
const Icon = (AntdIcons as any)[iconKey]; const Icon = (AntdIcons as any)[iconKey];
return Icon ? React.createElement(Icon) : null; return Icon ? <Icon /> : null;
}; };
// 获取所有可用的图标列表 // 获取所有可用的图标列表

View File

@ -1,143 +1,202 @@
import {useState, useCallback, useEffect} from 'react'; import { useState, useCallback, useEffect } from 'react';
import {message, Modal} from 'antd'; import { message, Modal } from 'antd';
import type {TableState} from '@/types/table'; import type { Page } from '@/types/base/page';
import {createInitialState} from '@/utils/table'; import type { TablePaginationConfig } from 'antd/es/table';
import request from '@/utils/request'; import type { FilterValue, SorterResult } from 'antd/es/table/interface';
import type {Page} from '@/types/base/page';
// 修改TableService接口 // 表格服务接口
export interface TableService<T, P = any> { export interface TableService<T, Q = any, C = Partial<T>, U = Partial<T>> {
baseUrl: string; // 基础URL list: (params: Q & { pageNum?: number; pageSize?: number }) => Promise<Page<T>>;
create?: (data: C) => Promise<T>;
update?: (id: number, data: U) => Promise<T>;
delete?: (id: number) => Promise<void>;
} }
interface SortParams { // 表格状态
sortField?: string; interface TableState<T> {
sortOrder?: 'ascend' | 'descend'; list: T[];
pagination: TablePaginationConfig;
loading: boolean;
selectedRowKeys?: React.Key[];
selectedRows?: T[];
} }
export function useTableData<T extends { id: number }, P extends SortParams>({ // 表格配置
service, interface TableConfig {
defaultParams, defaultPageSize?: number;
config = {} selection?: boolean;
message?: {
createSuccess?: string;
updateSuccess?: string;
deleteSuccess?: string;
};
}
export function useTableData<
T extends { id: number },
Q extends Record<string, any> = any,
C = any,
U = any
>({
service,
defaultParams,
config = {}
}: { }: {
service: TableService<T, P>; service: TableService<T, Q, C, U>;
defaultParams?: Partial<P>; defaultParams?: Partial<Q>;
config?: any; config?: TableConfig;
}) { }) {
const [state, setState] = useState<TableState<T>>(() => // 初始化状态
createInitialState(config.defaultPageSize || 10) const [state, setState] = useState<TableState<T>>({
); list: [],
pagination: {
current: 1,
pageSize: config.defaultPageSize || 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`
},
loading: false
});
// 加载数据 // 加载数据
const loadData = useCallback(async (params?: Partial<P>) => { const loadData = useCallback(async (params?: Partial<Q>) => {
setState(prev => ({ ...prev, loading: true })); setState(prev => ({ ...prev, loading: true }));
try { try {
const pageData = await request.get<Page<T>>(`${service.baseUrl}/page`, { const pageData = await service.list({
params: { ...defaultParams,
...defaultParams, ...params,
...params, pageNum: state.pagination.current,
pageNum: state.pagination.current, pageSize: state.pagination.pageSize
pageSize: state.pagination.pageSize, });
sortField: params?.sortField,
sortOrder: params?.sortOrder === 'ascend' ? 'asc' : params?.sortOrder === 'descend' ? 'desc' : undefined
}
});
setState(prev => ({ setState(prev => ({
...prev, ...prev,
list: pageData.content || [], list: pageData.content || [],
pagination: { pagination: {
...prev.pagination, ...prev.pagination,
total: pageData.totalElements || 0 total: pageData.totalElements || 0
}, },
loading: false loading: false
})); }));
} catch (error) { } catch (error) {
setState(prev => ({ ...prev, loading: false })); setState(prev => ({ ...prev, loading: false }));
} throw error;
}, [service, defaultParams, state.pagination]); }
}, [service, defaultParams, state.pagination]);
// CRUD操作 // 表格变化处理
const handleCreate = useCallback(async (data: Partial<T>) => { const handleTableChange = (
try { pagination: TablePaginationConfig,
await request.post<T>(service.baseUrl, data); filters: Record<string, FilterValue | null>,
message.success("创建成功") sorter: SorterResult<T> | SorterResult<T>[]
await loadData(); ) => {
return true; const { current, pageSize } = pagination;
} catch (error) { setState(prev => ({
return false; ...prev,
} pagination: {
}, [service, loadData]); ...prev.pagination,
current,
pageSize
}
}));
const handleUpdate = useCallback(async (id: number, data: Partial<T>) => { const params: Record<string, any> = {};
try {
await request.put<T>(`${service.baseUrl}/${id}`, data);
message.success("更新成功")
await loadData();
return true;
} catch (error) {
return false;
}
}, [service, loadData]);
const handleDelete = useCallback(async (id: number) => { // 处理排序
return new Promise((resolve) => { if (!Array.isArray(sorter)) {
Modal.confirm({ const { field, order } = sorter;
title: '确认删除', if (field && order) {
content: '确定要删除该记录吗?', params.sortField = field as string;
okText: '确定', params.sortOrder = order === 'ascend' ? 'asc' : 'desc';
okType: 'danger', }
cancelText: '取消', }
onOk: async () => {
try {
await request.delete(`${service.baseUrl}/${id}`);
message.success("删除成功")
loadData();
resolve(true);
} catch (error) {
resolve(false);
}
},
onCancel: () => resolve(false)
});
});
}, [service, loadData]);
// 分页变化 // 处理筛选
const onPageChange = useCallback((page: number, pageSize?: number) => { Object.entries(filters).forEach(([key, value]) => {
setState(prev => ({ if (value) {
...prev, params[key] = value;
pagination: { }
...prev.pagination, });
current: page,
pageSize: pageSize || prev.pagination.pageSize
}
}));
}, []);
// 选择行 loadData(params as Partial<Q>);
const onSelectChange = useCallback((selectedRowKeys: React.Key[], selectedRows: T[]) => { };
if (!config.selection) return;
setState(prev => ({
...prev,
selectedRowKeys,
selectedRows
}));
}, [config.selection]);
// 监听分页变化自动加载数据 // 创建
useEffect(() => { const handleCreate = async (data: C) => {
loadData(); if (!service.create) return false;
}, [state.pagination.current, state.pagination.pageSize]); try {
await service.create(data);
message.success(config.message?.createSuccess || '创建成功');
loadData();
return true;
} catch (error) {
return false;
}
};
return { // 更新
...state, const handleUpdate = async (id: number, data: U) => {
loadData, if (!service.update) return false;
onPageChange, try {
handleCreate, await service.update(id, data);
handleUpdate, message.success(config.message?.updateSuccess || '更新成功');
handleDelete, loadData();
onSelectChange, return true;
reset: () => setState(createInitialState(config.defaultPageSize || 10)) } catch (error) {
}; return false;
}
};
// 删除
const handleDelete = async (id: number) => {
if (!service.delete) return false;
return new Promise<boolean>((resolve) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该记录吗?',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
await service.delete(id);
message.success(config.message?.deleteSuccess || '删除成功');
loadData();
resolve(true);
} catch (error) {
resolve(false);
}
},
onCancel: () => resolve(false)
});
});
};
// 选择行
const handleSelectChange = (selectedRowKeys: React.Key[], selectedRows: T[]) => {
if (!config.selection) return;
setState(prev => ({
...prev,
selectedRowKeys,
selectedRows
}));
};
// 监听分页变化自动加载数据
useEffect(() => {
loadData();
}, [state.pagination.current, state.pagination.pageSize]);
return {
...state,
loadData,
handleTableChange,
handleCreate,
handleUpdate,
handleDelete,
handleSelectChange,
refresh: () => loadData()
};
} }

View File

@ -156,22 +156,13 @@ const BasicLayout: React.FC = () => {
// 将菜单数据转换为antd Menu需要的格式 // 将菜单数据转换为antd Menu需要的格式
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => { const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
return menuList return menuList
?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden) // 过滤掉按钮类型和隐藏的菜单 ?.filter(menu => menu.type !== MenuTypeEnum.BUTTON && !menu.hidden)
.map(menu => { .map(menu => {
// 确保path存在否则使用id
const key = menu.path || `menu-${menu.id}`;
// 如果有子菜单,递归处理子菜单
const children = menu.children && menu.children.length > 0
? getMenuItems(menu.children.filter(child =>
child.type !== MenuTypeEnum.BUTTON && !child.hidden))
: undefined;
return { return {
key, key: menu.path || String(menu.id),
icon: getIcon(menu.icon), icon: getIconComponent(menu.icon),
label: menu.name, label: menu.name,
children: children children: menu.children ? getMenuItems(menu.children) : undefined
}; };
}); });
}; };
@ -207,8 +198,6 @@ const BasicLayout: React.FC = () => {
); );
} }
console.log('Current location:', location.pathname);
console.log('Current menus:', menus);
return ( return (
<Layout style={{minHeight: '100vh'}}> <Layout style={{minHeight: '100vh'}}>

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Table, Button, Modal, Form, Input, Space, InputNumber, Switch, TreeSelect, Select, message } from 'antd'; import { Table, Button, Modal, Form, Input, Space, InputNumber, Switch, TreeSelect, Select, message, Card } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons';
import type { DepartmentResponse } from './types'; import type { DepartmentResponse } from './types';
import { getDepartmentTree } from './service'; import { getDepartmentTree } from './service';
import type { UserResponse } from '@/pages/System/User/types'; import type { UserResponse } from '@/pages/System/User/types';
@ -23,12 +23,21 @@ const DepartmentPage: React.FC = () => {
const [editingDepartment, setEditingDepartment] = useState<DepartmentResponse | null>(null); const [editingDepartment, setEditingDepartment] = useState<DepartmentResponse | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
// 处理树形数据,添加 hasChildren 属性
const processTreeData = (departments: DepartmentResponse[]) => {
return departments.map(dept => ({
...dept,
hasChildren: Boolean(dept.children?.length),
children: dept.children ? processTreeData(dept.children) : undefined
}));
};
// 加载部门树数据 // 加载部门树数据
const loadDepartmentTree = async () => { const loadDepartmentTree = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await getDepartmentTree(); const response = await getDepartmentTree();
setDepartmentTree(response || []); setDepartmentTree(processTreeData(response || []));
} catch (error) { } catch (error) {
message.error('加载部门数据失败'); message.error('加载部门数据失败');
} finally { } finally {
@ -191,9 +200,9 @@ const DepartmentPage: React.FC = () => {
]; ];
return ( return (
<div style={{padding: '24px'}}> <Card>
<div style={{marginBottom: 16}}> <div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined/>} onClick={handleAdd}> <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button> </Button>
</div> </div>
@ -203,11 +212,24 @@ const DepartmentPage: React.FC = () => {
columns={columns} columns={columns}
dataSource={departmentTree} dataSource={departmentTree}
rowKey="id" rowKey="id"
scroll={{x: 1200}} scroll={{ x: 1200 }}
pagination={false} pagination={false}
size="middle" size="middle"
bordered bordered
defaultExpandAllRows defaultExpandAllRows
expandable={{
showIcon: true,
expandIcon: ({ expanded, onExpand, record }) => {
if (!record.hasChildren) {
return null;
}
return expanded ? (
<CaretDownOutlined onClick={e => onExpand(record, e)} />
) : (
<CaretRightOutlined onClick={e => onExpand(record, e)} />
);
}
}}
/> />
<Modal <Modal
@ -317,7 +339,7 @@ const DepartmentPage: React.FC = () => {
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
</div> </Card>
); );
}; };

View File

@ -1,32 +1,116 @@
import React from 'react'; import React, { useState } from 'react';
import { Card, Table, Button, Space, Modal, Form, Input, message } from 'antd'; import { Card, Table, Button, Space, Modal, Form, Input, message, Select, InputNumber, Switch, Tag, Tooltip } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, LinkOutlined, MinusCircleOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { useTableData } from '@/hooks/useTableData';
interface ExternalSystem { import * as service from './service';
id: number; import { SystemType, AuthType, SyncStatus, ExternalSystemResponse, ExternalSystemRequest, ExternalSystemQuery } from './types';
name: string;
url: string;
description?: string;
createTime: string;
updateTime?: string;
}
const ExternalPage: React.FC = () => { const ExternalPage: React.FC = () => {
const [loading, setLoading] = React.useState(false); const [form] = Form.useForm();
const [data, setData] = React.useState<ExternalSystem[]>([]); const [modalVisible, setModalVisible] = useState(false);
const [editingSystem, setEditingSystem] = useState<ExternalSystemResponse | null>(null);
const columns: ColumnsType<ExternalSystem> = [ const {
list,
loading,
pagination,
handleTableChange,
handleCreate,
handleUpdate,
handleDelete,
refresh
} = useTableData<ExternalSystemResponse, ExternalSystemQuery, ExternalSystemRequest, ExternalSystemRequest>({
service: {
list: service.getExternalSystems,
create: service.createExternalSystem,
update: service.updateExternalSystem,
delete: service.deleteExternalSystem
},
config: {
message: {
createSuccess: '创建系统成功',
updateSuccess: '更新系统成功',
deleteSuccess: '删除系统成功'
}
}
});
const handleAdd = () => {
setEditingSystem(null);
form.resetFields();
form.setFieldsValue({
enabled: true,
sort: 1,
authType: AuthType.BASIC
});
setModalVisible(true);
};
const handleEdit = (record: ExternalSystemResponse) => {
setEditingSystem(record);
form.setFieldsValue({
...record,
password: undefined // 不显示密码
});
setModalVisible(true);
};
const handleTestConnection = async (id: number) => {
try {
const success = await service.testConnection(id);
message.success(success ? '连接成功' : '连接失败');
} catch (error) {
message.error('测试连接失败');
}
};
const handleStatusChange = async (id: number, enabled: boolean) => {
try {
await service.updateStatus(id, enabled);
message.success('更新状态成功');
refresh();
} catch (error) {
message.error('更新状态失败');
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingSystem) {
await handleUpdate(editingSystem.id, values);
} else {
await handleCreate(values);
}
setModalVisible(false);
} catch (error) {
console.error('操作失败:', error);
}
};
const columns: ColumnsType<ExternalSystemResponse> = [
{ {
title: '系统名称', title: '系统名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name',
width: 200, width: 200,
}, },
{
title: '系统类型',
dataIndex: 'type',
width: 120,
render: (type: SystemType) => {
const typeMap = {
[SystemType.JENKINS]: 'Jenkins',
[SystemType.GIT]: 'Git',
[SystemType.ZENTAO]: '禅道'
};
return typeMap[type];
}
},
{ {
title: '系统地址', title: '系统地址',
dataIndex: 'url', dataIndex: 'url',
key: 'url',
width: 300, width: 300,
render: (url: string) => ( render: (url: string) => (
<a href={url} target="_blank" rel="noopener noreferrer"> <a href={url} target="_blank" rel="noopener noreferrer">
@ -35,41 +119,75 @@ const ExternalPage: React.FC = () => {
), ),
}, },
{ {
title: '描述', title: '认证方式',
dataIndex: 'description', dataIndex: 'authType',
key: 'description', width: 120,
ellipsis: true, render: (authType: AuthType) => {
const authTypeMap = {
[AuthType.BASIC]: '用户名密码',
[AuthType.TOKEN]: '令牌',
[AuthType.OAUTH]: 'OAuth2'
};
return authTypeMap[authType];
}
}, },
{ {
title: '创建时间', title: '同步状态',
dataIndex: 'createTime', dataIndex: 'syncStatus',
key: 'createTime', width: 120,
width: 180, render: (status: SyncStatus, record) => {
const statusConfig = {
[SyncStatus.SUCCESS]: { icon: <CheckCircleOutlined />, color: 'success', text: '成功' },
[SyncStatus.FAILED]: { icon: <CloseCircleOutlined />, color: 'error', text: '失败' },
[SyncStatus.RUNNING]: { icon: <SyncOutlined spin />, color: 'processing', text: '同步中' },
NONE: { icon: <MinusCircleOutlined />, color: 'default', text: '未同步' }
};
const config = statusConfig[status] || statusConfig.NONE;
return (
<Tooltip title={record.lastSyncTime || '未同步'}>
<Tag icon={config.icon} color={config.color}>
{config.text}
</Tag>
</Tooltip>
);
}
}, },
{ {
title: '更新时间', title: '状态',
dataIndex: 'updateTime', dataIndex: 'enabled',
key: 'updateTime', width: 100,
width: 180, render: (enabled: boolean, record) => (
<Switch
checked={enabled}
onChange={(checked) => handleStatusChange(record.id, checked)}
/>
)
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 180, width: 280,
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
<Button <Button
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => message.info('编辑功能开发中')} onClick={() => handleEdit(record)}
> >
</Button> </Button>
<Button
type="link"
icon={<LinkOutlined />}
onClick={() => handleTestConnection(record.id)}
>
</Button>
<Button <Button
type="link" type="link"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={() => message.info('删除功能开发中')} onClick={() => handleDelete(record.id)}
> >
</Button> </Button>
@ -85,24 +203,140 @@ const ExternalPage: React.FC = () => {
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => message.info('新增功能开发中')} onClick={handleAdd}
> >
</Button> </Button>
</div> </div>
<Table <Table
columns={columns} columns={columns}
dataSource={data} dataSource={list}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
pagination={{ pagination={pagination}
total: data.length, onChange={handleTableChange}
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
/> />
</Card> </Card>
<Modal
title={editingSystem ? '编辑系统' : '新增系统'}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
width={600}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="name"
label="系统名称"
rules={[{ required: true, message: '请输入系统名称' }]}
>
<Input placeholder="请输入系统名称" />
</Form.Item>
<Form.Item
name="type"
label="系统类型"
rules={[{ required: true, message: '请选择系统类型' }]}
>
<Select>
<Select.Option value={SystemType.JENKINS}>Jenkins</Select.Option>
<Select.Option value={SystemType.GIT}>Git</Select.Option>
<Select.Option value={SystemType.ZENTAO}></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="url"
label="系统地址"
rules={[
{ required: true, message: '请输入系统地址' },
{ type: 'url', message: '请输入有效的URL' }
]}
>
<Input placeholder="请输入系统地址" />
</Form.Item>
<Form.Item
name="authType"
label="认证方式"
rules={[{ required: true, message: '请选择认证方式' }]}
>
<Select>
<Select.Option value={AuthType.BASIC}></Select.Option>
<Select.Option value={AuthType.TOKEN}></Select.Option>
<Select.Option value={AuthType.OAUTH}>OAuth2</Select.Option>
</Select>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.authType !== currentValues.authType}
>
{({ getFieldValue }) => {
const authType = getFieldValue('authType');
if (authType === AuthType.BASIC) {
return (
<>
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: !editingSystem, message: '请输入密码' }]}
>
<Input.Password placeholder={editingSystem ? '不修改请留空' : '请输入密码'} />
</Form.Item>
</>
);
}
if (authType === AuthType.TOKEN) {
return (
<Form.Item
name="token"
label="访问令牌"
rules={[{ required: !editingSystem, message: '请输入访问令牌' }]}
>
<Input.Password placeholder={editingSystem ? '不修改请留空' : '请输入访问令牌'} />
</Form.Item>
);
}
return null;
}}
</Form.Item>
<Form.Item
name="sort"
label="显示排序"
rules={[{ required: true, message: '请输入显示排序' }]}
>
<InputNumber style={{ width: '100%' }} min={0} placeholder="请输入显示排序" />
</Form.Item>
<Form.Item
name="remark"
label="备注"
>
<Input.TextArea rows={4} placeholder="请输入备注" />
</Form.Item>
<Form.Item
name="enabled"
label="状态"
valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item>
</Form>
</Modal>
</div> </div>
); );
}; };

View File

@ -0,0 +1,29 @@
import request from '@/utils/request';
import type { Page } from '@/types/base/page';
import type { ExternalSystemResponse, ExternalSystemRequest, ExternalSystemQuery } from './types';
const BASE_URL = '/api/v1/external-system';
// 获取三方系统列表
export const getExternalSystems = (params: ExternalSystemQuery) =>
request.get<Page<ExternalSystemResponse>>(`${BASE_URL}/page`, { params });
// 创建三方系统
export const createExternalSystem = (data: ExternalSystemRequest) =>
request.post<ExternalSystemResponse>(BASE_URL, data);
// 更新三方系统
export const updateExternalSystem = (id: number, data: ExternalSystemRequest) =>
request.put<ExternalSystemResponse>(`${BASE_URL}/${id}`, data);
// 删除三方系统
export const deleteExternalSystem = (id: number) =>
request.delete(`${BASE_URL}/${id}`);
// 测试连接
export const testConnection = (id: number) =>
request.get<boolean>(`${BASE_URL}/${id}/test-connection`);
// 更新状态
export const updateStatus = (id: number, enabled: boolean) =>
request.put(`${BASE_URL}/${id}/status`, null, { params: { enabled } });

View File

@ -0,0 +1,62 @@
import { BaseResponse } from '@/types/base/response';
import { BaseQuery } from '@/types/base/query';
// 系统类型枚举
export enum SystemType {
JENKINS = 'JENKINS',
GIT = 'GIT',
ZENTAO = 'ZENTAO'
}
// 认证方式枚举
export enum AuthType {
BASIC = 'BASIC',
TOKEN = 'TOKEN',
OAUTH = 'OAUTH'
}
// 同步状态枚举
export enum SyncStatus {
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
RUNNING = 'RUNNING'
}
// 查询参数接口
export interface ExternalSystemQuery extends BaseQuery {
name?: string;
type?: SystemType;
enabled?: boolean;
}
// 响应数据接口
export interface ExternalSystemResponse extends BaseResponse {
name: string;
type: SystemType;
url: string;
remark?: string;
sort: number;
enabled: boolean;
authType: AuthType;
username?: string;
password?: string;
token?: string;
syncStatus: SyncStatus;
lastSyncTime?: string;
config?: string;
}
// 请求数据接口
export interface ExternalSystemRequest {
name: string;
type: SystemType;
url: string;
remark?: string;
sort: number;
enabled: boolean;
authType: AuthType;
username?: string;
password?: string;
token?: string;
config?: string;
}

View File

@ -1,58 +1,46 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Table, Button, Modal, Form, Input, Space, Switch, Select, TreeSelect, Tooltip, InputNumber, Tree } from 'antd'; import { Card, Table, Button, Modal, Form, Input, Space, message, Select, TreeSelect, Tooltip, Switch } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined, FolderOutlined, MenuOutlined, ToolOutlined, CaretRightOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import type { TablePaginationConfig } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import type { FilterValue, SorterResult } from 'antd/es/table/interface'; import * as service from './service';
import { getMenuTree, getCurrentUserMenus } from './service'; import { MenuTypeEnum, MenuResponse, MenuRequest } from './types';
import type { MenuResponse } from './types';
import { MenuTypeEnum } from './types';
import IconSelect from '@/components/IconSelect'; import IconSelect from '@/components/IconSelect';
import { useTableData } from '@/hooks/useTableData';
import * as AntdIcons from '@ant-design/icons';
import {FixedType} from "rc-table/lib/interface";
import { getIconComponent } from '@/config/icons.tsx';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setMenus } from '@/store/userSlice'; import { setMenus } from '@/store/userSlice';
import { message } from 'antd'; import { getIconComponent } from '@/config/icons.tsx';
const MenuPage: React.FC = () => { const MenuPage: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const [form] = Form.useForm();
list: menus,
loading,
loadData: fetchMenus,
handleCreate,
handleUpdate,
handleDelete
} = useTableData({
service: {
baseUrl: '/api/v1/menu'
},
defaultParams: {
sortField: 'sort',
sortOrder: 'asc'
}
});
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [editingMenu, setEditingMenu] = useState<MenuResponse | null>(null); const [editingMenu, setEditingMenu] = useState<MenuResponse | null>(null);
const [menuTree, setMenuTree] = useState<MenuResponse[]>([]);
const [iconSelectVisible, setIconSelectVisible] = useState(false); const [iconSelectVisible, setIconSelectVisible] = useState(false);
const [form] = Form.useForm(); const [loading, setLoading] = useState(false);
const [menuTree, setMenuTree] = useState<MenuResponse[]>([]);
// 加载菜单树
const loadMenuTree = async () => {
try {
setLoading(true);
const data = await service.getMenuTree();
setMenuTree(data);
} catch (error) {
message.error('加载菜单数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => { useEffect(() => {
getMenuTree().then(menus => setMenuTree(menus)); loadMenuTree();
}, []); }, []);
const handleAdd = () => { const handleAdd = () => {
setEditingMenu(null); setEditingMenu(null);
form.resetFields(); form.resetFields();
const maxSort = Math.max(0, ...menus.map(menu => menu.sort));
form.setFieldsValue({ form.setFieldsValue({
type: MenuTypeEnum.MENU, type: MenuTypeEnum.MENU,
sort: maxSort + 10, sort: 1,
hidden: false hidden: false
}); });
setModalVisible(true); setModalVisible(true);
@ -67,45 +55,50 @@ const MenuPage: React.FC = () => {
setModalVisible(true); setModalVisible(true);
}; };
const handleTableChange = ( const handleDelete = async (id: number) => {
pagination: TablePaginationConfig, try {
filters: Record<string, FilterValue | null>, await service.deleteMenu(id);
sorter: SorterResult<MenuResponse> | SorterResult<MenuResponse>[] message.success('删除成功');
) => { loadMenuTree();
const { field, order } = Array.isArray(sorter) ? sorter[0] : sorter; await updateReduxMenus();
fetchMenus({ } catch (error) {
sortField: field as string, message.error('删除失败');
sortOrder: order }
});
}; };
const buildMenuTree = (menuList: MenuResponse[]): MenuResponse[] => { const handleSubmit = async () => {
const menuMap = new Map<number, MenuResponse>(); try {
const result: MenuResponse[] = []; const values = await form.validateFields();
if (editingMenu) {
menuList.forEach(menu => { await service.updateMenu(editingMenu.id, {
menuMap.set(menu.id, { ...menu }); ...values,
}); version: editingMenu.version
});
menuList.forEach(menu => { message.success('更新成功');
const node = menuMap.get(menu.id)!;
if (menu.parentId === 0 || !menuMap.has(menu.parentId)) {
result.push(node);
} else { } else {
const parent = menuMap.get(menu.parentId)!; await service.createMenu(values);
if (!parent.children) { message.success('创建成功');
parent.children = [];
}
parent.children.push(node);
} }
}); setModalVisible(false);
loadMenuTree();
return result; await updateReduxMenus();
} catch (error) {
message.error('操作失败');
}
}; };
const getTreeSelectData = () => { // 更新 Redux 中的菜单数据
const menuTree = buildMenuTree(menus); const updateReduxMenus = async () => {
return menuTree.map(menu => ({ try {
const menus = await service.getCurrentUserMenus();
dispatch(setMenus(menus));
} catch (error) {
console.error('更新菜单数据失败:', error);
}
};
const getTreeSelectData = (menuList: MenuResponse[]) => {
return menuList.map(menu => ({
title: menu.name, title: menu.name,
value: menu.id, value: menu.id,
children: menu.children?.map(child => ({ children: menu.children?.map(child => ({
@ -116,79 +109,34 @@ const MenuPage: React.FC = () => {
})); }));
}; };
const updateReduxMenus = async () => { const columns: ColumnsType<MenuResponse> = [
try {
const menus = await getCurrentUserMenus();
dispatch(setMenus(menus));
} catch (error) {
console.error('更新菜单数据失败:', error);
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
if (editingMenu) {
const success = await handleUpdate(editingMenu.id, {
...values,
version: editingMenu.version
});
if (success) {
setModalVisible(false);
fetchMenus();
await updateReduxMenus();
message.success('更新成功');
}
} else {
const success = await handleCreate(values);
if (success) {
setModalVisible(false);
fetchMenus();
await updateReduxMenus();
message.success('创建成功');
}
}
} catch (error) {
console.error('操作失败:', error);
}
};
const handleMenuDelete = async (id: number) => {
try {
const success = await handleDelete(id);
if (success) {
fetchMenus();
await updateReduxMenus();
message.success('删除成功');
}
} catch (error) {
console.error('删除失败:', error);
}
};
const getIcon = getIconComponent;
const columns = [
{ {
title: '菜单名称', title: '菜单名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', width: 200
width: 250,
fixed: 'left' as FixedType,
sorter: true
}, },
{ {
title: '图标', title: '图标',
dataIndex: 'icon', dataIndex: 'icon',
key: 'icon', width: 100,
width: 80, render: (icon: string) => {
render: (icon: string) => getIcon(icon) return getIconComponent(icon);
}
}, },
{ {
title: '类型', title: '路由地址',
dataIndex: 'path',
width: 200
},
{
title: '组件路径',
dataIndex: 'component',
width: 200
},
{
title: '菜单类型',
dataIndex: 'type', dataIndex: 'type',
key: 'type', width: 120,
width: 100,
render: (type: MenuTypeEnum) => { render: (type: MenuTypeEnum) => {
const typeMap = { const typeMap = {
[MenuTypeEnum.DIRECTORY]: '目录', [MenuTypeEnum.DIRECTORY]: '目录',
@ -199,63 +147,28 @@ const MenuPage: React.FC = () => {
} }
}, },
{ {
title: '路由地址', title: '显示排序',
dataIndex: 'path',
key: 'path',
width: 200,
ellipsis: true
},
{
title: '组件路径',
dataIndex: 'component',
key: 'component',
width: 200,
ellipsis: true
},
{
title: '权限标识',
dataIndex: 'permission',
key: 'permission',
width: 150,
ellipsis: true
},
{
title: '排序',
dataIndex: 'sort', dataIndex: 'sort',
key: 'sort', width: 100
width: 80,
sorter: true
},
{
title: '状态',
dataIndex: 'enabled',
key: 'enabled',
width: 80,
render: (enabled: boolean) => (
<Switch checked={enabled} disabled size="small"/>
)
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 160, width: 200,
fixed: 'right' as FixedType, render: (_, record) => (
render: (_: any, record: MenuResponse) => ( <Space>
<Space size={0}>
<Button <Button
type="link" type="link"
size="small" icon={<EditOutlined />}
icon={<EditOutlined/>}
onClick={() => handleEdit(record)} onClick={() => handleEdit(record)}
> >
</Button> </Button>
<Button <Button
type="link" type="link"
size="small"
danger danger
icon={<DeleteOutlined/>} icon={<DeleteOutlined />}
onClick={() => handleMenuDelete(record.id)} onClick={() => handleDelete(record.id)}
disabled={record.children?.length > 0} disabled={record.children?.length > 0}
> >
@ -266,18 +179,22 @@ const MenuPage: React.FC = () => {
]; ];
return ( return (
<div style={{padding: '24px'}}> <Card>
<div style={{marginBottom: 16}}> <div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined/>} onClick={handleAdd}> <Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
>
</Button> </Button>
</div> </div>
<Table <Table
loading={loading}
columns={columns} columns={columns}
dataSource={menuTree} dataSource={menuTree}
rowKey={(record) => String(record.id)} rowKey="id"
loading={loading}
pagination={false} pagination={false}
size="middle" size="middle"
bordered bordered
@ -290,21 +207,11 @@ const MenuPage: React.FC = () => {
onOk={handleSubmit} onOk={handleSubmit}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
width={600} width={600}
destroyOnClose
> >
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ type: MenuTypeEnum.MENU, sort: 0 }}
> >
<Form.Item
name="name"
label="菜单名称"
rules={[{ required: true, message: '请输入菜单名称' }]}
>
<Input placeholder="请输入菜单名称" />
</Form.Item>
<Form.Item <Form.Item
name="type" name="type"
label="菜单类型" label="菜单类型"
@ -317,6 +224,14 @@ const MenuPage: React.FC = () => {
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item
name="name"
label="菜单名称"
rules={[{ required: true, message: '请输入菜单名称' }]}
>
<Input placeholder="请输入菜单名称" />
</Form.Item>
<Form.Item <Form.Item
name="parentId" name="parentId"
label={ label={
@ -329,7 +244,7 @@ const MenuPage: React.FC = () => {
} }
> >
<TreeSelect <TreeSelect
treeData={getTreeSelectData()} treeData={getTreeSelectData(menuTree)}
placeholder="不选择则为顶级菜单" placeholder="不选择则为顶级菜单"
allowClear allowClear
treeDefaultExpandAll treeDefaultExpandAll
@ -339,62 +254,64 @@ const MenuPage: React.FC = () => {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="path" noStyle
label="路由地址" shouldUpdate={(prevValues, currentValues) => prevValues.type !== currentValues.type}
rules={[{ required: true, message: '请输入路由地址' }]}
> >
<Input placeholder="请输入路由地址" /> {({ getFieldValue }) => {
</Form.Item> const type = getFieldValue('type');
if (type === MenuTypeEnum.BUTTON) {
return null;
}
return (
<>
<Form.Item
name="icon"
label="菜单图标"
>
<Input
placeholder="请选择图标"
readOnly
onClick={() => setIconSelectVisible(true)}
suffix={getIconComponent(form.getFieldValue('icon'))}
/>
</Form.Item>
<Form.Item <Form.Item
name="component" name="path"
label="组件路径" label="路由地址"
> rules={[{ required: true, message: '请输入路由地址' }]}
<Input placeholder="请输入组件路径" /> >
</Form.Item> <Input placeholder="请输入路由地址" />
</Form.Item>
<Form.Item {type === MenuTypeEnum.MENU && (
name="permission" <Form.Item
label="权限标识" name="component"
> label="组件路径"
<Input placeholder="请输入权限标识" /> rules={[{ required: true, message: '请输入组件路径' }]}
</Form.Item> >
<Input placeholder="请输入组件路径" />
<Form.Item </Form.Item>
name="icon" )}
label="图标" </>
> );
<Input }}
placeholder="请选择图标"
readOnly
onClick={() => setIconSelectVisible(true)}
suffix={form.getFieldValue('icon') && getIcon(form.getFieldValue('icon'))}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="sort" name="sort"
label="显示排序" label="显示排序"
tooltip="值越大排序越靠后,默认为当前最大值+10" rules={[{ required: true, message: '请输入显示排序' }]}
rules={[{ required: true, message: '请输显示排序' }]}
> >
<InputNumber <Input type="number" placeholder="请输入显示排序" />
style={{ width: '100%' }}
min={0}
placeholder="请输入显示排序"
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="hidden" name="hidden"
label="隐藏菜单" label="是否隐藏"
valuePropName="checked" valuePropName="checked"
tooltip="设置为是则该菜单不会显示在导航栏中"
> >
<Switch <Switch checkedChildren="是" unCheckedChildren="否" />
checkedChildren="是"
unCheckedChildren="否"
/>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
@ -408,7 +325,7 @@ const MenuPage: React.FC = () => {
setIconSelectVisible(false); setIconSelectVisible(false);
}} }}
/> />
</div> </Card>
); );
}; };

View File

@ -1,15 +1,13 @@
import React, {useEffect, useState} from 'react'; import React, { useEffect, useState } from 'react';
import {Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect, Select, Tag} from 'antd'; import { Card, Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect, Select, Tag, Dropdown } from 'antd';
import {PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined} from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined, MoreOutlined } from '@ant-design/icons';
import type {UserResponse, Role} from './types'; import type { ColumnsType } from 'antd/es/table';
import type {DepartmentDTO} from '../Department/types'; import type { MenuProps } from 'antd';
import {resetPassword, assignRoles, getAllRoles} from './service'; import { useTableData } from '@/hooks/useTableData';
import {useTableData} from '@/hooks/useTableData'; import * as service from './service';
import type {FixedType, AlignType, SortOrder} from 'rc-table/lib/interface'; import type { UserResponse, UserRequest, UserQuery, Role } from './types';
import {Response} from "@/utils/request.ts"; import type { DepartmentResponse } from '../Department/types';
import {TablePaginationConfig} from "antd/es/table"; import { getDepartmentTree } from '../Department/service';
import {FilterValue, SorterResult} from "antd/es/table/interface";
import { getDepartmentTree } from '../Department/service'; // 导入部门树接口
interface TreeNode { interface TreeNode {
title: string; title: string;
@ -18,39 +16,68 @@ interface TreeNode {
} }
const UserPage: React.FC = () => { const UserPage: React.FC = () => {
const { const [form] = Form.useForm();
list: users, const [passwordForm] = Form.useForm();
pagination,
loading,
loadData: fetchUsers,
handleCreate,
handleUpdate,
handleDelete
} = useTableData({
service: {
baseUrl: '/api/v1/user'
},
defaultParams: {
sortField: 'createTime',
sortOrder: 'descend'
}
});
const [departments, setDepartments] = useState<DepartmentDTO[]>([]);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false); const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false);
const [editingUser, setEditingUser] = useState<UserResponse | null>(null); const [editingUser, setEditingUser] = useState<UserResponse | null>(null);
const [form] = Form.useForm(); const [departments, setDepartments] = useState<DepartmentResponse[]>([]);
const [passwordForm] = Form.useForm();
const [roleModalVisible, setRoleModalVisible] = useState(false); const [roleModalVisible, setRoleModalVisible] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserResponse | null>(null); const [selectedUser, setSelectedUser] = useState<UserResponse | null>(null);
const [selectedRoles, setSelectedRoles] = useState<number[]>([]); const [selectedRoles, setSelectedRoles] = useState<number[]>([]);
const [allRoles, setAllRoles] = useState<RoleResponse[]>([]); const [allRoles, setAllRoles] = useState<Role[]>([]);
const {
list,
loading,
pagination,
handleTableChange,
handleCreate,
handleUpdate,
handleDelete,
refresh
} = useTableData<UserResponse, UserQuery, UserRequest, UserRequest>({
service: {
list: service.getUsers,
create: service.createUser,
update: service.updateUser,
delete: service.deleteUser
},
defaultParams: {
sortField: 'createTime',
sortOrder: 'desc'
},
config: {
message: {
createSuccess: '创建用户成功',
updateSuccess: '更新用户成功',
deleteSuccess: '删除用户成功'
}
}
});
useEffect(() => { useEffect(() => {
getAllRoles().then(roles => setAllRoles(roles)); service.getAllRoles().then(roles => setAllRoles(roles));
loadDepartmentTree();
}, []); }, []);
const loadDepartmentTree = async () => {
try {
const data = await getDepartmentTree();
setDepartments(data);
} catch (error) {
message.error('加载部门数据失败');
}
};
const getTreeData = (departments: DepartmentResponse[]): TreeNode[] => {
return departments.map(dept => ({
title: dept.name,
value: dept.id,
children: dept.children ? getTreeData(dept.children) : undefined
}));
};
const handleAdd = () => { const handleAdd = () => {
setEditingUser(null); setEditingUser(null);
form.resetFields(); form.resetFields();
@ -63,7 +90,8 @@ const UserPage: React.FC = () => {
const handleEdit = (record: UserResponse) => { const handleEdit = (record: UserResponse) => {
setEditingUser(record); setEditingUser(record);
form.setFieldsValue({ form.setFieldsValue({
...record ...record,
password: undefined // 不显示密码
}); });
setModalVisible(true); setModalVisible(true);
}; };
@ -78,64 +106,31 @@ const UserPage: React.FC = () => {
try { try {
const values = await passwordForm.validateFields(); const values = await passwordForm.validateFields();
if (editingUser) { if (editingUser) {
await resetPassword(editingUser.id, values.password); await service.resetPassword(editingUser.id, values.password);
message.success('密码重置成功'); message.success('密码重置成功');
setResetPasswordModalVisible(false); setResetPasswordModalVisible(false);
refresh();
} }
} catch (error: any) { } catch (error: any) {
message.error(error.message || '密码重置失败'); message.error(error.message || '密码重置失败');
} }
}; };
const handleSubmit = async (values: any) => { const handleSubmit = async () => {
try { try {
const values = await form.validateFields();
if (editingUser) { if (editingUser) {
const success = await handleUpdate(editingUser.id, { await handleUpdate(editingUser.id, values);
...values,
enabled: values.enabled ?? true
});
if (success) {
message.success('更新成功');
setModalVisible(false);
fetchUsers();
}
} else { } else {
const success = await handleCreate({ await handleCreate(values);
...values,
enabled: values.enabled ?? true
});
if (success) {
message.success('创建成功');
setModalVisible(false);
fetchUsers();
}
} }
setModalVisible(false);
refresh();
} catch (error: any) { } catch (error: any) {
message.error(error.message || '操作失败'); message.error(error.message || '操作失败');
} }
}; };
const loadDepartmentTree = async () => {
try {
const data = await getDepartmentTree();
setDepartments(data);
} catch (error) {
message.error('加载部门数据失败');
}
};
useEffect(() => {
loadDepartmentTree();
}, []);
const getTreeData = (departments: DepartmentDTO[]): TreeNode[] => {
return departments.map(dept => ({
title: dept.name,
value: dept.id,
children: dept.children ? getTreeData(dept.children) : undefined
}));
};
const handleAssignRoles = (record: UserResponse) => { const handleAssignRoles = (record: UserResponse) => {
setSelectedUser(record); setSelectedUser(record);
setSelectedRoles(record.roles?.map(role => role.id) || []); setSelectedRoles(record.roles?.map(role => role.id) || []);
@ -145,56 +140,48 @@ const UserPage: React.FC = () => {
const handleAssignRoleSubmit = async () => { const handleAssignRoleSubmit = async () => {
if (selectedUser) { if (selectedUser) {
try { try {
await assignRoles(selectedUser.id, selectedRoles); await service.assignRoles(selectedUser.id, selectedRoles);
message.success("角色分配成功") message.success('角色分配成功');
setRoleModalVisible(false); setRoleModalVisible(false);
fetchUsers(); refresh();
} catch (error) { } catch (error) {
message.error("角色分配失败") message.error('角色分配失败');
console.log(error);
} }
} }
}; };
const columns = [ const columns: ColumnsType<UserResponse> = [
{ {
title: 'ID', title: 'ID',
dataIndex: 'id', dataIndex: 'id',
key: 'id',
width: 60, width: 60,
fixed: 'left' as FixedType, fixed: 'left',
sorter: true sorter: true
}, },
{ {
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'username',
key: 'username',
width: 100, width: 100,
sorter: true sorter: true
}, },
{ {
title: '昵称', title: '昵称',
dataIndex: 'nickname', dataIndex: 'nickname',
key: 'nickname',
width: 100, width: 100,
}, },
{ {
title: '邮箱', title: '邮箱',
dataIndex: 'email', dataIndex: 'email',
key: 'email',
width: 200, width: 200,
}, },
{ {
title: '部门', title: '部门',
dataIndex: 'departmentName', dataIndex: 'departmentName',
key: 'departmentName',
width: 150, width: 150,
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'enabled', dataIndex: 'enabled',
key: 'enabled',
width: 100, width: 100,
render: (enabled: boolean) => ( render: (enabled: boolean) => (
<Tag color={enabled ? 'success' : 'error'}> <Tag color={enabled ? 'success' : 'error'}>
@ -205,13 +192,11 @@ const UserPage: React.FC = () => {
{ {
title: '手机号', title: '手机号',
dataIndex: 'phone', dataIndex: 'phone',
key: 'phone',
width: 120, width: 120,
}, },
{ {
title: '角色', title: '角色',
dataIndex: 'roles', dataIndex: 'roles',
key: 'roles',
width: 120, width: 120,
ellipsis: true, ellipsis: true,
render: (roles: Role[]) => roles?.map(role => role.name).join(', ') || '-' render: (roles: Role[]) => roles?.map(role => role.name).join(', ') || '-'
@ -219,104 +204,98 @@ const UserPage: React.FC = () => {
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime',
width: 150, width: 150,
sorter: true, sorter: true,
defaultSortOrder: 'descend' as SortOrder
}, },
{ {
title: '更新时间', title: '更新时间',
dataIndex: 'updateTime', dataIndex: 'updateTime',
key: 'updateTime',
width: 150, width: 150,
sorter: true sorter: true
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 320, width: 180,
fixed: 'right' as FixedType, fixed: 'right',
render: (_: any, record: UserResponse) => ( render: (_, record) => {
<Space size={0}> const items: MenuProps['items'] = [
<Button {
type="link" key: 'resetPassword',
size="small" icon: <KeyOutlined />,
icon={<EditOutlined/>} label: '重置密码',
onClick={() => handleEdit(record)} onClick: () => handleResetPassword(record)
> },
{
</Button> key: 'assignRoles',
<Button icon: <TeamOutlined />,
type="link" label: '分配角色',
size="small" onClick: () => handleAssignRoles(record),
icon={<KeyOutlined/>} disabled: record.username === 'admin'
onClick={() => handleResetPassword(record)} }
> ];
</Button> // 如果不是 admin 用户,添加删除选项
<Button if (record.username !== 'admin') {
type="link" items.push({
size="small" key: 'delete',
danger icon: <DeleteOutlined />,
icon={<DeleteOutlined/>} label: '删除',
onClick={() => handleDelete(record.id)} danger: true,
disabled={record.username === 'admin'} onClick: () => handleDelete(record.id)
> });
}
</Button>
<Button return (
type="link" <Space>
size="small" <Button
icon={<TeamOutlined/>} type="link"
onClick={() => handleAssignRoles(record)} icon={<EditOutlined />}
disabled={record.username === 'admin'} onClick={() => handleEdit(record)}
> >
</Button> </Button>
</Space> <Dropdown
), menu={{ items }}
placement="bottomRight"
trigger={['click']}
>
<Button
type="text"
icon={<MoreOutlined />}
style={{ padding: '4px 8px' }}
/>
</Dropdown>
</Space>
);
}
}, },
]; ];
const handleTableChange = (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<UserResponse> | SorterResult<UserResponse>[]
) => {
const {field, order} = Array.isArray(sorter) ? sorter[0] : sorter;
fetchUsers({
sortField: field as string,
sortOrder: order
});
};
return ( return (
<div style={{padding: '24px'}}> <Card>
<div style={{marginBottom: 16}}> <div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined/>} onClick={handleAdd}> <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button> </Button>
</div> </div>
<Table <Table
loading={loading}
columns={columns} columns={columns}
dataSource={users} dataSource={list}
rowKey="id" rowKey="id"
scroll={{x: 1500}} loading={loading}
scroll={{ x: 1500 }}
pagination={pagination} pagination={pagination}
onChange={handleTableChange} onChange={handleTableChange}
size="middle"
bordered
/> />
<Modal <Modal
title={editingUser ? '编辑用户' : '新增用户'} title={editingUser ? '编辑用户' : '新增用户'}
open={modalVisible} open={modalVisible}
onOk={() => form.validateFields().then(handleSubmit)} onOk={handleSubmit}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
width={600} width={600}
destroyOnClose
> >
<Form <Form
form={form} form={form}
@ -325,9 +304,9 @@ const UserPage: React.FC = () => {
<Form.Item <Form.Item
name="username" name="username"
label="用户名" label="用户名"
rules={[{required: true, message: '请输入用户名'}]} rules={[{ required: true, message: '请输入用户名' }]}
> >
<Input placeholder="请输入用户名" disabled={!!editingUser}/> <Input placeholder="请输入用户名" disabled={!!editingUser} />
</Form.Item> </Form.Item>
{!editingUser && ( {!editingUser && (
@ -335,11 +314,11 @@ const UserPage: React.FC = () => {
name="password" name="password"
label="密码" label="密码"
rules={[ rules={[
{required: true, message: '请输入密码'}, { required: true, message: '请输入密码' },
{min: 6, message: '密码长度不能小于6位'} { min: 6, message: '密码长度不能小于6位' }
]} ]}
> >
<Input.Password placeholder="请输入密码"/> <Input.Password placeholder="请输入密码" />
</Form.Item> </Form.Item>
)} )}
@ -347,22 +326,28 @@ const UserPage: React.FC = () => {
name="nickname" name="nickname"
label="昵称" label="昵称"
> >
<Input placeholder="请输入昵称"/> <Input placeholder="请输入昵称" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="email" name="email"
label="邮箱" label="邮箱"
rules={[{type: 'email', message: '请输入正确的邮箱格式'}]} rules={[{ type: 'email', message: '请输入正确的邮箱格式' }]}
> >
<Input placeholder="请输入邮箱"/> <Input placeholder="请输入邮箱" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="phone" name="phone"
label="手机号" label="手机号"
> >
<Input placeholder="请输入手机号"/> <Input placeholder="请输入手机号" />
</Form.Item> </Form.Item>
<Form.Item name="departmentId" label="所属部门">
<Form.Item
name="departmentId"
label="所属部门"
>
<TreeSelect <TreeSelect
treeData={getTreeData(departments)} treeData={getTreeData(departments)}
placeholder="请选择所属部门" placeholder="请选择所属部门"
@ -372,41 +357,23 @@ const UserPage: React.FC = () => {
treeNodeFilterProp="title" treeNodeFilterProp="title"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="enabled" name="enabled"
label="状态" label="状态"
valuePropName="checked" valuePropName="checked"
initialValue={true} initialValue={true}
> >
<Switch/> <Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
{/* RoleModal
{selectedUser && (
<RoleModal
userId={selectedUser.id}
visible={roleModalVisible}
onCancel={() => {
setRoleModalVisible(false);
setSelectedUser(null);
}}
onSuccess={() => {
setRoleModalVisible(false);
setSelectedUser(null);
fetchUsers();
}}
/>
)}
*/}
<Modal <Modal
title="重置密码" title="重置密码"
open={resetPasswordModalVisible} open={resetPasswordModalVisible}
onOk={handleResetPasswordSubmit} onOk={handleResetPasswordSubmit}
onCancel={() => setResetPasswordModalVisible(false)} onCancel={() => setResetPasswordModalVisible(false)}
destroyOnClose
> >
<Form <Form
form={passwordForm} form={passwordForm}
@ -416,19 +383,20 @@ const UserPage: React.FC = () => {
name="password" name="password"
label="新密码" label="新密码"
rules={[ rules={[
{required: true, message: '请输入新密码'}, { required: true, message: '请输入新密码' },
{min: 6, message: '密码长度不能小于6位'} { min: 6, message: '密码长度不能小于6位' }
]} ]}
> >
<Input.Password placeholder="请输入新密码"/> <Input.Password placeholder="请输入新密码" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="confirmPassword" name="confirmPassword"
label="确认密码" label="确认密码"
dependencies={['password']} dependencies={['password']}
rules={[ rules={[
{required: true, message: '请确认新密码'}, { required: true, message: '请确认新密码' },
({getFieldValue}) => ({ ({ getFieldValue }) => ({
validator(_, value) { validator(_, value) {
if (!value || getFieldValue('password') === value) { if (!value || getFieldValue('password') === value) {
return Promise.resolve(); return Promise.resolve();
@ -438,7 +406,7 @@ const UserPage: React.FC = () => {
}), }),
]} ]}
> >
<Input.Password placeholder="请确认新密码"/> <Input.Password placeholder="请确认新密码" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
@ -449,7 +417,6 @@ const UserPage: React.FC = () => {
onOk={handleAssignRoleSubmit} onOk={handleAssignRoleSubmit}
onCancel={() => setRoleModalVisible(false)} onCancel={() => setRoleModalVisible(false)}
width={600} width={600}
destroyOnClose
> >
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label="选择角色"> <Form.Item label="选择角色">
@ -458,7 +425,7 @@ const UserPage: React.FC = () => {
placeholder="请选择角色" placeholder="请选择角色"
value={selectedRoles} value={selectedRoles}
onChange={setSelectedRoles} onChange={setSelectedRoles}
style={{width: '100%'}} style={{ width: '100%' }}
> >
{allRoles.map(role => ( {allRoles.map(role => (
<Select.Option key={role.id} value={role.id}> <Select.Option key={role.id} value={role.id}>
@ -469,7 +436,7 @@ const UserPage: React.FC = () => {
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
</div> </Card>
); );
}; };