| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import React, { useEffect, useState, useRef } from 'react';
|
| import { useTranslation } from 'react-i18next';
|
| import {
|
| API,
|
| showError,
|
| showSuccess,
|
| renderQuota,
|
| renderQuotaWithPrompt,
|
| } from '../../../../helpers';
|
| import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
| import {
|
| Button,
|
| Modal,
|
| SideSheet,
|
| Space,
|
| Spin,
|
| Typography,
|
| Card,
|
| Tag,
|
| Form,
|
| Avatar,
|
| Row,
|
| Col,
|
| Input,
|
| InputNumber,
|
| } from '@douyinfe/semi-ui';
|
| import {
|
| IconUser,
|
| IconSave,
|
| IconClose,
|
| IconLink,
|
| IconUserGroup,
|
| IconPlus,
|
| } from '@douyinfe/semi-icons';
|
|
|
| const { Text, Title } = Typography;
|
|
|
| const EditUserModal = (props) => {
|
| const { t } = useTranslation();
|
| const userId = props.editingUser.id;
|
| const [loading, setLoading] = useState(true);
|
| const [addQuotaModalOpen, setIsModalOpen] = useState(false);
|
| const [addQuotaLocal, setAddQuotaLocal] = useState('');
|
| const isMobile = useIsMobile();
|
| const [groupOptions, setGroupOptions] = useState([]);
|
| const formApiRef = useRef(null);
|
|
|
| const isEdit = Boolean(userId);
|
|
|
| const getInitValues = () => ({
|
| username: '',
|
| display_name: '',
|
| password: '',
|
| github_id: '',
|
| oidc_id: '',
|
| discord_id: '',
|
| wechat_id: '',
|
| telegram_id: '',
|
| email: '',
|
| quota: 0,
|
| group: 'default',
|
| remark: '',
|
| });
|
|
|
| const fetchGroups = async () => {
|
| try {
|
| let res = await API.get(`/api/group/`);
|
| setGroupOptions(res.data.data.map((g) => ({ label: g, value: g })));
|
| } catch (e) {
|
| showError(e.message);
|
| }
|
| };
|
|
|
| const handleCancel = () => props.handleClose();
|
|
|
| const loadUser = async () => {
|
| setLoading(true);
|
| const url = userId ? `/api/user/${userId}` : `/api/user/self`;
|
| const res = await API.get(url);
|
| const { success, message, data } = res.data;
|
| if (success) {
|
| data.password = '';
|
| formApiRef.current?.setValues({ ...getInitValues(), ...data });
|
| } else {
|
| showError(message);
|
| }
|
| setLoading(false);
|
| };
|
|
|
| useEffect(() => {
|
| loadUser();
|
| if (userId) fetchGroups();
|
| }, [props.editingUser.id]);
|
|
|
|
|
| const submit = async (values) => {
|
| setLoading(true);
|
| let payload = { ...values };
|
| if (typeof payload.quota === 'string')
|
| payload.quota = parseInt(payload.quota) || 0;
|
| if (userId) {
|
| payload.id = parseInt(userId);
|
| }
|
| const url = userId ? `/api/user/` : `/api/user/self`;
|
| const res = await API.put(url, payload);
|
| const { success, message } = res.data;
|
| if (success) {
|
| showSuccess(t('用户信息更新成功!'));
|
| props.refresh();
|
| props.handleClose();
|
| } else {
|
| showError(message);
|
| }
|
| setLoading(false);
|
| };
|
|
|
|
|
| const addLocalQuota = () => {
|
| const current = parseInt(formApiRef.current?.getValue('quota') || 0);
|
| const delta = parseInt(addQuotaLocal) || 0;
|
| formApiRef.current?.setValue('quota', current + delta);
|
| };
|
|
|
|
|
| return (
|
| <>
|
| <SideSheet
|
| placement='right'
|
| title={
|
| <Space>
|
| <Tag color='blue' shape='circle'>
|
| {t(isEdit ? '编辑' : '新建')}
|
| </Tag>
|
| <Title heading={4} className='m-0'>
|
| {isEdit ? t('编辑用户') : t('创建用户')}
|
| </Title>
|
| </Space>
|
| }
|
| bodyStyle={{ padding: 0 }}
|
| visible={props.visible}
|
| width={isMobile ? '100%' : 600}
|
| footer={
|
| <div className='flex justify-end bg-white'>
|
| <Space>
|
| <Button
|
| theme='solid'
|
| onClick={() => formApiRef.current?.submitForm()}
|
| icon={<IconSave />}
|
| loading={loading}
|
| >
|
| {t('提交')}
|
| </Button>
|
| <Button
|
| theme='light'
|
| type='primary'
|
| onClick={handleCancel}
|
| icon={<IconClose />}
|
| >
|
| {t('取消')}
|
| </Button>
|
| </Space>
|
| </div>
|
| }
|
| closeIcon={null}
|
| onCancel={handleCancel}
|
| >
|
| <Spin spinning={loading}>
|
| <Form
|
| initValues={getInitValues()}
|
| getFormApi={(api) => (formApiRef.current = api)}
|
| onSubmit={submit}
|
| >
|
| {({ values }) => (
|
| <div className='p-2'>
|
| {/* 基本信息 */}
|
| <Card className='!rounded-2xl shadow-sm border-0'>
|
| <div className='flex items-center mb-2'>
|
| <Avatar
|
| size='small'
|
| color='blue'
|
| className='mr-2 shadow-md'
|
| >
|
| <IconUser size={16} />
|
| </Avatar>
|
| <div>
|
| <Text className='text-lg font-medium'>
|
| {t('基本信息')}
|
| </Text>
|
| <div className='text-xs text-gray-600'>
|
| {t('用户的基本账户信息')}
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <Row gutter={12}>
|
| <Col span={24}>
|
| <Form.Input
|
| field='username'
|
| label={t('用户名')}
|
| placeholder={t('请输入新的用户名')}
|
| rules={[{ required: true, message: t('请输入用户名') }]}
|
| showClear
|
| />
|
| </Col>
|
|
|
| <Col span={24}>
|
| <Form.Input
|
| field='password'
|
| label={t('密码')}
|
| placeholder={t('请输入新的密码,最短 8 位')}
|
| mode='password'
|
| showClear
|
| />
|
| </Col>
|
|
|
| <Col span={24}>
|
| <Form.Input
|
| field='display_name'
|
| label={t('显示名称')}
|
| placeholder={t('请输入新的显示名称')}
|
| showClear
|
| />
|
| </Col>
|
|
|
| <Col span={24}>
|
| <Form.Input
|
| field='remark'
|
| label={t('备注')}
|
| placeholder={t('请输入备注(仅管理员可见)')}
|
| showClear
|
| />
|
| </Col>
|
| </Row>
|
| </Card>
|
|
|
| {/* 权限设置 */}
|
| {userId && (
|
| <Card className='!rounded-2xl shadow-sm border-0'>
|
| <div className='flex items-center mb-2'>
|
| <Avatar
|
| size='small'
|
| color='green'
|
| className='mr-2 shadow-md'
|
| >
|
| <IconUserGroup size={16} />
|
| </Avatar>
|
| <div>
|
| <Text className='text-lg font-medium'>
|
| {t('权限设置')}
|
| </Text>
|
| <div className='text-xs text-gray-600'>
|
| {t('用户分组和额度管理')}
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <Row gutter={12}>
|
| <Col span={24}>
|
| <Form.Select
|
| field='group'
|
| label={t('分组')}
|
| placeholder={t('请选择分组')}
|
| optionList={groupOptions}
|
| allowAdditions
|
| search
|
| rules={[{ required: true, message: t('请选择分组') }]}
|
| />
|
| </Col>
|
|
|
| <Col span={10}>
|
| <Form.InputNumber
|
| field='quota'
|
| label={t('剩余额度')}
|
| placeholder={t('请输入新的剩余额度')}
|
| step={500000}
|
| extraText={renderQuotaWithPrompt(values.quota || 0)}
|
| rules={[{ required: true, message: t('请输入额度') }]}
|
| style={{ width: '100%' }}
|
| />
|
| </Col>
|
|
|
| <Col span={14}>
|
| <Form.Slot label={t('添加额度')}>
|
| <Button
|
| icon={<IconPlus />}
|
| onClick={() => setIsModalOpen(true)}
|
| />
|
| </Form.Slot>
|
| </Col>
|
| </Row>
|
| </Card>
|
| )}
|
|
|
| {/* 绑定信息 */}
|
| <Card className='!rounded-2xl shadow-sm border-0'>
|
| <div className='flex items-center mb-2'>
|
| <Avatar
|
| size='small'
|
| color='purple'
|
| className='mr-2 shadow-md'
|
| >
|
| <IconLink size={16} />
|
| </Avatar>
|
| <div>
|
| <Text className='text-lg font-medium'>
|
| {t('绑定信息')}
|
| </Text>
|
| <div className='text-xs text-gray-600'>
|
| {t('第三方账户绑定状态(只读)')}
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <Row gutter={12}>
|
| {[
|
| 'github_id',
|
| 'discord_id',
|
| 'oidc_id',
|
| 'wechat_id',
|
| 'email',
|
| 'telegram_id',
|
| ].map((field) => (
|
| <Col span={24} key={field}>
|
| <Form.Input
|
| field={field}
|
| label={t(
|
| `已绑定的 ${field.replace('_id', '').toUpperCase()} 账户`,
|
| )}
|
| readonly
|
| placeholder={t(
|
| '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
| )}
|
| />
|
| </Col>
|
| ))}
|
| </Row>
|
| </Card>
|
| </div>
|
| )}
|
| </Form>
|
| </Spin>
|
| </SideSheet>
|
|
|
| {/* 添加额度模态框 */}
|
| <Modal
|
| centered
|
| visible={addQuotaModalOpen}
|
| onOk={() => {
|
| addLocalQuota();
|
| setIsModalOpen(false);
|
| }}
|
| onCancel={() => setIsModalOpen(false)}
|
| closable={null}
|
| title={
|
| <div className='flex items-center'>
|
| <IconPlus className='mr-2' />
|
| {t('添加额度')}
|
| </div>
|
| }
|
| >
|
| <div className='mb-4'>
|
| {(() => {
|
| const current = formApiRef.current?.getValue('quota') || 0;
|
| return (
|
| <Text type='secondary' className='block mb-2'>
|
| {`${t('新额度:')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
|
| </Text>
|
| );
|
| })()}
|
| </div>
|
| <InputNumber
|
| placeholder={t('需要添加的额度(支持负数)')}
|
| value={addQuotaLocal}
|
| onChange={setAddQuotaLocal}
|
| style={{ width: '100%' }}
|
| showClear
|
| step={500000}
|
| />
|
| </Modal>
|
| </>
|
| );
|
| };
|
|
|
| export default EditUserModal;
|
|
|