🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)

- Ran: bun run eslint:fix && bun run lint:fix
- Inserted AGPL license header via eslint-plugin-header
- Enforced no-multiple-empty-lines and other lint rules
- Formatted code using Prettier v3 (@so1ve/prettier-config)
- No functional changes; formatting-only baseline across JS/JSX files
This commit is contained in:
t0ng7u
2025-08-30 21:15:10 +08:00
parent 41cf516ec5
commit 0d57b1acd4
274 changed files with 11025 additions and 7659 deletions
@@ -27,7 +27,7 @@ import {
Avatar,
Tabs,
TabPane,
Popover
Popover,
} from '@douyinfe/semi-ui';
import {
IconMail,
@@ -35,7 +35,7 @@ import {
IconGithubLogo,
IconKey,
IconLock,
IconDelete
IconDelete,
} from '@douyinfe/semi-icons';
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
import { UserPlus, ShieldCheck } from 'lucide-react';
@@ -43,7 +43,7 @@ import TelegramLoginButton from 'react-telegram-login';
import {
onGitHubOAuthClicked,
onOIDCClicked,
onLinuxDOOAuthClicked
onLinuxDOOAuthClicked,
} from '../../../../helpers';
import TwoFASetting from '../components/TwoFASetting';
@@ -57,77 +57,89 @@ const AccountManagement = ({
generateAccessToken,
handleSystemTokenClick,
setShowChangePasswordModal,
setShowAccountDeleteModal
setShowAccountDeleteModal,
}) => {
const renderAccountInfo = (accountId, label) => {
if (!accountId || accountId === '') {
return <span className="text-gray-500">{t('未绑定')}</span>;
return <span className='text-gray-500'>{t('未绑定')}</span>;
}
const popContent = (
<div className="text-xs p-2">
<div className='text-xs p-2'>
<Typography.Paragraph copyable={{ content: accountId }}>
{accountId}
</Typography.Paragraph>
{label ? (
<div className="mt-1 text-[11px] text-gray-500">{label}</div>
<div className='mt-1 text-[11px] text-gray-500'>{label}</div>
) : null}
</div>
);
return (
<Popover content={popContent} position="top" trigger="hover">
<span className="block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer">
<Popover content={popContent} position='top' trigger='hover'>
<span className='block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer'>
{accountId}
</span>
</Popover>
);
};
return (
<Card className="!rounded-2xl">
<Card className='!rounded-2xl'>
{/* 卡片头部 */}
<div className="flex items-center mb-4">
<Avatar size="small" color="teal" className="mr-3 shadow-md">
<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>
<Typography.Text className='text-lg font-medium'>
{t('账户管理')}
</Typography.Text>
<div className='text-xs text-gray-600'>
{t('账户绑定、安全设置和身份验证')}
</div>
</div>
</div>
<Tabs type="card" defaultActiveKey="binding">
<Tabs type='card' defaultActiveKey='binding'>
{/* 账户绑定 Tab */}
<TabPane
tab={
<div className="flex items-center">
<UserPlus size={16} className="mr-2" />
<div className='flex items-center'>
<UserPlus size={16} className='mr-2' />
{t('账户绑定')}
</div>
}
itemKey="binding"
itemKey='binding'
>
<div className="py-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<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 gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<IconMail size="default" className="text-slate-600 dark:text-slate-300" />
<Card className='!rounded-xl'>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center flex-1 min-w-0'>
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
<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">
{renderAccountInfo(userState.user?.email, t('邮箱地址'))}
<div className='flex-1 min-w-0'>
<div className='font-medium text-gray-900'>
{t('邮箱')}
</div>
<div className='text-sm text-gray-500 truncate'>
{renderAccountInfo(
userState.user?.email,
t('邮箱地址'),
)}
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className='flex-shrink-0'>
<Button
type="primary"
theme="outline"
size="small"
type='primary'
theme='outline'
size='small'
onClick={() => setShowEmailBindModal(true)}
>
{userState.user && userState.user.email !== ''
@@ -139,26 +151,31 @@ const AccountManagement = ({
</Card>
{/* 微信绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
<Card className='!rounded-xl'>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center flex-1 min-w-0'>
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
<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">
<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>
<div className="flex-shrink-0">
<div className='flex-shrink-0'>
<Button
type="primary"
theme="outline"
size="small"
type='primary'
theme='outline'
size='small'
disabled={!status.wechat_login}
onClick={() => setShowWeChatBindModal(true)}
>
@@ -173,25 +190,35 @@ const AccountManagement = ({
</Card>
{/* GitHub绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
<Card className='!rounded-xl'>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center flex-1 min-w-0'>
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
<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">
{renderAccountInfo(userState.user?.github_id, t('GitHub ID'))}
<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'>
{renderAccountInfo(
userState.user?.github_id,
t('GitHub ID'),
)}
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className='flex-shrink-0'>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
type='primary'
theme='outline'
size='small'
onClick={() =>
onGitHubOAuthClicked(status.github_client_id)
}
disabled={
(userState.user && userState.user.github_id !== '') ||
!status.github_oauth
@@ -204,28 +231,38 @@ const AccountManagement = ({
</Card>
{/* OIDC绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<IconShield size="default" className="text-slate-600 dark:text-slate-300" />
<Card className='!rounded-xl'>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center flex-1 min-w-0'>
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
<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">
{renderAccountInfo(userState.user?.oidc_id, t('OIDC ID'))}
<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'>
{renderAccountInfo(
userState.user?.oidc_id,
t('OIDC ID'),
)}
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className='flex-shrink-0'>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onOIDCClicked(
status.oidc_authorization_endpoint,
status.oidc_client_id,
)}
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
@@ -238,27 +275,35 @@ const AccountManagement = ({
</Card>
{/* Telegram绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
<Card className='!rounded-xl'>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center flex-1 min-w-0'>
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
<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">
{renderAccountInfo(userState.user?.telegram_id, t('Telegram ID'))}
<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'>
{renderAccountInfo(
userState.user?.telegram_id,
t('Telegram ID'),
)}
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className='flex-shrink-0'>
{status.telegram_oauth ? (
userState.user.telegram_id !== '' ? (
<Button disabled={true} size="small">
<Button disabled={true} size='small'>
{t('已绑定')}
</Button>
) : (
<div className="scale-75">
<div className='scale-75'>
<TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind'
botName={status.telegram_bot_name}
@@ -266,7 +311,7 @@ const AccountManagement = ({
</div>
)
) : (
<Button disabled={true} size="small">
<Button disabled={true} size='small'>
{t('未启用')}
</Button>
)}
@@ -275,25 +320,35 @@ const AccountManagement = ({
</Card>
{/* LinuxDO绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
<Card className='!rounded-xl'>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center flex-1 min-w-0'>
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
<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">
{renderAccountInfo(userState.user?.linux_do_id, t('LinuxDO ID'))}
<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'>
{renderAccountInfo(
userState.user?.linux_do_id,
t('LinuxDO ID'),
)}
</div>
</div>
</div>
<div className="flex-shrink-0">
<div className='flex-shrink-0'>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
type='primary'
theme='outline'
size='small'
onClick={() =>
onLinuxDOOAuthClicked(status.linuxdo_client_id)
}
disabled={
(userState.user && userState.user.linux_do_id !== '') ||
!status.linuxdo_oauth
@@ -311,37 +366,37 @@ const AccountManagement = ({
{/* 安全设置 Tab */}
<TabPane
tab={
<div className="flex items-center">
<ShieldCheck size={16} className="mr-2" />
<div className='flex items-center'>
<ShieldCheck size={16} className='mr-2' />
{t('安全设置')}
</div>
}
itemKey="security"
itemKey='security'
>
<div className="py-4">
<div className="space-y-6">
<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" />
<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">
<div className='flex-1'>
<Typography.Title heading={6} className='mb-1'>
{t('系统访问令牌')}
</Typography.Title>
<Typography.Text type="tertiary" className="text-sm">
<Typography.Text type='tertiary' className='text-sm'>
{t('用于API调用的身份验证令牌,请妥善保管')}
</Typography.Text>
{systemToken && (
<div className="mt-3">
<div className='mt-3'>
<Input
readonly
value={systemToken}
onClick={handleSystemTokenClick}
size="large"
size='large'
prefix={<IconKey />}
/>
</div>
@@ -349,10 +404,10 @@ const AccountManagement = ({
</div>
</div>
<Button
type="primary"
theme="solid"
type='primary'
theme='solid'
onClick={generateAccessToken}
className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
icon={<IconKey />}
>
{systemToken ? t('重新生成') : t('生成令牌')}
@@ -361,26 +416,26 @@ const AccountManagement = ({
</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" />
<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">
<Typography.Title heading={6} className='mb-1'>
{t('密码管理')}
</Typography.Title>
<Typography.Text type="tertiary" className="text-sm">
<Typography.Text type='tertiary' className='text-sm'>
{t('定期更改密码可以提高账户安全性')}
</Typography.Text>
</div>
</div>
<Button
type="primary"
theme="solid"
type='primary'
theme='solid'
onClick={() => setShowChangePasswordModal(true)}
className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
className='!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto'
icon={<IconLock />}
>
{t('修改密码')}
@@ -392,26 +447,29 @@ const AccountManagement = ({
<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" />
<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">
<Typography.Title
heading={6}
className='mb-1 text-slate-700'
>
{t('删除账户')}
</Typography.Title>
<Typography.Text type="tertiary" className="text-sm">
<Typography.Text type='tertiary' className='text-sm'>
{t('此操作不可逆,所有数据将被永久删除')}
</Typography.Text>
</div>
</div>
<Button
type="danger"
theme="solid"
type='danger'
theme='solid'
onClick={() => setShowAccountDeleteModal(true)}
className="w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
className='w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600'
icon={<IconDelete />}
>
{t('删除账户')}
@@ -27,13 +27,13 @@ import {
Tabs,
TabPane,
Typography,
Avatar
Avatar,
} from '@douyinfe/semi-ui';
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
import {
IconChevronDown,
IconChevronUp
} from '@douyinfe/semi-icons';
IllustrationNoContent,
IllustrationNoContentDark,
} from '@douyinfe/semi-illustrations';
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
import { Settings } from 'lucide-react';
import { renderModelTag, getModelCategories } from '../../../../helpers';
@@ -52,38 +52,48 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
}, [isModelsExpanded]);
return (
<div className="py-4">
<div className='py-4'>
{/* 卡片头部 */}
<div className="flex items-center mb-4">
<Avatar size="small" color="green" className="mr-3 shadow-md">
<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>
<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">
<div className='bg-gray-50 dark:bg-gray-800 rounded-xl'>
{modelsLoading ? (
// 骨架屏加载状态 - 模拟实际加载后的布局
<div className="space-y-4">
<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">
<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
}} />
<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">
<div className='flex flex-wrap gap-2'>
{Array.from({ length: 20 }).map((_, index) => (
<Skeleton.Button
key={`model-${index}`}
@@ -91,17 +101,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
width: 100 + Math.random() * 100,
height: 32,
borderRadius: 16,
margin: '4px'
margin: '4px',
}}
/>
))}
</div>
</div>
) : models.length === 0 ? (
<div className="py-8">
<div className='py-8'>
<Empty
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
image={
<IllustrationNoContent style={{ width: 150, height: 150 }} />
}
darkModeImage={
<IllustrationNoContentDark
style={{ width: 150, height: 150 }}
/>
}
description={t('没有可用模型')}
style={{ padding: '24px 0' }}
/>
@@ -109,59 +125,81 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
) : (
<>
{/* 模型分类标签页 */}
<div className="mb-4">
<div className='mb-4'>
<Tabs
type="card"
type='card'
activeKey={activeModelCategory}
onChange={key => setActiveModelCategory(key)}
className="mt-2"
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;
{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;
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}
/>
);
})}
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">
<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 }));
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 }} />}
image={
<IllustrationNoContent
style={{ width: 120, height: 120 }}
/>
}
darkModeImage={
<IllustrationNoContentDark
style={{ width: 120, height: 120 }}
/>
}
description={t('该分类下没有可用模型')}
style={{ padding: '16px 0' }}
/>
@@ -171,13 +209,13 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
return (
<Space wrap>
{filteredModels.map((model) => (
{filteredModels.map((model) =>
renderModelTag(model, {
size: 'small',
shape: 'circle',
onClick: () => copyText(model),
})
))}
}),
)}
</Space>
);
} else {
@@ -185,17 +223,17 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
<>
<Collapsible isOpen={isModelsExpanded}>
<Space wrap>
{filteredModels.map((model) => (
{filteredModels.map((model) =>
renderModelTag(model, {
size: 'small',
shape: 'circle',
onClick: () => copyText(model),
})
))}
}),
)}
<Tag
color='grey'
type='light'
className="cursor-pointer !rounded-lg"
className='cursor-pointer !rounded-lg'
onClick={() => setIsModelsExpanded(false)}
icon={<IconChevronUp />}
>
@@ -207,21 +245,23 @@ const ModelsList = ({ t, models, modelsLoading, copyText }) => {
<Space wrap>
{filteredModels
.slice(0, MODELS_DISPLAY_COUNT)
.map((model) => (
.map((model) =>
renderModelTag(model, {
size: 'small',
shape: 'circle',
onClick: () => copyText(model),
})
))}
}),
)}
<Tag
color='grey'
type='light'
className="cursor-pointer !rounded-lg"
className='cursor-pointer !rounded-lg'
onClick={() => setIsModelsExpanded(true)}
icon={<IconChevronDown />}
>
{t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
{t('更多')}{' '}
{filteredModels.length - MODELS_DISPLAY_COUNT}{' '}
{t('个模型')}
</Tag>
</Space>
)}
@@ -27,14 +27,9 @@ import {
Radio,
Toast,
Tabs,
TabPane
TabPane,
} from '@douyinfe/semi-ui';
import {
IconMail,
IconKey,
IconBell,
IconLink
} from '@douyinfe/semi-icons';
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';
@@ -43,7 +38,7 @@ const NotificationSettings = ({
t,
notificationSettings,
handleNotificationSettingChange,
saveNotificationSettings
saveNotificationSettings,
}) => {
const formApiRef = useRef(null);
@@ -62,7 +57,8 @@ const NotificationSettings = ({
// 表单提交
const handleSubmit = () => {
if (formApiRef.current) {
formApiRef.current.validate()
formApiRef.current
.validate()
.then(() => {
saveNotificationSettings();
})
@@ -77,26 +73,27 @@ const NotificationSettings = ({
return (
<Card
className="!rounded-2xl shadow-sm border-0"
className='!rounded-2xl shadow-sm border-0'
footer={
<div className="flex justify-end">
<Button
type='primary'
onClick={handleSubmit}
>
<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">
<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>
<Typography.Text className='text-lg font-medium'>
{t('其他设置')}
</Typography.Text>
<div className='text-xs text-gray-600'>
{t('通知、价格和隐私相关设置')}
</div>
</div>
</div>
@@ -106,18 +103,18 @@ const NotificationSettings = ({
onSubmit={handleSubmit}
>
{() => (
<Tabs type="card" defaultActiveKey="notification">
<Tabs type='card' defaultActiveKey='notification'>
{/* 通知配置 Tab */}
<TabPane
tab={
<div className="flex items-center">
<Bell size={16} className="mr-2" />
<div className='flex items-center'>
<Bell size={16} className='mr-2' />
{t('通知配置')}
</div>
}
itemKey="notification"
itemKey='notification'
>
<div className="py-4">
<div className='py-4'>
<Form.RadioGroup
field='warningType'
label={t('通知方式')}
@@ -125,15 +122,18 @@ const NotificationSettings = ({
onChange={(value) => handleFormChange('warningType', value)}
rules={[{ required: true, message: t('请选择通知方式') }]}
>
<Radio value="email">{t('邮件通知')}</Radio>
<Radio value="webhook">{t('Webhook通知')}</Radio>
<Radio value='email'>{t('邮件通知')}</Radio>
<Radio value='webhook'>{t('Webhook通知')}</Radio>
</Form.RadioGroup>
<Form.AutoComplete
field='warningThreshold'
label={
<span>
{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
{t('额度预警阈值')}{' '}
{renderQuotaWithPrompt(
notificationSettings.warningThreshold,
)}
</span>
}
placeholder={t('请输入预警额度')}
@@ -145,7 +145,9 @@ const NotificationSettings = ({
]}
onChange={(val) => handleFormChange('warningThreshold', val)}
prefix={<IconBell />}
extraText={t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
extraText={t(
'当剩余额度低于此数值时,系统将通过选择的方式发送通知',
)}
style={{ width: '100%', maxWidth: '300px' }}
rules={[
{ required: true, message: t('请输入预警阈值') },
@@ -156,8 +158,8 @@ const NotificationSettings = ({
return Promise.reject(t('预警阈值必须为正数'));
}
return Promise.resolve();
}
}
},
},
]}
/>
@@ -167,9 +169,13 @@ const NotificationSettings = ({
field='notificationEmail'
label={t('通知邮箱')}
placeholder={t('留空则使用账号绑定的邮箱')}
onChange={(val) => handleFormChange('notificationEmail', val)}
onChange={(val) =>
handleFormChange('notificationEmail', val)
}
prefix={<IconMail />}
extraText={t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
extraText={t(
'设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
)}
showClear
/>
)}
@@ -180,20 +186,25 @@ const NotificationSettings = ({
<Form.Input
field='webhookUrl'
label={t('Webhook地址')}
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
placeholder={t(
'请输入Webhook地址,例如: https://example.com/webhook',
)}
onChange={(val) => handleFormChange('webhookUrl', val)}
prefix={<IconLink />}
extraText={t('只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求')}
extraText={t(
'只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求',
)}
showClear
rules={[
{
required: notificationSettings.warningType === 'webhook',
message: t('请输入Webhook地址')
required:
notificationSettings.warningType === 'webhook',
message: t('请输入Webhook地址'),
},
{
pattern: /^https:\/\/.+/,
message: t('Webhook地址必须以https://开头')
}
message: t('Webhook地址必须以https://开头'),
},
]}
/>
@@ -203,7 +214,9 @@ const NotificationSettings = ({
placeholder={t('请输入密钥')}
onChange={(val) => handleFormChange('webhookSecret', val)}
prefix={<IconKey />}
extraText={t('密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性')}
extraText={t(
'密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性',
)}
showClear
/>
@@ -212,22 +225,36 @@ const NotificationSettings = ({
<div style={{ height: '200px', marginBottom: '12px' }}>
<CodeViewer
content={{
"type": "quota_exceed",
"title": "额度预警通知",
"content": "您的额度即将用尽,当前剩余额度为 {{value}}",
"values": ["$0.99"],
"timestamp": 1739950503
type: 'quota_exceed',
title: '额度预警通知',
content:
'您的额度即将用尽,当前剩余额度为 {{value}}',
values: ['$0.99'],
timestamp: 1739950503,
}}
title="webhook"
language="json"
title='webhook'
language='json'
/>
</div>
<div className="text-xs text-gray-500 leading-relaxed">
<div><strong>type:</strong> {t('通知类型 (quota_exceed: 额度预警)')} </div>
<div><strong>title:</strong> {t('通知标题')}</div>
<div><strong>content:</strong> {t('通知内容,支持 {{value}} 变量占位符')}</div>
<div><strong>values:</strong> {t('按顺序替换content中的变量占位符')}</div>
<div><strong>timestamp:</strong> {t('Unix时间戳')}</div>
<div className='text-xs text-gray-500 leading-relaxed'>
<div>
<strong>type:</strong>{' '}
{t('通知类型 (quota_exceed: 额度预警)')}{' '}
</div>
<div>
<strong>title:</strong> {t('通知标题')}
</div>
<div>
<strong>content:</strong>{' '}
{t('通知内容,支持 {{value}} 变量占位符')}
</div>
<div>
<strong>values:</strong>{' '}
{t('按顺序替换content中的变量占位符')}
</div>
<div>
<strong>timestamp:</strong> {t('Unix时间戳')}
</div>
</div>
</div>
</Form.Slot>
@@ -239,21 +266,25 @@ const NotificationSettings = ({
{/* 价格设置 Tab */}
<TabPane
tab={
<div className="flex items-center">
<DollarSign size={16} className="mr-2" />
<div className='flex items-center'>
<DollarSign size={16} className='mr-2' />
{t('价格设置')}
</div>
}
itemKey="pricing"
itemKey='pricing'
>
<div className="py-4">
<div className='py-4'>
<Form.Switch
field='acceptUnsetModelRatioModel'
label={t('接受未设置价格模型')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) => handleFormChange('acceptUnsetModelRatioModel', value)}
extraText={t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
onChange={(value) =>
handleFormChange('acceptUnsetModelRatioModel', value)
}
extraText={t(
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
)}
/>
</div>
</TabPane>
@@ -261,21 +292,23 @@ const NotificationSettings = ({
{/* 隐私设置 Tab */}
<TabPane
tab={
<div className="flex items-center">
<ShieldCheck size={16} className="mr-2" />
<div className='flex items-center'>
<ShieldCheck size={16} className='mr-2' />
{t('隐私设置')}
</div>
}
itemKey="privacy"
itemKey='privacy'
>
<div className="py-4">
<div className='py-4'>
<Form.Switch
field='recordIpLog'
label={t('记录请求与错误日志IP')}
checkedText={t('开')}
uncheckedText={t('关')}
onChange={(value) => handleFormChange('recordIpLog', value)}
extraText={t('开启后,仅"消费"和"错误"日志将记录您的客户端IP地址')}
extraText={t(
'开启后,仅"消费"和"错误"日志将记录您的客户端IP地址',
)}
/>
</div>
</TabPane>