feat(前端):用户列表

master
shaot 2025-08-12 16:38:00 +08:00
parent b3304804a1
commit 6cf65aeb83
6 changed files with 343 additions and 92 deletions

View File

@ -28,6 +28,11 @@ export const PRIORITY_MAP = {
3: '三级', 3: '三级',
} as const; } as const;
export const STATUS_MAP = {
1: '启用',
2: '禁用',
} as const;
export const USER_TYPE_OPTIONS = [ export const USER_TYPE_OPTIONS = [
{ value: 1, label: '域用户' }, { value: 1, label: '域用户' },
{ value: 0, label: '本地用户' }, { value: 0, label: '本地用户' },

View File

@ -1,5 +1,5 @@
.user_content { .user_content {
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #f7f8fa; background-color: #f7f8fa;
@ -29,8 +29,10 @@
background-color: #fff; background-color: #fff;
padding: 8px; padding: 8px;
.teble_box { .teble_box {
display: flex;
flex-direction: column;
width: 100%; width: 100%;
height: calc(100% - 40px); height: calc(100% - 50px);
overflow: hidden; overflow: hidden;
} }
} }
@ -41,5 +43,60 @@
position: absolute; position: absolute;
left: 5px; left: 5px;
} }
.images-list-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
// 表格适应样式
.ant-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
}
}
}
}
}
}
} }
} }

View File

@ -8,7 +8,8 @@ import {
DeleteOutlined, DeleteOutlined,
DownOutlined, DownOutlined,
PlusOutlined, PlusOutlined,
TeamOutlined, RedoOutlined,
GoldOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
Button, Button,
@ -185,7 +186,7 @@ const UserListPage: React.FC = () => {
title: '序号', title: '序号',
dataIndex: 'order', dataIndex: 'order',
key: 'order', key: 'order',
width: 80, width: 60,
align: 'center', align: 'center',
render: (_: any, record: any, index: number) => <span>{index + 1}</span>, render: (_: any, record: any, index: number) => <span>{index + 1}</span>,
}, },
@ -193,41 +194,53 @@ const UserListPage: React.FC = () => {
title: '终端名称', title: '终端名称',
dataIndex: 'device_name', dataIndex: 'device_name',
key: 'device_name', key: 'device_name',
width: 220, width: 250,
ellipsis: true,
align: 'center', align: 'center',
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
}, },
}, },
{ {
title: '序列号', title: '序列号',
dataIndex: 'device_id', dataIndex: 'device_id',
key: 'device_id', key: 'device_id',
width: 220, width: 250,
align: 'center', align: 'center',
ellipsis: true,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
}, },
}, },
{ {
title: '终端分组', title: '终端分组',
dataIndex: 'device_group_name', dataIndex: 'device_group_name',
key: 'device_group_name', key: 'device_group_name',
width: 220, width: 200,
align: 'center', align: 'center',
ellipsis: true,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
}, },
}, },
{ {
title: '终端类型', title: '类型',
dataIndex: 'device_type', dataIndex: 'device_type',
key: 'device_type', key: 'device_type',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text: number) => { render: (text: number) => {
const key = text as keyof typeof DEVICE_TYPE_MAP; const key = text as keyof typeof DEVICE_TYPE_MAP;
return <Tooltip>{DEVICE_TYPE_MAP[key] || '--'}</Tooltip>; return (
<Tooltip title={DEVICE_TYPE_MAP[key] || '--'}>
{DEVICE_TYPE_MAP[key] || '--'}
</Tooltip>
);
}, },
}, },
{ {
@ -236,18 +249,28 @@ const UserListPage: React.FC = () => {
key: 'model', key: 'model',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
}, },
}, },
{ {
title: 'IP地址', title: 'IP地址',
dataIndex: 'ip_addr', dataIndex: 'ip_addr',
key: 'ip_addr', key: 'ip_addr',
width: 150, width: 200,
ellipsis: true,
align: 'center', align: 'center',
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
}, },
}, },
{ {
@ -256,8 +279,13 @@ const UserListPage: React.FC = () => {
key: 'mac_addr', key: 'mac_addr',
ellipsis: true, ellipsis: true,
align: 'center', align: 'center',
width: 200,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
}, },
}, },
{ {
@ -266,8 +294,13 @@ const UserListPage: React.FC = () => {
key: 'description', key: 'description',
ellipsis: true, ellipsis: true,
align: 'center', align: 'center',
width: 200,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
}, },
}, },
{ {
@ -403,6 +436,14 @@ const UserListPage: React.FC = () => {
<div className={styles.left_content}> <div className={styles.left_content}>
<div className={styles.search}> <div className={styles.search}>
<div style={{ paddingBottom: '5px' }}> <div style={{ paddingBottom: '5px' }}>
<Button
type="text"
style={{ marginRight: '8px', fontSize: '16px' }}
icon={<RedoOutlined />}
onClick={() => getGroupList()}
title="刷新"
loading={spinning}
/>
<Button <Button
type="text" type="text"
style={{ marginRight: '8px', fontSize: '16px' }} style={{ marginRight: '8px', fontSize: '16px' }}
@ -422,6 +463,7 @@ const UserListPage: React.FC = () => {
type="text" type="text"
style={{ fontSize: '16px' }} style={{ fontSize: '16px' }}
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
disabled={!selectedOrg}
/> />
</Popconfirm> </Popconfirm>
</div> </div>
@ -441,8 +483,10 @@ const UserListPage: React.FC = () => {
childrenField="children" childrenField="children"
defaultExpandAll={true} defaultExpandAll={true}
onSelect={onOrgSelect} onSelect={onOrgSelect}
showIcon={true}
selectedKeys={selectedOrg ? [selectedOrg] : []} selectedKeys={selectedOrg ? [selectedOrg] : []}
icon={<TeamOutlined style={{ fontSize: '15px' }} />} // switcherIcon={<TeamOutlined style={{ fontSize: '15px' }}/>}
icon={<GoldOutlined style={{ fontSize: '15px' }} />}
/> />
</Spin> </Spin>
</div> </div>
@ -473,9 +517,9 @@ const UserListPage: React.FC = () => {
</Button> </Button>
</Popconfirm> */} </Popconfirm> */}
<Button style={{ marginRight: '8px' }} onClick={getDataSource}> {/* <Button style={{ marginRight: '8px' }} onClick={getDataSource}>
</Button> </Button> */}
</div> </div>
<div> <div>
<div> <div>
@ -489,34 +533,52 @@ const UserListPage: React.FC = () => {
setCurrentPage(1); // Reset to first page when searching setCurrentPage(1); // Reset to first page when searching
}} }}
/> />
<Button
style={{ marginRight: '8px', marginLeft: '8px' }}
onClick={getDataSource}
icon={<RedoOutlined />}
title={'刷新'}
loading={loading}
/>
</div> </div>
</div> </div>
</div> </div>
<div className={styles.teble_box}> <div className={styles.teble_box}>
<Table <div className="images-list-table">
columns={columns} <Table
dataSource={dataSource} columns={columns}
loading={loading} dataSource={dataSource}
rowKey="id" loading={loading}
// rowSelection={{ rowKey="id"
// selectedRowKeys, // rowSelection={{
// onChange: onSelectChange, // selectedRowKeys,
// }} // onChange: onSelectChange,
pagination={{ // }}
current: currentPage, pagination={{
pageSize: pageSize, current: currentPage,
total: total, pageSize: pageSize,
onChange: handlePageChange, total: total,
onShowSizeChange: handlePageSizeChange, onChange: handlePageChange,
showSizeChanger: true, onShowSizeChange: handlePageSizeChange,
showQuickJumper: true, showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'], showQuickJumper: true,
showTotal: (total) => { pageSizeOptions: ['10', '20', '50', '100'],
return `${total}条数据`; showTotal: (total) => {
}, return `${total}条数据`;
}} },
scroll={{ x: 'max-content', y: 55 * 12 }} }}
/> // scroll={{ x: 'max-content', y: 55 * 12 }}
scroll={{
x: 'max-content',
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
.user_content { .user_content {
display: flex; display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #f7f8fa; background-color: #f7f8fa;
@ -29,8 +29,10 @@
background-color: #fff; background-color: #fff;
padding: 8px; padding: 8px;
.teble_box { .teble_box {
display: flex;
flex-direction: column;
width: 100%; width: 100%;
height: calc(100% - 40px); height: calc(100% - 50px);
overflow: hidden; overflow: hidden;
} }
} }
@ -41,5 +43,60 @@
position: absolute; position: absolute;
left: 5px; left: 5px;
} }
.images-list-table {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
// 表格适应样式
.ant-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-spin-nested-loading {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-spin-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.ant-table {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.ant-table-header {
flex-shrink: 0;
}
.ant-table-body {
flex: 1;
overflow: auto !important;
}
}
// 确保分页器在底部正确显示
.ant-table-pagination {
flex-shrink: 0;
// 确保分页器始终可见
position: relative;
z-index: 1;
}
}
}
}
}
}
} }
} }

View File

@ -3,6 +3,7 @@ import {
ERROR_CODE, ERROR_CODE,
GENDER_MAP, GENDER_MAP,
PRIORITY_MAP, PRIORITY_MAP,
STATUS_MAP,
USER_TYPE_MAP, USER_TYPE_MAP,
} from '@/constants/constants'; } from '@/constants/constants';
import CustomTree from '@/pages/components/customTree'; import CustomTree from '@/pages/components/customTree';
@ -204,8 +205,9 @@ const UserListPage: React.FC = () => {
key: 'user_name', key: 'user_name',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
}, },
}, },
{ {
@ -214,8 +216,9 @@ const UserListPage: React.FC = () => {
key: 'user_group_name', key: 'user_group_name',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
}, },
}, },
{ {
@ -224,8 +227,14 @@ const UserListPage: React.FC = () => {
key: 'status', key: 'status',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text) => { render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>; const key = text as keyof typeof STATUS_MAP;
return (
<Tooltip title={STATUS_MAP[key] || ''}>
{STATUS_MAP[key] || '--'}
</Tooltip>
);
}, },
}, },
{ {
@ -234,9 +243,14 @@ const UserListPage: React.FC = () => {
key: 'user_type', key: 'user_type',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text: number) => { render: (text: number) => {
const key = text as keyof typeof USER_TYPE_MAP; const key = text as keyof typeof USER_TYPE_MAP;
return <Tooltip>{USER_TYPE_MAP[key] || '--'}</Tooltip>; return (
<Tooltip title={USER_TYPE_MAP[key] || ''}>
{USER_TYPE_MAP[key] || '--'}
</Tooltip>
);
}, },
}, },
{ {
@ -245,9 +259,14 @@ const UserListPage: React.FC = () => {
key: 'priority', key: 'priority',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text: number) => { render: (text: number) => {
const key = text as keyof typeof PRIORITY_MAP; const key = text as keyof typeof PRIORITY_MAP;
return <Tooltip>{PRIORITY_MAP[key] || '--'}</Tooltip>; return (
<Tooltip title={PRIORITY_MAP[key] || ''}>
{PRIORITY_MAP[key] || '--'}
</Tooltip>
);
}, },
}, },
{ {
@ -256,46 +275,65 @@ const UserListPage: React.FC = () => {
key: 'gender', key: 'gender',
width: 150, width: 150,
align: 'center', align: 'center',
ellipsis: true,
render: (text: number) => { render: (text: number) => {
const key = text as keyof typeof GENDER_MAP; const key = text as keyof typeof GENDER_MAP;
return <Tooltip>{GENDER_MAP[key] || '--'}</Tooltip>; return (
<Tooltip title={GENDER_MAP[key] || ''}>
{GENDER_MAP[key] || '--'}
</Tooltip>
);
}, },
}, },
{ {
title: '电话', title: '电话',
dataIndex: 'cell_phone', dataIndex: 'cell_phone',
key: 'cell_phone', key: 'cell_phone',
width: 150, width: 180,
align: 'center', align: 'center',
ellipsis: true,
render: (text: any) => { render: (text: any) => {
return <Tooltip>{text || '--'}</Tooltip>; return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
}, },
}, },
{ {
title: '出生日期', title: '出生日期',
dataIndex: 'birthday', dataIndex: 'birthday',
key: 'birthday', key: 'birthday',
width: 150, width: 180,
align: 'center', align: 'center',
ellipsis: true,
render: (text: any) => { render: (text: any) => {
return <Tooltip>{text || '--'}</Tooltip>; return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
}, },
}, },
{ {
title: '身份证号', title: '身份证号',
dataIndex: 'identity_no', dataIndex: 'identity_no',
key: 'identity_no', key: 'identity_no',
width: 150, width: 230,
ellipsis: true,
align: 'center', align: 'center',
render: (text: any) => { render: (text: any) => {
return <Tooltip>{text || '--'}</Tooltip>; return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
},
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
width: 150,
align: 'center',
ellipsis: true,
render: (text: any) => {
return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
}, },
}, },
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
align: 'center', align: 'center',
width: 150, width: 160,
fixed: 'right', fixed: 'right',
render: (_, record) => ( render: (_, record) => (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
@ -427,6 +465,7 @@ const UserListPage: React.FC = () => {
defaultExpandAll defaultExpandAll
onSelect={onOrgSelect} onSelect={onOrgSelect}
selectedKeys={selectedOrg ? [selectedOrg] : []} selectedKeys={selectedOrg ? [selectedOrg] : []}
showIcon={true}
icon={<TeamOutlined style={{ fontSize: '15px' }} />} icon={<TeamOutlined style={{ fontSize: '15px' }} />}
/> />
</Spin> </Spin>
@ -465,9 +504,6 @@ const UserListPage: React.FC = () => {
</Button> </Button>
</Popconfirm> */} </Popconfirm> */}
<Button style={{ marginRight: '8px' }} onClick={getDataSource}>
</Button>
</div> </div>
<div> <div>
<div> <div>
@ -481,34 +517,51 @@ const UserListPage: React.FC = () => {
setCurrentPage(1); // Reset to first page when searching setCurrentPage(1); // Reset to first page when searching
}} }}
/> />
<Button
style={{ marginRight: '8px', marginLeft: '8px' }}
icon={<RedoOutlined />}
onClick={getDataSource}
loading={loading}
title="刷新"
/>
</div> </div>
</div> </div>
</div> </div>
<div className={styles.teble_box}> <div className={styles.teble_box}>
<Table <div className="images-list-table">
columns={columns} <Table
dataSource={dataSource} columns={columns}
loading={loading} dataSource={dataSource}
rowKey="id" loading={loading}
pagination={{ rowKey="id"
current: currentPage, pagination={{
pageSize: pageSize, current: currentPage,
total: total, pageSize: pageSize,
onChange: handlePageChange, total: total,
onShowSizeChange: handlePageSizeChange, onChange: handlePageChange,
showSizeChanger: true, onShowSizeChange: handlePageSizeChange,
showQuickJumper: true, showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'], showQuickJumper: true,
showTotal: (total) => { pageSizeOptions: ['10', '20', '50', '100'],
return `${total}条数据`; showTotal: (total) => {
}, return `${total}条数据`;
}} },
// rowSelection={{ }}
// selectedRowKeys, // rowSelection={{
// onChange: onSelectChange, // selectedRowKeys,
// }} // onChange: onSelectChange,
scroll={{ x: 'max-content', y: 55 * 12 }} // }}
/> // scroll={{ x: 'max-content', y: 55 * 12 }}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -16,6 +16,7 @@ import {
Select, Select,
TreeSelect, TreeSelect,
} from 'antd'; } from 'antd';
import moment from 'moment';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
interface UserEditModalProps { interface UserEditModalProps {
@ -43,7 +44,11 @@ const UserEditModal: React.FC<UserEditModalProps> = ({
if (id) { if (id) {
getUserById({ id }).then((res) => { getUserById({ id }).then((res) => {
const { data } = res; const { data } = res;
form.setFieldsValue(data); const { birthday } = data || {};
const record = { ...data };
delete record.birthday;
if (birthday) record.birthday = moment(birthday);
form.setFieldsValue(record);
}); });
} else { } else {
const initialValues = { const initialValues = {
@ -56,8 +61,12 @@ const UserEditModal: React.FC<UserEditModalProps> = ({
const handleOk = async () => { const handleOk = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
const { birthday } = values || {};
const record = { ...values };
delete record.birthday;
if (birthday) record.birthday = moment(birthday).format('YYYY-MM-DD');
try { try {
const params = { ...values }; const params = { ...record };
if (id) { if (id) {
params.id = id; params.id = id;
} }
@ -154,7 +163,7 @@ const UserEditModal: React.FC<UserEditModalProps> = ({
> >
<Form.Item <Form.Item
name="user_name" name="user_name"
label="用户名" label="用户名"
rules={[{ required: true, message: '请输入用户姓名' }]} rules={[{ required: true, message: '请输入用户姓名' }]}
> >
<Input placeholder="请输入用户姓名" /> <Input placeholder="请输入用户姓名" />
@ -203,9 +212,11 @@ const UserEditModal: React.FC<UserEditModalProps> = ({
> >
<Select placeholder="请选择用户类别" options={USER_TYPE_OPTIONS} /> <Select placeholder="请选择用户类别" options={USER_TYPE_OPTIONS} />
</Form.Item> </Form.Item>
<Form.Item name="birthday" label="出生日期"> <Form.Item name="birthday" label="出生日期">
<DatePicker style={{ width: '414px' }} /> <DatePicker format={'YYYY-MM-DD'} style={{ width: '414px' }} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="priority" name="priority"
label="优先级" label="优先级"
@ -227,7 +238,7 @@ const UserEditModal: React.FC<UserEditModalProps> = ({
{ required: false, message: '请输入身份证号' }, { required: false, message: '请输入身份证号' },
{ {
pattern: pattern:
/^[1-9]\d{5}(18|19|20|21|22|23|24|25|26|27|28|29|30|31)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20)$/, /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[\dXx]$/,
message: '请输入有效的身份证号', message: '请输入有效的身份证号',
}, },
]} ]}
@ -239,7 +250,13 @@ const UserEditModal: React.FC<UserEditModalProps> = ({
<Form.Item <Form.Item
name="cell_phone" name="cell_phone"
label="电话号码" label="电话号码"
rules={[{ required: false, message: '请输入电话号码' }]} rules={[
{ required: false, message: '请输入电话号码' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入有效的手机号码',
},
]}
> >
<Input placeholder="请输入电话号码" /> <Input placeholder="请输入电话号码" />
</Form.Item> </Form.Item>