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: '三级',
} as const;
export const STATUS_MAP = {
1: '启用',
2: '禁用',
} as const;
export const USER_TYPE_OPTIONS = [
{ value: 1, label: '域用户' },
{ value: 0, label: '本地用户' },

View File

@ -1,5 +1,5 @@
.user_content {
display: flex;
display: flex;
width: 100%;
height: 100%;
background-color: #f7f8fa;
@ -29,8 +29,10 @@
background-color: #fff;
padding: 8px;
.teble_box {
display: flex;
flex-direction: column;
width: 100%;
height: calc(100% - 40px);
height: calc(100% - 50px);
overflow: hidden;
}
}
@ -41,5 +43,60 @@
position: absolute;
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,
DownOutlined,
PlusOutlined,
TeamOutlined,
RedoOutlined,
GoldOutlined,
} from '@ant-design/icons';
import {
Button,
@ -185,7 +186,7 @@ const UserListPage: React.FC = () => {
title: '序号',
dataIndex: 'order',
key: 'order',
width: 80,
width: 60,
align: 'center',
render: (_: any, record: any, index: number) => <span>{index + 1}</span>,
},
@ -193,41 +194,53 @@ const UserListPage: React.FC = () => {
title: '终端名称',
dataIndex: 'device_name',
key: 'device_name',
width: 220,
width: 250,
ellipsis: true,
align: 'center',
render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>;
return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
},
},
{
title: '序列号',
dataIndex: 'device_id',
key: 'device_id',
width: 220,
width: 250,
align: 'center',
ellipsis: true,
render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>;
return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
},
},
{
title: '终端分组',
dataIndex: 'device_group_name',
key: 'device_group_name',
width: 220,
width: 200,
align: 'center',
ellipsis: true,
render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>;
return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
},
},
{
title: '终端类型',
title: '类型',
dataIndex: 'device_type',
key: 'device_type',
width: 150,
align: 'center',
ellipsis: true,
render: (text: number) => {
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',
width: 150,
align: 'center',
ellipsis: true,
render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>;
return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
},
},
{
title: 'IP地址',
dataIndex: 'ip_addr',
key: 'ip_addr',
width: 150,
width: 200,
ellipsis: true,
align: 'center',
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',
ellipsis: true,
align: 'center',
width: 200,
render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>;
return (
<div>
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
</div>
);
},
},
{
@ -266,8 +294,13 @@ const UserListPage: React.FC = () => {
key: 'description',
ellipsis: true,
align: 'center',
width: 200,
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.search}>
<div style={{ paddingBottom: '5px' }}>
<Button
type="text"
style={{ marginRight: '8px', fontSize: '16px' }}
icon={<RedoOutlined />}
onClick={() => getGroupList()}
title="刷新"
loading={spinning}
/>
<Button
type="text"
style={{ marginRight: '8px', fontSize: '16px' }}
@ -422,6 +463,7 @@ const UserListPage: React.FC = () => {
type="text"
style={{ fontSize: '16px' }}
icon={<DeleteOutlined />}
disabled={!selectedOrg}
/>
</Popconfirm>
</div>
@ -441,8 +483,10 @@ const UserListPage: React.FC = () => {
childrenField="children"
defaultExpandAll={true}
onSelect={onOrgSelect}
showIcon={true}
selectedKeys={selectedOrg ? [selectedOrg] : []}
icon={<TeamOutlined style={{ fontSize: '15px' }} />}
// switcherIcon={<TeamOutlined style={{ fontSize: '15px' }}/>}
icon={<GoldOutlined style={{ fontSize: '15px' }} />}
/>
</Spin>
</div>
@ -473,9 +517,9 @@ const UserListPage: React.FC = () => {
</Button>
</Popconfirm> */}
<Button style={{ marginRight: '8px' }} onClick={getDataSource}>
{/* <Button style={{ marginRight: '8px' }} onClick={getDataSource}>
</Button>
</Button> */}
</div>
<div>
<div>
@ -489,34 +533,52 @@ const UserListPage: React.FC = () => {
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 className={styles.teble_box}>
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey="id"
// rowSelection={{
// selectedRowKeys,
// onChange: onSelectChange,
// }}
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total) => {
return `${total}条数据`;
},
}}
scroll={{ x: 'max-content', y: 55 * 12 }}
/>
<div className="images-list-table">
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey="id"
// rowSelection={{
// selectedRowKeys,
// onChange: onSelectChange,
// }}
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total) => {
return `${total}条数据`;
},
}}
// 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>

View File

@ -1,5 +1,5 @@
.user_content {
display: flex;
display: flex;
width: 100%;
height: 100%;
background-color: #f7f8fa;
@ -29,8 +29,10 @@
background-color: #fff;
padding: 8px;
.teble_box {
display: flex;
flex-direction: column;
width: 100%;
height: calc(100% - 40px);
height: calc(100% - 50px);
overflow: hidden;
}
}
@ -41,5 +43,60 @@
position: absolute;
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,
GENDER_MAP,
PRIORITY_MAP,
STATUS_MAP,
USER_TYPE_MAP,
} from '@/constants/constants';
import CustomTree from '@/pages/components/customTree';
@ -204,8 +205,9 @@ const UserListPage: React.FC = () => {
key: 'user_name',
width: 150,
align: 'center',
ellipsis: true,
render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>;
return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
},
},
{
@ -214,8 +216,9 @@ const UserListPage: React.FC = () => {
key: 'user_group_name',
width: 150,
align: 'center',
ellipsis: true,
render: (text) => {
return <Tooltip>{text || '--'}</Tooltip>;
return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
},
},
{
@ -224,8 +227,14 @@ const UserListPage: React.FC = () => {
key: 'status',
width: 150,
align: 'center',
ellipsis: true,
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',
width: 150,
align: 'center',
ellipsis: true,
render: (text: number) => {
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',
width: 150,
align: 'center',
ellipsis: true,
render: (text: number) => {
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',
width: 150,
align: 'center',
ellipsis: true,
render: (text: number) => {
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: '电话',
dataIndex: 'cell_phone',
key: 'cell_phone',
width: 150,
width: 180,
align: 'center',
ellipsis: true,
render: (text: any) => {
return <Tooltip>{text || '--'}</Tooltip>;
return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
},
},
{
title: '出生日期',
dataIndex: 'birthday',
key: 'birthday',
width: 150,
width: 180,
align: 'center',
ellipsis: true,
render: (text: any) => {
return <Tooltip>{text || '--'}</Tooltip>;
return <Tooltip title={text || ''}>{text || '--'}</Tooltip>;
},
},
{
title: '身份证号',
dataIndex: 'identity_no',
key: 'identity_no',
width: 150,
width: 230,
ellipsis: true,
align: 'center',
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: '操作',
key: 'actions',
align: 'center',
width: 150,
width: 160,
fixed: 'right',
render: (_, record) => (
<div style={{ display: 'flex' }}>
@ -427,6 +465,7 @@ const UserListPage: React.FC = () => {
defaultExpandAll
onSelect={onOrgSelect}
selectedKeys={selectedOrg ? [selectedOrg] : []}
showIcon={true}
icon={<TeamOutlined style={{ fontSize: '15px' }} />}
/>
</Spin>
@ -465,9 +504,6 @@ const UserListPage: React.FC = () => {
</Button>
</Popconfirm> */}
<Button style={{ marginRight: '8px' }} onClick={getDataSource}>
</Button>
</div>
<div>
<div>
@ -481,34 +517,51 @@ const UserListPage: React.FC = () => {
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 className={styles.teble_box}>
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total) => {
return `${total}条数据`;
},
}}
// rowSelection={{
// selectedRowKeys,
// onChange: onSelectChange,
// }}
scroll={{ x: 'max-content', y: 55 * 12 }}
/>
<div className="images-list-table">
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total) => {
return `${total}条数据`;
},
}}
// rowSelection={{
// selectedRowKeys,
// onChange: onSelectChange,
// }}
// scroll={{ x: 'max-content', y: 55 * 12 }}
scroll={{
y: 'max-content', // 关键:允许内容决定高度
}}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
/>
</div>
</div>
</div>
</div>

View File

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