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