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