feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

View 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;

View 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;

View 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;

View 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;
}