♻️ 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:
t0ng7u
2025-08-17 00:49:54 +08:00
parent a1cab158ea
commit 9805d35a5d
11 changed files with 1629 additions and 1266 deletions

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

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

View File

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