Commit 334011d4 by Apple\Apple

🍎refactor(PersonalSettings): Comprehensive UI/UX redesign and improvements

- Unify input field styles with size="large" for consistent appearance
- Replace account binding sections with Semi UI Card components for better visual hierarchy
- Redesign invitation statistics cards with responsive layout and Card components
- Enhance security settings with structured Card layout and improved visual design
- Complete i18n internationalization for all text strings and placeholders
- Optimize main profile card responsive design following TopUp page patterns
- Update avatar component to display first two characters with animated border
- Improve user role display with three-tier system (Super Admin/Admin/User)
- Enhance tag visibility with white background for better contrast on purple gradient
- Implement dynamic colors for model tags using stringToColor function
- Add hover effects and improved accessibility across all components
- Maintain consistent design language throughout the interface

This refactor significantly improves the user experience with modern UI patterns,
better responsiveness, and enhanced visual appeal while maintaining all existing
functionality.
parent aae61434
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
API, API,
copy, copy,
isRoot, isRoot,
isAdmin,
showError, showError,
showInfo, showInfo,
showSuccess, showSuccess,
...@@ -20,7 +21,6 @@ import { ...@@ -20,7 +21,6 @@ import {
Banner, Banner,
Button, Button,
Card, Card,
Descriptions,
Image, Image,
Input, Input,
InputNumber, InputNumber,
...@@ -30,7 +30,6 @@ import { ...@@ -30,7 +30,6 @@ import {
Tag, Tag,
Typography, Typography,
Collapsible, Collapsible,
Select,
Radio, Radio,
RadioGroup, RadioGroup,
AutoComplete, AutoComplete,
...@@ -39,6 +38,23 @@ import { ...@@ -39,6 +38,23 @@ import {
TabPane, TabPane,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconMail,
IconLock,
IconShield,
IconUser,
IconSetting,
IconBell,
IconGithubLogo,
IconKey,
IconCreditCard,
IconLink,
IconDelete,
IconChevronDown,
IconChevronUp,
} from '@douyinfe/semi-icons';
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
import { Bell, Shield, Webhook, Globe, Settings, UserPlus, ShieldCheck } from 'lucide-react';
import {
getQuotaPerUnit, getQuotaPerUnit,
renderQuota, renderQuota,
renderQuotaWithPrompt, renderQuotaWithPrompt,
...@@ -82,7 +98,7 @@ const PersonalSetting = () => { ...@@ -82,7 +98,7 @@ const PersonalSetting = () => {
const savedState = localStorage.getItem('modelsExpanded'); const savedState = localStorage.getItem('modelsExpanded');
return savedState ? JSON.parse(savedState) : false; return savedState ? JSON.parse(savedState) : false;
}); });
const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量 const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
const [notificationSettings, setNotificationSettings] = useState({ const [notificationSettings, setNotificationSettings] = useState({
warningType: 'email', warningType: 'email',
warningThreshold: 100000, warningThreshold: 100000,
...@@ -91,7 +107,7 @@ const PersonalSetting = () => { ...@@ -91,7 +107,7 @@ const PersonalSetting = () => {
notificationEmail: '', notificationEmail: '',
acceptUnsetModelRatioModel: false, acceptUnsetModelRatioModel: false,
}); });
const [showWebhookDocs, setShowWebhookDocs] = useState(false); const [showWebhookDocs, setShowWebhookDocs] = useState(true);
useEffect(() => { useEffect(() => {
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
...@@ -295,7 +311,7 @@ const PersonalSetting = () => { ...@@ -295,7 +311,7 @@ const PersonalSetting = () => {
} }
setDisableButton(true); setDisableButton(true);
if (turnstileEnabled && turnstileToken === '') { if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
return; return;
} }
setLoading(true); setLoading(true);
...@@ -339,6 +355,15 @@ const PersonalSetting = () => { ...@@ -339,6 +355,15 @@ const PersonalSetting = () => {
} }
}; };
const getAvatarText = () => {
const username = getUsername();
if (username && username.length > 0) {
// 获取前两个字符,支持中文和英文
return username.slice(0, 2).toUpperCase();
}
return 'NA';
};
const handleCancel = () => { const handleCancel = () => {
setOpenTransfer(false); setOpenTransfer(false);
}; };
...@@ -385,11 +410,17 @@ const PersonalSetting = () => { ...@@ -385,11 +410,17 @@ const PersonalSetting = () => {
}; };
return ( return (
<div> <div className="min-h-screen bg-gray-50">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
{/* 划转模态框 */}
<Modal <Modal
title={t('请输入要划转的数量')} title={
<div className="flex items-center">
<IconCreditCard className="mr-2" />
{t('请输入要划转的数量')}
</div>
}
visible={openTransfer} visible={openTransfer}
onOk={transfer} onOk={transfer}
onCancel={handleCancel} onCancel={handleCancel}
...@@ -397,85 +428,214 @@ const PersonalSetting = () => { ...@@ -397,85 +428,214 @@ const PersonalSetting = () => {
size={'small'} size={'small'}
centered={true} centered={true}
> >
<div style={{ marginTop: 20 }}> <div className="space-y-4 py-4">
<Typography.Text> <div>
{t('可用额度')} <Typography.Text strong className="block mb-2">
{renderQuotaWithPrompt(userState?.user?.aff_quota)} {t('可用额度')} {renderQuotaWithPrompt(userState?.user?.aff_quota)}
</Typography.Text> </Typography.Text>
<Input <Input
style={{ marginTop: 5 }}
value={userState?.user?.aff_quota} value={userState?.user?.aff_quota}
disabled={true} disabled={true}
></Input> size="large"
className="!rounded-lg"
/>
</div> </div>
<div style={{ marginTop: 20 }}> <div>
<Typography.Text> <Typography.Text strong className="block mb-2">
{t('划转额度')} {t('划转额度')} {renderQuotaWithPrompt(transferAmount)}{' '}
{renderQuotaWithPrompt(transferAmount)}{' '}
{t('最低') + renderQuota(getQuotaPerUnit())} {t('最低') + renderQuota(getQuotaPerUnit())}
</Typography.Text> </Typography.Text>
<div>
<InputNumber <InputNumber
min={0} min={0}
style={{ marginTop: 5 }}
value={transferAmount} value={transferAmount}
onChange={(value) => setTransferAmount(value)} onChange={(value) => setTransferAmount(value)}
disabled={false} disabled={false}
></InputNumber> size="large"
className="!rounded-lg w-full"
/>
</div> </div>
</div> </div>
</Modal> </Modal>
<div>
<div className="flex justify-center">
<div className="w-full max-w-6xl">
{/* 主卡片容器 */}
<Card className="!rounded-2xl shadow-lg border-0">
{/* 顶部用户信息区域 */}
<Card <Card
title={ className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden"
<Card.Meta style={{
avatar={ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 25%, #a855f7 50%, #c084fc 75%, #d8b4fe 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-white opacity-5 rounded-full"></div>
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-3 rounded-full"></div>
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div>
</div>
<div className="relative p-4 sm:p-6 md:p-8" style={{ color: 'white' }}>
<div className="flex justify-between items-start mb-4 sm:mb-6">
<div className="flex items-center flex-1 min-w-0">
<Avatar <Avatar
size='default' size='large'
color={stringToColor(getUsername())} color={stringToColor(getUsername())}
style={{ marginRight: 4 }} border={{ motion: true }}
contentMotion={true}
className="mr-3 sm:mr-4 shadow-lg flex-shrink-0"
> >
{typeof getUsername() === 'string' && {getAvatarText()}
getUsername().slice(0, 1)}
</Avatar> </Avatar>
} <div className="flex-1 min-w-0">
title={<Typography.Text>{getUsername()}</Typography.Text>} <div className="text-base sm:text-lg font-semibold truncate" style={{ color: 'white' }}>
description={ {getUsername()}
isRoot() ? ( </div>
<Tag color='red'>{t('管理员')}</Tag> <div className="mt-1 flex flex-wrap gap-1 sm:gap-2">
{isRoot() ? (
<Tag
color='red'
size='small'
style={{
backgroundColor: 'rgba(255, 255, 255, 0.95)',
color: '#dc2626',
fontWeight: '600'
}}
className="!rounded-full"
>
{t('超级管理员')}
</Tag>
) : isAdmin() ? (
<Tag
color='orange'
size='small'
style={{
backgroundColor: 'rgba(255, 255, 255, 0.95)',
color: '#ea580c',
fontWeight: '600'
}}
className="!rounded-full"
>
{t('管理员')}
</Tag>
) : ( ) : (
<Tag color='blue'>{t('普通用户')}</Tag> <Tag
) color='blue'
} size='small'
></Card.Meta> style={{
} backgroundColor: 'rgba(255, 255, 255, 0.95)',
headerExtraContent={ color: '#2563eb',
<> fontWeight: '600'
<Space vertical align='start'> }}
<Tag color='green'>{'ID: ' + userState?.user?.id}</Tag> className="!rounded-full"
<Tag color='blue'>{userState?.user?.group}</Tag> >
</Space> {t('普通用户')}
</> </Tag>
} )}
footer={ <Tag
<> color='green'
size='small'
className="!rounded-full"
style={{
backgroundColor: 'rgba(255, 255, 255, 0.95)',
color: '#16a34a',
fontWeight: '600'
}}
>
ID: {userState?.user?.id}
</Tag>
</div>
</div>
</div>
<div <div
style={{ display: 'flex', alignItems: 'center', gap: 8 }} className="w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-lg flex-shrink-0 ml-2"
style={{
background: `linear-gradient(135deg, ${stringToColor(getUsername())} 0%, #f59e0b 100%)`
}}
> >
<Typography.Title heading={6}> <IconUser size="default" style={{ color: 'white' }} />
{t('可用模型')} </div>
</Typography.Title>
</div> </div>
<div style={{ marginTop: 10 }}>
<div className="mb-4 sm:mb-6">
<div className="text-xs sm:text-sm mb-1 sm:mb-2" style={{ color: 'rgba(255, 255, 255, 0.7)' }}>
{t('当前余额')}
</div>
<div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide" style={{ color: 'white' }}>
{renderQuota(userState?.user?.quota)}
</div>
</div>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-end">
<div className="grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0">
<div className="text-center sm:text-left">
<div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}>
{t('历史消耗')}
</div>
<div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}>
{renderQuota(userState?.user?.used_quota)}
</div>
</div>
<div className="text-center sm:text-left">
<div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}>
{t('请求次数')}
</div>
<div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}>
{userState.user?.request_count || 0}
</div>
</div>
<div className="text-center sm:text-left">
<div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}>
{t('用户分组')}
</div>
<div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}>
{userState?.user?.group || t('默认')}
</div>
</div>
</div>
</div>
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-yellow-400 via-orange-400 to-red-400" style={{ opacity: 0.6 }}></div>
</div>
</Card>
{/* 主内容区域 - 使用Tabs组织不同功能模块 */}
<div className="p-4">
<Tabs type='line' defaultActiveKey='models' className="modern-tabs">
{/* 模型与邀请Tab */}
<TabPane
tab={
<div className="flex items-center">
<Settings size={16} className="mr-2" />
{t('模型与邀请')}
</div>
}
itemKey='models'
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 py-4">
{/* 可用模型部分 */}
<div className="bg-gray-50 rounded-xl">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-3">
<Settings size={20} className="text-purple-500" />
</div>
<div>
<Typography.Title heading={6} className="mb-0">{t('可用模型')}</Typography.Title>
<div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div>
</div>
</div>
<div className="bg-white rounded-lg p-3">
{models.length <= MODELS_DISPLAY_COUNT ? ( {models.length <= MODELS_DISPLAY_COUNT ? (
<Space wrap> <Space wrap>
{models.map((model) => ( {models.map((model) => (
<Tag <Tag
key={model} key={model}
color='cyan' color={stringToColor(model)}
onClick={() => { onClick={() => copyText(model)}
copyText(model); className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
}}
> >
{model} {model}
</Tag> </Tag>
...@@ -488,19 +648,19 @@ const PersonalSetting = () => { ...@@ -488,19 +648,19 @@ const PersonalSetting = () => {
{models.map((model) => ( {models.map((model) => (
<Tag <Tag
key={model} key={model}
color='cyan' color={stringToColor(model)}
onClick={() => { onClick={() => copyText(model)}
copyText(model); className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
}}
> >
{model} {model}
</Tag> </Tag>
))} ))}
<Tag <Tag
color='blue' color='grey'
type='light' type='light'
style={{ cursor: 'pointer' }} className="cursor-pointer !rounded-lg"
onClick={() => setIsModelsExpanded(false)} onClick={() => setIsModelsExpanded(false)}
icon={<IconChevronUp />}
> >
{t('收起')} {t('收起')}
</Tag> </Tag>
...@@ -513,133 +673,174 @@ const PersonalSetting = () => { ...@@ -513,133 +673,174 @@ const PersonalSetting = () => {
.map((model) => ( .map((model) => (
<Tag <Tag
key={model} key={model}
color='cyan' color={stringToColor(model)}
onClick={() => { onClick={() => copyText(model)}
copyText(model); className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
}}
> >
{model} {model}
</Tag> </Tag>
))} ))}
<Tag <Tag
color='blue' color='grey'
type='light' type='light'
style={{ cursor: 'pointer' }} className="cursor-pointer !rounded-lg"
onClick={() => setIsModelsExpanded(true)} onClick={() => setIsModelsExpanded(true)}
icon={<IconChevronDown />}
> >
{t('更多')} {models.length - MODELS_DISPLAY_COUNT}{' '} {t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
{t('个模型')}
</Tag> </Tag>
</Space> </Space>
)} )}
</> </>
)} )}
</div> </div>
</> </div>
}
> {/* 邀请信息部分 */}
<Descriptions row> <div className="bg-gray-50 rounded-xl">
<Descriptions.Item itemKey={t('当前余额')}> <div className="flex items-center mb-4">
{renderQuota(userState?.user?.quota)} <div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-3">
</Descriptions.Item> <IconLink size={20} className="text-orange-500" />
<Descriptions.Item itemKey={t('历史消耗')}> </div>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('请求次数')}>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
</Card>
<Card
style={{ marginTop: 10 }}
footer={
<div> <div>
<Typography.Text>{t('邀请链接')}</Typography.Text> <Typography.Title heading={6} className="mb-0">{t('邀请信息')}</Typography.Title>
<Input <div className="text-gray-500 text-sm">{t('管理您的邀请链接和收益')}</div>
style={{ marginTop: 10 }}
value={affLink}
onClick={handleAffLinkClick}
readOnly
/>
</div> </div>
} </div>
<div className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
> >
<Typography.Title heading={6}>{t('邀请信息')}</Typography.Title> <div className="text-gray-600 text-xs font-medium">{t('待使用收益')}</div>
<div style={{ marginTop: 10 }}> <div className="text-gray-900 text-lg font-bold mt-1">
<Descriptions row>
<Descriptions.Item itemKey={t('待使用收益')}>
<span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
{renderQuota(userState?.user?.aff_quota)} {renderQuota(userState?.user?.aff_quota)}
</span> </div>
<Button <Button
type={'secondary'} type="primary"
theme="solid"
onClick={() => setOpenTransfer(true)} onClick={() => setOpenTransfer(true)}
size={'small'} size="small"
style={{ marginLeft: 10 }} className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 mt-2 w-full"
icon={<IconCreditCard />}
> >
{t('划转')} {t('划转')}
</Button> </Button>
</Descriptions.Item> </Card>
<Descriptions.Item itemKey={t('总收益')}> <Card
className="!rounded-2xl text-center"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="text-gray-600 text-xs font-medium">{t('总收益')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{renderQuota(userState?.user?.aff_history_quota)} {renderQuota(userState?.user?.aff_history_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey={t('邀请人数')}>
{userState?.user?.aff_count}
</Descriptions.Item>
</Descriptions>
</div> </div>
</Card> </Card>
<Card style={{ marginTop: 10 }}> <Card
<Typography.Title heading={6}>{t('个人信息')}</Typography.Title> className="!rounded-2xl text-center"
<div style={{ marginTop: 20 }}> bodyStyle={{ padding: '16px' }}
<Typography.Text strong>{t('邮箱')}</Typography.Text> shadows='hover'
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
> >
<div> <div className="text-gray-600 text-xs font-medium">{t('邀请人数')}</div>
<div className="text-gray-900 text-lg font-bold mt-1">
{userState?.user?.aff_count || 0}
</div>
</Card>
</div>
<div className="bg-white rounded-lg p-3">
<Typography.Text strong className="block mb-2 text-sm">{t('邀请链接')}</Typography.Text>
<Input <Input
value={ value={affLink}
userState.user && userState.user.email !== '' onClick={handleAffLinkClick}
? userState.user.email readOnly
: t('未绑定') size="large"
className="!rounded-lg"
prefix={<IconLink />}
/>
</div>
</div>
</div>
</div>
</TabPane>
{/* 账户绑定Tab */}
<TabPane
tab={
<div className="flex items-center">
<UserPlus size={16} className="mr-2" />
{t('账户绑定')}
</div>
} }
readonly={true} itemKey='account'
></Input> >
<div className="py-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* 邮箱绑定 */}
<Card
className="!rounded-xl transition-shadow"
bodyStyle={{ padding: '16px' }}
shadows='hover'
>
<div className="flex items-center justify-between">
<div className="flex items-center flex-1">
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mr-3">
<IconMail size="default" className="text-red-500" />
</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> </div>
<div>
<Button <Button
onClick={() => { type="primary"
setShowEmailBindModal(true); theme="outline"
}} size="small"
onClick={() => setShowEmailBindModal(true)}
className="!rounded-lg"
> >
{userState.user && userState.user.email !== '' {userState.user && userState.user.email !== ''
? t('修改绑定') ? t('修改绑定')
: t('绑定邮箱')} : t('绑定邮箱')}
</Button> </Button>
</div> </div>
</div> </Card>
</div>
<div style={{ marginTop: 10 }}> {/* 微信绑定 */}
<Typography.Text strong>{t('微信')}</Typography.Text> <Card
<div className="!rounded-xl transition-shadow"
style={{ display: 'flex', justifyContent: 'space-between' }} bodyStyle={{ padding: '16px' }}
shadows='hover'
> >
<div> <div className="flex items-center justify-between">
<Input <div className="flex items-center flex-1">
value={ <div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-3">
userState.user && userState.user.wechat_id !== '' <SiWechat size={20} className="text-green-500" />
</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('已绑定')
: t('未绑定') : t('未绑定')}
} </div>
readonly={true} </div>
></Input>
</div> </div>
<div>
<Button <Button
type="primary"
theme="outline"
size="small"
disabled={!status.wechat_login} disabled={!status.wechat_login}
onClick={() => { onClick={() => setShowWeChatBindModal(true)}
setShowWeChatBindModal(true); className="!rounded-lg"
}}
> >
{userState.user && userState.user.wechat_id !== '' {userState.user && userState.user.wechat_id !== ''
? t('修改绑定') ? t('修改绑定')
...@@ -648,250 +849,387 @@ const PersonalSetting = () => { ...@@ -648,250 +849,387 @@ const PersonalSetting = () => {
: t('未启用')} : t('未启用')}
</Button> </Button>
</div> </div>
</div> </Card>
</div>
<div style={{ marginTop: 10 }}> {/* GitHub绑定 */}
<Typography.Text strong>{t('GitHub')}</Typography.Text> <Card
<div className="!rounded-xl transition-shadow"
style={{ display: 'flex', justifyContent: 'space-between' }} bodyStyle={{ padding: '16px' }}
shadows='hover'
> >
<div> <div className="flex items-center justify-between">
<Input <div className="flex items-center flex-1">
value={ <div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mr-3">
userState.user && userState.user.github_id !== '' <IconGithubLogo size="default" className="text-gray-700" />
</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 ? userState.user.github_id
: t('未绑定') : t('未绑定')}
} </div>
readonly={true} </div>
></Input>
</div> </div>
<div>
<Button <Button
onClick={() => { type="primary"
onGitHubOAuthClicked(status.github_client_id); theme="outline"
}} size="small"
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
disabled={ disabled={
(userState.user && userState.user.github_id !== '') || (userState.user && userState.user.github_id !== '') ||
!status.github_oauth !status.github_oauth
} }
className="!rounded-lg"
> >
{status.github_oauth ? t('绑定') : t('未启用')} {status.github_oauth ? t('绑定') : t('未启用')}
</Button> </Button>
</div> </div>
</div> </Card>
</div>
<div style={{ marginTop: 10 }}> {/* OIDC绑定 */}
<Typography.Text strong>{t('OIDC')}</Typography.Text> <Card
<div className="!rounded-xl transition-shadow"
style={{ display: 'flex', justifyContent: 'space-between' }} bodyStyle={{ padding: '16px' }}
shadows='hover'
> >
<div> <div className="flex items-center justify-between">
<Input <div className="flex items-center flex-1">
value={ <div className="w-10 h-10 rounded-full bg-indigo-50 flex items-center justify-center mr-3">
userState.user && userState.user.oidc_id !== '' <IconShield size="default" className="text-indigo-500" />
</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 ? userState.user.oidc_id
: t('未绑定') : t('未绑定')}
} </div>
readonly={true} </div>
></Input>
</div> </div>
<div>
<Button <Button
onClick={() => { type="primary"
onOIDCClicked( theme="outline"
size="small"
onClick={() => onOIDCClicked(
status.oidc_authorization_endpoint, status.oidc_authorization_endpoint,
status.oidc_client_id, status.oidc_client_id,
); )}
}}
disabled={ disabled={
(userState.user && userState.user.oidc_id !== '') || (userState.user && userState.user.oidc_id !== '') ||
!status.oidc_enabled !status.oidc_enabled
} }
className="!rounded-lg"
> >
{status.oidc_enabled ? t('绑定') : t('未启用')} {status.oidc_enabled ? t('绑定') : t('未启用')}
</Button> </Button>
</div> </div>
</div> </Card>
</div>
<div style={{ marginTop: 10 }}> {/* Telegram绑定 */}
<Typography.Text strong>{t('Telegram')}</Typography.Text> <Card
<div className="!rounded-xl transition-shadow"
style={{ display: 'flex', justifyContent: 'space-between' }} bodyStyle={{ padding: '16px' }}
shadows='hover'
> >
<div> <div className="flex items-center justify-between">
<Input <div className="flex items-center flex-1">
value={ <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-3">
userState.user && userState.user.telegram_id !== '' <SiTelegram size={20} className="text-blue-500" />
</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 ? userState.user.telegram_id
: t('未绑定') : t('未绑定')}
}
readonly={true}
></Input>
</div> </div>
<div> </div>
</div>
<div className="flex-shrink-0">
{status.telegram_oauth ? ( {status.telegram_oauth ? (
userState.user.telegram_id !== '' ? ( userState.user.telegram_id !== '' ? (
<Button disabled={true}>{t('已绑定')}</Button> <Button disabled={true} size="small" className="!rounded-lg">
{t('已绑定')}
</Button>
) : ( ) : (
<div className="scale-75">
<TelegramLoginButton <TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind' dataAuthUrl='/api/oauth/telegram/bind'
botName={status.telegram_bot_name} botName={status.telegram_bot_name}
/> />
</div>
) )
) : ( ) : (
<Button disabled={true}>{t('未启用')}</Button> <Button disabled={true} size="small" className="!rounded-lg">
{t('未启用')}
</Button>
)} )}
</div> </div>
</div> </div>
</div> </Card>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('LinuxDO')}</Typography.Text> {/* LinuxDO绑定 */}
<div <Card
style={{ display: 'flex', justifyContent: 'space-between' }} className="!rounded-xl transition-shadow"
bodyStyle={{ padding: '16px' }}
shadows='hover'
> >
<div> <div className="flex items-center justify-between">
<Input <div className="flex items-center flex-1">
value={ <div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-3">
userState.user && userState.user.linux_do_id !== '' <SiLinux size={20} className="text-orange-500" />
</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 ? userState.user.linux_do_id
: t('未绑定') : t('未绑定')}
} </div>
readonly={true} </div>
></Input>
</div> </div>
<div>
<Button <Button
onClick={() => { type="primary"
onLinuxDOOAuthClicked(status.linuxdo_client_id); theme="outline"
}} size="small"
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
disabled={ disabled={
(userState.user && userState.user.linux_do_id !== '') || (userState.user && userState.user.linux_do_id !== '') ||
!status.linuxdo_oauth !status.linuxdo_oauth
} }
className="!rounded-lg"
> >
{status.linuxdo_oauth ? t('绑定') : t('未启用')} {status.linuxdo_oauth ? t('绑定') : t('未启用')}
</Button> </Button>
</div> </div>
</Card>
</div> </div>
</div> </div>
<div style={{ marginTop: 10 }}> </TabPane>
<Space>
<Button onClick={generateAccessToken}> {/* 安全设置Tab */}
{t('生成系统访问令牌')} <TabPane
</Button> tab={
<Button <div className="flex items-center">
onClick={() => { <ShieldCheck size={16} className="mr-2" />
setShowChangePasswordModal(true); {t('安全设置')}
}} </div>
}
itemKey='security'
> >
{t('修改密码')} <div className="py-4">
</Button> <div className="space-y-6">
<Button <Space vertical className='w-full'>
type={'danger'} {/* 系统访问令牌 */}
onClick={() => { <Card
setShowAccountDeleteModal(true); className="!rounded-xl w-full"
}} bodyStyle={{ padding: '20px' }}
shadows='hover'
> >
{t('删除个人账户')} <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
</Button> <div className="flex items-start w-full sm:w-auto">
</Space> <div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mr-4 flex-shrink-0">
<IconKey size="large" className="text-blue-500" />
</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 && ( {systemToken && (
<div className="mt-3">
<Input <Input
readOnly readOnly
value={systemToken} value={systemToken}
onClick={handleSystemTokenClick} onClick={handleSystemTokenClick}
style={{ marginTop: '10px' }} size="large"
className="!rounded-lg"
prefix={<IconKey />}
/> />
</div>
)} )}
<Modal </div>
onCancel={() => setShowWeChatBindModal(false)} </div>
visible={showWeChatBindModal} <Button
size={'small'} type="primary"
theme="solid"
onClick={generateAccessToken}
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 w-full sm:w-auto"
icon={<IconKey />}
> >
<Image src={status.wechat_qrcode} /> {systemToken ? t('重新生成') : t('生成令牌')}
<div style={{ textAlign: 'center' }}> </Button>
<p>
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
</p>
</div> </div>
<Input </Card>
placeholder='验证码'
name='wechat_verification_code' {/* 密码管理 */}
value={inputs.wechat_verification_code} <Card
onChange={(v) => className="!rounded-xl w-full"
handleInputChange('wechat_verification_code', v) bodyStyle={{ padding: '20px' }}
} shadows='hover'
/> >
<Button color='' fluid size='large' onClick={bindWeChat}> <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
{t('绑定')} <div className="flex items-start w-full sm:w-auto">
<div className="w-12 h-12 rounded-full bg-orange-50 flex items-center justify-center mr-4 flex-shrink-0">
<IconLock size="large" className="text-orange-500" />
</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-orange-500 hover:!bg-orange-600 w-full sm:w-auto"
icon={<IconLock />}
>
{t('修改密码')}
</Button> </Button>
</Modal>
</div> </div>
</Card> </Card>
<Card style={{ marginTop: 10 }}>
<Tabs type='line' defaultActiveKey='notification'> {/* 危险区域 */}
<TabPane tab={t('通知设置')} itemKey='notification'> <Card
<div style={{ marginTop: 20 }}> className="!rounded-xl border-red-200 w-full"
<Typography.Text strong>{t('通知方式')}</Typography.Text> bodyStyle={{ padding: '20px' }}
<div style={{ marginTop: 10 }}> shadows='hover'
>
<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-red-50 flex items-center justify-center mr-4 flex-shrink-0">
<IconDelete size="large" className="text-red-500" />
</div>
<div>
<Typography.Title heading={6} className="mb-1 text-red-600">
{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"
icon={<IconDelete />}
>
{t('删除账户')}
</Button>
</div>
</Card>
</Space>
</div>
</div>
</TabPane>
{/* 通知设置Tab */}
<TabPane
tab={
<div className="flex items-center">
<Bell size={16} className="mr-2" />
{t('通知设置')}
</div>
}
itemKey='notification'
>
<div className="py-4">
<Tabs type='card' defaultActiveKey='notify' className="!rounded-lg">
<TabPane
tab={t('通知设置')}
itemKey='notify'
>
<div className="space-y-6">
{/* 通知方式选择 */}
<div className="bg-gray-50 rounded-xl">
<Typography.Text strong className="block mb-4 pt-4">{t('通知方式')}</Typography.Text>
<RadioGroup <RadioGroup
value={notificationSettings.warningType} value={notificationSettings.warningType}
onChange={(value) => onChange={(value) =>
handleNotificationSettingChange('warningType', value) handleNotificationSettingChange('warningType', value)
} }
type="pureCard"
> >
<Radio value='email'>{t('邮件通知')}</Radio> <Radio value='email' className="!p-4 !rounded-lg">
<Radio value='webhook'>{t('Webhook通知')}</Radio> <div className="flex items-center">
</RadioGroup> <IconMail className="mr-2 text-blue-500" />
<div>
<div className="font-medium">{t('邮件通知')}</div>
<div className="text-sm text-gray-500">{t('通过邮件接收通知')}</div>
</div>
</div>
</Radio>
<Radio value='webhook' className="!p-4 !rounded-lg">
<div className="flex items-center">
<Webhook size={16} className="mr-2 text-green-500" />
<div>
<div className="font-medium">{t('Webhook通知')}</div>
<div className="text-sm text-gray-500">{t('通过HTTP请求接收通知')}</div>
</div> </div>
</div> </div>
</Radio>
</RadioGroup>
</div>
{/* Webhook设置 */}
{notificationSettings.warningType === 'webhook' && ( {notificationSettings.warningType === 'webhook' && (
<> <div className="space-y-4">
<div style={{ marginTop: 20 }}> <div className="bg-white rounded-xl">
<Typography.Text strong> <Typography.Text strong className="block mb-3">{t('Webhook地址')}</Typography.Text>
{t('Webhook地址')}
</Typography.Text>
<div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.webhookUrl} value={notificationSettings.webhookUrl}
onChange={(val) => onChange={(val) =>
handleNotificationSettingChange('webhookUrl', val) handleNotificationSettingChange('webhookUrl', val)
} }
placeholder={t( placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
'请输入Webhook地址,例如: https://example.com/webhook', size="large"
)} className="!rounded-lg"
prefix={<Webhook size={16} className="m-2" />}
/> />
<Typography.Text <div className="text-gray-500 text-sm mt-2">
type='secondary' {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
style={{ marginTop: 8, display: 'block' }} </div>
> </div>
{t(
'只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求', <div className="bg-white rounded-xl">
)} <Typography.Text strong className="block mb-3">{t('接口凭证(可选)')}</Typography.Text>
</Typography.Text> <Input
<Typography.Text value={notificationSettings.webhookSecret}
type='secondary' onChange={(val) =>
style={{ marginTop: 8, display: 'block' }} handleNotificationSettingChange('webhookSecret', val)
>
<div
style={{ cursor: 'pointer' }}
onClick={() =>
setShowWebhookDocs(!showWebhookDocs)
} }
> placeholder={t('请输入密钥')}
{t('Webhook请求结构')}{' '} size="large"
{showWebhookDocs ? '▼' : '▶'} className="!rounded-lg"
prefix={<IconKey />}
/>
<div className="text-gray-500 text-sm mt-2">
{t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
</div>
</div>
<div className="bg-yellow-50 rounded-xl">
<div className="flex items-center justify-between cursor-pointer" onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
<div className="flex items-center">
<Globe size={16} className="mr-2 text-yellow-600" />
<Typography.Text strong className="text-yellow-800">
{t('Webhook请求结构')}
</Typography.Text>
</div>
{showWebhookDocs ? <IconChevronUp /> : <IconChevronDown />}
</div> </div>
<Collapsible isOpen={showWebhookDocs}> <Collapsible isOpen={showWebhookDocs}>
<pre <pre className="mt-4 bg-gray-800 text-gray-100 rounded-lg text-sm overflow-x-auto">
style={{
marginTop: 4,
background: 'var(--semi-color-fill-0)',
padding: 8,
borderRadius: 4,
}}
>
{`{ {`{
"type": "quota_exceed", // 通知类型 "type": "quota_exceed", // 通知类型
"title": "标题", // 通知标题 "title": "标题", // 通知标题
...@@ -910,84 +1248,42 @@ const PersonalSetting = () => { ...@@ -910,84 +1248,42 @@ const PersonalSetting = () => {
}`} }`}
</pre> </pre>
</Collapsible> </Collapsible>
</Typography.Text>
</div> </div>
</div> </div>
<div style={{ marginTop: 20 }}>
<Typography.Text strong>
{t('接口凭证(可选)')}
</Typography.Text>
<div style={{ marginTop: 10 }}>
<Input
value={notificationSettings.webhookSecret}
onChange={(val) =>
handleNotificationSettingChange(
'webhookSecret',
val,
)
}
placeholder={t('请输入密钥')}
/>
<Typography.Text
type='secondary'
style={{ marginTop: 8, display: 'block' }}
>
{t(
'密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性',
)}
</Typography.Text>
<Typography.Text
type='secondary'
style={{ marginTop: 4, display: 'block' }}
>
{t('Authorization: Bearer your-secret-key')}
</Typography.Text>
</div>
</div>
</>
)} )}
{/* 邮件设置 */}
{notificationSettings.warningType === 'email' && ( {notificationSettings.warningType === 'email' && (
<div style={{ marginTop: 20 }}> <div className="bg-white rounded-xl">
<Typography.Text strong>{t('通知邮箱')}</Typography.Text> <Typography.Text strong className="block mb-3">{t('通知邮箱')}</Typography.Text>
<div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.notificationEmail} value={notificationSettings.notificationEmail}
onChange={(val) => onChange={(val) =>
handleNotificationSettingChange( handleNotificationSettingChange('notificationEmail', val)
'notificationEmail',
val,
)
} }
placeholder={t('留空则使用账号绑定的邮箱')} placeholder={t('留空则使用账号绑定的邮箱')}
size="large"
className="!rounded-lg"
prefix={<IconMail />}
/> />
<Typography.Text <div className="text-gray-500 text-sm mt-2">
type='secondary' {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
style={{ marginTop: 8, display: 'block' }}
>
{t(
'设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
)}
</Typography.Text>
</div> </div>
</div> </div>
)} )}
<div style={{ marginTop: 20 }}>
<Typography.Text strong> {/* 预警阈值 */}
{t('额度预警阈值')}{' '} <div className="bg-white rounded-xl">
{renderQuotaWithPrompt( <Typography.Text strong className="block mb-3">
notificationSettings.warningThreshold, {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
)}
</Typography.Text> </Typography.Text>
<div style={{ marginTop: 10 }}>
<AutoComplete <AutoComplete
value={notificationSettings.warningThreshold} value={notificationSettings.warningThreshold}
onChange={(val) => onChange={(val) =>
handleNotificationSettingChange( handleNotificationSettingChange('warningThreshold', val)
'warningThreshold',
val,
)
} }
style={{ width: 200 }} size="large"
className="!rounded-lg w-full max-w-xs"
placeholder={t('请输入预警额度')} placeholder={t('请输入预警额度')}
data={[ data={[
{ value: 100000, label: '0.2$' }, { value: 100000, label: '0.2$' },
...@@ -995,198 +1291,312 @@ const PersonalSetting = () => { ...@@ -995,198 +1291,312 @@ const PersonalSetting = () => {
{ value: 1000000, label: '5$' }, { value: 1000000, label: '5$' },
{ value: 5000000, label: '10$' }, { value: 5000000, label: '10$' },
]} ]}
prefix={<IconBell />}
/> />
<div className="text-gray-500 text-sm mt-2">
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
</div>
</div> </div>
<Typography.Text
type='secondary'
style={{ marginTop: 10, display: 'block' }}
>
{t(
'当剩余额度低于此数值时,系统将通过选择的方式发送通知',
)}
</Typography.Text>
</div> </div>
</TabPane> </TabPane>
<TabPane tab={t('价格设置')} itemKey='price'>
<div style={{ marginTop: 20 }}> <TabPane
<Typography.Text strong> tab={t('价格设置')}
itemKey='price'
>
<div className="py-4">
<div className="bg-white rounded-xl">
<div className="flex items-start">
<div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mt-1">
<Shield size={20} className="text-orange-500" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<div>
<Typography.Text strong className="block mb-2">
{t('接受未设置价格模型')} {t('接受未设置价格模型')}
</Typography.Text> </Typography.Text>
<div style={{ marginTop: 10 }}> <div className="text-gray-500 text-sm">
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
</div>
</div>
<Checkbox <Checkbox
checked={ checked={notificationSettings.acceptUnsetModelRatioModel}
notificationSettings.acceptUnsetModelRatioModel
}
onChange={(e) => onChange={(e) =>
handleNotificationSettingChange( handleNotificationSettingChange(
'acceptUnsetModelRatioModel', 'acceptUnsetModelRatioModel',
e.target.checked, e.target.checked,
) )
} }
> className="ml-4"
{t('接受未设置价格模型')} />
</Checkbox> </div>
<Typography.Text </div>
type='secondary' </div>
style={{ marginTop: 8, display: 'block' }}
>
{t(
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
)}
</Typography.Text>
</div> </div>
</div> </div>
</TabPane> </TabPane>
</Tabs> </Tabs>
<div style={{ marginTop: 20 }}>
<Button type='primary' onClick={saveNotificationSettings}> <div className="mt-6 flex justify-end">
<Button
type='primary'
onClick={saveNotificationSettings}
size="large"
className="!rounded-lg !bg-purple-500 hover:!bg-purple-600"
icon={<IconSetting />}
>
{t('保存设置')} {t('保存设置')}
</Button> </Button>
</div> </div>
</div>
</TabPane>
</Tabs>
</div>
</Card> </Card>
</div>
</div>
</Layout.Content>
</Layout>
{/* 邮箱绑定模态框 */}
<Modal <Modal
title={
<div className="flex items-center">
<IconMail className="mr-2 text-blue-500" />
{t('绑定邮箱地址')}
</div>
}
visible={showEmailBindModal}
onCancel={() => setShowEmailBindModal(false)} onCancel={() => setShowEmailBindModal(false)}
onOk={bindEmail} onOk={bindEmail}
visible={showEmailBindModal}
size={'small'} size={'small'}
centered={true} centered={true}
maskClosable={false} maskClosable={false}
className="modern-modal"
> >
<Typography.Title heading={6}> <div className="space-y-4 py-4">
{t('绑定邮箱地址')} <div className="flex gap-3">
</Typography.Title>
<div
style={{
marginTop: 20,
display: 'flex',
justifyContent: 'space-between',
}}
>
<Input <Input
fluid placeholder={t('输入邮箱地址')}
placeholder='输入邮箱地址'
onChange={(value) => handleInputChange('email', value)} onChange={(value) => handleInputChange('email', value)}
name='email' name='email'
type='email' type='email'
size="large"
className="!rounded-lg flex-1"
prefix={<IconMail />}
/> />
<Button <Button
onClick={sendVerificationCode} onClick={sendVerificationCode}
disabled={disableButton || loading} disabled={disableButton || loading}
className="!rounded-lg"
type="primary"
theme="outline"
size='large'
> >
{disableButton ? `重新发送 (${countdown})` : '获取验证码'} {disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
</Button> </Button>
</div> </div>
<div style={{ marginTop: 10 }}>
<Input <Input
fluid placeholder={t('验证码')}
placeholder='验证码'
name='email_verification_code' name='email_verification_code'
value={inputs.email_verification_code} value={inputs.email_verification_code}
onChange={(value) => onChange={(value) =>
handleInputChange('email_verification_code', value) handleInputChange('email_verification_code', value)
} }
size="large"
className="!rounded-lg"
prefix={<IconKey />}
/> />
</div>
{turnstileEnabled ? ( {turnstileEnabled && (
<div className="flex justify-center">
<Turnstile <Turnstile
sitekey={turnstileSiteKey} sitekey={turnstileSiteKey}
onVerify={(token) => { onVerify={(token) => {
setTurnstileToken(token); setTurnstileToken(token);
}} }}
/> />
) : ( </div>
<></>
)} )}
</div>
</Modal> </Modal>
{/* 微信绑定模态框 */}
<Modal <Modal
onCancel={() => setShowAccountDeleteModal(false)} title={
visible={showAccountDeleteModal} <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'} size={'small'}
centered={true} 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-green-500 hover:!bg-green-600"
icon={<SiWechat size={16} />}
>
{t('绑定')}
</Button>
</div>
</Modal>
{/* 账户删除模态框 */}
<Modal
title={
<div className="flex items-center">
<IconDelete className="mr-2 text-red-500" />
{t('删除账户确认')}
</div>
}
visible={showAccountDeleteModal}
onCancel={() => setShowAccountDeleteModal(false)}
onOk={deleteAccount} onOk={deleteAccount}
size={'small'}
centered={true}
className="modern-modal"
> >
<div style={{ marginTop: 20 }}> <div className="space-y-4 py-4">
<Banner <Banner
type='danger' type='danger'
description='您正在删除自己的帐户,将清空所有数据且不可恢复' description={t('您正在删除自己的帐户,将清空所有数据且不可恢复')}
closeIcon={null} closeIcon={null}
className="!rounded-lg"
/> />
</div>
<div style={{ marginTop: 20 }}> <div>
<Typography.Text strong className="block mb-2 text-red-600">
{t('请输入您的用户名以确认删除')}
</Typography.Text>
<Input <Input
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`} placeholder={t('输入你的账户名{{username}}以确认删除', { username: ` ${userState?.user?.username} ` })}
name='self_account_deletion_confirmation' name='self_account_deletion_confirmation'
value={inputs.self_account_deletion_confirmation} value={inputs.self_account_deletion_confirmation}
onChange={(value) => onChange={(value) =>
handleInputChange( handleInputChange('self_account_deletion_confirmation', value)
'self_account_deletion_confirmation',
value,
)
} }
size="large"
className="!rounded-lg"
prefix={<IconUser />}
/> />
{turnstileEnabled ? ( </div>
{turnstileEnabled && (
<div className="flex justify-center">
<Turnstile <Turnstile
sitekey={turnstileSiteKey} sitekey={turnstileSiteKey}
onVerify={(token) => { onVerify={(token) => {
setTurnstileToken(token); setTurnstileToken(token);
}} }}
/> />
) : ( </div>
<></>
)} )}
</div> </div>
</Modal> </Modal>
{/* 修改密码模态框 */}
<Modal <Modal
onCancel={() => setShowChangePasswordModal(false)} title={
<div className="flex items-center">
<IconLock className="mr-2 text-orange-500" />
{t('修改密码')}
</div>
}
visible={showChangePasswordModal} visible={showChangePasswordModal}
onCancel={() => setShowChangePasswordModal(false)}
onOk={changePassword}
size={'small'} size={'small'}
centered={true} centered={true}
onOk={changePassword} className="modern-modal"
> >
<div style={{ marginTop: 20 }}> <div className="space-y-4 py-4">
<div>
<Typography.Text strong className="block mb-2">{t('原密码')}</Typography.Text>
<Input <Input
name='original_password' name='original_password'
placeholder={t('原密码')} placeholder={t('请输入原密码')}
type='password' type='password'
value={inputs.original_password} value={inputs.original_password}
onChange={(value) => onChange={(value) =>
handleInputChange('original_password', 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 <Input
style={{ marginTop: 20 }}
name='set_new_password' name='set_new_password'
placeholder={t('新密码')} placeholder={t('请输入新密码')}
type='password'
value={inputs.set_new_password} value={inputs.set_new_password}
onChange={(value) => onChange={(value) =>
handleInputChange('set_new_password', 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 <Input
style={{ marginTop: 20 }}
name='set_new_password_confirmation' name='set_new_password_confirmation'
placeholder={t('确认新密码')} placeholder={t('请再次输入新密码')}
type='password'
value={inputs.set_new_password_confirmation} value={inputs.set_new_password_confirmation}
onChange={(value) => onChange={(value) =>
handleInputChange('set_new_password_confirmation', value) handleInputChange('set_new_password_confirmation', value)
} }
size="large"
className="!rounded-lg"
prefix={<IconLock />}
/> />
{turnstileEnabled ? ( </div>
{turnstileEnabled && (
<div className="flex justify-center">
<Turnstile <Turnstile
sitekey={turnstileSiteKey} sitekey={turnstileSiteKey}
onVerify={(token) => { onVerify={(token) => {
setTurnstileToken(token); setTurnstileToken(token);
}} }}
/> />
) : ( </div>
<></>
)} )}
</div> </div>
</Modal> </Modal>
</div> </div>
</Layout.Content>
</Layout>
</div>
); );
}; };
......
...@@ -727,7 +727,7 @@ ...@@ -727,7 +727,7 @@
"小时": "Hour", "小时": "Hour",
"新密码": "New Password", "新密码": "New Password",
"重置邮件发送成功,请检查邮箱!": "The reset email was sent successfully, please check your email!", "重置邮件发送成功,请检查邮箱!": "The reset email was sent successfully, please check your email!",
"请输入你的账户名以确认删除!": "Please enter your account name to confirm deletion!", "输入你的账户名{{username}}以确认删除": "Enter your account name{{username}}to confirm deletion",
"账户已删除!": "Account has been deleted!", "账户已删除!": "Account has been deleted!",
"微信账户绑定成功!": "WeChat account bound successfully!", "微信账户绑定成功!": "WeChat account bound successfully!",
"两次输入的密码不一致!": "The passwords entered twice are inconsistent!", "两次输入的密码不一致!": "The passwords entered twice are inconsistent!",
...@@ -1455,5 +1455,30 @@ ...@@ -1455,5 +1455,30 @@
"尊敬的": "Dear", "尊敬的": "Dear",
"请输入兑换码": "Please enter the redemption code", "请输入兑换码": "Please enter the redemption code",
"在线充值功能未开启": "Online recharge function is not enabled", "在线充值功能未开启": "Online recharge function is not enabled",
"管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "The administrator has not enabled the online recharge function, please contact the administrator to enable it or recharge with a redemption code." "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "The administrator has not enabled the online recharge function, please contact the administrator to enable it or recharge with a redemption code.",
"点击模型名称可复制": "Click the model name to copy",
"管理您的邀请链接和收益": "Manage your invitation link and earnings",
"模型与邀请": "Model and Invitation",
"账户绑定": "Account Binding",
"安全设置": "Security Settings",
"系统访问令牌": "System Access Token",
"用于API调用的身份验证令牌,请妥善保管": "Authentication token for API calls, please keep it safe",
"密码管理": "Password Management",
"定期更改密码可以提高账户安全性": "Regularly changing your password can improve account security",
"删除账户": "Delete Account",
"此操作不可逆,所有数据将被永久删除": "This operation is irreversible, all data will be permanently deleted",
"生成令牌": "Generate Token",
"通过邮件接收通知": "Receive notifications via email",
"通过HTTP请求接收通知": "Receive notifications via HTTP request",
"价格设置": "Price Settings",
"重新生成": "Regenerate",
"绑定微信账户": "Bind WeChat Account",
"原密码": "Original Password",
"请输入原密码": "Please enter the original password",
"请输入新密码": "Please enter the new password",
"请再次输入新密码": "Please enter the new password again",
"删除账户确认": "Delete Account Confirmation",
"请输入您的用户名以确认删除": "Please enter your username to confirm deletion",
"接受未设置价格模型": "Accept models without price settings",
"当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "Accept calls even if the model has no price settings, use only when you trust the website, which may incur high costs"
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment