Commit 70a4b576 by t0ng7u

feat(homepage): enhance banner visuals & UX

• Added read-only Base URL input that shows `status.server_address` (fallback `window.location.origin`) and copies value on click.
• Embedded `ScrollList` as input `suffix`; auto-cycles common endpoints every 3 s and allows manual selection.
• Introduced `API_ENDPOINTS` array in `web/src/constants/common.constant.js` for centralized endpoint management.
• Implemented custom CSS to hide ScrollList wheel indicators / scrollbars for a cleaner look.
• Created two blurred colour spheres behind the banner (`blur-ball-indigo`, `blur-ball-teal`) with light-/dark-mode opacity tweaks and lower vertical placement.
• Increased letter-spacing for Chinese heading via conditional `tracking-wide` / `md:tracking-wider` classes to improve readability.
• Misc: updated imports, helper functions, and responsive sizes to keep UI consistent across devices.
parent 6220b3ee
...@@ -11,7 +11,7 @@ import { API, getLogo, getSystemName, showError, setStatusData } from '../../hel ...@@ -11,7 +11,7 @@ import { API, getLogo, getSystemName, showError, setStatusData } from '../../hel
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
import { StatusContext } from '../../context/Status/index.js'; import { StatusContext } from '../../context/Status/index.js';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
const { Sider, Content, Header, Footer } = Layout; const { Sider, Content, Header } = Layout;
const PageLayout = () => { const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
...@@ -94,8 +94,6 @@ const PageLayout = () => { ...@@ -94,8 +94,6 @@ const PageLayout = () => {
</Header> </Header>
<Layout <Layout
style={{ style={{
marginTop: '64px',
height: 'calc(100vh - 64px)',
overflow: styleState.isMobile ? 'visible' : 'auto', overflow: styleState.isMobile ? 'visible' : 'auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
......
...@@ -379,257 +379,268 @@ const PersonalSetting = () => { ...@@ -379,257 +379,268 @@ const PersonalSetting = () => {
}; };
return ( return (
<div className="bg-gray-50"> <div className="bg-gray-50 mt-[64px]">
<Layout> <div className="flex justify-center">
<Layout.Content> <div className="w-full">
{/* 主卡片容器 */}
<div className="flex justify-center"> <Card className="!rounded-2xl shadow-lg border-0">
<div className="w-full"> {/* 顶部用户信息区域 */}
{/* 主卡片容器 */} <Card
<Card className="!rounded-2xl shadow-lg border-0"> className="!rounded-2xl !border-0 !shadow-lg overflow-hidden"
{/* 顶部用户信息区域 */} style={{
<Card background: theme === 'dark'
className="!rounded-2xl !border-0 !shadow-lg overflow-hidden" ? 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)'
style={{ : 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)',
background: theme === 'dark' position: 'relative'
? 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)' }}
: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)', bodyStyle={{ padding: 0 }}
position: 'relative' >
}} {/* 装饰性背景元素 */}
bodyStyle={{ padding: 0 }} <div className="absolute inset-0 overflow-hidden">
> <div className="absolute -top-10 -right-10 w-40 h-40 bg-slate-400 dark:bg-slate-500 opacity-5 rounded-full"></div>
{/* 装饰性背景元素 */} <div className="absolute -bottom-16 -left-16 w-48 h-48 bg-slate-300 dark:bg-slate-400 opacity-8 rounded-full"></div>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute top-1/2 right-1/4 w-24 h-24 bg-slate-400 dark:bg-slate-500 opacity-6 rounded-full"></div>
<div className="absolute -top-10 -right-10 w-40 h-40 bg-slate-400 dark:bg-slate-500 opacity-5 rounded-full"></div> </div>
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-slate-300 dark:bg-slate-400 opacity-8 rounded-full"></div>
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-slate-400 dark:bg-slate-500 opacity-6 rounded-full"></div> <div className="relative p-4 sm:p-6 md:p-8 text-gray-600 dark:text-gray-300">
<div className="flex justify-between items-start mb-4 sm:mb-6">
<div className="flex items-center flex-1 min-w-0">
<Avatar
size='large'
className="mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400"
>
{getAvatarText()}
</Avatar>
<div className="flex-1 min-w-0">
<div className="text-base sm:text-lg font-semibold truncate text-gray-800 dark:text-gray-100">
{getUsername()}
</div>
<div className="mt-1 flex flex-wrap gap-1 sm:gap-2">
{isRoot() ? (
<Tag
size='small'
className="!rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
style={{ fontWeight: '500' }}
>
{t('超级管理员')}
</Tag>
) : isAdmin() ? (
<Tag
size='small'
className="!rounded-full bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
style={{ fontWeight: '500' }}
>
{t('管理员')}
</Tag>
) : (
<Tag
size='small'
className="!rounded-full bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
style={{ fontWeight: '500' }}
>
{t('普通用户')}
</Tag>
)}
<Tag
size='small'
className="!rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
style={{ fontWeight: '500' }}
>
ID: {userState?.user?.id}
</Tag>
</div>
</div>
</div> </div>
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-md flex-shrink-0 ml-2 bg-slate-400 dark:bg-slate-500">
<IconUser size="default" className="text-white" />
</div>
</div>
<div className="relative p-4 sm:p-6 md:p-8 text-gray-600 dark:text-gray-300"> <div className="mb-4 sm:mb-6">
<div className="flex justify-between items-start mb-4 sm:mb-6"> <div className="text-xs sm:text-sm mb-1 sm:mb-2 text-gray-500 dark:text-gray-400">
<div className="flex items-center flex-1 min-w-0"> {t('当前余额')}
<Avatar </div>
size='large' <div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide text-gray-900 dark:text-gray-100">
className="mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400" {renderQuota(userState?.user?.quota)}
> </div>
{getAvatarText()} </div>
</Avatar>
<div className="flex-1 min-w-0"> <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end">
<div className="text-base sm:text-lg font-semibold truncate text-gray-800 dark:text-gray-100"> <div className="grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0">
{getUsername()} <div className="text-center sm:text-left">
</div> <div className="text-xs text-gray-400 dark:text-gray-500">
<div className="mt-1 flex flex-wrap gap-1 sm:gap-2"> {t('历史消耗')}
{isRoot() ? (
<Tag
size='small'
className="!rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
style={{ fontWeight: '500' }}
>
{t('超级管理员')}
</Tag>
) : isAdmin() ? (
<Tag
size='small'
className="!rounded-full bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
style={{ fontWeight: '500' }}
>
{t('管理员')}
</Tag>
) : (
<Tag
size='small'
className="!rounded-full bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
style={{ fontWeight: '500' }}
>
{t('普通用户')}
</Tag>
)}
<Tag
size='small'
className="!rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"
style={{ fontWeight: '500' }}
>
ID: {userState?.user?.id}
</Tag>
</div>
</div>
</div> </div>
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-md flex-shrink-0 ml-2 bg-slate-400 dark:bg-slate-500"> <div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
<IconUser size="default" className="text-white" /> {renderQuota(userState?.user?.used_quota)}
</div> </div>
</div> </div>
<div className="text-center sm:text-left">
<div className="mb-4 sm:mb-6"> <div className="text-xs text-gray-400 dark:text-gray-500">
<div className="text-xs sm:text-sm mb-1 sm:mb-2 text-gray-500 dark:text-gray-400"> {t('请求次数')}
{t('当前余额')} </div>
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
{userState.user?.request_count || 0}
</div> </div>
<div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide text-gray-900 dark:text-gray-100"> </div>
{renderQuota(userState?.user?.quota)} <div className="text-center sm:text-left">
<div className="text-xs text-gray-400 dark:text-gray-500">
{t('用户分组')}
</div>
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
{userState?.user?.group || t('默认')}
</div> </div>
</div> </div>
</div>
</div>
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-end"> <div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"></div>
<div className="grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0"> </div>
<div className="text-center sm:text-left"> </Card>
<div className="text-xs text-gray-400 dark:text-gray-500">
{t('历史消耗')} {/* 主内容区域 - 使用Tabs组织不同功能模块 */}
</div> <div className="p-4">
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300"> <Tabs type='line' defaultActiveKey='models' className="modern-tabs">
{renderQuota(userState?.user?.used_quota)} {/* 可用模型Tab */}
</div> <TabPane
</div> tab={
<div className="text-center sm:text-left"> <div className="flex items-center">
<div className="text-xs text-gray-400 dark:text-gray-500"> <Settings size={16} className="mr-2" />
{t('请求次数')} {t('可用模型')}
</div> </div>
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300"> }
{userState.user?.request_count || 0} itemKey='models'
</div> >
<div className="gap-6 py-4">
{/* 可用模型部分 */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<Settings size={20} className="text-slate-600 dark:text-slate-300" />
</div> </div>
<div className="text-center sm:text-left"> <div>
<div className="text-xs text-gray-400 dark:text-gray-500"> <Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title>
{t('用户分组')} <div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div>
</div>
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
{userState?.user?.group || t('默认')}
</div>
</div> </div>
</div> </div>
</div>
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"></div> {modelsLoading ? (
</div> // 骨架屏加载状态 - 模拟实际加载后的布局
</Card> <div className="space-y-4">
{/* 模拟分类标签 */}
{/* 主内容区域 - 使用Tabs组织不同功能模块 */} <div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
<div className="p-4"> <div className="flex overflow-x-auto py-2 gap-2">
<Tabs type='line' defaultActiveKey='models' className="modern-tabs"> {Array.from({ length: 8 }).map((_, index) => (
{/* 可用模型Tab */} <Skeleton.Button key={`cat-${index}`} style={{
<TabPane width: index === 0 ? 130 : 100 + Math.random() * 50,
tab={ height: 36,
<div className="flex items-center"> borderRadius: 8
<Settings size={16} className="mr-2" /> }} />
{t('可用模型')} ))}
</div>
}
itemKey='models'
>
<div className="gap-6 py-4">
{/* 可用模型部分 */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="flex items-center mb-4">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<Settings size={20} className="text-slate-600 dark:text-slate-300" />
</div>
<div>
<Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title>
<div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div>
</div> </div>
</div> </div>
{modelsLoading ? ( {/* 模拟模型标签列表 */}
// 骨架屏加载状态 - 模拟实际加载后的布局 <div className="flex flex-wrap gap-2">
<div className="space-y-4"> {Array.from({ length: 20 }).map((_, index) => (
{/* 模拟分类标签 */} <Skeleton.Button
<div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}> key={`model-${index}`}
<div className="flex overflow-x-auto py-2 gap-2"> style={{
{Array.from({ length: 8 }).map((_, index) => ( width: 100 + Math.random() * 100,
<Skeleton.Button key={`cat-${index}`} style={{ height: 32,
width: index === 0 ? 130 : 100 + Math.random() * 50, borderRadius: 16,
height: 36, margin: '4px'
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>
<> </div>
{/* 模型分类标签页 */} ) : models.length === 0 ? (
<div className="mb-4"> <div className="py-8">
<Tabs <Empty
type="card" image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
activeKey={activeModelCategory} darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
onChange={key => setActiveModelCategory(key)} description={t('没有可用模型')}
className="mt-2" style={{ padding: '24px 0' }}
> />
{Object.entries(getModelCategories(t)).map(([key, category]) => { </div>
// 计算该分类下的模型数量 ) : (
const modelCount = key === 'all' <>
? models.length {/* 模型分类标签页 */}
: models.filter(model => category.filter({ model_name: model })).length; <div className="mb-4">
<Tabs
if (modelCount === 0 && key !== 'all') return null; type="card"
activeKey={activeModelCategory}
return ( onChange={key => setActiveModelCategory(key)}
<TabPane className="mt-2"
tab={ >
<span className="flex items-center gap-2"> {Object.entries(getModelCategories(t)).map(([key, category]) => {
{category.icon && <span className="w-4 h-4">{category.icon}</span>} // 计算该分类下的模型数量
{category.label} const modelCount = key === 'all'
<Tag ? models.length
color={activeModelCategory === key ? 'red' : 'grey'} : models.filter(model => category.filter({ model_name: model })).length;
size='small'
shape='circle' if (modelCount === 0 && key !== 'all') return null;
>
{modelCount} return (
</Tag> <TabPane
</span> tab={
} <span className="flex items-center gap-2">
itemKey={key} {category.icon && <span className="w-4 h-4">{category.icon}</span>}
key={key} {category.label}
/> <Tag
); color={activeModelCategory === key ? 'red' : 'grey'}
})} size='small'
</Tabs> shape='circle'
</div> >
{modelCount}
<div className="bg-white dark:bg-gray-700 rounded-lg p-3"> </Tag>
{(() => { </span>
// 根据当前选中的分类过滤模型 }
const categories = getModelCategories(t); itemKey={key}
const filteredModels = activeModelCategory === 'all' key={key}
? models />
: models.filter(model => categories[activeModelCategory].filter({ model_name: model })); );
})}
// 如果过滤后没有模型,显示空状态 </Tabs>
if (filteredModels.length === 0) { </div>
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) { <div className="bg-white dark:bg-gray-700 rounded-lg p-3">
return ( {(() => {
// 根据当前选中的分类过滤模型
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: 'large',
shape: 'circle',
onClick: () => copyText(model),
})
))}
</Space>
);
} else {
return (
<>
<Collapsible isOpen={isModelsExpanded}>
<Space wrap> <Space wrap>
{filteredModels.map((model) => ( {filteredModels.map((model) => (
renderModelTag(model, { renderModelTag(model, {
...@@ -638,527 +649,513 @@ const PersonalSetting = () => { ...@@ -638,527 +649,513 @@ const PersonalSetting = () => {
onClick: () => copyText(model), onClick: () => copyText(model),
}) })
))} ))}
<Tag
color='grey'
type='light'
className="cursor-pointer !rounded-lg"
onClick={() => setIsModelsExpanded(false)}
icon={<IconChevronUp />}
>
{t('收起')}
</Tag>
</Space> </Space>
); </Collapsible>
} else { {!isModelsExpanded && (
return ( <Space wrap>
<> {filteredModels
<Collapsible isOpen={isModelsExpanded}> .slice(0, MODELS_DISPLAY_COUNT)
<Space wrap> .map((model) => (
{filteredModels.map((model) => ( renderModelTag(model, {
renderModelTag(model, { size: 'large',
size: 'large', shape: 'circle',
shape: 'circle', onClick: () => copyText(model),
onClick: () => copyText(model), })
}) ))}
))} <Tag
<Tag color='grey'
color='grey' type='light'
type='light' className="cursor-pointer !rounded-lg"
className="cursor-pointer !rounded-lg" onClick={() => setIsModelsExpanded(true)}
onClick={() => setIsModelsExpanded(false)} icon={<IconChevronDown />}
icon={<IconChevronUp />} >
> {t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
{t('收起')} </Tag>
</Tag> </Space>
</Space> )}
</Collapsible> </>
{!isModelsExpanded && ( );
<Space wrap> }
{filteredModels })()}
.slice(0, MODELS_DISPLAY_COUNT) </div>
.map((model) => ( </>
renderModelTag(model, { )}
size: 'large', </div>
shape: 'circle', </div>
onClick: () => copyText(model), </TabPane>
})
))} {/* 账户绑定Tab */}
<Tag <TabPane
color='grey' tab={
type='light' <div className="flex items-center">
className="cursor-pointer !rounded-lg" <UserPlus size={16} className="mr-2" />
onClick={() => setIsModelsExpanded(true)} {t('账户绑定')}
icon={<IconChevronDown />} </div>
> }
{t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')} itemKey='account'
</Tag> >
</Space> <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-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>
)} </div>
</div> <Button
</div> type="primary"
</TabPane> theme="outline"
size="small"
{/* 账户绑定Tab */} onClick={() => setShowEmailBindModal(true)}
<TabPane className="!rounded-lg"
tab={ >
<div className="flex items-center"> {userState.user && userState.user.email !== ''
<UserPlus size={16} className="mr-2" /> ? t('修改绑定')
{t('账户绑定')} : t('绑定')}
</Button>
</div> </div>
} </Card>
itemKey='account'
> {/* 微信绑定 */}
<div className="py-4"> <Card
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> className="!rounded-xl transition-shadow"
{/* 邮箱绑定 */} bodyStyle={{ padding: '16px' }}
<Card shadows='hover'
className="!rounded-xl transition-shadow" >
bodyStyle={{ padding: '16px' }} <div className="flex items-center justify-between">
shadows='hover' <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"
> >
<div className="flex items-center justify-between"> {userState.user && userState.user.wechat_id !== ''
<div className="flex items-center flex-1"> ? t('修改绑定')
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3"> : status.wechat_login
<IconMail size="default" className="text-slate-600 dark:text-slate-300" /> ? t('绑定')
</div> : t('未启用')}
<div className="flex-1 min-w-0"> </Button>
<div className="font-medium text-gray-900">{t('邮箱')}</div> </div>
<div className="text-sm text-gray-500 truncate"> </Card>
{userState.user && userState.user.email !== ''
? userState.user.email {/* GitHub绑定 */}
: t('未绑定')} <Card
</div> className="!rounded-xl transition-shadow"
</div> 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-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>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => setShowEmailBindModal(true)}
className="!rounded-lg"
>
{userState.user && userState.user.email !== ''
? t('修改绑定')
: t('绑定')}
</Button>
</div> </div>
</Card> </div>
<Button
{/* 微信绑定 */} type="primary"
<Card theme="outline"
className="!rounded-xl transition-shadow" size="small"
bodyStyle={{ padding: '16px' }} onClick={() => onGitHubOAuthClicked(status.github_client_id)}
shadows='hover' disabled={
(userState.user && userState.user.github_id !== '') ||
!status.github_oauth
}
className="!rounded-lg"
> >
<div className="flex items-center justify-between"> {status.github_oauth ? t('绑定') : t('未启用')}
<div className="flex items-center flex-1"> </Button>
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3"> </div>
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" /> </Card>
</div>
<div className="flex-1 min-w-0"> {/* OIDC绑定 */}
<div className="font-medium text-gray-900">{t('微信')}</div> <Card
<div className="text-sm text-gray-500 truncate"> className="!rounded-xl transition-shadow"
{userState.user && userState.user.wechat_id !== '' bodyStyle={{ padding: '16px' }}
? t('已绑定') shadows='hover'
: t('未绑定')} >
</div> <div className="flex items-center justify-between">
</div> <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>
<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> </div>
</Card> </div>
<Button
{/* GitHub绑定 */} type="primary"
<Card theme="outline"
className="!rounded-xl transition-shadow" size="small"
bodyStyle={{ padding: '16px' }} onClick={() => onOIDCClicked(
shadows='hover' status.oidc_authorization_endpoint,
status.oidc_client_id,
)}
disabled={
(userState.user && userState.user.oidc_id !== '') ||
!status.oidc_enabled
}
className="!rounded-lg"
> >
<div className="flex items-center justify-between"> {status.oidc_enabled ? t('绑定') : t('未启用')}
<div className="flex items-center flex-1"> </Button>
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3"> </div>
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" /> </Card>
</div>
<div className="flex-1 min-w-0"> {/* Telegram绑定 */}
<div className="font-medium text-gray-900">{t('GitHub')}</div> <Card
<div className="text-sm text-gray-500 truncate"> className="!rounded-xl transition-shadow"
{userState.user && userState.user.github_id !== '' bodyStyle={{ padding: '16px' }}
? userState.user.github_id shadows='hover'
: t('未绑定')} >
</div> <div className="flex items-center justify-between">
</div> <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>
<Button </div>
type="primary" </div>
theme="outline" <div className="flex-shrink-0">
size="small" {status.telegram_oauth ? (
onClick={() => onGitHubOAuthClicked(status.github_client_id)} userState.user.telegram_id !== '' ? (
disabled={ <Button disabled={true} size="small" className="!rounded-lg">
(userState.user && userState.user.github_id !== '') || {t('已绑定')}
!status.github_oauth </Button>
} ) : (
className="!rounded-lg" <div className="scale-75">
> <TelegramLoginButton
{status.github_oauth ? t('绑定') : t('未启用')} dataAuthUrl='/api/oauth/telegram/bind'
botName={status.telegram_bot_name}
/>
</div>
)
) : (
<Button disabled={true} size="small" className="!rounded-lg">
{t('未启用')}
</Button> </Button>
)}
</div>
</div>
</Card>
{/* LinuxDO绑定 */}
<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-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>
</Card> <div className="flex-1 min-w-0">
<div className="font-medium text-gray-900">{t('LinuxDO')}</div>
{/* OIDC绑定 */} <div className="text-sm text-gray-500 truncate">
<Card {userState.user && userState.user.linux_do_id !== ''
className="!rounded-xl transition-shadow" ? userState.user.linux_do_id
bodyStyle={{ padding: '16px' }} : t('未绑定')}
shadows='hover' </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"
> >
<div className="flex items-center justify-between"> {status.linuxdo_oauth ? t('绑定') : t('未启用')}
<div className="flex items-center flex-1"> </Button>
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3"> </div>
<IconShield size="default" className="text-slate-600 dark:text-slate-300" /> </Card>
</div> </div>
<div className="flex-1 min-w-0"> </div>
<div className="font-medium text-gray-900">{t('OIDC')}</div> </TabPane>
<div className="text-sm text-gray-500 truncate">
{userState.user && userState.user.oidc_id !== '' {/* 安全设置Tab */}
? userState.user.oidc_id <TabPane
: t('未绑定')} tab={
</div> <div className="flex items-center">
</div> <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"
bodyStyle={{ padding: '20px' }}
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-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
<IconKey size="large" className="text-slate-600" />
</div> </div>
<Button <div className="flex-1">
type="primary" <Typography.Title heading={6} className="mb-1">
theme="outline" {t('系统访问令牌')}
size="small" </Typography.Title>
onClick={() => onOIDCClicked( <Typography.Text type="tertiary" className="text-sm">
status.oidc_authorization_endpoint, {t('用于API调用的身份验证令牌,请妥善保管')}
status.oidc_client_id, </Typography.Text>
{systemToken && (
<div className="mt-3">
<Input
readOnly
value={systemToken}
onClick={handleSystemTokenClick}
size="large"
className="!rounded-lg"
prefix={<IconKey />}
/>
</div>
)} )}
disabled={ </div>
(userState.user && userState.user.oidc_id !== '') ||
!status.oidc_enabled
}
className="!rounded-lg"
>
{status.oidc_enabled ? t('绑定') : t('未启用')}
</Button>
</div> </div>
</Card> <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>
{/* Telegram绑定 */} {/* 密码管理 */}
<Card <Card
className="!rounded-xl transition-shadow" className="!rounded-xl w-full"
bodyStyle={{ padding: '16px' }} bodyStyle={{ padding: '20px' }}
shadows='hover' shadows='hover'
> >
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
<div className="flex items-center flex-1"> <div className="flex items-start w-full sm:w-auto">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3"> <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" /> <IconLock size="large" className="text-slate-600" />
</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>
<div className="flex-shrink-0"> <div>
{status.telegram_oauth ? ( <Typography.Title heading={6} className="mb-1">
userState.user.telegram_id !== '' ? ( {t('密码管理')}
<Button disabled={true} size="small" className="!rounded-lg"> </Typography.Title>
{t('已绑定')} <Typography.Text type="tertiary" className="text-sm">
</Button> {t('定期更改密码可以提高账户安全性')}
) : ( </Typography.Text>
<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>
</div> </div>
</Card> <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>
{/* LinuxDO绑定 */} {/* 危险区域 */}
<Card <Card
className="!rounded-xl transition-shadow" className="!rounded-xl border-red-200 w-full"
bodyStyle={{ padding: '16px' }} bodyStyle={{ padding: '20px' }}
shadows='hover' shadows='hover'
> >
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
<div className="flex items-center flex-1"> <div className="flex items-start w-full sm:w-auto">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3"> <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" /> <IconDelete size="large" className="text-slate-600" />
</div> </div>
<div className="flex-1 min-w-0"> <div>
<div className="font-medium text-gray-900">{t('LinuxDO')}</div> <Typography.Title heading={6} className="mb-1 text-slate-700">
<div className="text-sm text-gray-500 truncate"> {t('删除账户')}
{userState.user && userState.user.linux_do_id !== '' </Typography.Title>
? userState.user.linux_do_id <Typography.Text type="tertiary" className="text-sm">
: t('未绑定')} {t('此操作不可逆,所有数据将被永久删除')}
</div> </Typography.Text>
</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> </div>
</Card> <Button
</div> type="danger"
</div> theme="solid"
</TabPane> onClick={() => setShowAccountDeleteModal(true)}
className="!rounded-lg w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
{/* 安全设置Tab */} icon={<IconDelete />}
<TabPane >
tab={ {t('删除账户')}
<div className="flex items-center"> </Button>
<ShieldCheck size={16} className="mr-2" /> </div>
{t('安全设置')} </Card>
</div> </Space>
} </div>
itemKey='security' </div>
> </TabPane>
<div className="py-4">
{/* 通知设置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="space-y-6">
<Space vertical className='w-full'> {/* 通知方式选择 */}
{/* 系统访问令牌 */} <div className="bg-gray-50 rounded-xl">
<Card <Typography.Text strong className="block mb-4 pt-4">{t('通知方式')}</Typography.Text>
className="!rounded-xl w-full" <RadioGroup
bodyStyle={{ padding: '20px' }} value={notificationSettings.warningType}
shadows='hover' onChange={(value) =>
handleNotificationSettingChange('warningType', value)
}
type="pureCard"
> >
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4"> <Radio value='email' className="!p-4 !rounded-lg">
<div className="flex items-start w-full sm:w-auto"> <div className="flex items-center">
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0"> <IconMail className="mr-2 text-slate-600" />
<IconKey size="large" className="text-slate-600" /> <div>
</div> <div className="font-medium">{t('邮件通知')}</div>
<div className="flex-1"> <div className="text-sm text-gray-500">{t('通过邮件接收通知')}</div>
<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>
</div> </div>
<Button </Radio>
type="primary" <Radio value='webhook' className="!p-4 !rounded-lg">
theme="solid" <div className="flex items-center">
onClick={generateAccessToken} <Webhook size={16} className="mr-2 text-slate-600" />
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"
bodyStyle={{ padding: '20px' }}
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-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
<IconLock size="large" className="text-slate-600" />
</div>
<div> <div>
<Typography.Title heading={6} className="mb-1"> <div className="font-medium">{t('Webhook通知')}</div>
{t('密码管理')} <div className="text-sm text-gray-500">{t('通过HTTP请求接收通知')}</div>
</Typography.Title>
<Typography.Text type="tertiary" className="text-sm">
{t('定期更改密码可以提高账户安全性')}
</Typography.Text>
</div> </div>
</div> </div>
<Button </Radio>
type="primary" </RadioGroup>
theme="solid" </div>
onClick={() => setShowChangePasswordModal(true)}
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
icon={<IconLock />}
>
{t('修改密码')}
</Button>
</div>
</Card>
{/* 危险区域 */} {/* Webhook设置 */}
<Card {notificationSettings.warningType === 'webhook' && (
className="!rounded-xl border-red-200 w-full" <div className="space-y-4">
bodyStyle={{ padding: '20px' }} <div className="bg-white rounded-xl">
shadows='hover' <Typography.Text strong className="block mb-3">{t('Webhook地址')}</Typography.Text>
> <Input
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4"> value={notificationSettings.webhookUrl}
<div className="flex items-start w-full sm:w-auto"> onChange={(val) =>
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0"> handleNotificationSettingChange('webhookUrl', val)
<IconDelete size="large" className="text-slate-600" /> }
</div> placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
<div> size="large"
<Typography.Title heading={6} className="mb-1 text-slate-700"> className="!rounded-lg"
{t('删除账户')} prefix={<Webhook size={16} className="m-2" />}
</Typography.Title> />
<Typography.Text type="tertiary" className="text-sm"> <div className="text-gray-500 text-sm mt-2">
{t('此操作不可逆,所有数据将被永久删除')} {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
</Typography.Text>
</div>
</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> </div>
</Card>
</Space> <div className="bg-white rounded-xl">
</div> <Typography.Text strong className="block mb-3">{t('接口凭证(可选)')}</Typography.Text>
</div> <Input
</TabPane> value={notificationSettings.webhookSecret}
onChange={(val) =>
{/* 通知设置Tab */} handleNotificationSettingChange('webhookSecret', val)
<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
value={notificationSettings.warningType}
onChange={(value) =>
handleNotificationSettingChange('warningType', value)
} }
type="pureCard" placeholder={t('请输入密钥')}
> size="large"
<Radio value='email' className="!p-4 !rounded-lg"> className="!rounded-lg"
<div className="flex items-center"> prefix={<IconKey />}
<IconMail className="mr-2 text-slate-600" /> />
<div> <div className="text-gray-500 text-sm mt-2">
<div className="font-medium">{t('邮件通知')}</div> {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
<div className="text-sm text-gray-500">{t('通过邮件接收通知')}</div> </div>
</div>
</div>
</Radio>
<Radio value='webhook' className="!p-4 !rounded-lg">
<div className="flex items-center">
<Webhook size={16} className="mr-2 text-slate-600" />
<div>
<div className="font-medium">{t('Webhook通知')}</div>
<div className="text-sm text-gray-500">{t('通过HTTP请求接收通知')}</div>
</div>
</div>
</Radio>
</RadioGroup>
</div> </div>
{/* Webhook设置 */} <div className="bg-slate-50 rounded-xl">
{notificationSettings.warningType === 'webhook' && ( <div className="flex items-center justify-between cursor-pointer" onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
<div className="space-y-4"> <div className="flex items-center">
<div className="bg-white rounded-xl"> <Globe size={16} className="mr-2 text-slate-600" />
<Typography.Text strong className="block mb-3">{t('Webhook地址')}</Typography.Text> <Typography.Text strong className="text-slate-700">
<Input {t('Webhook请求结构')}
value={notificationSettings.webhookUrl} </Typography.Text>
onChange={(val) =>
handleNotificationSettingChange('webhookUrl', val)
}
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
size="large"
className="!rounded-lg"
prefix={<Webhook size={16} className="m-2" />}
/>
<div className="text-gray-500 text-sm mt-2">
{t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
</div>
</div>
<div className="bg-white rounded-xl">
<Typography.Text strong className="block mb-3">{t('接口凭证(可选)')}</Typography.Text>
<Input
value={notificationSettings.webhookSecret}
onChange={(val) =>
handleNotificationSettingChange('webhookSecret', val)
}
placeholder={t('请输入密钥')}
size="large"
className="!rounded-lg"
prefix={<IconKey />}
/>
<div className="text-gray-500 text-sm mt-2">
{t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
</div>
</div> </div>
{showWebhookDocs ? <IconChevronUp /> : <IconChevronDown />}
<div className="bg-slate-50 rounded-xl"> </div>
<div className="flex items-center justify-between cursor-pointer" onClick={() => setShowWebhookDocs(!showWebhookDocs)}> <Collapsible isOpen={showWebhookDocs}>
<div className="flex items-center"> <pre className="mt-4 bg-gray-800 text-gray-100 rounded-lg text-sm overflow-x-auto">
<Globe size={16} className="mr-2 text-slate-600" /> {`{
<Typography.Text strong className="text-slate-700">
{t('Webhook请求结构')}
</Typography.Text>
</div>
{showWebhookDocs ? <IconChevronUp /> : <IconChevronDown />}
</div>
<Collapsible isOpen={showWebhookDocs}>
<pre className="mt-4 bg-gray-800 text-gray-100 rounded-lg text-sm overflow-x-auto">
{`{
"type": "quota_exceed", // 通知类型 "type": "quota_exceed", // 通知类型
"title": "标题", // 通知标题 "title": "标题", // 通知标题
"content": "通知内容", // 通知内容,支持 {{value}} 变量占位符 "content": "通知内容", // 通知内容,支持 {{value}} 变量占位符
...@@ -1174,158 +1171,156 @@ const PersonalSetting = () => { ...@@ -1174,158 +1171,156 @@ const PersonalSetting = () => {
"values": ["$0.99"], "values": ["$0.99"],
"timestamp": 1739950503 "timestamp": 1739950503
}`} }`}
</pre> </pre>
</Collapsible> </Collapsible>
</div> </div>
</div> </div>
)} )}
{/* 邮件设置 */}
{notificationSettings.warningType === 'email' && (
<div className="bg-white rounded-xl">
<Typography.Text strong className="block mb-3">{t('通知邮箱')}</Typography.Text>
<Input
value={notificationSettings.notificationEmail}
onChange={(val) =>
handleNotificationSettingChange('notificationEmail', val)
}
placeholder={t('留空则使用账号绑定的邮箱')}
size="large"
className="!rounded-lg"
prefix={<IconMail />}
/>
<div className="text-gray-500 text-sm mt-2">
{t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
</div>
</div>
)}
{/* 预警阈值 */} {/* 邮件设置 */}
<div className="bg-white rounded-xl"> {notificationSettings.warningType === 'email' && (
<Typography.Text strong className="block mb-3"> <div className="bg-white rounded-xl">
{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)} <Typography.Text strong className="block mb-3">{t('通知邮箱')}</Typography.Text>
</Typography.Text> <Input
<AutoComplete value={notificationSettings.notificationEmail}
value={notificationSettings.warningThreshold} onChange={(val) =>
onChange={(val) => handleNotificationSettingChange('notificationEmail', val)
handleNotificationSettingChange('warningThreshold', val) }
} placeholder={t('留空则使用账号绑定的邮箱')}
size="large" size="large"
className="!rounded-lg w-full max-w-xs" className="!rounded-lg"
placeholder={t('请输入预警额度')} prefix={<IconMail />}
data={[ />
{ value: 100000, label: '0.2$' }, <div className="text-gray-500 text-sm mt-2">
{ value: 500000, label: '1$' }, {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
{ value: 1000000, label: '5$' },
{ value: 5000000, label: '10$' },
]}
prefix={<IconBell />}
/>
<div className="text-gray-500 text-sm mt-2">
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
</div>
</div> </div>
</div> </div>
</TabPane> )}
<TabPane {/* 预警阈值 */}
tab={t('价格设置')} <div className="bg-white rounded-xl">
itemKey='price' <Typography.Text strong className="block mb-3">
> {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}
<div className="py-4"> </Typography.Text>
<div className="space-y-4"> <AutoComplete
{/* 接受未设置价格模型 */} value={notificationSettings.warningThreshold}
<div className="bg-white rounded-xl"> onChange={(val) =>
<div className="flex items-start"> handleNotificationSettingChange('warningThreshold', val)
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1"> }
<Shield size={20} className="text-slate-600" /> size="large"
</div> className="!rounded-lg w-full max-w-xs"
<div className="flex-1"> placeholder={t('请输入预警额度')}
<div className="flex items-center justify-between"> data={[
<div> { value: 100000, label: '0.2$' },
<Typography.Text strong className="block mb-2"> { value: 500000, label: '1$' },
{t('接受未设置价格模型')} { value: 1000000, label: '5$' },
</Typography.Text> { value: 5000000, label: '10$' },
<div className="text-gray-500 text-sm"> ]}
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} prefix={<IconBell />}
</div> />
</div> <div className="text-gray-500 text-sm mt-2">
<Checkbox {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
checked={notificationSettings.acceptUnsetModelRatioModel} </div>
onChange={(e) => </div>
handleNotificationSettingChange( </div>
'acceptUnsetModelRatioModel', </TabPane>
e.target.checked,
) <TabPane
} tab={t('价格设置')}
className="ml-4" itemKey='price'
/> >
<div className="py-4">
<div className="space-y-4">
{/* 接受未设置价格模型 */}
<div className="bg-white rounded-xl">
<div className="flex items-start">
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
<Shield size={20} className="text-slate-600" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<div>
<Typography.Text strong className="block mb-2">
{t('接受未设置价格模型')}
</Typography.Text>
<div className="text-gray-500 text-sm">
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
</div> </div>
</div> </div>
<Checkbox
checked={notificationSettings.acceptUnsetModelRatioModel}
onChange={(e) =>
handleNotificationSettingChange(
'acceptUnsetModelRatioModel',
e.target.checked,
)
}
className="ml-4"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</TabPane> </div>
</div>
<TabPane </TabPane>
tab={t('IP记录')}
itemKey='ip' <TabPane
> tab={t('IP记录')}
<div className="py-4"> itemKey='ip'
<div className="bg-white rounded-xl"> >
<div className="flex items-start"> <div className="py-4">
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1"> <div className="bg-white rounded-xl">
<ShieldCheck size={20} className="text-slate-600" /> <div className="flex items-start">
</div> <div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
<div className="flex-1"> <ShieldCheck size={20} className="text-slate-600" />
<div className="flex items-center justify-between"> </div>
<div> <div className="flex-1">
<Typography.Text strong className="block mb-2"> <div className="flex items-center justify-between">
{t('记录请求与错误日志 IP')} <div>
</Typography.Text> <Typography.Text strong className="block mb-2">
<div className="text-gray-500 text-sm"> {t('记录请求与错误日志 IP')}
{t('开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址')} </Typography.Text>
</div> <div className="text-gray-500 text-sm">
</div> {t('开启后,仅“消费”和“错误”日志将记录您的客户端 IP 地址')}
<Checkbox
checked={notificationSettings.recordIpLog}
onChange={(e) =>
handleNotificationSettingChange(
'recordIpLog',
e.target.checked,
)
}
className="ml-4"
/>
</div> </div>
</div> </div>
<Checkbox
checked={notificationSettings.recordIpLog}
onChange={(e) =>
handleNotificationSettingChange(
'recordIpLog',
e.target.checked,
)
}
className="ml-4"
/>
</div> </div>
</div> </div>
</div> </div>
</TabPane> </div>
</Tabs>
<div className="mt-6 flex justify-end">
<Button
type='primary'
onClick={saveNotificationSettings}
size="large"
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
icon={<IconSetting />}
>
{t('保存设置')}
</Button>
</div> </div>
</div> </TabPane>
</TabPane> </Tabs>
</Tabs>
</div> <div className="mt-6 flex justify-end">
</Card> <Button
type='primary'
onClick={saveNotificationSettings}
size="large"
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
icon={<IconSetting />}
>
{t('保存设置')}
</Button>
</div>
</div>
</TabPane>
</Tabs>
</div> </div>
</div> </Card>
</Layout.Content> </div>
</Layout> </div>
{/* 邮箱绑定模态框 */} {/* 邮箱绑定模态框 */}
<Modal <Modal
......
...@@ -523,10 +523,10 @@ const ModelPricing = () => { ...@@ -523,10 +523,10 @@ const ModelPricing = () => {
<div className="bg-gray-50"> <div className="bg-gray-50">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
<div className="flex justify-center p-4 sm:p-6 md:p-8"> <div className="flex justify-center">
<div className="w-full"> <div className="w-full">
{/* 主卡片容器 */} {/* 主卡片容器 */}
<Card className="!rounded-2xl shadow-lg border-0"> <Card bordered={false} className="!rounded-2xl shadow-lg border-0">
{/* 顶部状态卡片 */} {/* 顶部状态卡片 */}
<Card <Card
className="!rounded-2xl !border-0 !shadow-md overflow-hidden mb-6" className="!rounded-2xl !border-0 !shadow-md overflow-hidden mb-6"
......
...@@ -2,4 +2,19 @@ export const ITEMS_PER_PAGE = 10; // this value must keep same as the one define ...@@ -2,4 +2,19 @@ export const ITEMS_PER_PAGE = 10; // this value must keep same as the one define
export const DEFAULT_ENDPOINT = '/api/ratio_config'; export const DEFAULT_ENDPOINT = '/api/ratio_config';
export const TABLE_COMPACT_MODES_KEY = 'table_compact_modes'; export const TABLE_COMPACT_MODES_KEY = 'table_compact_modes';
\ No newline at end of file
export const API_ENDPOINTS = [
'/v1/chat/completions',
'/v1/responses',
'/v1/messages',
'/v1beta/models',
'/v1/embeddings',
'/v1/rerank',
'/v1/images/generations',
'/v1/images/edits',
'/v1/images/variations',
'/v1/audio/speech',
'/v1/audio/transcriptions',
'/v1/audio/translations'
];
\ No newline at end of file
...@@ -1422,8 +1422,8 @@ ...@@ -1422,8 +1422,8 @@
"初始化系统": "Initialize system", "初始化系统": "Initialize system",
"支持众多的大模型供应商": "Supporting various LLM providers", "支持众多的大模型供应商": "Supporting various LLM providers",
"统一的大模型接口网关": "The Unified LLMs API Gateway", "统一的大模型接口网关": "The Unified LLMs API Gateway",
"更好的价格,更好的稳定性,无需订阅": "Better price, better stability, no subscription required", "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Better price, better stability, no subscription required, just replace the model BASE URL with: ",
"开始使用": "Get Started", "获取密钥": "Get Key",
"关于我们": "About Us", "关于我们": "About Us",
"关于项目": "About Project", "关于项目": "About Project",
"联系我们": "Contact Us", "联系我们": "Contact Us",
......
...@@ -530,4 +530,66 @@ code { ...@@ -530,4 +530,66 @@ code {
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
}
/* ==================== ScrollList 定制样式 ==================== */
.semi-scrolllist,
.semi-scrolllist * {
-ms-overflow-style: none;
/* IE, Edge */
scrollbar-width: none;
/* Firefox */
background: transparent !important;
}
.semi-scrolllist::-webkit-scrollbar,
.semi-scrolllist *::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
display: none !important;
}
.semi-scrolllist-body {
padding: 1px !important;
}
.semi-scrolllist-list-outer {
padding-right: 0 !important;
}
/* ==================== Banner 背景模糊球 ==================== */
.blur-ball {
position: absolute;
width: 360px;
height: 360px;
border-radius: 50%;
filter: blur(120px);
pointer-events: none;
z-index: -1;
}
.blur-ball-indigo {
background: #6366f1;
/* indigo-500 */
top: 40px;
left: 50%;
transform: translateX(-50%);
opacity: 0.5;
}
.blur-ball-teal {
background: #14b8a6;
/* teal-400 */
top: 200px;
left: 30%;
opacity: 0.4;
}
/* 浅色主题下让模糊球更柔和 */
html:not(.dark) .blur-ball-indigo {
opacity: 0.25;
}
html:not(.dark) .blur-ball-teal {
opacity: 0.2;
} }
\ No newline at end of file
...@@ -5,7 +5,6 @@ import '@douyinfe/semi-ui/dist/css/semi.css'; ...@@ -5,7 +5,6 @@ import '@douyinfe/semi-ui/dist/css/semi.css';
import { UserProvider } from './context/User'; import { UserProvider } from './context/User';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { StatusProvider } from './context/Status'; import { StatusProvider } from './context/Status';
import { Layout } from '@douyinfe/semi-ui';
import { ThemeProvider } from './context/Theme'; import { ThemeProvider } from './context/Theme';
import { StyleProvider } from './context/Style/index.js'; import { StyleProvider } from './context/Style/index.js';
import PageLayout from './components/layout/PageLayout.js'; import PageLayout from './components/layout/PageLayout.js';
...@@ -15,7 +14,6 @@ import './index.css'; ...@@ -15,7 +14,6 @@ import './index.css';
// initialization // initialization
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
const { Sider, Content, Header, Footer } = Layout;
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<StatusProvider> <StatusProvider>
......
...@@ -105,7 +105,7 @@ const About = () => { ...@@ -105,7 +105,7 @@ const About = () => {
); );
return ( return (
<> <div className="mt-[64px]">
{aboutLoaded && about === '' ? ( {aboutLoaded && about === '' ? (
<div className="flex justify-center items-center h-screen p-8"> <div className="flex justify-center items-center h-screen p-8">
<Empty <Empty
...@@ -132,7 +132,7 @@ const About = () => { ...@@ -132,7 +132,7 @@ const About = () => {
)} )}
</> </>
)} )}
</> </div>
); );
}; };
......
...@@ -3,9 +3,9 @@ import ChannelsTable from '../../components/table/ChannelsTable'; ...@@ -3,9 +3,9 @@ import ChannelsTable from '../../components/table/ChannelsTable';
const File = () => { const File = () => {
return ( return (
<> <div className="mt-[64px]">
<ChannelsTable /> <ChannelsTable />
</> </div>
); );
}; };
......
...@@ -37,12 +37,12 @@ const ChatPage = () => { ...@@ -37,12 +37,12 @@ const ChatPage = () => {
return !isLoading && iframeSrc ? ( return !isLoading && iframeSrc ? (
<iframe <iframe
src={iframeSrc} src={iframeSrc}
style={{ width: '100%', height: '100%', border: 'none' }} style={{ width: '100%', height: 'calc(100vh - 64px)', border: 'none', marginTop: '64px' }}
title='Token Frame' title='Token Frame'
allow='camera;microphone' allow='camera;microphone'
/> />
) : ( ) : (
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]"> <div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000] mt-[64px]">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<Spin <Spin
size="large" size="large"
......
...@@ -17,7 +17,7 @@ const chat2page = () => { ...@@ -17,7 +17,7 @@ const chat2page = () => {
} }
return ( return (
<div> <div className="mt-[64px]">
<h3>正在加载,请稍候...</h3> <h3>正在加载,请稍候...</h3>
</div> </div>
); );
......
...@@ -984,7 +984,7 @@ const Detail = (props) => { ...@@ -984,7 +984,7 @@ const Detail = (props) => {
}, []); }, []);
return ( return (
<div className="bg-gray-50 h-full"> <div className="bg-gray-50 h-full mt-[64px]">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-semibold text-gray-800">{getGreeting}</h2> <h2 className="text-2xl font-semibold text-gray-800">{getGreeting}</h2>
<div className="flex gap-3"> <div className="flex gap-3">
......
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Button, Typography, Tag } from '@douyinfe/semi-ui'; import { Button, Typography, Tag, Input, ScrollList, ScrollItem } from '@douyinfe/semi-ui';
import { API, showError, isMobile } from '../../helpers'; import { API, showError, isMobile, copy, showSuccess } from '../../helpers';
import { API_ENDPOINTS } from '../../constants/common.constant';
import { StatusContext } from '../../context/Status'; import { StatusContext } from '../../context/Status';
import { marked } from 'marked'; import { marked } from 'marked';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IconGithubLogo, IconPlay, IconFile } from '@douyinfe/semi-icons'; import { IconGithubLogo, IconPlay, IconFile, IconCopy } from '@douyinfe/semi-icons';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NoticeModal from '../../components/layout/NoticeModal'; import NoticeModal from '../../components/layout/NoticeModal';
import { Moonshot, OpenAI, XAI, Zhipu, Volcengine, Cohere, Claude, Gemini, Suno, Minimax, Wenxin, Spark, Qingyan, DeepSeek, Qwen, Midjourney, Grok, AzureAI, Hunyuan, Xinference } from '@lobehub/icons'; import { Moonshot, OpenAI, XAI, Zhipu, Volcengine, Cohere, Claude, Gemini, Suno, Minimax, Wenxin, Spark, Qingyan, DeepSeek, Qwen, Midjourney, Grok, AzureAI, Hunyuan, Xinference } from '@lobehub/icons';
...@@ -17,29 +18,12 @@ const Home = () => { ...@@ -17,29 +18,12 @@ const Home = () => {
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState(''); const [homePageContent, setHomePageContent] = useState('');
const [noticeVisible, setNoticeVisible] = useState(false); const [noticeVisible, setNoticeVisible] = useState(false);
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false; const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
const docsLink = statusState?.status?.docs_link || ''; const docsLink = statusState?.status?.docs_link || '';
const serverAddress = statusState?.status?.server_address || window.location.origin;
useEffect(() => { const endpointItems = API_ENDPOINTS.map((e) => ({ value: e }));
const checkNoticeAndShow = async () => { const [endpointIndex, setEndpointIndex] = useState(0);
const lastCloseDate = localStorage.getItem('notice_close_date'); const isChinese = i18n.language.startsWith('zh');
const today = new Date().toDateString();
if (lastCloseDate !== today) {
try {
const res = await API.get('/api/notice');
const { success, data } = res.data;
if (success && data && data.trim() !== '') {
setNoticeVisible(true);
}
} catch (error) {
console.error('获取公告失败:', error);
}
}
};
checkNoticeAndShow();
}, []);
const displayHomePageContent = async () => { const displayHomePageContent = async () => {
setHomePageContent(localStorage.getItem('home_page_content') || ''); setHomePageContent(localStorage.getItem('home_page_content') || '');
...@@ -71,10 +55,44 @@ const Home = () => { ...@@ -71,10 +55,44 @@ const Home = () => {
setHomePageContentLoaded(true); setHomePageContentLoaded(true);
}; };
const handleCopyBaseURL = async () => {
const ok = await copy(serverAddress);
if (ok) {
showSuccess(t('已复制到剪切板'));
}
};
useEffect(() => {
const checkNoticeAndShow = async () => {
const lastCloseDate = localStorage.getItem('notice_close_date');
const today = new Date().toDateString();
if (lastCloseDate !== today) {
try {
const res = await API.get('/api/notice');
const { success, data } = res.data;
if (success && data && data.trim() !== '') {
setNoticeVisible(true);
}
} catch (error) {
console.error('获取公告失败:', error);
}
}
};
checkNoticeAndShow();
}, []);
useEffect(() => { useEffect(() => {
displayHomePageContent().then(); displayHomePageContent().then();
}, []); }, []);
useEffect(() => {
const timer = setInterval(() => {
setEndpointIndex((prev) => (prev + 1) % endpointItems.length);
}, 3000);
return () => clearInterval(timer);
}, [endpointItems.length]);
return ( return (
<div className="w-full overflow-x-hidden"> <div className="w-full overflow-x-hidden">
<NoticeModal <NoticeModal
...@@ -86,30 +104,63 @@ const Home = () => { ...@@ -86,30 +104,63 @@ const Home = () => {
<div className="w-full overflow-x-hidden"> <div className="w-full overflow-x-hidden">
{/* Banner 部分 */} {/* Banner 部分 */}
<div className="w-full border-b border-semi-color-border min-h-[500px] md:min-h-[600px] lg:min-h-[700px] relative overflow-x-hidden"> <div className="w-full border-b border-semi-color-border min-h-[500px] md:min-h-[600px] lg:min-h-[700px] relative overflow-x-hidden">
<div className="flex items-center justify-center h-full px-4 py-20 md:py-24 lg:py-32"> {/* 背景模糊晕染球*/}
<div className="blur-ball blur-ball-indigo" />
<div className="blur-ball blur-ball-teal" />
<div className="flex items-center justify-center h-full px-4 py-20 md:py-24 lg:py-32 mt-10">
{/* 居中内容区 */} {/* 居中内容区 */}
<div className="flex flex-col items-center justify-center text-center max-w-4xl mx-auto"> <div className="flex flex-col items-center justify-center text-center max-w-4xl mx-auto">
<div className="flex flex-col items-center justify-center mb-6 md:mb-8"> <div className="flex flex-col items-center justify-center mb-6 md:mb-8">
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight"> <h1 className={`text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight ${isChinese ? 'tracking-wide md:tracking-wider' : ''}`}>
{i18n.language === 'en' ? ( {i18n.language === 'en' ? (
<> <>
The Unified<br /> The Unified<br />
LLMs API Gateway <span className="shine-text">LLMs API Gateway</span>
</> </>
) : ( ) : (
t('统一的大模型接口网关') <>
统一的<br />
<span className="shine-text">大模型接口网关</span>
</>
)} )}
</h1> </h1>
<p className="text-lg md:text-xl lg:text-2xl text-semi-color-text-1 mt-4 md:mt-6"> <p className="text-base md:text-lg lg:text-xl text-semi-color-text-1 mt-4 md:mt-6 max-w-xl">
{t('更好的价格,更好的稳定性,无需订阅')} {t('更好的价格,更好的稳定性,只需要将模型基址替换为:')}
</p> </p>
{/* BASE URL 与端点选择 */}
<div className="flex flex-col md:flex-row items-center justify-center gap-4 w-full mt-4 md:mt-6 max-w-md">
<Input
readOnly
value={serverAddress}
className="flex-1 !rounded-full"
size={isMobile() ? 'default' : 'large'}
suffix={
<div className="flex items-center gap-2">
<ScrollList bodyHeight={32} style={{ border: 'unset', boxShadow: 'unset' }}>
<ScrollItem
mode="wheel"
cycled={true}
list={endpointItems}
selectedIndex={endpointIndex}
onSelect={({ index }) => setEndpointIndex(index)}
/>
</ScrollList>
<Button
type="primary"
onClick={handleCopyBaseURL}
icon={<IconCopy />}
/>
</div>
}
/>
</div>
</div> </div>
{/* 操作按钮 */} {/* 操作按钮 */}
<div className="flex flex-row gap-4 justify-center items-center"> <div className="flex flex-row gap-4 justify-center items-center">
<Link to="/console"> <Link to="/console">
<Button theme="solid" type="primary" size={isMobile() ? "default" : "large"} className="!rounded-3xl px-8 py-2" icon={<IconPlay />}> <Button theme="solid" type="primary" size={isMobile() ? "default" : "large"} className="!rounded-3xl px-8 py-2" icon={<IconPlay />}>
{t('开始使用')} {t('获取密钥')}
</Button> </Button>
</Link> </Link>
{isDemoSiteMode && statusState?.status?.version ? ( {isDemoSiteMode && statusState?.status?.version ? (
......
...@@ -2,9 +2,9 @@ import React from 'react'; ...@@ -2,9 +2,9 @@ import React from 'react';
import LogsTable from '../../components/table/LogsTable'; import LogsTable from '../../components/table/LogsTable';
const Token = () => ( const Token = () => (
<> <div className="mt-[64px]">
<LogsTable /> <LogsTable />
</> </div>
); );
export default Token; export default Token;
...@@ -2,9 +2,9 @@ import React from 'react'; ...@@ -2,9 +2,9 @@ import React from 'react';
import MjLogsTable from '../../components/table/MjLogsTable'; import MjLogsTable from '../../components/table/MjLogsTable';
const Midjourney = () => ( const Midjourney = () => (
<> <div className="mt-[64px]">
<MjLogsTable /> <MjLogsTable />
</> </div>
); );
export default Midjourney; export default Midjourney;
...@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; ...@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
const NotFound = () => { const NotFound = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex justify-center items-center h-screen p-8"> <div className="flex justify-center items-center h-screen p-8 mt-[64px]">
<Empty <Empty
image={<IllustrationNotFound style={{ width: 250, height: 250 }} />} image={<IllustrationNotFound style={{ width: 250, height: 250 }} />}
darkModeImage={<IllustrationNotFoundDark style={{ width: 250, height: 250 }} />} darkModeImage={<IllustrationNotFoundDark style={{ width: 250, height: 250 }} />}
......
...@@ -363,7 +363,7 @@ const Playground = () => { ...@@ -363,7 +363,7 @@ const Playground = () => {
}, [setMessage, saveMessagesImmediately]); }, [setMessage, saveMessagesImmediately]);
return ( return (
<div className="h-full bg-gray-50"> <div className="h-full bg-gray-50 mt-[64px]">
<Layout style={{ height: '100%', background: 'transparent' }} className="flex flex-col md:flex-row"> <Layout style={{ height: '100%', background: 'transparent' }} className="flex flex-col md:flex-row">
{(showSettings || !styleState.isMobile) && ( {(showSettings || !styleState.isMobile) && (
<Layout.Sider <Layout.Sider
......
...@@ -2,9 +2,9 @@ import React from 'react'; ...@@ -2,9 +2,9 @@ import React from 'react';
import ModelPricing from '../../components/table/ModelPricing.js'; import ModelPricing from '../../components/table/ModelPricing.js';
const Pricing = () => ( const Pricing = () => (
<> <div className="mt-[64px]">
<ModelPricing /> <ModelPricing />
</> </div>
); );
export default Pricing; export default Pricing;
...@@ -3,9 +3,9 @@ import RedemptionsTable from '../../components/table/RedemptionsTable'; ...@@ -3,9 +3,9 @@ import RedemptionsTable from '../../components/table/RedemptionsTable';
const Redemption = () => { const Redemption = () => {
return ( return (
<> <div className="mt-[64px]">
<RedemptionsTable /> <RedemptionsTable />
</> </div>
); );
}; };
......
...@@ -150,7 +150,7 @@ const Setting = () => { ...@@ -150,7 +150,7 @@ const Setting = () => {
} }
}, [location.search]); }, [location.search]);
return ( return (
<div> <div className="mt-[64px]">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
<Tabs <Tabs
......
...@@ -133,7 +133,7 @@ const Setup = () => { ...@@ -133,7 +133,7 @@ const Setup = () => {
}; };
return ( return (
<div className="bg-gray-50"> <div className="bg-gray-50 mt-[64px]">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
<div className="flex justify-center px-4 py-8"> <div className="flex justify-center px-4 py-8">
......
...@@ -2,9 +2,9 @@ import React from 'react'; ...@@ -2,9 +2,9 @@ import React from 'react';
import TaskLogsTable from '../../components/table/TaskLogsTable.js'; import TaskLogsTable from '../../components/table/TaskLogsTable.js';
const Task = () => ( const Task = () => (
<> <div className="mt-[64px]">
<TaskLogsTable /> <TaskLogsTable />
</> </div>
); );
export default Task; export default Task;
...@@ -3,9 +3,9 @@ import TokensTable from '../../components/table/TokensTable'; ...@@ -3,9 +3,9 @@ import TokensTable from '../../components/table/TokensTable';
const Token = () => { const Token = () => {
return ( return (
<> <div className="mt-[64px]">
<TokensTable /> <TokensTable />
</> </div>
); );
}; };
......
...@@ -382,7 +382,7 @@ const TopUp = () => { ...@@ -382,7 +382,7 @@ const TopUp = () => {
}; };
return ( return (
<div className='mx-auto relative min-h-screen lg:min-h-0'> <div className='mx-auto relative min-h-screen lg:min-h-0 mt-[64px]'>
{/* 划转模态框 */} {/* 划转模态框 */}
<Modal <Modal
title={ title={
......
...@@ -3,9 +3,9 @@ import UsersTable from '../../components/table/UsersTable'; ...@@ -3,9 +3,9 @@ import UsersTable from '../../components/table/UsersTable';
const User = () => { const User = () => {
return ( return (
<> <div className="mt-[64px]">
<UsersTable /> <UsersTable />
</> </div>
); );
}; };
......
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