This commit is contained in:
asp_ly 2024-12-27 21:51:01 +08:00
parent df175bbf65
commit 58e4cf2743
4 changed files with 392 additions and 257 deletions

View File

@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLTableProps<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLTableSectionProps
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLTableSectionProps
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLTableSectionProps
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLTableRowProps
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -14,21 +14,52 @@ import type {ProjectGroup, ProjectGroupQueryParams} from './types';
import {ProjectGroupTypeEnum} from './types';
import {getProjectTypeInfo} from './utils';
import ProjectGroupModal from './components/ProjectGroupModal';
import {ProTable} from '@ant-design/pro-components';
import type {ProColumns, ActionType} from '@ant-design/pro-components';
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from "@/components/ui/table";
interface Column {
accessorKey?: keyof ProjectGroup;
id?: string;
header: string;
size: number;
cell?: (props: { row: { original: ProjectGroup } }) => React.ReactNode;
}
const ProjectGroupList: React.FC = () => {
const [modalVisible, setModalVisible] = useState(false);
const [currentProject, setCurrentProject] = useState<ProjectGroup>();
const actionRef = React.useRef<ActionType>();
const [list, setList] = useState<ProjectGroup[]>([]);
const [loading, setLoading] = useState(false);
const [searchForm] = Form.useForm();
const {message: messageApi} = App.useApp();
const loadData = async (params?: ProjectGroupQueryParams) => {
setLoading(true);
try {
const data = await getProjectGroupPage(params);
setList(data.content);
} catch (error) {
messageApi.error('加载数据失败');
} finally {
setLoading(false);
}
};
React.useEffect(() => {
loadData();
}, []);
const handleDelete = async (id: number) => {
try {
await deleteProjectGroup(id);
messageApi.success('删除成功');
actionRef.current?.reload();
loadData(searchForm.getFieldsValue());
} catch (error) {
messageApi.error('删除失败');
}
@ -52,45 +83,35 @@ const ProjectGroupList: React.FC = () => {
const handleSuccess = () => {
setModalVisible(false);
setCurrentProject(undefined);
actionRef.current?.reload();
loadData(searchForm.getFieldsValue());
};
const handleSearch = async (values: any) => {
actionRef.current?.reload();
loadData(values);
};
const columns: ProColumns<ProjectGroup>[] = [
const columns: Column[] = [
{
title: '项目组编码',
dataIndex: 'projectGroupCode',
width: 120,
copyable: true,
ellipsis: true,
fixed: 'left',
filters: true,
filterSearch: true,
onFilter: (value: string, record) => record.projectGroupCode.toLowerCase().includes(value.toLowerCase()),
accessorKey: 'projectGroupCode',
header: '项目组编码',
size: 120,
},
{
title: '项目组名称',
dataIndex: 'projectGroupName',
width: 150,
ellipsis: true,
filters: true,
filterSearch: true,
onFilter: (value: string, record) => record.projectGroupName.toLowerCase().includes(value.toLowerCase()),
accessorKey: 'projectGroupName',
header: '项目组名称',
size: 150,
},
{
title: '项目组描述',
dataIndex: 'projectGroupDesc',
ellipsis: true,
accessorKey: 'projectGroupDesc',
header: '项目组描述',
size: 200,
},
{
title: '项目组类型',
dataIndex: 'type',
width: 150,
render: (type) => {
const typeInfo = getProjectTypeInfo(type as ProjectGroupTypeEnum);
accessorKey: 'type',
header: '项目组类型',
size: 150,
cell: ({ row }) => {
const typeInfo = getProjectTypeInfo(row.original.type);
return (
<Tag color={typeInfo.color}>
<Space>
@ -100,84 +121,76 @@ const ProjectGroupList: React.FC = () => {
</Tag>
);
},
filters: [
{text: '产品型', value: ProjectGroupTypeEnum.PRODUCT},
{text: '项目型', value: ProjectGroupTypeEnum.PROJECT},
],
filterMode: 'menu',
filtered: false,
},
{
title: '状态',
dataIndex: 'enabled',
width: 100,
valueEnum: {
true: {text: '启用', status: 'Success'},
false: {text: '禁用', status: 'Default'},
},
accessorKey: 'enabled',
header: '状态',
size: 100,
cell: ({ row }) => (
<Tag color={row.original.enabled ? 'success' : 'default'}>
{row.original.enabled ? '启用' : '禁用'}
</Tag>
),
},
{
title: '环境数量',
dataIndex: 'environments',
width: 100,
render: (_, record) => (
accessorKey: 'totalEnvironments',
header: '环境数量',
size: 100,
cell: ({ row }) => (
<Space>
<EnvironmentOutlined/>
{record?.totalEnvironments || 0}
{row.original.totalEnvironments || 0}
</Space>
),
},
{
title: '项目数量',
dataIndex: 'applications',
width: 100,
render: (_, record) => (
accessorKey: 'totalApplications',
header: '项目数量',
size: 100,
cell: ({ row }) => (
<Space>
<TeamOutlined/>
{record?.totalApplications || 0}
{row.original.totalApplications || 0}
</Space>
),
},
{
title: '排序',
dataIndex: 'sort',
width: 80,
sorter: true,
accessorKey: 'sort',
header: '排序',
size: 80,
},
{
title: '操作',
width: 180,
key: 'action',
valueType: 'option',
fixed: 'right',
render: (_, record) => [
<Button
key="edit"
type="link"
onClick={() => handleEdit(record)}
>
<Space>
<EditOutlined/>
</Space>
</Button>,
<Popconfirm
key="delete"
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(record.id)}
>
id: 'actions',
header: '操作',
size: 180,
cell: ({ row }) => (
<Space>
<Button
type="link"
danger
onClick={() => handleEdit(row.original)}
>
<Space>
<DeleteOutlined/>
<EditOutlined/>
</Space>
</Button>
</Popconfirm>
],
<Popconfirm
title="确定要删除该项目组吗?"
description="删除后将无法恢复,请谨慎操作"
onConfirm={() => handleDelete(row.original.id)}
>
<Button
type="link"
danger
>
<Space>
<DeleteOutlined/>
</Space>
</Button>
</Popconfirm>
</Space>
),
},
];
@ -233,75 +246,57 @@ const ProjectGroupList: React.FC = () => {
</Form.Item>
</Form>
</Card>
<ProTable<ProjectGroup>
columns={columns}
actionRef={actionRef}
scroll={{x: 'max-content'}}
cardBordered
rowKey="id"
search={false}
options={{
setting: false,
density: false,
fullScreen: false,
reload: false,
}}
toolbar={{
actions: [
<Button
key="add"
type="primary"
onClick={handleAdd}
icon={<PlusOutlined/>}
>
</Button>
],
}}
form={{
syncToUrl: true,
ignoreRules: false,
}}
pagination={{
pageSize: 10,
showQuickJumper: true,
}}
request={async (params) => {
try {
const formValues = searchForm.getFieldsValue();
const queryParams: ProjectGroupQueryParams = {
pageSize: params.pageSize,
pageNum: params.current,
projectGroupCode: formValues.projectGroupCode?.trim(),
projectGroupName: formValues.projectGroupName?.trim(),
type: formValues.type,
enabled: formValues.enabled,
};
const data = await getProjectGroupPage(queryParams);
return {
data: data.content || [],
success: true,
total: data.totalElements || 0,
};
} catch (error) {
messageApi.error('获取项目组列表失败');
return {
data: [],
success: false,
total: 0,
};
}
}}
/>
{modalVisible && (
<ProjectGroupModal
open={modalVisible}
onCancel={handleModalClose}
onSuccess={handleSuccess}
initialValues={currentProject}
/>
)}
<Card>
<div className="flex justify-between items-center mb-4">
<Button
type="primary"
icon={<PlusOutlined/>}
onClick={handleAdd}
>
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={column.accessorKey || column.id}
style={{ width: column.size }}
>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{list.map((row) => (
<TableRow key={row.id}>
{columns.map((column) => (
<TableCell key={column.accessorKey || column.id}>
{column.cell
? column.cell({ row: { original: row } })
: column.accessorKey
? String(row[column.accessorKey])
: null}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
</Card>
<ProjectGroupModal
open={modalVisible}
onCancel={handleModalClose}
onSuccess={handleSuccess}
initialValues={currentProject}
/>
</PageContainer>
);
};

View File

@ -1,13 +1,28 @@
import React, { useEffect, useState } from 'react';
import { Card, Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect, Select, Tag, Dropdown } from 'antd';
import { Button, Modal, Form, Input, Space, message, Switch, TreeSelect, Select, Tag, Dropdown } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined, MoreOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { MenuProps } from 'antd';
import { useTableData } from '@/hooks/useTableData';
import * as service from './service';
import type { UserResponse, UserRequest, UserQuery, Role } from './types';
import type { DepartmentResponse } from '../Department/types';
import { getDepartmentTree } from '../Department/service';
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from "@/components/ui/table";
interface Column {
accessorKey?: keyof UserResponse;
id?: string;
header: string;
size: number;
cell?: (props: { row: { original: UserResponse } }) => React.ReactNode;
}
interface TreeNode {
title: string;
@ -150,75 +165,68 @@ const UserPage: React.FC = () => {
}
};
const columns: ColumnsType<UserResponse> = [
const columns: Column[] = [
{
title: 'ID',
dataIndex: 'id',
width: 60,
fixed: 'left',
sorter: true
accessorKey: 'id',
header: 'ID',
size: 60,
},
{
title: '用户名',
dataIndex: 'username',
width: 100,
sorter: true
accessorKey: 'username',
header: '用户名',
size: 100,
},
{
title: '昵称',
dataIndex: 'nickname',
width: 100,
accessorKey: 'nickname',
header: '昵称',
size: 100,
},
{
title: '邮箱',
dataIndex: 'email',
width: 200,
accessorKey: 'email',
header: '邮箱',
size: 200,
},
{
title: '部门',
dataIndex: 'departmentName',
width: 150,
accessorKey: 'departmentName',
header: '部门',
size: 150,
},
{
title: '状态',
dataIndex: 'enabled',
width: 100,
render: (enabled: boolean) => (
<Tag color={enabled ? 'success' : 'error'}>
{enabled ? '启用' : '禁用'}
accessorKey: 'enabled',
header: '状态',
size: 100,
cell: ({ row }) => (
<Tag color={row.original.enabled ? 'success' : 'error'}>
{row.original.enabled ? '启用' : '禁用'}
</Tag>
),
},
{
title: '手机号',
dataIndex: 'phone',
width: 120,
accessorKey: 'phone',
header: '手机号',
size: 120,
},
{
title: '角色',
dataIndex: 'roles',
width: 120,
ellipsis: true,
render: (roles: Role[]) => roles?.map(role => role.name).join(', ') || '-'
header: '角色',
size: 120,
cell: ({ row }) => row.original.roles?.map(role => role.name).join(', ') || '-',
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
sorter: true,
accessorKey: 'createTime',
header: '创建时间',
size: 150,
},
{
title: '更新时间',
dataIndex: 'updateTime',
width: 150,
sorter: true
accessorKey: 'updateTime',
header: '更新时间',
size: 150,
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
render: (_, record) => {
id: 'actions',
header: '操作',
size: 180,
cell: ({ row }) => {
const record = row.original;
const items: MenuProps['items'] = [
{
key: 'resetPassword',
@ -235,7 +243,6 @@ const UserPage: React.FC = () => {
}
];
// 如果不是 admin 用户,添加删除选项
if (record.username !== 'admin') {
items.push({
key: 'delete',
@ -255,40 +262,58 @@ const UserPage: React.FC = () => {
>
</Button>
<Dropdown
menu={{ items }}
placement="bottomRight"
trigger={['click']}
>
<Button
type="text"
icon={<MoreOutlined />}
style={{ padding: '4px 8px' }}
/>
<Dropdown menu={{ items }} trigger={['click']}>
<Button type="link" icon={<MoreOutlined />} />
</Dropdown>
</Space>
);
}
},
},
];
return (
<Card>
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAdd}
>
</Button>
</div>
<Table
columns={columns}
dataSource={list}
rowKey="id"
loading={loading}
scroll={{ x: 1500 }}
pagination={pagination}
onChange={handleTableChange}
/>
<div className="rounded-md border bg-white">
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={column.accessorKey || column.id}
style={{ width: column.size }}
>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{list.map((row) => (
<TableRow key={row.id}>
{columns.map((column) => (
<TableCell key={column.accessorKey || column.id}>
{column.cell
? column.cell({ row: { original: row } })
: column.accessorKey
? String(row[column.accessorKey])
: null}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
<Modal
title={editingUser ? '编辑用户' : '新增用户'}
@ -436,7 +461,7 @@ const UserPage: React.FC = () => {
</Form.Item>
</Form>
</Modal>
</Card>
</div>
);
};

View File

@ -1,46 +1,47 @@
import type { BaseResponse } from '@/types/base/response';
import {BaseQuery} from "@/types/base";
import type { BaseResponse } from '@/types/base';
// 用户查询参数
export interface UserQuery extends BaseQuery {
username?: string;
nickname?: string;
email?: string;
enabled?: boolean;
export interface UserQuery {
username?: string;
nickname?: string;
email?: string;
enabled?: boolean;
pageNum?: number;
pageSize?: number;
sortField?: string;
sortOrder?: string;
}
// 用户请求参数
export interface UserRequest {
username: string;
nickname?: string;
email?: string;
phone?: string;
password?: string;
enabled?: boolean;
departmentId?: number;
username: string;
nickname?: string;
email?: string;
phone?: string;
password?: string;
enabled?: boolean;
departmentId?: number;
}
// 角色类型定义
export interface Role {
id: number;
code: string;
name: string;
description?: string;
type: number;
sort: number;
enabled: boolean;
id: number;
code: string;
name: string;
description?: string;
type: number;
sort: number;
enabled: boolean;
}
// 用户响应类型
export interface UserResponse extends BaseResponse {
username: string;
nickname?: string;
email?: string;
phone?: string;
enabled: boolean;
roles: Role[];
departmentId?: number;
departmentName?: string;
createTime: string;
updateTime: string;
username: string;
nickname?: string;
email?: string;
phone?: string;
enabled: boolean;
roles: Role[];
departmentId?: number;
departmentName?: string;
}