♻️ refactor(personal-settings): Break down PersonalSetting.js into modular components
- Split the 1554-line PersonalSetting.js file into smaller, maintainable components - Created organized folder structure under personal/: - components/: UserInfoHeader for shared user info display - tabs/: ModelsList, AccountBinding, SecuritySettings, NotificationSettings - modals/: EmailBindModal, WeChatBindModal, AccountDeleteModal, ChangePasswordModal - Refactored main PersonalSetting component to use composition pattern - Improved code maintainability and separation of concerns - Added collapsible prop to ModelsList tabs for better UX - Fixed import path for TwoFASetting component in SecuritySettings - Preserved all existing functionality and user interactions This refactoring reduces the main file from 1554 to 484 lines and makes the codebase more modular, testable, and easier to maintain.
This commit is contained in:
411
web/src/components/settings/personal/cards/AccountManagement.js
Normal file
411
web/src/components/settings/personal/cards/AccountManagement.js
Normal file
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Space,
|
||||
Typography,
|
||||
Avatar,
|
||||
Tabs,
|
||||
TabPane
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconMail,
|
||||
IconShield,
|
||||
IconGithubLogo,
|
||||
IconKey,
|
||||
IconLock,
|
||||
IconDelete
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
||||
import { UserPlus, ShieldCheck } from 'lucide-react';
|
||||
import TelegramLoginButton from 'react-telegram-login';
|
||||
import {
|
||||
onGitHubOAuthClicked,
|
||||
onOIDCClicked,
|
||||
onLinuxDOOAuthClicked
|
||||
} from '../../../../helpers';
|
||||
import TwoFASetting from '../components/TwoFASetting';
|
||||
|
||||
const AccountManagement = ({
|
||||
t,
|
||||
userState,
|
||||
status,
|
||||
systemToken,
|
||||
setShowEmailBindModal,
|
||||
setShowWeChatBindModal,
|
||||
generateAccessToken,
|
||||
handleSystemTokenClick,
|
||||
setShowChangePasswordModal,
|
||||
setShowAccountDeleteModal
|
||||
}) => {
|
||||
return (
|
||||
<Card className="!rounded-2xl">
|
||||
{/* 卡片头部 */}
|
||||
<div className="flex items-center mb-4">
|
||||
<Avatar size="small" color="teal" className="mr-3 shadow-md">
|
||||
<UserPlus size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className="text-lg font-medium">{t('账户管理')}</Typography.Text>
|
||||
<div className="text-xs text-gray-600">{t('账户绑定、安全设置和身份验证')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs type="line" defaultActiveKey="binding">
|
||||
{/* 账户绑定 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<UserPlus size={16} className="mr-2" />
|
||||
{t('账户绑定')}
|
||||
</div>
|
||||
}
|
||||
itemKey="binding"
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* 邮箱绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<IconMail size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('邮箱')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{userState.user && userState.user.email !== ''
|
||||
? userState.user.email
|
||||
: t('未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
onClick={() => setShowEmailBindModal(true)}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{userState.user && userState.user.email !== ''
|
||||
? t('修改绑定')
|
||||
: t('绑定')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 微信绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('微信')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{userState.user && userState.user.wechat_id !== ''
|
||||
? t('已绑定')
|
||||
: t('未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
disabled={!status.wechat_login}
|
||||
onClick={() => setShowWeChatBindModal(true)}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{userState.user && userState.user.wechat_id !== ''
|
||||
? t('修改绑定')
|
||||
: status.wechat_login
|
||||
? t('绑定')
|
||||
: t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* GitHub绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('GitHub')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{userState.user && userState.user.github_id !== ''
|
||||
? userState.user.github_id
|
||||
: t('未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
|
||||
disabled={
|
||||
(userState.user && userState.user.github_id !== '') ||
|
||||
!status.github_oauth
|
||||
}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{status.github_oauth ? t('绑定') : t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* OIDC绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<IconShield size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('OIDC')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{userState.user && userState.user.oidc_id !== ''
|
||||
? userState.user.oidc_id
|
||||
: t('未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
onClick={() => onOIDCClicked(
|
||||
status.oidc_authorization_endpoint,
|
||||
status.oidc_client_id,
|
||||
)}
|
||||
disabled={
|
||||
(userState.user && userState.user.oidc_id !== '') ||
|
||||
!status.oidc_enabled
|
||||
}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{status.oidc_enabled ? t('绑定') : t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Telegram绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('Telegram')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{userState.user && userState.user.telegram_id !== ''
|
||||
? userState.user.telegram_id
|
||||
: t('未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
{status.telegram_oauth ? (
|
||||
userState.user.telegram_id !== '' ? (
|
||||
<Button disabled={true} size="small" className="!rounded-lg">
|
||||
{t('已绑定')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="scale-75">
|
||||
<TelegramLoginButton
|
||||
dataAuthUrl='/api/oauth/telegram/bind'
|
||||
botName={status.telegram_bot_name}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Button disabled={true} size="small" className="!rounded-lg">
|
||||
{t('未启用')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* LinuxDO绑定 */}
|
||||
<Card className="!rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('LinuxDO')}</div>
|
||||
<div className="text-sm text-gray-500 truncate">
|
||||
{userState.user && userState.user.linux_do_id !== ''
|
||||
? userState.user.linux_do_id
|
||||
: t('未绑定')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size="small"
|
||||
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
|
||||
disabled={
|
||||
(userState.user && userState.user.linux_do_id !== '') ||
|
||||
!status.linuxdo_oauth
|
||||
}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
{/* 安全设置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<ShieldCheck size={16} className="mr-2" />
|
||||
{t('安全设置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="security"
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className="space-y-6">
|
||||
<Space vertical className='w-full'>
|
||||
{/* 系统访问令牌 */}
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconKey size="large" className="text-slate-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Typography.Title heading={6} className="mb-1">
|
||||
{t('系统访问令牌')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
{t('用于API调用的身份验证令牌,请妥善保管')}
|
||||
</Typography.Text>
|
||||
{systemToken && (
|
||||
<div className="mt-3">
|
||||
<Input
|
||||
readonly
|
||||
value={systemToken}
|
||||
onClick={handleSystemTokenClick}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconKey />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={generateAccessToken}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||
icon={<IconKey />}
|
||||
>
|
||||
{systemToken ? t('重新生成') : t('生成令牌')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 密码管理 */}
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconLock size="large" className="text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-1">
|
||||
{t('密码管理')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
{t('定期更改密码可以提高账户安全性')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={() => setShowChangePasswordModal(true)}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||
icon={<IconLock />}
|
||||
>
|
||||
{t('修改密码')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 两步验证设置 */}
|
||||
<TwoFASetting t={t} />
|
||||
|
||||
{/* 危险区域 */}
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconDelete size="large" className="text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-1 text-slate-700">
|
||||
{t('删除账户')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
{t('此操作不可逆,所有数据将被永久删除')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="danger"
|
||||
theme="solid"
|
||||
onClick={() => setShowAccountDeleteModal(true)}
|
||||
className="!rounded-lg w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
|
||||
icon={<IconDelete />}
|
||||
>
|
||||
{t('删除账户')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountManagement;
|
||||
240
web/src/components/settings/personal/cards/ModelsList.js
Normal file
240
web/src/components/settings/personal/cards/ModelsList.js
Normal file
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Empty,
|
||||
Skeleton,
|
||||
Space,
|
||||
Tag,
|
||||
Collapsible,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Typography,
|
||||
Avatar
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { renderModelTag, getModelCategories } from '../../../../helpers';
|
||||
|
||||
const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
|
||||
// Initialize from localStorage if available
|
||||
const savedState = localStorage.getItem('modelsExpanded');
|
||||
return savedState ? JSON.parse(savedState) : false;
|
||||
});
|
||||
const [activeModelCategory, setActiveModelCategory] = useState('all');
|
||||
const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
|
||||
|
||||
// Save models expanded state to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded));
|
||||
}, [isModelsExpanded]);
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
{/* 卡片头部 */}
|
||||
<div className="flex items-center mb-4">
|
||||
<Avatar size="small" color="green" className="mr-3 shadow-md">
|
||||
<Settings size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className="text-lg font-medium">{t('可用模型')}</Typography.Text>
|
||||
<div className="text-xs text-gray-600">{t('查看当前可用的所有模型')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可用模型部分 */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
{modelsLoading ? (
|
||||
// 骨架屏加载状态 - 模拟实际加载后的布局
|
||||
<div className="space-y-4">
|
||||
{/* 模拟分类标签 */}
|
||||
<div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
|
||||
<div className="flex overflow-x-auto py-2 gap-2">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<Skeleton.Button key={`cat-${index}`} style={{
|
||||
width: index === 0 ? 130 : 100 + Math.random() * 50,
|
||||
height: 36,
|
||||
borderRadius: 8
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模拟模型标签列表 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 20 }).map((_, index) => (
|
||||
<Skeleton.Button
|
||||
key={`model-${index}`}
|
||||
style={{
|
||||
width: 100 + Math.random() * 100,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
margin: '4px'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : models.length === 0 ? (
|
||||
<div className="py-8">
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('没有可用模型')}
|
||||
style={{ padding: '24px 0' }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 模型分类标签页 */}
|
||||
<div className="mb-4">
|
||||
<Tabs
|
||||
type="card"
|
||||
activeKey={activeModelCategory}
|
||||
onChange={key => setActiveModelCategory(key)}
|
||||
className="mt-2"
|
||||
collapsible
|
||||
>
|
||||
{Object.entries(getModelCategories(t)).map(([key, category]) => {
|
||||
// 计算该分类下的模型数量
|
||||
const modelCount = key === 'all'
|
||||
? models.length
|
||||
: models.filter(model => category.filter({ model_name: model })).length;
|
||||
|
||||
if (modelCount === 0 && key !== 'all') return null;
|
||||
|
||||
return (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className="flex items-center gap-2">
|
||||
{category.icon && <span className="w-4 h-4">{category.icon}</span>}
|
||||
{category.label}
|
||||
<Tag
|
||||
color={activeModelCategory === key ? 'red' : 'grey'}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{modelCount}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
itemKey={key}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-700 rounded-lg p-3">
|
||||
{(() => {
|
||||
// 根据当前选中的分类过滤模型
|
||||
const categories = getModelCategories(t);
|
||||
const filteredModels = activeModelCategory === 'all'
|
||||
? models
|
||||
: models.filter(model => categories[activeModelCategory].filter({ model_name: model }));
|
||||
|
||||
// 如果过滤后没有模型,显示空状态
|
||||
if (filteredModels.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 120, height: 120 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />}
|
||||
description={t('该分类下没有可用模型')}
|
||||
style={{ padding: '16px 0' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
|
||||
return (
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) => (
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Collapsible isOpen={isModelsExpanded}>
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) => (
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
onClick={() => setIsModelsExpanded(false)}
|
||||
icon={<IconChevronUp />}
|
||||
>
|
||||
{t('收起')}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Collapsible>
|
||||
{!isModelsExpanded && (
|
||||
<Space wrap>
|
||||
{filteredModels
|
||||
.slice(0, MODELS_DISPLAY_COUNT)
|
||||
.map((model) => (
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
onClick={() => setIsModelsExpanded(true)}
|
||||
icon={<IconChevronDown />}
|
||||
>
|
||||
{t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsList;
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Card,
|
||||
Avatar,
|
||||
Form,
|
||||
Radio,
|
||||
Toast,
|
||||
Tabs,
|
||||
TabPane
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconMail,
|
||||
IconKey,
|
||||
IconBell,
|
||||
IconLink
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { ShieldCheck, Bell, DollarSign } from 'lucide-react';
|
||||
import { renderQuotaWithPrompt } from '../../../../helpers';
|
||||
import CodeViewer from '../../../playground/CodeViewer';
|
||||
|
||||
const NotificationSettings = ({
|
||||
t,
|
||||
notificationSettings,
|
||||
handleNotificationSettingChange,
|
||||
saveNotificationSettings
|
||||
}) => {
|
||||
const formApiRef = useRef(null);
|
||||
|
||||
// 初始化表单值
|
||||
useEffect(() => {
|
||||
if (formApiRef.current && notificationSettings) {
|
||||
formApiRef.current.setValues(notificationSettings);
|
||||
}
|
||||
}, [notificationSettings]);
|
||||
|
||||
// 处理表单字段变化
|
||||
const handleFormChange = (field, value) => {
|
||||
handleNotificationSettingChange(field, value);
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const handleSubmit = () => {
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.validate()
|
||||
.then(() => {
|
||||
saveNotificationSettings();
|
||||
})
|
||||
.catch((errors) => {
|
||||
console.log('表单验证失败:', errors);
|
||||
Toast.error(t('请检查表单填写是否正确'));
|
||||
});
|
||||
} else {
|
||||
saveNotificationSettings();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl shadow-sm border-0"
|
||||
footer={
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t('保存设置')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* 卡片头部 */}
|
||||
<div className="flex items-center mb-4">
|
||||
<Avatar size="small" color="blue" className="mr-3 shadow-md">
|
||||
<Bell size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className="text-lg font-medium">{t('其他设置')}</Typography.Text>
|
||||
<div className="text-xs text-gray-600">{t('通知、价格和隐私相关设置')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
getFormApi={(api) => (formApiRef.current = api)}
|
||||
initValues={notificationSettings}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Tabs type="line" defaultActiveKey="notification">
|
||||
{/* 通知配置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<Bell size={16} className="mr-2" />
|
||||
{t('通知配置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="notification"
|
||||
>
|
||||
<div className="py-4">
|
||||
<Form.RadioGroup
|
||||
field='warningType'
|
||||
label={t('通知方式')}
|
||||
initValue={notificationSettings.warningType}
|
||||
onChange={(value) => handleFormChange('warningType', value)}
|
||||
rules={[{ required: true, message: t('请选择通知方式') }]}
|
||||
>
|
||||
<Radio value="email">{t('邮件通知')}</Radio>
|
||||
<Radio value="webhook">{t('Webhook通知')}</Radio>
|
||||
</Form.RadioGroup>
|
||||
|
||||
<Form.AutoComplete
|
||||
field='warningThreshold'
|
||||
label={
|
||||
<span>
|
||||
{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
|
||||
</span>
|
||||
}
|
||||
placeholder={t('请输入预警额度')}
|
||||
data={[
|
||||
{ value: 100000, label: '0.2$' },
|
||||
{ value: 500000, label: '1$' },
|
||||
{ value: 1000000, label: '5$' },
|
||||
{ value: 5000000, label: '10$' },
|
||||
]}
|
||||
onChange={(val) => handleFormChange('warningThreshold', val)}
|
||||
prefix={<IconBell />}
|
||||
extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
|
||||
style={{ width: '100%', maxWidth: '300px' }}
|
||||
rules={[
|
||||
{ required: true, message: t('请输入预警阈值') },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
const numValue = Number(value);
|
||||
if (isNaN(numValue) || numValue <= 0) {
|
||||
return Promise.reject(t('预警阈值必须为正数'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 邮件通知设置 */}
|
||||
{notificationSettings.warningType === 'email' && (
|
||||
<Form.Input
|
||||
field='notificationEmail'
|
||||
label={t('通知邮箱')}
|
||||
placeholder={t('留空则使用账号绑定的邮箱')}
|
||||
onChange={(val) => handleFormChange('notificationEmail', val)}
|
||||
prefix={<IconMail />}
|
||||
extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Webhook通知设置 */}
|
||||
{notificationSettings.warningType === 'webhook' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field='webhookUrl'
|
||||
label={t('Webhook地址')}
|
||||
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
|
||||
onChange={(val) => handleFormChange('webhookUrl', val)}
|
||||
prefix={<IconLink />}
|
||||
extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')}
|
||||
showClear
|
||||
rules={[
|
||||
{
|
||||
required: notificationSettings.warningType === 'webhook',
|
||||
message: t('请输入Webhook地址')
|
||||
},
|
||||
{
|
||||
pattern: /^https:\/\/.+/,
|
||||
message: t('Webhook地址必须以https://开头')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field='webhookSecret'
|
||||
label={t('接口凭证')}
|
||||
placeholder={t('请输入密钥')}
|
||||
onChange={(val) => handleFormChange('webhookSecret', val)}
|
||||
prefix={<IconKey />}
|
||||
extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')}
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Form.Slot label={t('Webhook请求结构说明')}>
|
||||
<div>
|
||||
<div style={{ height: '200px', marginBottom: '12px' }}>
|
||||
<CodeViewer
|
||||
content={{
|
||||
"type": "quota_exceed",
|
||||
"title": "额度预警通知",
|
||||
"content": "您的额度即将用尽,当前剩余额度为 {{value}}",
|
||||
"values": ["$0.99"],
|
||||
"timestamp": 1739950503
|
||||
}}
|
||||
title="webhook"
|
||||
language="json"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 leading-relaxed">
|
||||
<div><strong>type:</strong> 通知类型 (quota_exceed: 额度预警)</div>
|
||||
<div><strong>title:</strong> 通知标题</div>
|
||||
<div><strong>content:</strong> 通知内容,支持 {`{{value}}`} 变量占位符</div>
|
||||
<div><strong>values:</strong> 按顺序替换content中的变量占位符</div>
|
||||
<div><strong>timestamp:</strong> Unix时间戳</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Slot>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
{/* 价格设置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<DollarSign size={16} className="mr-2" />
|
||||
{t('价格设置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="pricing"
|
||||
>
|
||||
<div className="py-4">
|
||||
<Form.Switch
|
||||
field='acceptUnsetModelRatioModel'
|
||||
label={t('接受未设置价格模型')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) => handleFormChange('acceptUnsetModelRatioModel', value)}
|
||||
extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
{/* 隐私设置 Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<ShieldCheck size={16} className="mr-2" />
|
||||
{t('隐私设置')}
|
||||
</div>
|
||||
}
|
||||
itemKey="privacy"
|
||||
>
|
||||
<div className="py-4">
|
||||
<Form.Switch
|
||||
field='recordIpLog'
|
||||
label={t('记录请求与错误日志IP')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) => handleFormChange('recordIpLog', value)}
|
||||
extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationSettings;
|
||||
663
web/src/components/settings/personal/components/TwoFASetting.js
Normal file
663
web/src/components/settings/personal/components/TwoFASetting.js
Normal file
@@ -0,0 +1,663 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { API, showError, showSuccess, showWarning } from '../../../../helpers';
|
||||
import { Banner, Button, Card, Checkbox, Divider, Input, Modal, Tag, Typography, Steps, Space, Badge } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconShield,
|
||||
IconAlertTriangle,
|
||||
IconRefresh,
|
||||
IconCopy
|
||||
} from '@douyinfe/semi-icons';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
const TwoFASetting = ({ t }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState({
|
||||
enabled: false,
|
||||
locked: false,
|
||||
backup_codes_remaining: 0
|
||||
});
|
||||
|
||||
// 模态框状态
|
||||
const [setupModalVisible, setSetupModalVisible] = useState(false);
|
||||
const [enableModalVisible, setEnableModalVisible] = useState(false);
|
||||
const [disableModalVisible, setDisableModalVisible] = useState(false);
|
||||
const [backupModalVisible, setBackupModalVisible] = useState(false);
|
||||
|
||||
// 表单数据
|
||||
const [setupData, setSetupData] = useState(null);
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [backupCodes, setBackupCodes] = useState([]);
|
||||
const [confirmDisable, setConfirmDisable] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
// 获取2FA状态
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const res = await API.get('/api/user/2fa/status');
|
||||
if (res.data.success) {
|
||||
setStatus(res.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('获取2FA状态失败'));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
}, []);
|
||||
|
||||
// 初始化2FA设置
|
||||
const handleSetup2FA = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/2fa/setup');
|
||||
if (res.data.success) {
|
||||
setSetupData(res.data.data);
|
||||
setSetupModalVisible(true);
|
||||
setCurrentStep(0);
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('设置2FA失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 启用2FA
|
||||
const handleEnable2FA = async () => {
|
||||
if (!verificationCode) {
|
||||
showWarning(t('请输入验证码'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/2fa/enable', {
|
||||
code: verificationCode
|
||||
});
|
||||
if (res.data.success) {
|
||||
showSuccess(t('两步验证启用成功!'));
|
||||
setEnableModalVisible(false);
|
||||
setSetupModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setCurrentStep(0);
|
||||
fetchStatus();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('启用2FA失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 禁用2FA
|
||||
const handleDisable2FA = async () => {
|
||||
if (!verificationCode) {
|
||||
showWarning(t('请输入验证码或备用码'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmDisable) {
|
||||
showWarning(t('请确认您已了解禁用两步验证的后果'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/2fa/disable', {
|
||||
code: verificationCode
|
||||
});
|
||||
if (res.data.success) {
|
||||
showSuccess(t('两步验证已禁用'));
|
||||
setDisableModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setConfirmDisable(false);
|
||||
fetchStatus();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('禁用2FA失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重新生成备用码
|
||||
const handleRegenerateBackupCodes = async () => {
|
||||
if (!verificationCode) {
|
||||
showWarning(t('请输入验证码'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/2fa/backup_codes', {
|
||||
code: verificationCode
|
||||
});
|
||||
if (res.data.success) {
|
||||
setBackupCodes(res.data.data.backup_codes);
|
||||
showSuccess(t('备用码重新生成成功'));
|
||||
setVerificationCode('');
|
||||
fetchStatus();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('重新生成备用码失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 通用复制函数
|
||||
const copyTextToClipboard = (text, successMessage = t('已复制到剪贴板')) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showSuccess(successMessage);
|
||||
}).catch(() => {
|
||||
showError(t('复制失败,请手动复制'));
|
||||
});
|
||||
};
|
||||
|
||||
const copyBackupCodes = () => {
|
||||
const codesText = backupCodes.join('\n');
|
||||
copyTextToClipboard(codesText, t('备用码已复制到剪贴板'));
|
||||
};
|
||||
|
||||
// 备用码展示组件
|
||||
const BackupCodesDisplay = ({ codes, title, onCopy }) => {
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-xl"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text strong className="text-slate-700 dark:text-slate-200">
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{codes.map((code, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text code className="text-sm font-mono text-slate-700 dark:text-slate-200">
|
||||
{code}
|
||||
</Text>
|
||||
<Text type="quaternary" className="text-xs">
|
||||
#{(index + 1).toString().padStart(2, '0')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Divider margin={12} />
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
icon={<IconCopy />}
|
||||
onClick={onCopy}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full"
|
||||
>
|
||||
{t('复制所有代码')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染设置模态框footer
|
||||
const renderSetupModalFooter = () => {
|
||||
return (
|
||||
<>
|
||||
{currentStep > 0 && (
|
||||
<Button
|
||||
onClick={() => setCurrentStep(currentStep - 1)}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{t('上一步')}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < 2 ? (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={() => setCurrentStep(currentStep + 1)}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
>
|
||||
{t('下一步')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
if (!verificationCode) {
|
||||
showWarning(t('请输入验证码'));
|
||||
return;
|
||||
}
|
||||
handleEnable2FA();
|
||||
}}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
>
|
||||
{t('完成设置并启用两步验证')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染禁用模态框footer
|
||||
const renderDisableModalFooter = () => {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisableModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setConfirmDisable(false);
|
||||
}}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
theme="solid"
|
||||
loading={loading}
|
||||
disabled={!confirmDisable || !verificationCode}
|
||||
onClick={handleDisable2FA}
|
||||
className="!rounded-lg !bg-slate-500 hover:!bg-slate-600"
|
||||
>
|
||||
{t('确认禁用')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染重新生成模态框footer
|
||||
const renderRegenerateModalFooter = () => {
|
||||
if (backupCodes.length > 0) {
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={() => {
|
||||
setBackupModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setBackupCodes([]);
|
||||
}}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
>
|
||||
{t('完成')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setBackupModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setBackupCodes([]);
|
||||
}}
|
||||
className="!rounded-lg"
|
||||
>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
loading={loading}
|
||||
disabled={!verificationCode}
|
||||
onClick={handleRegenerateBackupCodes}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
>
|
||||
{t('生成新的备用码')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="!rounded-xl w-full">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconShield size="large" className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Typography.Title heading={6} className="mb-0">
|
||||
{t('两步验证设置')}
|
||||
</Typography.Title>
|
||||
{status.enabled ? (
|
||||
<Tag color="green" shape="circle" size="small">{t('已启用')}</Tag>
|
||||
) : (
|
||||
<Tag color="red" shape="circle" size="small">{t('未启用')}</Tag>
|
||||
)}
|
||||
{status.locked && (
|
||||
<Tag color="orange" shape="circle" size="small">{t('账户已锁定')}</Tag>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
{t('两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。')}
|
||||
</Typography.Text>
|
||||
{status.enabled && (
|
||||
<div className="mt-2">
|
||||
<Text size="small" type="secondary">{t('剩余备用码:')}{status.backup_codes_remaining || 0}{t('个')}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2 w-full sm:w-auto">
|
||||
{!status.enabled ? (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size="default"
|
||||
onClick={handleSetup2FA}
|
||||
loading={loading}
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
icon={<IconShield />}
|
||||
>
|
||||
{t('启用验证')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Button
|
||||
type="danger"
|
||||
theme="solid"
|
||||
size="default"
|
||||
onClick={() => setDisableModalVisible(true)}
|
||||
className="!rounded-lg !bg-slate-500 hover:!bg-slate-600"
|
||||
icon={<IconAlertTriangle />}
|
||||
>
|
||||
{t('禁用两步验证')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size="default"
|
||||
onClick={() => setBackupModalVisible(true)}
|
||||
className="!rounded-lg"
|
||||
icon={<IconRefresh />}
|
||||
>
|
||||
{t('重新生成备用码')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 2FA设置模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconShield className="mr-2 text-slate-600" />
|
||||
{t('设置两步验证')}
|
||||
</div>
|
||||
}
|
||||
visible={setupModalVisible}
|
||||
onCancel={() => {
|
||||
setSetupModalVisible(false);
|
||||
setSetupData(null);
|
||||
setCurrentStep(0);
|
||||
setVerificationCode('');
|
||||
}}
|
||||
footer={renderSetupModalFooter()}
|
||||
width={650}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
{setupData && (
|
||||
<div className="space-y-6">
|
||||
{/* 步骤进度 */}
|
||||
<Steps type="basic" size="small" current={currentStep}>
|
||||
<Steps.Step title={t('扫描二维码')} description={t('使用认证器应用扫描二维码')} />
|
||||
<Steps.Step title={t('保存备用码')} description={t('保存备用码以备不时之需')} />
|
||||
<Steps.Step title={t('验证设置')} description={t('输入验证码完成设置')} />
|
||||
</Steps>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
<div className="rounded-xl">
|
||||
{currentStep === 0 && (
|
||||
<div>
|
||||
<Paragraph className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{t('使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:')}
|
||||
</Paragraph>
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<QRCodeSVG value={setupData.qr_code_data} size={180} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
|
||||
<Text className="text-blue-800 dark:text-blue-200 text-sm">
|
||||
{t('或手动输入密钥:')}<Text code copyable className="ml-2">{setupData.secret}</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<div className="space-y-4">
|
||||
{/* 备用码展示 */}
|
||||
<BackupCodesDisplay
|
||||
codes={setupData.backup_codes}
|
||||
title={t('备用恢复代码')}
|
||||
onCopy={() => {
|
||||
const codesText = setupData.backup_codes.join('\n');
|
||||
copyTextToClipboard(codesText, t('备用码已复制到剪贴板'));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<Input
|
||||
placeholder={t('输入认证器应用显示的6位数字验证码')}
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
size="large"
|
||||
maxLength={6}
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 禁用2FA模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconAlertTriangle className="mr-2 text-red-500" />
|
||||
{t('禁用两步验证')}
|
||||
</div>
|
||||
}
|
||||
visible={disableModalVisible}
|
||||
onCancel={() => {
|
||||
setDisableModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setConfirmDisable(false);
|
||||
}}
|
||||
footer={renderDisableModalFooter()}
|
||||
width={550}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* 警告提示 */}
|
||||
<div className="rounded-xl">
|
||||
<Banner
|
||||
type="warning"
|
||||
description={t('警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!')}
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
|
||||
{t('禁用后的影响:')}
|
||||
</Text>
|
||||
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<Badge dot type='warning' />
|
||||
{t('降低您账户的安全性')}
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Badge dot type='warning' />
|
||||
{t('需要重新完整设置才能再次启用')}
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Badge dot type='danger' />
|
||||
{t('永久删除您的两步验证设置')}
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Badge dot type='danger' />
|
||||
{t('永久删除所有备用码(包括未使用的)')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Divider margin={16} />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
|
||||
{t('验证身份')}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder={t('请输入认证器验证码或备用码')}
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={confirmDisable}
|
||||
onChange={(e) => setConfirmDisable(e.target.checked)}
|
||||
className="text-sm"
|
||||
>
|
||||
{t('我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 重新生成备用码模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconRefresh className="mr-2 text-slate-600" />
|
||||
{t('重新生成备用码')}
|
||||
</div>
|
||||
}
|
||||
visible={backupModalVisible}
|
||||
onCancel={() => {
|
||||
setBackupModalVisible(false);
|
||||
setVerificationCode('');
|
||||
setBackupCodes([]);
|
||||
}}
|
||||
footer={renderRegenerateModalFooter()}
|
||||
width={500}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{backupCodes.length === 0 ? (
|
||||
<>
|
||||
{/* 警告提示 */}
|
||||
<div className="rounded-xl">
|
||||
<Banner
|
||||
type="warning"
|
||||
description={t('重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。')}
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 验证区域 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text strong className="block mb-2 text-slate-700 dark:text-slate-200">
|
||||
{t('验证身份')}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder={t('请输入认证器验证码')}
|
||||
value={verificationCode}
|
||||
onChange={setVerificationCode}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 成功提示 */}
|
||||
<Space vertical style={{ width: '100%' }}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Badge dot type='success' />
|
||||
<Text strong className="text-lg text-slate-700 dark:text-slate-200">
|
||||
{t('新的备用码已生成')}
|
||||
</Text>
|
||||
</div>
|
||||
<Text className="text-slate-500 dark:text-slate-400 text-sm">
|
||||
{t('旧的备用码已失效,请保存新的备用码')}
|
||||
</Text>
|
||||
|
||||
{/* 备用码展示 */}
|
||||
<BackupCodesDisplay
|
||||
codes={backupCodes}
|
||||
title={t('新的备用恢复代码')}
|
||||
onCopy={copyBackupCodes}
|
||||
/>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoFASetting;
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar, Card, Tag, Divider, Typography } from '@douyinfe/semi-ui';
|
||||
import { isRoot, isAdmin, renderQuota } from '../../../../helpers';
|
||||
import { useTheme } from '../../../../context/Theme';
|
||||
import { Coins, BarChart2, Users } from 'lucide-react';
|
||||
|
||||
const UserInfoHeader = ({ t, userState }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const getUsername = () => {
|
||||
if (userState.user) {
|
||||
return userState.user.username;
|
||||
} else {
|
||||
return 'null';
|
||||
}
|
||||
};
|
||||
|
||||
const getAvatarText = () => {
|
||||
const username = getUsername();
|
||||
if (username && username.length > 0) {
|
||||
// 获取前两个字符,支持中文和英文
|
||||
return username.slice(0, 2).toUpperCase();
|
||||
}
|
||||
return 'NA';
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl !border-0"
|
||||
style={{
|
||||
background: theme === 'dark'
|
||||
? 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)'
|
||||
: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-slate-400 dark:bg-slate-500 opacity-5 rounded-full"></div>
|
||||
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-slate-300 dark:bg-slate-400 opacity-8 rounded-full"></div>
|
||||
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-slate-400 dark:bg-slate-500 opacity-6 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative p-4 sm:p-6 md:p-8 text-gray-600 dark:text-gray-300">
|
||||
<div className="flex justify-between items-start mb-4 sm:mb-6">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<Avatar
|
||||
size='large'
|
||||
className="mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400"
|
||||
>
|
||||
{getAvatarText()}
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base sm:text-lg font-semibold truncate text-gray-800 dark:text-gray-100">
|
||||
{getUsername()}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap gap-1 sm:gap-2">
|
||||
{isRoot() ? (
|
||||
<Tag
|
||||
size='small'
|
||||
className="!rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
{t('超级管理员')}
|
||||
</Tag>
|
||||
) : isAdmin() ? (
|
||||
<Tag
|
||||
size='small'
|
||||
className="!rounded-full bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
{t('管理员')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag
|
||||
size='small'
|
||||
className="!rounded-full bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
{t('普通用户')}
|
||||
</Tag>
|
||||
)}
|
||||
<Tag
|
||||
size='small'
|
||||
className="!rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
ID: {userState?.user?.id}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右上角统计信息(Semi UI 卡片) */}
|
||||
<div className="hidden sm:block flex-shrink-0 ml-2">
|
||||
<Card size="small" className="!rounded-xl !border-0 shadow-sm" bodyStyle={{ padding: '8px 12px' }}>
|
||||
<div className="flex items-center gap-3 lg:gap-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Coins size={16} className="text-slate-600 dark:text-slate-300" />
|
||||
<div className="text-right">
|
||||
<Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
|
||||
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{renderQuota(userState?.user?.used_quota)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<BarChart2 size={16} className="text-slate-600 dark:text-slate-300" />
|
||||
<div className="text-right">
|
||||
<Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
|
||||
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{userState.user?.request_count || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Users size={16} className="text-slate-600 dark:text-slate-300" />
|
||||
<div className="text-right">
|
||||
<Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
|
||||
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{userState?.user?.group || t('默认')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<div className="text-xs sm:text-sm mb-1 sm:mb-2 text-gray-500 dark:text-gray-400">
|
||||
{t('当前余额')}
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide text-gray-900 dark:text-gray-100">
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移动端统计信息卡片(仅 xs 可见) */}
|
||||
<div className="sm:hidden">
|
||||
<Card size="small" className="!rounded-xl !border-0 shadow-sm" bodyStyle={{ padding: '10px 12px' }}>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins size={16} className="text-slate-600" />
|
||||
<Typography.Text size="small" type="tertiary">{t('历史消耗')}</Typography.Text>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-gray-800">{renderQuota(userState?.user?.used_quota)}</div>
|
||||
</div>
|
||||
<Divider margin='8px' />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart2 size={16} className="text-slate-600" />
|
||||
<Typography.Text size="small" type="tertiary">{t('请求次数')}</Typography.Text>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-gray-800">{userState.user?.request_count || 0}</div>
|
||||
</div>
|
||||
<Divider margin='8px' />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users size={16} className="text-slate-600" />
|
||||
<Typography.Text size="small" type="tertiary">{t('用户分组')}</Typography.Text>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-gray-800">{userState?.user?.group || t('默认')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"></div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserInfoHeader;
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Banner, Input, Modal, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconUser } from '@douyinfe/semi-icons';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const AccountDeleteModal = ({
|
||||
t,
|
||||
showAccountDeleteModal,
|
||||
setShowAccountDeleteModal,
|
||||
inputs,
|
||||
handleInputChange,
|
||||
deleteAccount,
|
||||
userState,
|
||||
turnstileEnabled,
|
||||
turnstileSiteKey,
|
||||
setTurnstileToken
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconDelete className="mr-2 text-red-500" />
|
||||
{t('删除账户确认')}
|
||||
</div>
|
||||
}
|
||||
visible={showAccountDeleteModal}
|
||||
onCancel={() => setShowAccountDeleteModal(false)}
|
||||
onOk={deleteAccount}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
className="modern-modal"
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<Banner
|
||||
type='danger'
|
||||
description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')}
|
||||
closeIcon={null}
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2 text-red-600">
|
||||
{t('请输入您的用户名以确认删除')}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
placeholder={t('输入你的账户名{{username}}以确认删除', { username: ` ${userState?.user?.username} ` })}
|
||||
name='self_account_deletion_confirmation'
|
||||
value={inputs.self_account_deletion_confirmation}
|
||||
onChange={(value) =>
|
||||
handleInputChange('self_account_deletion_confirmation', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconUser />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center">
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountDeleteModal;
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Input, Modal, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconLock } from '@douyinfe/semi-icons';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const ChangePasswordModal = ({
|
||||
t,
|
||||
showChangePasswordModal,
|
||||
setShowChangePasswordModal,
|
||||
inputs,
|
||||
handleInputChange,
|
||||
changePassword,
|
||||
turnstileEnabled,
|
||||
turnstileSiteKey,
|
||||
setTurnstileToken
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconLock className="mr-2 text-orange-500" />
|
||||
{t('修改密码')}
|
||||
</div>
|
||||
}
|
||||
visible={showChangePasswordModal}
|
||||
onCancel={() => setShowChangePasswordModal(false)}
|
||||
onOk={changePassword}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
className="modern-modal"
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">{t('原密码')}</Typography.Text>
|
||||
<Input
|
||||
name='original_password'
|
||||
placeholder={t('请输入原密码')}
|
||||
type='password'
|
||||
value={inputs.original_password}
|
||||
onChange={(value) =>
|
||||
handleInputChange('original_password', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconLock />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">{t('新密码')}</Typography.Text>
|
||||
<Input
|
||||
name='set_new_password'
|
||||
placeholder={t('请输入新密码')}
|
||||
type='password'
|
||||
value={inputs.set_new_password}
|
||||
onChange={(value) =>
|
||||
handleInputChange('set_new_password', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconLock />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">{t('确认新密码')}</Typography.Text>
|
||||
<Input
|
||||
name='set_new_password_confirmation'
|
||||
placeholder={t('请再次输入新密码')}
|
||||
type='password'
|
||||
value={inputs.set_new_password_confirmation}
|
||||
onChange={(value) =>
|
||||
handleInputChange('set_new_password_confirmation', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconLock />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center">
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordModal;
|
||||
106
web/src/components/settings/personal/modals/EmailBindModal.js
Normal file
106
web/src/components/settings/personal/modals/EmailBindModal.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Input, Modal } from '@douyinfe/semi-ui';
|
||||
import { IconMail, IconKey } from '@douyinfe/semi-icons';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const EmailBindModal = ({
|
||||
t,
|
||||
showEmailBindModal,
|
||||
setShowEmailBindModal,
|
||||
inputs,
|
||||
handleInputChange,
|
||||
sendVerificationCode,
|
||||
bindEmail,
|
||||
disableButton,
|
||||
loading,
|
||||
countdown,
|
||||
turnstileEnabled,
|
||||
turnstileSiteKey,
|
||||
setTurnstileToken
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconMail className="mr-2 text-blue-500" />
|
||||
{t('绑定邮箱地址')}
|
||||
</div>
|
||||
}
|
||||
visible={showEmailBindModal}
|
||||
onCancel={() => setShowEmailBindModal(false)}
|
||||
onOk={bindEmail}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
className="modern-modal"
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
placeholder={t('输入邮箱地址')}
|
||||
onChange={(value) => handleInputChange('email', value)}
|
||||
name='email'
|
||||
type='email'
|
||||
size="large"
|
||||
className="!rounded-lg flex-1"
|
||||
prefix={<IconMail />}
|
||||
/>
|
||||
<Button
|
||||
onClick={sendVerificationCode}
|
||||
disabled={disableButton || loading}
|
||||
className="!rounded-lg"
|
||||
type="primary"
|
||||
theme="outline"
|
||||
size='large'
|
||||
>
|
||||
{disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
placeholder={t('验证码')}
|
||||
name='email_verification_code'
|
||||
value={inputs.email_verification_code}
|
||||
onChange={(value) =>
|
||||
handleInputChange('email_verification_code', value)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconKey />}
|
||||
/>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center">
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailBindModal;
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Input, Modal, Image } from '@douyinfe/semi-ui';
|
||||
import { IconKey } from '@douyinfe/semi-icons';
|
||||
import { SiWechat } from 'react-icons/si';
|
||||
|
||||
const WeChatBindModal = ({
|
||||
t,
|
||||
showWeChatBindModal,
|
||||
setShowWeChatBindModal,
|
||||
inputs,
|
||||
handleInputChange,
|
||||
bindWeChat,
|
||||
status
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<SiWechat className="mr-2 text-green-500" size={20} />
|
||||
{t('绑定微信账户')}
|
||||
</div>
|
||||
}
|
||||
visible={showWeChatBindModal}
|
||||
onCancel={() => setShowWeChatBindModal(false)}
|
||||
footer={null}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
className="modern-modal"
|
||||
>
|
||||
<div className="space-y-4 py-4 text-center">
|
||||
<Image src={status.wechat_qrcode} className="mx-auto" />
|
||||
<div className="text-gray-600">
|
||||
<p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('验证码')}
|
||||
name='wechat_verification_code'
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={(v) =>
|
||||
handleInputChange('wechat_verification_code', v)
|
||||
}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconKey />}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
size='large'
|
||||
onClick={bindWeChat}
|
||||
className="!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700"
|
||||
icon={<SiWechat size={16} />}
|
||||
>
|
||||
{t('绑定')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WeChatBindModal;
|
||||
Reference in New Issue
Block a user