feat: Add base project structure with API and web components
This commit is contained in:
106
web/src/views/UserManagement/components/CreateModal.tsx
Normal file
106
web/src/views/UserManagement/components/CreateModal.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { CreateModalData, CreateModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { addUser } from '@/api/user'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface CreateModalProps {
|
||||
refreshTable: () => void;
|
||||
}
|
||||
|
||||
const CreateModal = forwardRef<CreateModalRef, CreateModalProps>(({
|
||||
refreshTable
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<CreateModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
addUser(values)
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
refreshTable()
|
||||
handleClose()
|
||||
message.success(t('common.createSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('user.createUser')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="email"
|
||||
label={t('user.usernameOrAccount')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="username"
|
||||
label={t('user.displayName')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter',)} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="password"
|
||||
label={t('user.initialPassword')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default CreateModal;
|
||||
116
web/src/views/UserManagement/components/ResetPasswordModal.tsx
Normal file
116
web/src/views/UserManagement/components/ResetPasswordModal.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Button, Row, Col } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { randomString } from '@/utils/common'
|
||||
import { useUser } from '@/store/user';
|
||||
|
||||
import type { ResetPasswordModalRef, User } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { changePassword } from '@/api/user'
|
||||
|
||||
const ResetPasswordModal = forwardRef<ResetPasswordModalRef, { source?: 'resetPassword' | 'changePassword' }>(({ source = 'resetPassword' }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message, modal } = App.useApp();
|
||||
const { logout } = useUser();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{ new_password?: string }>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<User>()
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (user: User) => {
|
||||
form.resetFields();
|
||||
setEditVo(user)
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
changePassword({ user_id: editVo?.id as string, new_password: values.new_password as string })
|
||||
.then((res) => {
|
||||
handleClose()
|
||||
const password = typeof res === 'string' ? res : values.new_password as string
|
||||
if (source === 'changePassword') {
|
||||
logout()
|
||||
} else {
|
||||
modal.confirm({
|
||||
title: <>
|
||||
{t('user.resetPasswordSuccess')}
|
||||
<br />
|
||||
【{password}】
|
||||
</>,
|
||||
okText: t('common.copy'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
copy(password)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
// 自动生成长度为12的随机密码,包含字母、数字、特殊字符
|
||||
const handleAutoGenerate = () => {
|
||||
form.setFieldValue('new_password', randomString());
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('user.resetPassword')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
name="new_password"
|
||||
rules={[
|
||||
{ min: 6, message: t('user.passwordRule') }
|
||||
]}
|
||||
className="rb:mb-0! rb:w-[calc(100%-)]"
|
||||
>
|
||||
<Input placeholder={t('user.newPasswordPlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button onClick={handleAutoGenerate}>{t('user.autoGenerate')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ResetPasswordModal;
|
||||
154
web/src/views/UserManagement/index.tsx
Normal file
154
web/src/views/UserManagement/index.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Button, Space, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateModal from './components/CreateModal';
|
||||
import type { CreateModalRef, User, ResetPasswordModalRef } from './types'
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import Table, { type TableRef } from '@/components/Table'
|
||||
import StatusTag from '@/components/StatusTag'
|
||||
import { deleteUser, enableUser, getUserListUrl } from '@/api/user'
|
||||
import ResetPasswordModal from './components/ResetPasswordModal'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
const UserManagement: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { message, modal } = App.useApp();
|
||||
|
||||
const userFormRef = useRef<CreateModalRef>(null);
|
||||
const resetPasswordModalRef = useRef<ResetPasswordModalRef>(null);
|
||||
const tableRef = useRef<TableRef>(null);
|
||||
|
||||
// 打开新增用户弹窗
|
||||
const handleCreate = () => {
|
||||
userFormRef.current?.handleOpen();
|
||||
}
|
||||
// 重置密码
|
||||
const handleResetPassword = (user: User) => {
|
||||
resetPasswordModalRef.current?.handleOpen(user);
|
||||
};
|
||||
|
||||
// 刷新列表数据
|
||||
const refreshTable = () => {
|
||||
tableRef.current?.loadData()
|
||||
}
|
||||
|
||||
// 启用/停用
|
||||
const handleChangeStatus = async (record: User) => {
|
||||
modal.confirm({
|
||||
title: t(`user.${record.is_active ? 'disabled' : 'enabled'}Confirm`),
|
||||
okText: t('common.confirm'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const res = record.is_active ? deleteUser(record.id) : enableUser(record.id);
|
||||
|
||||
res.then(() => {
|
||||
message.success(t(`user.${record.is_active ? 'disabled' : 'enabled'}ConfirmSuccess`));
|
||||
refreshTable();
|
||||
})
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns: ColumnsType = [
|
||||
{
|
||||
title: t('user.userId'),
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: <>{t('user.username')}<div className="rb:text-[#5B6167] rb:text-[12px] rb:font-medium">({t(`user.subUsername`)})</div></>,
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: t('user.displayName'),
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: t('user.role'),
|
||||
dataIndex: 'is_superuser',
|
||||
key: 'is_superuser',
|
||||
render: (isSuperuser: boolean) => isSuperuser ? t('user.superuser') : t('user.normalUser'),
|
||||
},
|
||||
{
|
||||
title: t('user.status'),
|
||||
dataIndex: 'is_active',
|
||||
key: 'is_active',
|
||||
render: (isActive: boolean) => (
|
||||
<StatusTag
|
||||
text={isActive ? t('user.enabled') : t('user.disabled')}
|
||||
status={isActive ? 'success' : 'error'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('user.createTime'),
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (createdAt: string) => formatDateTime(createdAt, 'YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: t('user.lastLoginTime'),
|
||||
dataIndex: 'last_login_at',
|
||||
key: 'last_login_at',
|
||||
render: (lastLoginAt: string) => lastLoginAt ? formatDateTime(lastLoginAt, 'YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
{
|
||||
title: t('common.operation'),
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="large">
|
||||
{record.is_active &&
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleResetPassword(record as User)}
|
||||
>
|
||||
{t('user.resetPassword')}
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleChangeStatus(record as User)}
|
||||
>
|
||||
{t(`common.${record.is_active ? 'disabled' : 'enabled'}`)}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="rb:h-[calc(100vh-80px)] rb:overflow-hidden">
|
||||
<div className="rb:flex rb:justify-end rb:mb-[12px]">
|
||||
<Button type="primary" onClick={handleCreate}>
|
||||
{t('user.createUser')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
ref={tableRef}
|
||||
apiUrl={getUserListUrl}
|
||||
apiParams={{
|
||||
include_inactive: true,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
isScroll={true}
|
||||
/>
|
||||
|
||||
<CreateModal
|
||||
ref={userFormRef}
|
||||
refreshTable={refreshTable}
|
||||
/>
|
||||
<ResetPasswordModal
|
||||
ref={resetPasswordModalRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManagement;
|
||||
35
web/src/views/UserManagement/types.ts
Normal file
35
web/src/views/UserManagement/types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// 用户数据类型
|
||||
export interface User {
|
||||
username: string;
|
||||
email: string;
|
||||
id: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
created_at: string | number;
|
||||
last_login_at: string | number;
|
||||
current_workspace_id?: string;
|
||||
current_workspace_name?: string;
|
||||
role: 'member' | 'manager' | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 用户表单数据类型
|
||||
export interface CreateModalData {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 用户表单数据类型
|
||||
export interface CreateModalData {
|
||||
username: string;
|
||||
displayName: string;
|
||||
initialPassword?: string;
|
||||
}
|
||||
// 定义组件暴露的方法接口
|
||||
export interface CreateModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface ResetPasswordModalRef {
|
||||
handleOpen: (user: User) => void;
|
||||
}
|
||||
Reference in New Issue
Block a user