/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { API, showError, showSuccess, renderQuota, getCurrencyConfig, } from '../../../../helpers'; import { quotaToDisplayAmount, displayAmountToQuota, } from '../../../../helpers/quota'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { Button, Modal, SideSheet, Space, Spin, Typography, Card, Tag, Form, Avatar, Row, Col, InputNumber, RadioGroup, Radio, } from '@douyinfe/semi-ui'; import { IconUser, IconSave, IconClose, IconLink, IconUserGroup, IconEdit, } from '@douyinfe/semi-icons'; import UserBindingManagementModal from './UserBindingManagementModal'; const { Text, Title } = Typography; const EditUserModal = (props) => { const { t } = useTranslation(); const userId = props.editingUser.id; const [loading, setLoading] = useState(true); const [adjustModalOpen, setAdjustModalOpen] = useState(false); const [adjustQuotaLocal, setAdjustQuotaLocal] = useState(''); const [adjustAmountLocal, setAdjustAmountLocal] = useState(''); const [adjustMode, setAdjustMode] = useState('add'); const [adjustLoading, setAdjustLoading] = useState(false); const isMobile = useIsMobile(); const [groupOptions, setGroupOptions] = useState([]); const [bindingModalVisible, setBindingModalVisible] = useState(false); const formApiRef = useRef(null); const [showAdjustQuotaRaw, setShowAdjustQuotaRaw] = useState(false); const [showQuotaInput, setShowQuotaInput] = useState(false); const [inputs, setInputs] = useState(null); const isEdit = Boolean(userId); const getInitValues = () => ({ username: '', display_name: '', password: '', github_id: '', oidc_id: '', discord_id: '', wechat_id: '', telegram_id: '', linux_do_id: '', email: '', quota: 0, quota_amount: 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 = ''; data.quota_amount = Number( quotaToDisplayAmount(data.quota || 0).toFixed(6), ); setInputs({ ...getInitValues(), ...data }); } else { showError(message); } setLoading(false); }; useEffect(() => { if (inputs && formApiRef.current) { formApiRef.current.setValues(inputs); } }, [inputs]); useEffect(() => { loadUser(); if (userId) fetchGroups(); setBindingModalVisible(false); }, [props.editingUser.id]); const openBindingModal = () => { setBindingModalVisible(true); }; const closeBindingModal = () => { setBindingModalVisible(false); }; /* ----------------------- submit ----------------------- */ const submit = async (values) => { setLoading(true); let payload = { ...values }; delete payload.quota; delete payload.quota_amount; 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); }; /* --------------------- atomic quota adjust -------------------- */ const adjustQuota = async () => { const quotaVal = parseInt(adjustQuotaLocal) || 0; if (quotaVal <= 0 && adjustMode !== 'override') return; if (adjustMode === 'override' && (adjustQuotaLocal === '' || adjustQuotaLocal == null)) return; setAdjustLoading(true); try { const res = await API.post('/api/user/manage', { id: parseInt(userId), action: 'add_quota', mode: adjustMode, value: adjustMode === 'override' ? quotaVal : Math.abs(quotaVal), }); const { success, message } = res.data; if (success) { showSuccess(t('调整额度成功')); setAdjustModalOpen(false); setAdjustQuotaLocal(''); setAdjustAmountLocal(''); const userRes = await API.get(`/api/user/${userId}`); if (userRes.data.success) { const data = userRes.data.data; data.password = ''; data.quota_amount = Number( quotaToDisplayAmount(data.quota || 0).toFixed(6), ); setInputs({ ...getInitValues(), ...data }); } props.refresh(); } else { showError(message); } } catch (e) { showError(e.message); } setAdjustLoading(false); }; const getPreviewText = () => { const current = formApiRef.current?.getValue('quota') || 0; const val = parseInt(adjustQuotaLocal) || 0; let result; switch (adjustMode) { case 'add': result = current + Math.abs(val); return `${t('当前额度')}:${renderQuota(current)},+${renderQuota(Math.abs(val))} = ${renderQuota(result)}`; case 'subtract': result = current - Math.abs(val); return `${t('当前额度')}:${renderQuota(current)},-${renderQuota(Math.abs(val))} = ${renderQuota(result)}`; case 'override': return `${t('当前额度')}:${renderQuota(current)} → ${renderQuota(val)}`; default: return ''; } }; /* --------------------------- UI --------------------------- */ return ( <> {t(isEdit ? '编辑' : '新建')} {isEdit ? t('编辑用户') : t('创建用户')} } bodyStyle={{ padding: 0 }} visible={props.visible} width={isMobile ? '100%' : 600} footer={
} closeIcon={null} onCancel={handleCancel} >
(formApiRef.current = api)} onSubmit={submit} > {({ values }) => (
{/* 基本信息 */}
{t('基本信息')}
{t('用户的基本账户信息')}
{/* 权限设置 */} {userId && (
{t('权限设置')}
{t('用户分组和额度管理')}
setShowQuotaInput((v) => !v)} > {showQuotaInput ? `▾ ${t('收起原生额度输入')}` : `▸ ${t('使用原生额度输入')}`}
)} {/* 绑定信息入口 */} {userId && (
{t('绑定信息')}
{t('管理用户已绑定的第三方账户,支持筛选与解绑')}
)}
)}
{/* 调整额度模态框 */} { setAdjustModalOpen(false); setAdjustQuotaLocal(''); setAdjustAmountLocal(''); setAdjustMode('add'); }} confirmLoading={adjustLoading} closable={null} title={
{t('调整额度')}
} >
{getPreviewText()}
{t('操作')}
{ setAdjustMode(e.target.value); setAdjustQuotaLocal(''); setAdjustAmountLocal(''); }} style={{ width: '100%' }} > {t('添加')} {t('减少')} {t('覆盖')}
{t('金额')}
{ const amount = val === '' || val == null ? '' : val; setAdjustAmountLocal(amount); setAdjustQuotaLocal( amount === '' ? '' : adjustMode === 'override' ? displayAmountToQuota(amount) : displayAmountToQuota(Math.abs(amount)), ); }} style={{ width: '100%' }} showClear />
setShowAdjustQuotaRaw((v) => !v)} > {showAdjustQuotaRaw ? `▾ ${t('收起原生额度输入')}` : `▸ ${t('使用原生额度输入')}`}
{t('额度')}
{ const quota = val === '' || val == null ? '' : val; setAdjustQuotaLocal(quota); setAdjustAmountLocal( quota === '' ? '' : adjustMode === 'override' ? Number(quotaToDisplayAmount(quota).toFixed(6)) : Number(quotaToDisplayAmount(Math.abs(quota)).toFixed(6)), ); }} style={{ width: '100%' }} showClear step={500000} />
); }; export default EditUserModal;