Merge branch 'alpha' into feature/simple_stripe

This commit is contained in:
wzxjohn
2025-07-16 10:39:11 +08:00
committed by GitHub
170 changed files with 6242 additions and 3319 deletions

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
@@ -7,22 +7,28 @@ import Loading from '../common/Loading';
const OAuth2Callback = (props) => {
const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const [searchParams] = useSearchParams();
const [, userDispatch] = useContext(UserContext);
const navigate = useNavigate();
const [userState, userDispatch] = useContext(UserContext);
const [prompt, setPrompt] = useState(t('处理中...'));
// 最大重试次数
const MAX_RETRIES = 3;
let navigate = useNavigate();
const sendCode = async (code, state, retry = 0) => {
try {
const { data: resData } = await API.get(
`/api/oauth/${props.type}?code=${code}&state=${state}`,
);
const { success, message, data } = resData;
if (!success) {
throw new Error(message || 'OAuth2 callback error');
}
const sendCode = async (code, state, count) => {
const res = await API.get(
`/api/oauth/${props.type}?code=${code}&state=${state}`,
);
const { success, message, data } = res.data;
if (success) {
if (message === 'bind') {
showSuccess(t('绑定成功!'));
navigate('/console/setting');
navigate('/console/personal');
} else {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
@@ -31,27 +37,34 @@ const OAuth2Callback = (props) => {
showSuccess(t('登录成功!'));
navigate('/console/token');
}
} else {
showError(message);
if (count === 0) {
setPrompt(t('操作失败,重定向至登录界面中...'));
navigate('/console/setting'); // in case this is failed to bind GitHub
return;
} catch (error) {
if (retry < MAX_RETRIES) {
// 递增的退避等待
await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 2000));
return sendCode(code, state, retry + 1);
}
count++;
setPrompt(t('出现错误,第 ${count} 次重试中...', { count }));
await new Promise((resolve) => setTimeout(resolve, count * 2000));
await sendCode(code, state, count);
// 重试次数耗尽,提示错误并返回设置页面
showError(error.message || t('授权失败'));
navigate('/console/personal');
}
};
useEffect(() => {
let code = searchParams.get('code');
let state = searchParams.get('state');
sendCode(code, state, 0).then();
const code = searchParams.get('code');
const state = searchParams.get('state');
// 参数缺失直接返回
if (!code) {
showError(t('未获取到授权码'));
navigate('/console/personal');
return;
}
sendCode(code, state);
}, []);
return <Loading prompt={prompt} />;
return <Loading />;
};
export default OAuth2Callback;

View File

@@ -1,22 +1,14 @@
import React from 'react';
import { Spin } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const Loading = ({ prompt: name = '', size = 'large' }) => {
const { t } = useTranslation();
const Loading = ({ size = 'small' }) => {
return (
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]">
<div className="flex flex-col items-center">
<Spin
size={size}
spinning={true}
tip={null}
/>
<span className="whitespace-nowrap mt-2 text-center" style={{ color: 'var(--semi-color-primary)' }}>
{name ? t('{{name}}', { name }) : t('加载中...')}
</span>
</div>
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
<Spin
size={size}
spinning={true}
/>
</div>
);
};

View File

@@ -22,7 +22,7 @@ const FooterBar = () => {
const currentYear = new Date().getFullYear();
const customFooter = useMemo(() => (
<footer className="relative bg-semi-color-bg-2 h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden">
<footer className="relative h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden">
<div className="absolute hidden md:block top-[204px] left-[-100px] w-[151px] h-[151px] rounded-full bg-[#FFD166]"></div>
<div className="absolute md:hidden bottom-[20px] left-[-50px] w-[80px] h-[80px] rounded-full bg-[#FFD166] opacity-60"></div>

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState, useRef } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { UserContext } from '../../context/User/index.js';
import { useSetTheme, useTheme } from '../../context/Theme/index.js';
@@ -31,13 +31,15 @@ import {
Badge,
} from '@douyinfe/semi-ui';
import { StatusContext } from '../../context/Status/index.js';
import { useStyle, styleActions } from '../../context/Style/index.js';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
const HeaderBar = () => {
const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const { state: styleState, dispatch: styleDispatch } = useStyle();
const isMobile = useIsMobile();
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const [isLoading, setIsLoading] = useState(true);
let navigate = useNavigate();
const [currentLang, setCurrentLang] = useState(i18n.language);
@@ -45,6 +47,7 @@ const HeaderBar = () => {
const location = useLocation();
const [noticeVisible, setNoticeVisible] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
const loadingStartRef = useRef(Date.now());
const systemName = getSystemName();
const logo = getLogo();
@@ -194,11 +197,15 @@ const HeaderBar = () => {
}, [i18n]);
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 500);
return () => clearTimeout(timer);
}, []);
if (statusState?.status !== undefined) {
const elapsed = Date.now() - loadingStartRef.current;
const remaining = Math.max(0, 500 - elapsed);
const timer = setTimeout(() => {
setIsLoading(false);
}, remaining);
return () => clearTimeout(timer);
}
}, [statusState?.status]);
const handleLanguageChange = (lang) => {
i18n.changeLanguage(lang);
@@ -207,7 +214,7 @@ const HeaderBar = () => {
const handleNavLinkClick = (itemKey) => {
if (itemKey === 'home') {
styleDispatch(styleActions.setSider(false));
// styleDispatch(styleActions.setSider(false)); // This line is removed
}
setMobileMenuOpen(false);
};
@@ -221,7 +228,16 @@ const HeaderBar = () => {
.fill(null)
.map((_, index) => (
<div key={index} className={skeletonLinkClasses}>
<Skeleton.Title style={{ width: isMobileView ? 100 : 60, height: 16 }} />
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: isMobileView ? 100 : 60, height: 16 }}
/>
}
/>
</div>
));
}
@@ -272,9 +288,22 @@ const HeaderBar = () => {
if (isLoading) {
return (
<div className="flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1">
<Skeleton.Avatar size="extra-small" className="shadow-sm" />
<Skeleton
loading={true}
active
placeholder={<Skeleton.Avatar active size="extra-small" className="shadow-sm" />}
/>
<div className="ml-1.5 mr-1">
<Skeleton.Title style={{ width: styleState.isMobile ? 15 : 50, height: 12 }} />
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: isMobile ? 15 : 50, height: 12 }}
/>
}
/>
</div>
</div>
);
@@ -366,7 +395,7 @@ const HeaderBar = () => {
const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
if (showRegisterButton) {
if (styleState.isMobile) {
if (isMobile) {
loginButtonClasses += " !rounded-full";
} else {
loginButtonClasses += " !rounded-l-full !rounded-r-none";
@@ -414,7 +443,7 @@ const HeaderBar = () => {
<NoticeModal
visible={noticeVisible}
onClose={handleNoticeClose}
isMobile={styleState.isMobile}
isMobile={isMobile}
defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
unreadKeys={getUnreadKeys()}
/>
@@ -425,18 +454,18 @@ const HeaderBar = () => {
<Button
icon={
isConsoleRoute
? (styleState.showSider ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
? ((isMobile ? drawerOpen : collapsed) ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
: (mobileMenuOpen ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
}
aria-label={
isConsoleRoute
? (styleState.showSider ? t('关闭侧边栏') : t('打开侧边栏'))
? ((isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏'))
: (mobileMenuOpen ? t('关闭菜单') : t('打开菜单'))
}
onClick={() => {
if (isConsoleRoute) {
// 控制侧边栏的显示/隐藏,无论是否移动设备
styleDispatch(styleActions.toggleSider());
isMobile ? onMobileMenuToggle() : toggleCollapsed();
} else {
// 控制HeaderBar自己的移动菜单
setMobileMenuOpen(!mobileMenuOpen);
@@ -448,22 +477,35 @@ const HeaderBar = () => {
/>
</div>
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
{isLoading ? (
<Skeleton.Image className="h-7 md:h-8 !rounded-full" style={{ width: 32, height: 32 }} />
) : (
<Skeleton
loading={isLoading}
active
placeholder={
<Skeleton.Image
active
className="h-7 md:h-8 !rounded-full"
style={{ width: 32, height: 32 }}
/>
}
>
<img src={logo} alt="logo" className="h-7 md:h-8 transition-transform duration-300 ease-in-out group-hover:scale-105 rounded-full" />
)}
</Skeleton>
<div className="hidden md:flex items-center gap-2">
<div className="flex items-center gap-2">
{isLoading ? (
<Skeleton.Title style={{ width: 120, height: 24 }} />
) : (
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0
bg-gradient-to-r from-blue-500 to-purple-500 dark:from-blue-400 dark:to-purple-400
bg-clip-text text-transparent">
<Skeleton
loading={isLoading}
active
placeholder={
<Skeleton.Title
active
style={{ width: 120, height: 24 }}
/>
}
>
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0">
{systemName}
</Typography.Title>
)}
</Skeleton>
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
<Tag
color={isSelfUseMode ? 'purple' : 'blue'}

View File

@@ -4,8 +4,9 @@ import SiderBar from './SiderBar.js';
import App from '../../App.js';
import FooterBar from './Footer.js';
import { ToastContainer } from 'react-toastify';
import React, { useContext, useEffect } from 'react';
import { useStyle } from '../../context/Style/index.js';
import React, { useContext, useEffect, useState } from 'react';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js';
import { UserContext } from '../../context/User/index.js';
@@ -14,9 +15,11 @@ import { useLocation } from 'react-router-dom';
const { Sider, Content, Header } = Layout;
const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const { state: styleState } = useStyle();
const [, userDispatch] = useContext(UserContext);
const [, statusDispatch] = useContext(StatusContext);
const isMobile = useIsMobile();
const [collapsed, , setCollapsed] = useSidebarCollapsed();
const [drawerOpen, setDrawerOpen] = useState(false);
const { i18n } = useTranslation();
const location = useLocation();
@@ -26,6 +29,15 @@ const PageLayout = () => {
!location.pathname.startsWith('/console/chat') &&
location.pathname !== '/console/playground';
const isConsoleRoute = location.pathname.startsWith('/console');
const showSider = isConsoleRoute && (!isMobile || drawerOpen);
useEffect(() => {
if (isMobile && drawerOpen && collapsed) {
setCollapsed(false);
}
}, [isMobile, drawerOpen, collapsed, setCollapsed]);
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
@@ -63,7 +75,6 @@ const PageLayout = () => {
linkElement.href = logo;
}
}
// 从localStorage获取上次使用的语言
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) {
i18n.changeLanguage(savedLang);
@@ -76,7 +87,7 @@ const PageLayout = () => {
height: '100vh',
display: 'flex',
flexDirection: 'column',
overflow: styleState.isMobile ? 'visible' : 'hidden',
overflow: isMobile ? 'visible' : 'hidden',
}}
>
<Header
@@ -90,16 +101,16 @@ const PageLayout = () => {
zIndex: 100,
}}
>
<HeaderBar />
<HeaderBar onMobileMenuToggle={() => setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} />
</Header>
<Layout
style={{
overflow: styleState.isMobile ? 'visible' : 'auto',
overflow: isMobile ? 'visible' : 'auto',
display: 'flex',
flexDirection: 'column',
}}
>
{styleState.showSider && (
{showSider && (
<Sider
style={{
position: 'fixed',
@@ -109,21 +120,15 @@ const PageLayout = () => {
border: 'none',
paddingRight: '0',
height: 'calc(100vh - 64px)',
width: 'var(--sidebar-current-width)',
}}
>
<SiderBar />
<SiderBar onNavigate={() => { if (isMobile) setDrawerOpen(false); }} />
</Sider>
)}
<Layout
style={{
marginLeft: styleState.isMobile
? '0'
: styleState.showSider
? styleState.siderCollapsed
? '60px'
: '180px'
: '0',
transition: 'margin-left 0.3s ease',
marginLeft: isMobile ? '0' : showSider ? 'var(--sidebar-current-width)' : '0',
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
@@ -132,9 +137,9 @@ const PageLayout = () => {
<Content
style={{
flex: '1 0 auto',
overflowY: styleState.isMobile ? 'visible' : 'hidden',
overflowY: isMobile ? 'visible' : 'hidden',
WebkitOverflowScrolling: 'touch',
padding: shouldInnerPadding ? (styleState.isMobile ? '5px' : '24px') : '0',
padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0',
position: 'relative',
}}
>

View File

@@ -3,7 +3,7 @@ import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getLucideIcon, sidebarIconColors } from '../../helpers/render.js';
import { ChevronLeft } from 'lucide-react';
import { useStyle, styleActions } from '../../context/Style/index.js';
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
import {
isAdmin,
isRoot,
@@ -13,7 +13,7 @@ import {
import {
Nav,
Divider,
Tooltip,
Button,
} from '@douyinfe/semi-ui';
const routerMap = {
@@ -34,12 +34,11 @@ const routerMap = {
personal: '/console/personal',
};
const SiderBar = () => {
const SiderBar = ({ onNavigate = () => { } }) => {
const { t } = useTranslation();
const { state: styleState, dispatch: styleDispatch } = useStyle();
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const [selectedKeys, setSelectedKeys] = useState(['home']);
const [isCollapsed, setIsCollapsed] = useState(styleState.siderCollapsed);
const [chatItems, setChatItems] = useState([]);
const [openedKeys, setOpenedKeys] = useState([]);
const location = useLocation();
@@ -217,10 +216,14 @@ const SiderBar = () => {
}
}, [location.pathname, routerMapState]);
// 同步折叠状态
// 监控折叠状态变化以更新 body class
useEffect(() => {
setIsCollapsed(styleState.siderCollapsed);
}, [styleState.siderCollapsed]);
if (collapsed) {
document.body.classList.add('sidebar-collapsed');
} else {
document.body.classList.remove('sidebar-collapsed');
}
}, [collapsed]);
// 获取菜单项对应的颜色
const getItemColor = (itemKey) => {
@@ -323,32 +326,13 @@ const SiderBar = () => {
return (
<div
className="sidebar-container"
style={{ width: isCollapsed ? '60px' : '180px' }}
style={{ width: 'var(--sidebar-current-width)' }}
>
<Nav
className="sidebar-nav custom-sidebar-nav"
defaultIsCollapsed={styleState.siderCollapsed}
isCollapsed={isCollapsed}
onCollapseChange={(collapsed) => {
setIsCollapsed(collapsed);
styleDispatch(styleActions.setSiderCollapsed(collapsed));
// 确保在收起侧边栏时有选中的项目
if (selectedKeys.length === 0) {
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMapState).find(
(key) => routerMapState[key] === currentPath,
);
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/console/chat/')) {
setSelectedKeys(['chat']);
} else {
setSelectedKeys(['detail']); // 默认选中首页
}
}
}}
className="sidebar-nav"
defaultIsCollapsed={collapsed}
isCollapsed={collapsed}
onCollapseChange={toggleCollapsed}
selectedKeys={selectedKeys}
itemStyle="sidebar-nav-item"
hoverStyle="sidebar-nav-item:hover"
@@ -363,6 +347,7 @@ const SiderBar = () => {
<Link
style={{ textDecoration: 'none' }}
to={to}
onClick={onNavigate}
>
{itemElement}
</Link>
@@ -383,7 +368,7 @@ const SiderBar = () => {
>
{/* 聊天区域 */}
<div className="sidebar-section">
{!isCollapsed && (
{!collapsed && (
<div className="sidebar-group-label">{t('聊天')}</div>
)}
{chatMenuItems.map((item) => renderSubItem(item))}
@@ -392,7 +377,7 @@ const SiderBar = () => {
{/* 控制台区域 */}
<Divider className="sidebar-divider" />
<div>
{!isCollapsed && (
{!collapsed && (
<div className="sidebar-group-label">{t('控制台')}</div>
)}
{workspaceItems.map((item) => renderNavItem(item))}
@@ -403,7 +388,7 @@ const SiderBar = () => {
<>
<Divider className="sidebar-divider" />
<div>
{!isCollapsed && (
{!collapsed && (
<div className="sidebar-group-label">{t('管理员')}</div>
)}
{adminItems.map((item) => renderNavItem(item))}
@@ -414,7 +399,7 @@ const SiderBar = () => {
{/* 个人中心区域 */}
<Divider className="sidebar-divider" />
<div>
{!isCollapsed && (
{!collapsed && (
<div className="sidebar-group-label">{t('个人中心')}</div>
)}
{financeItems.map((item) => renderNavItem(item))}
@@ -422,24 +407,25 @@ const SiderBar = () => {
</Nav>
{/* 底部折叠按钮 */}
<div
className="sidebar-collapse-button"
onClick={() => {
const newCollapsed = !isCollapsed;
setIsCollapsed(newCollapsed);
styleDispatch(styleActions.setSiderCollapsed(newCollapsed));
}}
>
<Tooltip content={isCollapsed ? t('展开侧边栏') : t('收起侧边栏')} position="right">
<div className="sidebar-collapse-button-inner">
<span
className="sidebar-collapse-icon-container"
style={{ transform: isCollapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
>
<ChevronLeft size={16} strokeWidth={2.5} color="var(--semi-color-text-2)" />
</span>
</div>
</Tooltip>
<div className="sidebar-collapse-button">
<Button
theme="outline"
type="tertiary"
size="small"
icon={
<ChevronLeft
size={16}
strokeWidth={2.5}
color="var(--semi-color-text-2)"
style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
}
onClick={toggleCollapsed}
iconOnly={collapsed}
style={collapsed ? { padding: '4px', width: '100%' } : { padding: '4px 12px', width: '100%' }}
>
{!collapsed ? t('收起侧边栏') : null}
</Button>
</div>
</div>
);

View File

@@ -34,7 +34,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Temperature
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.temperature}
</Tag>
</div>
@@ -70,7 +70,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Top P
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.top_p}
</Tag>
</div>
@@ -106,7 +106,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Frequency Penalty
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.frequency_penalty}
</Tag>
</div>
@@ -142,7 +142,7 @@ const ParameterControl = ({
<Typography.Text strong className="text-sm">
Presence Penalty
</Typography.Text>
<Tag size="small" className="!rounded-full">
<Tag size="small" shape='circle'>
{inputs.presence_penalty}
</Tag>
</div>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { isMobile } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import {
Modal,
Table,
@@ -26,6 +26,7 @@ const ChannelSelectorModal = forwardRef(({
const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const isMobile = useIsMobile();
const [filteredData, setFilteredData] = useState([]);
@@ -118,25 +119,25 @@ const ChannelSelectorModal = forwardRef(({
switch (status) {
case 1:
return (
<Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知状态')}
</Tag>
);
@@ -186,7 +187,7 @@ const ChannelSelectorModal = forwardRef(({
onCancel={onCancel}
onOk={onOk}
title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
size={isMobile() ? 'full-width' : 'large'}
size={isMobile ? 'full-width' : 'large'}
keepDOM
lazyRender={false}
>

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui';
import SettingsChats from '../../pages/Setting/Chat/SettingsChats.js';
import { API, showError } from '../../helpers';
import { API, showError, toBoolean } from '../../helpers';
const ChatsSetting = () => {
let [inputs, setInputs] = useState({
@@ -21,7 +21,7 @@ const ChatsSetting = () => {
item.key.endsWith('Enabled') ||
['DefaultCollapseSidebar'].includes(item.key)
) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Card, Spin, Button, Modal } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../helpers';
import { API, showError, showSuccess, toBoolean } from '../../helpers';
import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
import SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js';
import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
@@ -45,7 +45,7 @@ const DashboardSetting = () => {
}
if (item.key.endsWith('Enabled') &&
(item.key === 'DataExportEnabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
}
});
setInputs(newInputs);

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui';
import SettingsDrawing from '../../pages/Setting/Drawing/SettingsDrawing.js';
import { API, showError } from '../../helpers';
import { API, showError, toBoolean } from '../../helpers';
const DrawingSetting = () => {
let [inputs, setInputs] = useState({
@@ -23,7 +23,7 @@ const DrawingSetting = () => {
let newInputs = {};
data.forEach((item) => {
if (item.key.endsWith('Enabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../helpers';
import { API, showError, showSuccess, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next';
import SettingGeminiModel from '../../pages/Setting/Model/SettingGeminiModel.js';
import SettingClaudeModel from '../../pages/Setting/Model/SettingClaudeModel.js';
@@ -44,7 +44,7 @@ const ModelSetting = () => {
}
}
if (item.key.endsWith('Enabled') || item.key.endsWith('enabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}

View File

@@ -5,7 +5,7 @@ import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensit
import SettingsLog from '../../pages/Setting/Operation/SettingsLog.js';
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring.js';
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js';
import { API, showError } from '../../helpers';
import { API, showError, toBoolean } from '../../helpers';
const OperationSetting = () => {
let [inputs, setInputs] = useState({
@@ -54,7 +54,7 @@ const OperationSetting = () => {
item.key.endsWith('Enabled') ||
['DefaultCollapseSidebar'].includes(item.key)
) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}

View File

@@ -3,7 +3,7 @@ import { Card, Spin } from '@douyinfe/semi-ui';
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway.js';
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe.js';
import { API, showError } from '../../helpers';
import { API, showError, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next';
const PaymentSetting = () => {
@@ -51,7 +51,7 @@ const PaymentSetting = () => {
break;
default:
if (item.key.endsWith('Enabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui';
import { API, showError } from '../../helpers/index.js';
import { API, showError, toBoolean } from '../../helpers/index.js';
import { useTranslation } from 'react-i18next';
import RequestRateLimit from '../../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
@@ -28,7 +28,7 @@ const RateLimitSetting = () => {
}
if (item.key.endsWith('Enabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}

View File

@@ -8,7 +8,7 @@ import ModelSettingsVisualEditor from '../../pages/Setting/Ratio/ModelSettingsVi
import ModelRatioNotSetEditor from '../../pages/Setting/Ratio/ModelRationNotSetEditor.js';
import UpstreamRatioSync from '../../pages/Setting/Ratio/UpstreamRatioSync.js';
import { API, showError } from '../../helpers';
import { API, showError, toBoolean } from '../../helpers';
const RatioSetting = () => {
const { t } = useTranslation();
@@ -51,7 +51,7 @@ const RatioSetting = () => {
}
}
if (['DefaultUseAutoGroup', 'ExposeRatioEnabled'].includes(item.key)) {
newInputs[item.key] = item.value === 'true' ? true : false;
newInputs[item.key] = toBoolean(item.value);
} else {
newInputs[item.key] = item.value;
}

View File

@@ -17,10 +17,13 @@ import {
removeTrailingSlash,
showError,
showSuccess,
toBoolean,
} from '../../helpers';
import axios from 'axios';
import { useTranslation } from 'react-i18next';
const SystemSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({
PasswordLoginEnabled: '',
PasswordRegisterEnabled: '',
@@ -57,13 +60,13 @@ const SystemSetting = () => {
EmailAliasRestrictionEnabled: '',
SMTPSSLEnabled: '',
EmailDomainWhitelist: [],
// telegram login
TelegramOAuthEnabled: '',
TelegramBotToken: '',
TelegramBotName: '',
LinuxDOOAuthEnabled: '',
LinuxDOClientId: '',
LinuxDOClientSecret: '',
ServerAddress: '',
});
const [originInputs, setOriginInputs] = useState({});
@@ -104,7 +107,7 @@ const SystemSetting = () => {
case 'LinuxDOOAuthEnabled':
case 'oidc.enabled':
case 'WorkerAllowHttpImageRequestEnabled':
item.value = item.value === 'true';
item.value = toBoolean(item.value);
break;
case 'Price':
case 'MinTopUp':
@@ -173,7 +176,7 @@ const SystemSetting = () => {
});
}
showSuccess('更新成功');
showSuccess(t('更新成功'));
// 更新本地状态
const newInputs = { ...inputs };
options.forEach((opt) => {
@@ -181,7 +184,7 @@ const SystemSetting = () => {
});
setInputs(newInputs);
} catch (error) {
showError('更新失败');
showError(t('更新失败'));
}
setLoading(false);
};
@@ -205,6 +208,11 @@ const SystemSetting = () => {
await updateOptions(options);
};
const submitServerAddress = async () => {
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
};
const submitSMTP = async () => {
const options = [];
@@ -244,7 +252,7 @@ const SystemSetting = () => {
},
]);
} else {
showError('邮箱域名白名单格式不正确');
showError(t('邮箱域名白名单格式不正确'));
}
};
@@ -256,19 +264,19 @@ const SystemSetting = () => {
const domainRegex =
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
if (!domainRegex.test(domain)) {
showError('邮箱域名格式不正确,请输入有效的域名,如 gmail.com');
showError(t('邮箱域名格式不正确,请输入有效的域名,如 gmail.com'));
return;
}
// 检查是否已存在
if (emailDomainWhitelist.includes(domain)) {
showError('该域名已存在于白名单中');
showError(t('该域名已存在于白名单中'));
return;
}
setEmailDomainWhitelist([...emailDomainWhitelist, domain]);
setEmailToAdd('');
showSuccess('已添加到白名单');
showSuccess(t('已添加到白名单'));
}
};
@@ -332,7 +340,7 @@ const SystemSetting = () => {
!inputs['oidc.well_known'].startsWith('http://') &&
!inputs['oidc.well_known'].startsWith('https://')
) {
showError('Well-Known URL 必须以 http:// 或 https:// 开头');
showError(t('Well-Known URL 必须以 http:// 或 https:// 开头'));
return;
}
try {
@@ -341,11 +349,11 @@ const SystemSetting = () => {
res.data['authorization_endpoint'];
inputs['oidc.token_endpoint'] = res.data['token_endpoint'];
inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint'];
showSuccess('获取 OIDC 配置成功!');
showSuccess(t('获取 OIDC 配置成功!'));
} catch (err) {
console.error(err);
showError(
'获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确',
t('获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确'),
);
return;
}
@@ -487,7 +495,25 @@ const SystemSetting = () => {
}}
>
<Card>
<Form.Section text='代理设置'>
<Form.Section text={t('通用设置')}>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
<Col xs={24} sm={24} md={24} lg={24} xl={24}>
<Form.Input
field='ServerAddress'
label={t('服务器地址')}
placeholder='https://yourdomain.com'
extraText={t('该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置')}
/>
</Col>
</Row>
<Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text={t('代理设置')}>
<Text>
支持{' '}
<a
@@ -505,14 +531,14 @@ const SystemSetting = () => {
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='WorkerUrl'
label='Worker地址'
label={t('Worker地址')}
placeholder='例如https://workername.yourdomain.workers.dev'
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='WorkerValidKey'
label='Worker密钥'
label={t('Worker密钥')}
placeholder='敏感信息不会发送到前端显示'
type='password'
/>
@@ -522,14 +548,14 @@ const SystemSetting = () => {
field='WorkerAllowHttpImageRequestEnabled'
noLabel
>
允许 HTTP 协议图片请求适用于自部署代理
{t('允许 HTTP 协议图片请求适用于自部署代理)')}
</Form.Checkbox>
<Button onClick={submitWorker}>更新Worker设置</Button>
<Button onClick={submitWorker}>{t('更新Worker设置')}</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置登录注册'>
<Form.Section text={t('配置登录注册')}>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
@@ -541,7 +567,7 @@ const SystemSetting = () => {
handleCheckboxChange('PasswordLoginEnabled', e)
}
>
允许通过密码进行登录
{t('允许通过密码进行登录')}
</Form.Checkbox>
<Form.Checkbox
field='PasswordRegisterEnabled'
@@ -550,7 +576,7 @@ const SystemSetting = () => {
handleCheckboxChange('PasswordRegisterEnabled', e)
}
>
允许通过密码进行注册
{t('允许通过密码进行注册')}
</Form.Checkbox>
<Form.Checkbox
field='EmailVerificationEnabled'
@@ -559,7 +585,7 @@ const SystemSetting = () => {
handleCheckboxChange('EmailVerificationEnabled', e)
}
>
通过密码注册时需要进行邮箱验证
{t('通过密码注册时需要进行邮箱验证')}
</Form.Checkbox>
<Form.Checkbox
field='RegisterEnabled'
@@ -568,7 +594,7 @@ const SystemSetting = () => {
handleCheckboxChange('RegisterEnabled', e)
}
>
允许新用户注册
{t('允许新用户注册')}
</Form.Checkbox>
<Form.Checkbox
field='TurnstileCheckEnabled'
@@ -577,7 +603,7 @@ const SystemSetting = () => {
handleCheckboxChange('TurnstileCheckEnabled', e)
}
>
启用 Turnstile 用户校验
{t('允许 Turnstile 用户校验')}
</Form.Checkbox>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
@@ -588,7 +614,7 @@ const SystemSetting = () => {
handleCheckboxChange('GitHubOAuthEnabled', e)
}
>
允许通过 GitHub 账户登录 & 注册
{t('允许通过 GitHub 账户登录 & 注册')}
</Form.Checkbox>
<Form.Checkbox
field='LinuxDOOAuthEnabled'
@@ -597,7 +623,7 @@ const SystemSetting = () => {
handleCheckboxChange('LinuxDOOAuthEnabled', e)
}
>
允许通过 Linux DO 账户登录 & 注册
{t('允许通过 Linux DO 账户登录 & 注册')}
</Form.Checkbox>
<Form.Checkbox
field='WeChatAuthEnabled'
@@ -606,7 +632,7 @@ const SystemSetting = () => {
handleCheckboxChange('WeChatAuthEnabled', e)
}
>
允许通过微信登录 & 注册
{t('允许通过微信登录 & 注册')}
</Form.Checkbox>
<Form.Checkbox
field='TelegramOAuthEnabled'
@@ -615,7 +641,7 @@ const SystemSetting = () => {
handleCheckboxChange('TelegramOAuthEnabled', e)
}
>
允许通过 Telegram 进行登录
{t('允许通过 Telegram 进行登录')}
</Form.Checkbox>
<Form.Checkbox
field="['oidc.enabled']"
@@ -624,7 +650,7 @@ const SystemSetting = () => {
handleCheckboxChange('oidc.enabled', e)
}
>
允许通过 OIDC 进行登录
{t('允许通过 OIDC 进行登录')}
</Form.Checkbox>
</Col>
</Row>
@@ -632,8 +658,8 @@ const SystemSetting = () => {
</Card>
<Card>
<Form.Section text='配置邮箱域名白名单'>
<Text>用以防止恶意用户利用临时邮箱批量注册</Text>
<Form.Section text={t('配置邮箱域名白名单')}>
<Text>{t('用以防止恶意用户利用临时邮箱批量注册')}</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
@@ -669,11 +695,11 @@ const SystemSetting = () => {
<TagInput
value={emailDomainWhitelist}
onChange={setEmailDomainWhitelist}
placeholder='输入域名后回车'
placeholder={t('输入域名后回车')}
style={{ width: '100%', marginTop: 16 }}
/>
<Form.Input
placeholder='输入要添加的邮箱域名'
placeholder={t('输入要添加的邮箱域名')}
value={emailToAdd}
onChange={(value) => setEmailToAdd(value)}
style={{ marginTop: 16 }}
@@ -683,7 +709,7 @@ const SystemSetting = () => {
type='primary'
onClick={handleAddEmail}
>
添加
{t('添加')}
</Button>
}
onEnterPress={handleAddEmail}
@@ -692,24 +718,24 @@ const SystemSetting = () => {
onClick={submitEmailDomainWhitelist}
style={{ marginTop: 10 }}
>
保存邮箱域名白名单设置
{t('保存邮箱域名白名单设置')}
</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置 SMTP'>
<Text>用以支持系统的邮件发送</Text>
<Form.Section text={t('配置 SMTP')}>
<Text>{t('用以支持系统的邮件发送')}</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input field='SMTPServer' label='SMTP 服务器地址' />
<Form.Input field='SMTPServer' label={t('SMTP 服务器地址')} />
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input field='SMTPPort' label='SMTP 端口' />
<Form.Input field='SMTPPort' label={t('SMTP 端口')} />
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input field='SMTPAccount' label='SMTP 账户' />
<Form.Input field='SMTPAccount' label={t('SMTP 账户')} />
</Col>
</Row>
<Row
@@ -717,12 +743,12 @@ const SystemSetting = () => {
style={{ marginTop: 16 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input field='SMTPFrom' label='SMTP 发送者邮箱' />
<Form.Input field='SMTPFrom' label={t('SMTP 发送者邮箱')} />
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='SMTPToken'
label='SMTP 访问凭证'
label={t('SMTP 访问凭证')}
type='password'
placeholder='敏感信息不会发送到前端显示'
/>
@@ -735,27 +761,25 @@ const SystemSetting = () => {
handleCheckboxChange('SMTPSSLEnabled', e)
}
>
启用SMTP SSL
{t('启用SMTP SSL')}
</Form.Checkbox>
</Col>
</Row>
<Button onClick={submitSMTP}>保存 SMTP 设置</Button>
<Button onClick={submitSMTP}>{t('保存 SMTP 设置')}</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置 OIDC'>
<Form.Section text={t('配置 OIDC')}>
<Text>
用以支持通过 OIDC 登录例如 OktaAuth0 等兼容 OIDC 协议的
IdP
{t('用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP')}
</Text>
<Banner
type='info'
description={`主页链接填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}重定向 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/oidc`}
description={`${t('主页链接填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}${t('重定向 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/oidc`}
style={{ marginBottom: 20, marginTop: 16 }}
/>
<Text>
若你的 OIDC Provider 支持 Discovery Endpoint你可以仅填写
OIDC Well-Known URL系统会自动获取 OIDC 配置
{t('若你的 OIDC Provider 支持 Discovery Endpoint你可以仅填写 OIDC Well-Known URL系统会自动获取 OIDC 配置')}
</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
@@ -763,15 +787,15 @@ const SystemSetting = () => {
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field="['oidc.well_known']"
label='Well-Known URL'
placeholder='请输入 OIDC 的 Well-Known URL'
label={t('Well-Known URL')}
placeholder={t('请输入 OIDC 的 Well-Known URL')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field="['oidc.client_id']"
label='Client ID'
placeholder='输入 OIDC 的 Client ID'
label={t('Client ID')}
placeholder={t('输入 OIDC 的 Client ID')}
/>
</Col>
</Row>
@@ -781,16 +805,16 @@ const SystemSetting = () => {
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field="['oidc.client_secret']"
label='Client Secret'
label={t('Client Secret')}
type='password'
placeholder='敏感信息不会发送到前端显示'
placeholder={t('敏感信息不会发送到前端显示')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field="['oidc.authorization_endpoint']"
label='Authorization Endpoint'
placeholder='输入 OIDC 的 Authorization Endpoint'
label={t('Authorization Endpoint')}
placeholder={t('输入 OIDC 的 Authorization Endpoint')}
/>
</Col>
</Row>
@@ -800,28 +824,28 @@ const SystemSetting = () => {
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field="['oidc.token_endpoint']"
label='Token Endpoint'
placeholder='输入 OIDC 的 Token Endpoint'
label={t('Token Endpoint')}
placeholder={t('输入 OIDC 的 Token Endpoint')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field="['oidc.user_info_endpoint']"
label='User Info Endpoint'
placeholder='输入 OIDC 的 Userinfo Endpoint'
label={t('User Info Endpoint')}
placeholder={t('输入 OIDC 的 Userinfo Endpoint')}
/>
</Col>
</Row>
<Button onClick={submitOIDCSettings}>保存 OIDC 设置</Button>
<Button onClick={submitOIDCSettings}>{t('保存 OIDC 设置')}</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置 GitHub OAuth App'>
<Text>用以支持通过 GitHub 进行登录注册</Text>
<Form.Section text={t('配置 GitHub OAuth App')}>
<Text>{t('用以支持通过 GitHub 进行登录注册')}</Text>
<Banner
type='info'
description={`Homepage URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}Authorization callback URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/github`}
description={`${t('Homepage URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}${t('Authorization callback URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/github`}
style={{ marginBottom: 20, marginTop: 16 }}
/>
<Row
@@ -830,27 +854,27 @@ const SystemSetting = () => {
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='GitHubClientId'
label='GitHub Client ID'
label={t('GitHub Client ID')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='GitHubClientSecret'
label='GitHub Client Secret'
label={t('GitHub Client Secret')}
type='password'
placeholder='敏感信息不会发送到前端显示'
placeholder={t('敏感信息不会发送到前端显示')}
/>
</Col>
</Row>
<Button onClick={submitGitHubOAuth}>
保存 GitHub OAuth 设置
{t('保存 GitHub OAuth 设置')}
</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置 Linux DO OAuth'>
<Form.Section text={t('配置 Linux DO OAuth')}>
<Text>
用以支持通过 Linux DO 进行登录注册
{t('用以支持通过 Linux DO 进行登录注册')}
<a
href='https://connect.linux.do/'
target='_blank'
@@ -861,13 +885,13 @@ const SystemSetting = () => {
marginRight: 4,
}}
>
点击此处
{t('点击此处')}
</a>
管理你的 LinuxDO OAuth App
{t('管理你的 LinuxDO OAuth App')}
</Text>
<Banner
type='info'
description={`回调 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/linuxdo`}
description={`${t('回调 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/linuxdo`}
style={{ marginBottom: 20, marginTop: 16 }}
/>
<Row
@@ -876,122 +900,122 @@ const SystemSetting = () => {
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='LinuxDOClientId'
label='Linux DO Client ID'
placeholder='输入你注册的 LinuxDO OAuth APP 的 ID'
label={t('Linux DO Client ID')}
placeholder={t('输入你注册的 LinuxDO OAuth APP 的 ID')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='LinuxDOClientSecret'
label='Linux DO Client Secret'
label={t('Linux DO Client Secret')}
type='password'
placeholder='敏感信息不会发送到前端显示'
placeholder={t('敏感信息不会发送到前端显示')}
/>
</Col>
</Row>
<Button onClick={submitLinuxDOOAuth}>
保存 Linux DO OAuth 设置
{t('保存 Linux DO OAuth 设置')}
</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置 WeChat Server'>
<Text>用以支持通过微信进行登录注册</Text>
<Form.Section text={t('配置 WeChat Server')}>
<Text>{t('用以支持通过微信进行登录注册')}</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WeChatServerAddress'
label='WeChat Server 服务器地址'
label={t('WeChat Server 服务器地址')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WeChatServerToken'
label='WeChat Server 访问凭证'
label={t('WeChat Server 访问凭证')}
type='password'
placeholder='敏感信息不会发送到前端显示'
placeholder={t('敏感信息不会发送到前端显示')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='WeChatAccountQRCodeImageURL'
label='微信公众号二维码图片链接'
label={t('微信公众号二维码图片链接')}
/>
</Col>
</Row>
<Button onClick={submitWeChat}>
保存 WeChat Server 设置
{t('保存 WeChat Server 设置')}
</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置 Telegram 登录'>
<Text>用以支持通过 Telegram 进行登录注册</Text>
<Form.Section text={t('配置 Telegram 登录')}>
<Text>{t('用以支持通过 Telegram 进行登录注册')}</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='TelegramBotToken'
label='Telegram Bot Token'
placeholder='敏感信息不会发送到前端显示'
label={t('Telegram Bot Token')}
placeholder={t('敏感信息不会发送到前端显示')}
type='password'
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='TelegramBotName'
label='Telegram Bot 名称'
label={t('Telegram Bot 名称')}
/>
</Col>
</Row>
<Button onClick={submitTelegramSettings}>
保存 Telegram 登录设置
{t('保存 Telegram 登录设置')}
</Button>
</Form.Section>
</Card>
<Card>
<Form.Section text='配置 Turnstile'>
<Text>用以支持用户校验</Text>
<Form.Section text={t('配置 Turnstile')}>
<Text>{t('用以支持用户校验')}</Text>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='TurnstileSiteKey'
label='Turnstile Site Key'
label={t('Turnstile Site Key')}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.Input
field='TurnstileSecretKey'
label='Turnstile Secret Key'
label={t('Turnstile Secret Key')}
type='password'
placeholder='敏感信息不会发送到前端显示'
placeholder={t('敏感信息不会发送到前端显示')}
/>
</Col>
</Row>
<Button onClick={submitTurnstile}>保存 Turnstile 设置</Button>
<Button onClick={submitTurnstile}>{t('保存 Turnstile 设置')}</Button>
</Form.Section>
</Card>
<Modal
title='确认取消密码登录'
title={t('确认取消密码登录')}
visible={showPasswordLoginConfirmModal}
onOk={handlePasswordLoginConfirm}
onCancel={() => {
setShowPasswordLoginConfirmModal(false);
formApiRef.current.setValue('PasswordLoginEnabled', true);
}}
okText='确认'
cancelText='取消'
okText={t('确认')}
cancelText={t('取消')}
>
<p>您确定要取消密码登录功能吗这可能会影响用户的登录方式</p>
<p>{t('您确定要取消密码登录功能吗这可能会影响用户的登录方式。')}</p>
</Modal>
</div>
)}

View File

@@ -42,18 +42,22 @@ import {
IconTreeTriangleDown,
IconSearch,
IconMore,
IconDescend2
} from '@douyinfe/semi-icons';
import { loadChannelModels, isMobile, copy } from '../../helpers';
import { loadChannelModels, copy } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import EditTagModal from '../../pages/Channel/EditTagModal.js';
import { useTranslation } from 'react-i18next';
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
import { FaRandom } from 'react-icons/fa';
const ChannelsTable = () => {
const { t } = useTranslation();
const isMobile = useIsMobile();
let type2label = undefined;
const renderType = (type) => {
const renderType = (type, channelInfo = undefined) => {
if (!type2label) {
type2label = new Map();
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
@@ -61,12 +65,30 @@ const ChannelsTable = () => {
}
type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
}
let icon = getChannelIcon(type);
if (channelInfo?.is_multi_key) {
icon = (
channelInfo?.multi_key_mode === 'random' ? (
<div className="flex items-center gap-1">
<FaRandom className="text-blue-500" />
{icon}
</div>
) : (
<div className="flex items-center gap-1">
<IconDescend2 className="text-blue-500" />
{icon}
</div>
)
)
}
return (
<Tag
size='large'
color={type2label[type]?.color}
shape='circle'
prefixIcon={getChannelIcon(type)}
prefixIcon={icon}
>
{type2label[type]?.label}
</Tag>
@@ -77,7 +99,6 @@ const ChannelsTable = () => {
return (
<Tag
color='light-blue'
size='large'
shape='circle'
type='light'
>
@@ -86,65 +107,107 @@ const ChannelsTable = () => {
);
};
const renderStatus = (status) => {
const renderStatus = (status, channelInfo = undefined) => {
if (channelInfo) {
if (channelInfo.is_multi_key) {
let keySize = channelInfo.multi_key_size;
let enabledKeySize = keySize;
if (channelInfo.multi_key_status_list) {
// multi_key_status_list is a map, key is key, value is status
// get multi_key_status_list length
enabledKeySize = keySize - Object.keys(channelInfo.multi_key_status_list).length;
}
return renderMultiKeyStatus(status, keySize, enabledKeySize);
}
}
switch (status) {
case 1:
return (
<Tag size='large' color='green' shape='circle'>
<Tag color='green' shape='circle'>
{t('已启用')}
</Tag>
);
case 2:
return (
<Tag size='large' color='red' shape='circle'>
<Tag color='red' shape='circle'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag size='large' color='yellow' shape='circle'>
<Tag color='yellow' shape='circle'>
{t('自动禁用')}
</Tag>
);
default:
return (
<Tag size='large' color='grey' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未知状态')}
</Tag>
);
}
};
const renderMultiKeyStatus = (status, keySize, enabledKeySize) => {
switch (status) {
case 1:
return (
<Tag color='green' shape='circle'>
{t('已启用')} {enabledKeySize}/{keySize}
</Tag>
);
case 2:
return (
<Tag color='red' shape='circle'>
{t('已禁用')} {enabledKeySize}/{keySize}
</Tag>
);
case 3:
return (
<Tag color='yellow' shape='circle'>
{t('自动禁用')} {enabledKeySize}/{keySize}
</Tag>
);
default:
return (
<Tag color='grey' shape='circle'>
{t('未知状态')} {enabledKeySize}/{keySize}
</Tag>
);
}
}
const renderResponseTime = (responseTime) => {
let time = responseTime / 1000;
time = time.toFixed(2) + t(' 秒');
if (responseTime === 0) {
return (
<Tag size='large' color='grey' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未测试')}
</Tag>
);
} else if (responseTime <= 1000) {
return (
<Tag size='large' color='green' shape='circle'>
<Tag color='green' shape='circle'>
{time}
</Tag>
);
} else if (responseTime <= 3000) {
return (
<Tag size='large' color='lime' shape='circle'>
<Tag color='lime' shape='circle'>
{time}
</Tag>
);
} else if (responseTime <= 5000) {
return (
<Tag size='large' color='yellow' shape='circle'>
<Tag color='yellow' shape='circle'>
{time}
</Tag>
);
} else {
return (
<Tag size='large' color='red' shape='circle'>
<Tag color='red' shape='circle'>
{time}
</Tag>
);
@@ -281,6 +344,11 @@ const ChannelsTable = () => {
dataIndex: 'type',
render: (text, record, index) => {
if (record.children === undefined) {
if (record.channel_info) {
if (record.channel_info.is_multi_key) {
return <>{renderType(text, record.channel_info)}</>;
}
}
return <>{renderType(text)}</>;
} else {
return <>{renderTagType()}</>;
@@ -304,12 +372,12 @@ const ChannelsTable = () => {
<Tooltip
content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
>
{renderStatus(text)}
{renderStatus(text, record.channel_info)}
</Tooltip>
</div>
);
} else {
return renderStatus(text);
return renderStatus(text, record.channel_info);
}
},
},
@@ -331,7 +399,7 @@ const ChannelsTable = () => {
<div>
<Space spacing={1}>
<Tooltip content={t('已用额度')}>
<Tag color='white' type='ghost' size='large' shape='circle'>
<Tag color='white' type='ghost' shape='circle'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
@@ -339,7 +407,6 @@ const ChannelsTable = () => {
<Tag
color='white'
type='ghost'
size='large'
shape='circle'
onClick={() => updateChannelBalance(record)}
>
@@ -352,7 +419,7 @@ const ChannelsTable = () => {
} else {
return (
<Tooltip content={t('已用额度')}>
<Tag color='white' type='ghost' size='large' shape='circle'>
<Tag color='white' type='ghost' shape='circle'>
{renderQuota(record.used_quota)}
</Tag>
</Tooltip>
@@ -482,9 +549,15 @@ const ChannelsTable = () => {
title: t('确定是否要删除此渠道?'),
content: t('此修改将不可逆'),
onOk: () => {
manageChannel(record.id, 'delete', record).then(() => {
removeRecord(record);
});
(async () => {
await manageChannel(record.id, 'delete', record);
await refresh();
setTimeout(() => {
if (channels.length === 0 && activePage > 1) {
refresh(activePage - 1);
}
}, 100);
})();
},
});
},
@@ -492,7 +565,7 @@ const ChannelsTable = () => {
{
node: 'item',
name: t('复制'),
type: 'primary',
type: 'tertiary',
onClick: () => {
Modal.confirm({
title: t('确定是否要复制此渠道?'),
@@ -510,15 +583,15 @@ const ChannelsTable = () => {
aria-label={t('测试单个渠道操作项目组')}
>
<Button
theme='light'
size="small"
type='tertiary'
onClick={() => testChannel(record, '')}
>
{t('测试')}
</Button>
<Button
theme='light'
size="small"
type='tertiary'
icon={<IconTreeTriangleDown />}
onClick={() => {
setCurrentTestChannel(record);
@@ -527,28 +600,66 @@ const ChannelsTable = () => {
/>
</SplitButtonGroup>
{record.status === 1 ? (
<Button
theme='light'
type='warning'
size="small"
onClick={() => manageChannel(record.id, 'disable', record)}
{record.channel_info?.is_multi_key ? (
<SplitButtonGroup
aria-label={t('多密钥渠道操作项目组')}
>
{t('禁用')}
</Button>
{
record.status === 1 ? (
<Button
type='danger'
size="small"
onClick={() => manageChannel(record.id, 'disable', record)}
>
{t('禁用')}
</Button>
) : (
<Button
size="small"
onClick={() => manageChannel(record.id, 'enable', record)}
>
{t('启用')}
</Button>
)
}
<Dropdown
trigger='click'
position='bottomRight'
menu={[
{
node: 'item',
name: t('启用全部密钥'),
onClick: () => manageChannel(record.id, 'enable_all', record),
}
]}
>
<Button
type='tertiary'
size="small"
icon={<IconTreeTriangleDown />}
/>
</Dropdown>
</SplitButtonGroup>
) : (
<Button
theme='light'
type='secondary'
size="small"
onClick={() => manageChannel(record.id, 'enable', record)}
>
{t('用')}
</Button>
record.status === 1 ? (
<Button
type='danger'
size="small"
onClick={() => manageChannel(record.id, 'disable', record)}
>
{t('用')}
</Button>
) : (
<Button
size="small"
onClick={() => manageChannel(record.id, 'enable', record)}
>
{t('启用')}
</Button>
)
)}
<Button
theme='light'
type='tertiary'
size="small"
onClick={() => {
@@ -566,7 +677,6 @@ const ChannelsTable = () => {
>
<Button
icon={<IconMore />}
theme='light'
type='tertiary'
size="small"
/>
@@ -578,23 +688,20 @@ const ChannelsTable = () => {
return (
<Space wrap>
<Button
theme='light'
type='secondary'
type='tertiary'
size="small"
onClick={() => manageTag(record.key, 'enable')}
>
{t('启用全部')}
</Button>
<Button
theme='light'
type='warning'
type='tertiary'
size="small"
onClick={() => manageTag(record.key, 'disable')}
>
{t('禁用全部')}
</Button>
<Button
theme='light'
type='tertiary'
size="small"
onClick={() => {
@@ -666,22 +773,13 @@ const ChannelsTable = () => {
onCancel={() => setShowColumnSelector(false)}
footer={
<div className="flex justify-end">
<Button
theme="light"
onClick={() => initDefaultColumns()}
>
<Button onClick={() => initDefaultColumns()}>
{t('重置')}
</Button>
<Button
theme="light"
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('确定')}
</Button>
</div>
@@ -868,42 +966,29 @@ const ChannelsTable = () => {
};
const copySelectedChannel = async (record) => {
const channelToCopy = { ...record };
channelToCopy.name += t('_复制');
channelToCopy.created_time = null;
channelToCopy.balance = 0;
channelToCopy.used_quota = 0;
delete channelToCopy.test_time;
delete channelToCopy.response_time;
if (!channelToCopy) {
showError(t('渠道未找到,请刷新页面后重试。'));
return;
}
try {
const newChannel = { ...channelToCopy, id: undefined };
const response = await API.post('/api/channel/', newChannel);
if (response.data.success) {
const res = await API.post(`/api/channel/copy/${record.id}`);
if (res?.data?.success) {
showSuccess(t('渠道复制成功'));
await refresh();
} else {
showError(response.data.message);
showError(res?.data?.message || t('渠道复制失败'));
}
} catch (error) {
showError(t('渠道复制失败: ') + error.message);
showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error));
}
};
const refresh = async () => {
const refresh = async (page = activePage) => {
const { searchKeyword, searchGroup, searchModel } = getFormValues();
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
await loadChannels(activePage, pageSize, idSort, enableTagMode);
await loadChannels(page, pageSize, idSort, enableTagMode);
} else {
await searchChannels(enableTagMode, activeTypeKey, statusFilter, activePage, pageSize, idSort);
await searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort);
}
};
useEffect(() => {
// console.log('default effect')
const localIdSort = localStorage.getItem('id-sort') === 'true';
const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
@@ -954,6 +1039,11 @@ const ChannelsTable = () => {
}
res = await API.put('/api/channel/', data);
break;
case 'enable_all':
data.channel_info = record.channel_info;
data.channel_info.multi_key_status_list = {};
res = await API.put('/api/channel/', data);
break;
}
const { success, message } = res.data;
if (success) {
@@ -1240,7 +1330,7 @@ const ChannelsTable = () => {
tab={
<span className="flex items-center gap-2">
{t('全部')}
<Tag color={activeTypeKey === 'all' ? 'red' : 'grey'} size='small' shape='circle'>
<Tag color={activeTypeKey === 'all' ? 'red' : 'grey'} shape='circle'>
{channelTypeCounts['all'] || 0}
</Tag>
</span>
@@ -1258,7 +1348,7 @@ const ChannelsTable = () => {
<span className="flex items-center gap-2">
{getChannelIcon(option.value)}
{option.label}
<Tag color={activeTypeKey === key ? 'red' : 'grey'} size='small' shape='circle'>
<Tag color={activeTypeKey === key ? 'red' : 'grey'} shape='circle'>
{count}
</Tag>
</span>
@@ -1453,6 +1543,11 @@ const ChannelsTable = () => {
if (success) {
showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data));
await refresh();
setTimeout(() => {
if (channels.length === 0 && activePage > 1) {
refresh(activePage - 1);
}
}, 100);
} else {
showError(message);
}
@@ -1461,7 +1556,7 @@ const ChannelsTable = () => {
const fixChannelsAbilities = async () => {
const res = await API.post(`/api/channel/fix`);
const { success, message, data } = res.data;
const { success, message, data } = res.data;
if (success) {
showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails));
await refresh();
@@ -1478,7 +1573,6 @@ const ChannelsTable = () => {
<Button
size='small'
disabled={!enableBatchDelete}
theme='light'
type='danger'
className="w-full md:w-auto"
onClick={() => {
@@ -1495,8 +1589,7 @@ const ChannelsTable = () => {
<Button
size='small'
disabled={!enableBatchDelete}
theme='light'
type='primary'
type='tertiary'
onClick={() => setShowBatchSetTag(true)}
className="w-full md:w-auto"
>
@@ -1511,8 +1604,7 @@ const ChannelsTable = () => {
<Dropdown.Item>
<Button
size='small'
theme='light'
type='warning'
type='tertiary'
className="w-full"
onClick={() => {
Modal.confirm({
@@ -1530,7 +1622,23 @@ const ChannelsTable = () => {
<Dropdown.Item>
<Button
size='small'
theme='light'
className="w-full"
onClick={() => {
Modal.confirm({
title: t('确定是否要修复数据库一致性?'),
content: t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'),
onOk: () => fixChannelsAbilities(),
size: 'sm',
centered: true,
});
}}
>
{t('修复数据库一致性')}
</Button>
</Dropdown.Item>
<Dropdown.Item>
<Button
size='small'
type='secondary'
className="w-full"
onClick={() => {
@@ -1549,7 +1657,6 @@ const ChannelsTable = () => {
<Dropdown.Item>
<Button
size='small'
theme='light'
type='danger'
className="w-full"
onClick={() => {
@@ -1565,25 +1672,6 @@ const ChannelsTable = () => {
{t('删除禁用通道')}
</Button>
</Dropdown.Item>
<Dropdown.Item>
<Button
size='small'
theme='light'
type='tertiary'
className="w-full"
onClick={() => {
Modal.confirm({
title: t('确定是否要修复数据库一致性?'),
content: t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'),
onOk: () => fixChannelsAbilities(),
size: 'sm',
centered: true,
});
}}
>
{t('修复数据库一致性')}
</Button>
</Dropdown.Item>
</Dropdown.Menu>
}
>
@@ -1594,8 +1682,7 @@ const ChannelsTable = () => {
<Button
size='small'
theme='light'
type='secondary'
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
>
@@ -1698,8 +1785,7 @@ const ChannelsTable = () => {
<Button
size='small'
theme='light'
type='primary'
type='tertiary'
className="w-full md:w-auto"
onClick={refresh}
>
@@ -1708,7 +1794,6 @@ const ChannelsTable = () => {
<Button
size='small'
theme='light'
type='tertiary'
onClick={() => setShowColumnSelector(true)}
className="w-full md:w-auto"
@@ -1771,7 +1856,7 @@ const ChannelsTable = () => {
</div>
<Button
size='small'
type="primary"
type="tertiary"
htmlType="submit"
loading={loading || searching}
className="w-full md:w-auto"
@@ -1780,7 +1865,7 @@ const ChannelsTable = () => {
</Button>
<Button
size='small'
theme='light'
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
@@ -1885,7 +1970,6 @@ const ChannelsTable = () => {
placeholder={t('请输入标签名称')}
value={batchSetTagValue}
onChange={(v) => setBatchSetTagValue(v)}
size='large'
/>
<div className="mt-4">
<Typography.Text type='secondary'>
@@ -1907,61 +1991,6 @@ const ChannelsTable = () => {
{t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
</Typography.Text>
</div>
{/* 搜索与操作按钮 */}
<div className="flex items-center justify-end gap-2 w-full">
<Input
placeholder={t('搜索模型...')}
value={modelSearchKeyword}
onChange={(v) => {
setModelSearchKeyword(v);
setModelTablePage(1);
}}
className="!w-full"
prefix={<IconSearch />}
showClear
/>
<Button
theme='light'
onClick={() => {
if (selectedModelKeys.length === 0) {
showError(t('请先选择模型!'));
return;
}
copy(selectedModelKeys.join(',')).then((ok) => {
if (ok) {
showSuccess(t('已复制 ${count} 个模型').replace('${count}', selectedModelKeys.length));
} else {
showError(t('复制失败,请手动复制'));
}
});
}}
>
{t('复制已选')}
</Button>
<Button
theme='light'
type='primary'
onClick={() => {
if (!currentTestChannel) return;
const successKeys = currentTestChannel.models
.split(',')
.filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase()))
.filter((m) => {
const result = modelTestResults[`${currentTestChannel.id}-${m}`];
return result && result.success;
});
if (successKeys.length === 0) {
showInfo(t('暂无成功模型'));
}
setSelectedModelKeys(successKeys);
}}
>
{t('选择成功')}
</Button>
</div>
</div>
)
}
@@ -1971,15 +2000,13 @@ const ChannelsTable = () => {
<div className="flex justify-end">
{isBatchTesting ? (
<Button
theme='light'
type='warning'
type='danger'
onClick={handleCloseModal}
>
{t('停止测试')}
</Button>
) : (
<Button
theme='light'
type='tertiary'
onClick={handleCloseModal}
>
@@ -1987,8 +2014,6 @@ const ChannelsTable = () => {
</Button>
)}
<Button
theme='light'
type='primary'
onClick={batchTestModels}
loading={isBatchTesting}
disabled={isBatchTesting}
@@ -2008,11 +2033,63 @@ const ChannelsTable = () => {
}
maskClosable={!isBatchTesting}
className="!rounded-lg"
size={isMobile() ? 'full-width' : 'large'}
size={isMobile ? 'full-width' : 'large'}
>
<div className="model-test-scroll">
{currentTestChannel && (
<div>
{/* 搜索与操作按钮 */}
<div className="flex items-center justify-end gap-2 w-full mb-2">
<Input
placeholder={t('搜索模型...')}
value={modelSearchKeyword}
onChange={(v) => {
setModelSearchKeyword(v);
setModelTablePage(1);
}}
className="!w-full"
prefix={<IconSearch />}
showClear
/>
<Button
onClick={() => {
if (selectedModelKeys.length === 0) {
showError(t('请先选择模型!'));
return;
}
copy(selectedModelKeys.join(',')).then((ok) => {
if (ok) {
showSuccess(t('已复制 ${count} 个模型').replace('${count}', selectedModelKeys.length));
} else {
showError(t('复制失败,请手动复制'));
}
});
}}
>
{t('复制已选')}
</Button>
<Button
type='tertiary'
onClick={() => {
if (!currentTestChannel) return;
const successKeys = currentTestChannel.models
.split(',')
.filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase()))
.filter((m) => {
const result = modelTestResults[`${currentTestChannel.id}-${m}`];
return result && result.success;
});
if (successKeys.length === 0) {
showInfo(t('暂无成功模型'));
}
setSelectedModelKeys(successKeys);
}}
>
{t('选择成功')}
</Button>
</div>
<Table
columns={[
{
@@ -2033,7 +2110,7 @@ const ChannelsTable = () => {
if (isTesting) {
return (
<Tag size='large' color='blue' shape='circle'>
<Tag color='blue' shape='circle'>
{t('测试中')}
</Tag>
);
@@ -2041,7 +2118,7 @@ const ChannelsTable = () => {
if (!testResult) {
return (
<Tag size='large' color='grey' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未开始')}
</Tag>
);
@@ -2050,7 +2127,6 @@ const ChannelsTable = () => {
return (
<div className="flex items-center gap-2">
<Tag
size='large'
color={testResult.success ? 'green' : 'red'}
shape='circle'
>
@@ -2072,8 +2148,7 @@ const ChannelsTable = () => {
const isTesting = testingModels.has(record.model);
return (
<Button
theme='light'
type='primary'
type='tertiary'
onClick={() => testChannel(currentTestChannel, record.model)}
loading={isTesting}
size='small'

View File

@@ -20,7 +20,7 @@ import {
renderQuota,
stringToColor,
getLogOther,
renderModelTag,
renderModelTag
} from '../../helpers';
import {
@@ -78,37 +78,37 @@ const LogsTable = () => {
switch (type) {
case 1:
return (
<Tag color='cyan' size='large' shape='circle'>
<Tag color='cyan' shape='circle'>
{t('充值')}
</Tag>
);
case 2:
return (
<Tag color='lime' size='large' shape='circle'>
<Tag color='lime' shape='circle'>
{t('消费')}
</Tag>
);
case 3:
return (
<Tag color='orange' size='large' shape='circle'>
<Tag color='orange' shape='circle'>
{t('管理')}
</Tag>
);
case 4:
return (
<Tag color='purple' size='large' shape='circle'>
<Tag color='purple' shape='circle'>
{t('系统')}
</Tag>
);
case 5:
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{t('错误')}
</Tag>
);
default:
return (
<Tag color='grey' size='large' shape='circle'>
<Tag color='grey' shape='circle'>
{t('未知')}
</Tag>
);
@@ -118,13 +118,13 @@ const LogsTable = () => {
function renderIsStream(bool) {
if (bool) {
return (
<Tag color='blue' size='large' shape='circle'>
<Tag color='blue' shape='circle'>
{t('流')}
</Tag>
);
} else {
return (
<Tag color='purple' size='large' shape='circle'>
<Tag color='purple' shape='circle'>
{t('非流')}
</Tag>
);
@@ -135,21 +135,21 @@ const LogsTable = () => {
const time = parseInt(type);
if (time < 101) {
return (
<Tag color='green' size='large' shape='circle'>
<Tag color='green' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 300) {
return (
<Tag color='orange' size='large' shape='circle'>
<Tag color='orange' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{' '}
{time} s{' '}
</Tag>
@@ -162,21 +162,21 @@ const LogsTable = () => {
time = time.toFixed(1);
if (time < 3) {
return (
<Tag color='green' size='large' shape='circle'>
<Tag color='green' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else if (time < 10) {
return (
<Tag color='orange' size='large' shape='circle'>
<Tag color='orange' shape='circle'>
{' '}
{time} s{' '}
</Tag>
);
} else {
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{' '}
{time} s{' '}
</Tag>
@@ -356,28 +356,34 @@ const LogsTable = () => {
dataIndex: 'channel',
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return isAdminUser ? (
record.type === 0 || record.type === 2 || record.type === 5 ? (
<div>
{
<Tooltip content={record.channel_name || '[未知]'}>
<Tag
color={colors[parseInt(text) % colors.length]}
size='large'
shape='circle'
>
{' '}
{text}{' '}
</Tag>
</Tooltip>
}
</div>
) : (
<></>
)
) : (
<></>
);
let isMultiKey = false
let multiKeyIndex = -1;
let other = getLogOther(record.other);
if (other?.admin_info) {
let adminInfo = other.admin_info;
if (adminInfo?.is_multi_key) {
isMultiKey = true;
multiKeyIndex = adminInfo.multi_key_index;
}
}
return isAdminUser && (record.type === 0 || record.type === 2 || record.type === 5) ? (
<Space>
<Tooltip content={record.channel_name || t('未知渠道')}>
<Tag
color={colors[parseInt(text) % colors.length]}
shape='circle'
>
{text}
</Tag>
</Tooltip>
{isMultiKey && (
<Tag color='white' shape='circle'>
{multiKeyIndex}
</Tag>
)}
</Space>
) : null;
},
},
{
@@ -389,7 +395,7 @@ const LogsTable = () => {
return isAdminUser ? (
<div>
<Avatar
size='small'
size='extra-small'
color={stringToColor(text)}
style={{ marginRight: 4 }}
onClick={(event) => {
@@ -415,7 +421,6 @@ const LogsTable = () => {
<div>
<Tag
color='grey'
size='large'
shape='circle'
onClick={(event) => {
//cancel the row click event
@@ -567,7 +572,6 @@ const LogsTable = () => {
<Tooltip content={text}>
<Tag
color='orange'
size='large'
shape='circle'
onClick={(event) => {
copyText(event, text);
@@ -693,22 +697,13 @@ const LogsTable = () => {
onCancel={() => setShowColumnSelector(false)}
footer={
<div className='flex justify-end'>
<Button
theme='light'
onClick={() => initDefaultColumns()}
>
<Button onClick={() => initDefaultColumns()}>
{t('重置')}
</Button>
<Button
theme='light'
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('确定')}
</Button>
</div>
@@ -1215,11 +1210,10 @@ const LogsTable = () => {
<Space>
<Tag
color='blue'
size='large'
style={{
padding: 15,
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
padding: 13,
}}
className='!rounded-lg'
>
@@ -1227,11 +1221,10 @@ const LogsTable = () => {
</Tag>
<Tag
color='pink'
size='large'
style={{
padding: 15,
fontWeight: 500,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
padding: 13,
}}
className='!rounded-lg'
>
@@ -1239,12 +1232,11 @@ const LogsTable = () => {
</Tag>
<Tag
color='white'
size='large'
style={{
padding: 15,
border: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
fontWeight: 500,
padding: 13,
}}
className='!rounded-lg'
>
@@ -1253,10 +1245,10 @@ const LogsTable = () => {
</Space>
<Button
theme='light'
type='secondary'
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
@@ -1287,6 +1279,7 @@ const LogsTable = () => {
placeholder={[t('开始时间'), t('结束时间')]}
showClear
pure
size="small"
/>
</div>
@@ -1297,6 +1290,7 @@ const LogsTable = () => {
placeholder={t('令牌名称')}
showClear
pure
size="small"
/>
<Form.Input
@@ -1305,6 +1299,7 @@ const LogsTable = () => {
placeholder={t('模型名称')}
showClear
pure
size="small"
/>
<Form.Input
@@ -1313,6 +1308,7 @@ const LogsTable = () => {
placeholder={t('分组')}
showClear
pure
size="small"
/>
{isAdminUser && (
@@ -1323,6 +1319,7 @@ const LogsTable = () => {
placeholder={t('渠道 ID')}
showClear
pure
size="small"
/>
<Form.Input
field='username'
@@ -1330,6 +1327,7 @@ const LogsTable = () => {
placeholder={t('用户名称')}
showClear
pure
size="small"
/>
</>
)}
@@ -1351,6 +1349,7 @@ const LogsTable = () => {
refresh();
}, 0);
}}
size="small"
>
<Form.Select.Option value='0'>
{t('全部')}
@@ -1375,14 +1374,15 @@ const LogsTable = () => {
<div className='flex gap-2 w-full sm:w-auto justify-end'>
<Button
type='primary'
type='tertiary'
htmlType='submit'
loading={loading}
size="small"
>
{t('查询')}
</Button>
<Button
theme='light'
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
@@ -1392,13 +1392,14 @@ const LogsTable = () => {
}, 100);
}
}}
size="small"
>
{t('重置')}
</Button>
<Button
theme='light'
type='tertiary'
onClick={() => setShowColumnSelector(true)}
size="small"
>
{t('列设置')}
</Button>

View File

@@ -185,115 +185,115 @@ const LogsTable = () => {
switch (type) {
case 'IMAGINE':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Palette size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Palette size={14} />}>
{t('绘图')}
</Tag>
);
case 'UPSCALE':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<ZoomIn size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<ZoomIn size={14} />}>
{t('放大')}
</Tag>
);
case 'VIDEO':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('视频')}
</Tag>
);
case 'EDITS':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
{t('编辑')}
</Tag>
);
case 'VARIATION':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('变换')}
</Tag>
);
case 'HIGH_VARIATION':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('强变换')}
</Tag>
);
case 'LOW_VARIATION':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Shuffle size={14} />}>
{t('弱变换')}
</Tag>
);
case 'PAN':
return (
<Tag color='cyan' size='large' shape='circle' prefixIcon={<Move size={14} />}>
<Tag color='cyan' shape='circle' prefixIcon={<Move size={14} />}>
{t('平移')}
</Tag>
);
case 'DESCRIBE':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<FileText size={14} />}>
{t('图生文')}
</Tag>
);
case 'BLEND':
return (
<Tag color='lime' size='large' shape='circle' prefixIcon={<Blend size={14} />}>
<Tag color='lime' shape='circle' prefixIcon={<Blend size={14} />}>
{t('图混合')}
</Tag>
);
case 'UPLOAD':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Upload size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Upload size={14} />}>
上传文件
</Tag>
);
case 'SHORTEN':
return (
<Tag color='pink' size='large' shape='circle' prefixIcon={<Minimize2 size={14} />}>
<Tag color='pink' shape='circle' prefixIcon={<Minimize2 size={14} />}>
{t('缩词')}
</Tag>
);
case 'REROLL':
return (
<Tag color='indigo' size='large' shape='circle' prefixIcon={<RotateCcw size={14} />}>
<Tag color='indigo' shape='circle' prefixIcon={<RotateCcw size={14} />}>
{t('重绘')}
</Tag>
);
case 'INPAINT':
return (
<Tag color='violet' size='large' shape='circle' prefixIcon={<PaintBucket size={14} />}>
<Tag color='violet' shape='circle' prefixIcon={<PaintBucket size={14} />}>
{t('局部重绘-提交')}
</Tag>
);
case 'ZOOM':
return (
<Tag color='teal' size='large' shape='circle' prefixIcon={<Focus size={14} />}>
<Tag color='teal' shape='circle' prefixIcon={<Focus size={14} />}>
{t('变焦')}
</Tag>
);
case 'CUSTOM_ZOOM':
return (
<Tag color='teal' size='large' shape='circle' prefixIcon={<Move3D size={14} />}>
<Tag color='teal' shape='circle' prefixIcon={<Move3D size={14} />}>
{t('自定义变焦-提交')}
</Tag>
);
case 'MODAL':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<Monitor size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<Monitor size={14} />}>
{t('窗口处理')}
</Tag>
);
case 'SWAP_FACE':
return (
<Tag color='light-green' size='large' shape='circle' prefixIcon={<UserCheck size={14} />}>
<Tag color='light-green' shape='circle' prefixIcon={<UserCheck size={14} />}>
{t('换脸')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -304,31 +304,31 @@ const LogsTable = () => {
switch (code) {
case 1:
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('已提交')}
</Tag>
);
case 21:
return (
<Tag color='lime' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color='lime' shape='circle' prefixIcon={<Clock size={14} />}>
{t('等待中')}
</Tag>
);
case 22:
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Copy size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Copy size={14} />}>
{t('重复提交')}
</Tag>
);
case 0:
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<FileX size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<FileX size={14} />}>
{t('未提交')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -339,43 +339,43 @@ const LogsTable = () => {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Loader size={14} />}>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('失败')}
</Tag>
);
case 'MODAL':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<AlertCircle size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
{t('窗口等待')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -405,7 +405,7 @@ const LogsTable = () => {
const color = durationSec > 60 ? 'red' : 'green';
return (
<Tag color={color} size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color={color} shape='circle' prefixIcon={<Clock size={14} />}>
{durationSec} {t('秒')}
</Tag>
);
@@ -439,7 +439,6 @@ const LogsTable = () => {
<div>
<Tag
color={colors[parseInt(text) % colors.length]}
size='large'
shape='circle'
prefixIcon={<Hash size={14} />}
onClick={() => {
@@ -523,6 +522,7 @@ const LogsTable = () => {
}
return (
<Button
size="small"
onClick={() => {
setModalImageUrl(text);
setIsModalOpenurl(true);
@@ -741,22 +741,13 @@ const LogsTable = () => {
onCancel={() => setShowColumnSelector(false)}
footer={
<div className="flex justify-end">
<Button
theme="light"
onClick={() => initDefaultColumns()}
>
<Button onClick={() => initDefaultColumns()}>
{t('重置')}
</Button>
<Button
theme="light"
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('确定')}
</Button>
</div>
@@ -831,10 +822,10 @@ const LogsTable = () => {
)}
</div>
<Button
theme='light'
type='secondary'
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
@@ -864,6 +855,7 @@ const LogsTable = () => {
placeholder={[t('开始时间'), t('结束时间')]}
showClear
pure
size="small"
/>
</div>
@@ -874,6 +866,7 @@ const LogsTable = () => {
placeholder={t('任务 ID')}
showClear
pure
size="small"
/>
{/* 渠道 ID - 仅管理员可见 */}
@@ -884,6 +877,7 @@ const LogsTable = () => {
placeholder={t('渠道 ID')}
showClear
pure
size="small"
/>
)}
</div>
@@ -893,14 +887,15 @@ const LogsTable = () => {
<div></div>
<div className="flex gap-2">
<Button
type='primary'
type='tertiary'
htmlType='submit'
loading={loading}
size="small"
>
{t('查询')}
</Button>
<Button
theme='light'
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
@@ -910,13 +905,14 @@ const LogsTable = () => {
}, 100);
}
}}
size="small"
>
{t('重置')}
</Button>
<Button
theme='light'
type='tertiary'
onClick={() => setShowColumnSelector(true)}
size="small"
>
{t('列设置')}
</Button>

View File

@@ -76,13 +76,13 @@ const ModelPricing = () => {
switch (type) {
case 1:
return (
<Tag color='teal' size='large' shape='circle'>
<Tag color='teal' shape='circle'>
{t('按次计费')}
</Tag>
);
case 0:
return (
<Tag color='violet' size='large' shape='circle'>
<Tag color='violet' shape='circle'>
{t('按量计费')}
</Tag>
);
@@ -116,7 +116,6 @@ const ModelPricing = () => {
<Tag
key={endpoint}
color={stringToColor(endpoint)}
size='large'
shape='circle'
>
{endpoint}
@@ -179,7 +178,7 @@ const ModelPricing = () => {
if (usableGroup[group]) {
if (group === selectedGroup) {
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<IconVerify />}>
<Tag color='blue' shape='circle' prefixIcon={<IconVerify />}>
{group}
</Tag>
);
@@ -187,7 +186,6 @@ const ModelPricing = () => {
return (
<Tag
color='blue'
size='large'
shape='circle'
onClick={() => {
setSelectedGroup(group);
@@ -392,7 +390,6 @@ const ModelPricing = () => {
{category.label}
<Tag
color={activeKey === key ? 'red' : 'grey'}
size='small'
shape='circle'
>
{modelCount}
@@ -436,7 +433,6 @@ const ModelPricing = () => {
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
size="large"
/>
</div>
<Button
@@ -446,7 +442,6 @@ const ModelPricing = () => {
onClick={() => copyText(selectedRowKeys)}
disabled={selectedRowKeys.length === 0}
className="!bg-blue-500 hover:!bg-blue-600 text-white"
size="large"
>
{t('复制选中模型')}
</Button>

View File

@@ -53,31 +53,31 @@ const RedemptionsTable = () => {
const renderStatus = (status, record) => {
if (isExpired(record)) {
return (
<Tag color='orange' size='large' shape='circle'>{t('已过期')}</Tag>
<Tag color='orange' shape='circle'>{t('已过期')}</Tag>
);
}
switch (status) {
case 1:
return (
<Tag color='green' size='large' shape='circle'>
<Tag color='green' shape='circle'>
{t('未使用')}
</Tag>
);
case 2:
return (
<Tag color='red' size='large' shape='circle'>
<Tag color='red' shape='circle'>
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='grey' size='large' shape='circle'>
<Tag color='grey' shape='circle'>
{t('已使用')}
</Tag>
);
default:
return (
<Tag color='black' size='large' shape='circle'>
<Tag color='black' shape='circle'>
{t('未知状态')}
</Tag>
);
@@ -107,7 +107,7 @@ const RedemptionsTable = () => {
render: (text, record, index) => {
return (
<div>
<Tag size={'large'} color={'grey'} shape='circle'>
<Tag color='grey' shape='circle'>
{renderQuota(parseInt(text))}
</Tag>
</div>
@@ -139,6 +139,7 @@ const RedemptionsTable = () => {
title: '',
dataIndex: 'operate',
fixed: 'right',
width: 205,
render: (text, record, index) => {
// 创建更多操作的下拉菜单项
const moreMenuItems = [
@@ -151,9 +152,15 @@ const RedemptionsTable = () => {
title: t('确定是否要删除此兑换码?'),
content: t('此修改将不可逆'),
onOk: () => {
manageRedemption(record.id, 'delete', record).then(() => {
removeRecord(record.key);
});
(async () => {
await manageRedemption(record.id, 'delete', record);
await refresh();
setTimeout(() => {
if (redemptions.length === 0 && activePage > 1) {
refresh(activePage - 1);
}
}, 100);
})();
},
});
},
@@ -185,7 +192,6 @@ const RedemptionsTable = () => {
<Space>
<Popover content={record.key} style={{ padding: 20 }} position='top'>
<Button
theme='light'
type='tertiary'
size="small"
>
@@ -193,8 +199,6 @@ const RedemptionsTable = () => {
</Button>
</Popover>
<Button
theme='light'
type='secondary'
size="small"
onClick={async () => {
await copyText(record.key);
@@ -203,7 +207,6 @@ const RedemptionsTable = () => {
{t('复制')}
</Button>
<Button
theme='light'
type='tertiary'
size="small"
onClick={() => {
@@ -220,7 +223,6 @@ const RedemptionsTable = () => {
menu={moreMenuItems}
>
<Button
theme='light'
type='tertiary'
size="small"
icon={<IconMore />}
@@ -320,8 +322,13 @@ const RedemptionsTable = () => {
});
}, [pageSize]);
const refresh = async () => {
await loadRedemptions(activePage - 1, pageSize);
const refresh = async (page = activePage) => {
const { searchKeyword } = getFormValues();
if (searchKeyword === '') {
await loadRedemptions(page, pageSize);
} else {
await searchRedemptions(searchKeyword, page, pageSize);
}
};
const manageRedemption = async (id, action, record) => {
@@ -424,10 +431,10 @@ const RedemptionsTable = () => {
<Text>{t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')}</Text>
</div>
<Button
theme='light'
type='secondary'
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
@@ -440,7 +447,6 @@ const RedemptionsTable = () => {
<div className="flex flex-col sm:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
<div className="flex gap-2 w-full sm:w-auto">
<Button
theme='light'
type='primary'
className="w-full sm:w-auto"
onClick={() => {
@@ -449,11 +455,12 @@ const RedemptionsTable = () => {
});
setShowEdit(true);
}}
size="small"
>
{t('添加兑换码')}
</Button>
<Button
type='warning'
type='tertiary'
className="w-full sm:w-auto"
onClick={async () => {
if (selectedKeys.length === 0) {
@@ -467,6 +474,7 @@ const RedemptionsTable = () => {
}
await copyText(keys);
}}
size="small"
>
{t('复制所选兑换码到剪贴板')}
</Button>
@@ -492,6 +500,7 @@ const RedemptionsTable = () => {
},
});
}}
size="small"
>
{t('清除失效兑换码')}
</Button>
@@ -519,23 +528,24 @@ const RedemptionsTable = () => {
placeholder={t('关键字(id或者名称)')}
showClear
pure
size="small"
/>
</div>
<div className="flex gap-2 w-full md:w-auto">
<Button
type="primary"
type="tertiary"
htmlType="submit"
loading={loading || searching}
className="flex-1 md:flex-initial md:w-auto"
size="small"
>
{t('查询')}
</Button>
<Button
theme="light"
type="tertiary"
onClick={() => {
if (formApi) {
formApi.reset();
// 重置后立即查询使用setTimeout确保表单重置完成
setTimeout(() => {
setActivePage(1);
loadRedemptions(1, pageSize);
@@ -543,6 +553,7 @@ const RedemptionsTable = () => {
}
}}
className="flex-1 md:flex-initial md:w-auto"
size="small"
>
{t('重置')}
</Button>

View File

@@ -34,7 +34,6 @@ import {
Layout,
Modal,
Progress,
Skeleton,
Table,
Tag,
Typography
@@ -106,7 +105,7 @@ function renderDuration(submit_time, finishTime) {
// 返回带有样式的颜色标签
return (
<Tag color={color} size='large' prefixIcon={<Clock size={14} />}>
<Tag color={color} prefixIcon={<Clock size={14} />}>
{durationSec}
</Tag>
);
@@ -198,31 +197,31 @@ const LogsTable = () => {
switch (type) {
case 'MUSIC':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Music size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Music size={14} />}>
{t('生成音乐')}
</Tag>
);
case 'LYRICS':
return (
<Tag color='pink' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
<Tag color='pink' shape='circle' prefixIcon={<FileText size={14} />}>
{t('生成歌词')}
</Tag>
);
case TASK_ACTION_GENERATE:
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Sparkles size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
{t('图生视频')}
</Tag>
);
case TASK_ACTION_TEXT_GENERATE:
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Sparkles size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
{t('文生视频')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -233,25 +232,25 @@ const LogsTable = () => {
switch (platform) {
case 'suno':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<Music size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<Music size={14} />}>
Suno
</Tag>
);
case 'kling':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Video size={14} />}>
Kling
</Tag>
);
case 'jimeng':
return (
<Tag color='purple' size='large' shape='circle' prefixIcon={<Video size={14} />}>
<Tag color='purple' shape='circle' prefixIcon={<Video size={14} />}>
Jimeng
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -262,55 +261,55 @@ const LogsTable = () => {
switch (type) {
case 'SUCCESS':
return (
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
<Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('成功')}
</Tag>
);
case 'NOT_START':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Pause size={14} />}>
{t('未启动')}
</Tag>
);
case 'SUBMITTED':
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
{t('队列中')}
</Tag>
);
case 'IN_PROGRESS':
return (
<Tag color='blue' size='large' shape='circle' prefixIcon={<Play size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<Play size={14} />}>
{t('执行中')}
</Tag>
);
case 'FAILURE':
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('失败')}
</Tag>
);
case 'QUEUED':
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<List size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<List size={14} />}>
{t('排队中')}
</Tag>
);
case 'UNKNOWN':
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
case '':
return (
<Tag color='grey' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<Loader size={14} />}>
{t('正在提交')}
</Tag>
);
default:
return (
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知')}
</Tag>
);
@@ -596,22 +595,13 @@ const LogsTable = () => {
onCancel={() => setShowColumnSelector(false)}
footer={
<div className="flex justify-end">
<Button
theme="light"
onClick={() => initDefaultColumns()}
>
<Button onClick={() => initDefaultColumns()}>
{t('重置')}
</Button>
<Button
theme="light"
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('取消')}
</Button>
<Button
type='primary'
onClick={() => setShowColumnSelector(false)}
>
<Button onClick={() => setShowColumnSelector(false)}>
{t('确定')}
</Button>
</div>
@@ -665,23 +655,13 @@ const LogsTable = () => {
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
<IconEyeOpened className="mr-2" />
{loading ? (
<Skeleton.Title
style={{
width: 300,
marginBottom: 0,
marginTop: 0
}}
/>
) : (
<Text>{t('任务记录')}</Text>
)}
<Text>{t('任务记录')}</Text>
</div>
<Button
theme='light'
type='secondary'
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
@@ -711,6 +691,7 @@ const LogsTable = () => {
placeholder={[t('开始时间'), t('结束时间')]}
showClear
pure
size="small"
/>
</div>
@@ -721,6 +702,7 @@ const LogsTable = () => {
placeholder={t('任务 ID')}
showClear
pure
size="small"
/>
{/* 渠道 ID - 仅管理员可见 */}
@@ -731,6 +713,7 @@ const LogsTable = () => {
placeholder={t('渠道 ID')}
showClear
pure
size="small"
/>
)}
</div>
@@ -740,14 +723,15 @@ const LogsTable = () => {
<div></div>
<div className="flex gap-2">
<Button
type='primary'
type='tertiary'
htmlType='submit'
loading={loading}
size="small"
>
{t('查询')}
</Button>
<Button
theme='light'
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
@@ -757,13 +741,14 @@ const LogsTable = () => {
}, 100);
}
}}
size="small"
>
{t('重置')}
</Button>
<Button
theme='light'
type='tertiary'
onClick={() => setShowColumnSelector(true)}
size="small"
>
{t('列设置')}
</Button>

View File

@@ -7,7 +7,7 @@ import {
timestamp2string,
renderGroup,
renderQuota,
getQuotaPerUnit
getModelCategories
} from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import {
@@ -22,6 +22,12 @@ import {
SplitButtonGroup,
Table,
Tag,
AvatarGroup,
Avatar,
Tooltip,
Progress,
Switch,
Input,
Typography
} from '@douyinfe/semi-ui';
import {
@@ -31,7 +37,9 @@ import {
import {
IconSearch,
IconTreeTriangleDown,
IconMore,
IconCopy,
IconEyeOpened,
IconEyeClosed,
} from '@douyinfe/semi-icons';
import { Key } from 'lucide-react';
import EditToken from '../../pages/Token/EditToken';
@@ -47,49 +55,6 @@ function renderTimestamp(timestamp) {
const TokensTable = () => {
const { t } = useTranslation();
const renderStatus = (status, model_limits_enabled = false) => {
switch (status) {
case 1:
if (model_limits_enabled) {
return (
<Tag color='green' size='large' shape='circle' >
{t('已启用:限制模型')}
</Tag>
);
} else {
return (
<Tag color='green' size='large' shape='circle' >
{t('已启用')}
</Tag>
);
}
case 2:
return (
<Tag color='red' size='large' shape='circle' >
{t('已禁用')}
</Tag>
);
case 3:
return (
<Tag color='yellow' size='large' shape='circle' >
{t('已过期')}
</Tag>
);
case 4:
return (
<Tag color='grey' size='large' shape='circle' >
{t('已耗尽')}
</Tag>
);
default:
return (
<Tag color='black' size='large' shape='circle' >
{t('未知状态')}
</Tag>
);
}
};
const columns = [
{
title: t('名称'),
@@ -99,64 +64,253 @@ const TokensTable = () => {
title: t('状态'),
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
return (
<div>
<Space>
{renderStatus(text, record.model_limits_enabled)}
{renderGroup(record.group)}
</Space>
</div>
);
},
},
{
title: t('已用额度'),
dataIndex: 'used_quota',
render: (text, record, index) => {
return (
<div>
<Tag size={'large'} color={'grey'} shape='circle' >
{renderQuota(parseInt(text))}
</Tag>
</div>
);
},
},
{
title: t('剩余额度'),
dataIndex: 'remain_quota',
render: (text, record, index) => {
const getQuotaColor = (quotaValue) => {
const quotaPerUnit = getQuotaPerUnit();
const dollarAmount = quotaValue / quotaPerUnit;
if (dollarAmount <= 0) {
return 'red';
} else if (dollarAmount <= 100) {
return 'yellow';
render: (text, record) => {
const enabled = text === 1;
const handleToggle = (checked) => {
if (checked) {
manageToken(record.id, 'enable', record);
} else {
return 'green';
manageToken(record.id, 'disable', record);
}
};
return (
<div>
{record.unlimited_quota ? (
<Tag size={'large'} color={'white'} shape='circle' >
{t('无限制')}
</Tag>
) : (
<Tag
size={'large'}
color={getQuotaColor(parseInt(text))}
shape='circle'
>
{renderQuota(parseInt(text))}
</Tag>
)}
let tagColor = 'black';
let tagText = t('未知状态');
if (enabled) {
tagColor = 'green';
tagText = t('已启用');
} else if (text === 2) {
tagColor = 'red';
tagText = t('已禁用');
} else if (text === 3) {
tagColor = 'yellow';
tagText = t('已过期');
} else if (text === 4) {
tagColor = 'grey';
tagText = t('已耗尽');
}
const used = parseInt(record.used_quota) || 0;
const remain = parseInt(record.remain_quota) || 0;
const total = used + remain;
const percent = total > 0 ? (remain / total) * 100 : 0;
const getProgressColor = (pct) => {
if (pct === 100) return 'var(--semi-color-success)';
if (pct <= 10) return 'var(--semi-color-danger)';
if (pct <= 30) return 'var(--semi-color-warning)';
return undefined;
};
const quotaSuffix = record.unlimited_quota ? (
<div className='text-xs'>{t('无限额度')}</div>
) : (
<div className='flex flex-col items-end'>
<span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
<Progress
percent={percent}
stroke={getProgressColor(percent)}
aria-label='quota usage'
format={() => `${percent.toFixed(0)}%`}
style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
/>
</div>
);
const content = (
<Tag
color={tagColor}
shape='circle'
size='large'
prefixIcon={
<Switch
size='small'
checked={enabled}
onChange={handleToggle}
aria-label='token status switch'
/>
}
suffixIcon={quotaSuffix}
>
{tagText}
</Tag>
);
if (record.unlimited_quota) {
return content;
}
return (
<Tooltip
content={
<div className='text-xs'>
<div>{t('已用额度')}: {renderQuota(used)}</div>
<div>{t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)</div>
<div>{t('总额度')}: {renderQuota(total)}</div>
</div>
}
>
{content}
</Tooltip>
);
},
},
{
title: t('分组'),
dataIndex: 'group',
key: 'group',
render: (text) => {
if (text === 'auto') {
return (
<Tooltip
content={t('当前分组为 auto会自动选择最优分组当一个组不可用时自动降级到下一个组熔断机制')}
position='top'
>
<Tag color='white' shape='circle'> {t('智能熔断')} </Tag>
</Tooltip>
);
}
return renderGroup(text);
},
},
{
title: t('密钥'),
key: 'token_key',
render: (text, record) => {
const fullKey = 'sk-' + record.key;
const maskedKey = 'sk-' + record.key.slice(0, 4) + '**********' + record.key.slice(-4);
const revealed = !!showKeys[record.id];
return (
<div className='w-[200px]'>
<Input
readOnly
value={revealed ? fullKey : maskedKey}
size='small'
suffix={
<div className='flex items-center'>
<Button
theme='borderless'
size='small'
type='tertiary'
icon={revealed ? <IconEyeClosed /> : <IconEyeOpened />}
aria-label='toggle token visibility'
onClick={(e) => {
e.stopPropagation();
setShowKeys(prev => ({ ...prev, [record.id]: !revealed }));
}}
/>
<Button
theme='borderless'
size='small'
type='tertiary'
icon={<IconCopy />}
aria-label='copy token key'
onClick={async (e) => {
e.stopPropagation();
await copyText(fullKey);
}}
/>
</div>
}
/>
</div>
);
},
},
{
title: t('可用模型'),
dataIndex: 'model_limits',
render: (text, record) => {
if (record.model_limits_enabled && text) {
const models = text.split(',').filter(Boolean);
const categories = getModelCategories(t);
const vendorAvatars = [];
const matchedModels = new Set();
Object.entries(categories).forEach(([key, category]) => {
if (key === 'all') return;
if (!category.icon || !category.filter) return;
const vendorModels = models.filter((m) => category.filter({ model_name: m }));
if (vendorModels.length > 0) {
vendorAvatars.push(
<Tooltip key={key} content={vendorModels.join(', ')} position='top' showArrow>
<Avatar size='extra-extra-small' alt={category.label} color='transparent'>
{category.icon}
</Avatar>
</Tooltip>
);
vendorModels.forEach((m) => matchedModels.add(m));
}
});
const unmatchedModels = models.filter((m) => !matchedModels.has(m));
if (unmatchedModels.length > 0) {
vendorAvatars.push(
<Tooltip key='unknown' content={unmatchedModels.join(', ')} position='top' showArrow>
<Avatar size='extra-extra-small' alt='unknown'>
{t('其他')}
</Avatar>
</Tooltip>
);
}
return (
<AvatarGroup size='extra-extra-small'>
{vendorAvatars}
</AvatarGroup>
);
} else {
return (
<Tag color='white' shape='circle'>
{t('无限制')}
</Tag>
);
}
},
},
{
title: t('IP限制'),
dataIndex: 'allow_ips',
render: (text) => {
if (!text || text.trim() === '') {
return (
<Tag color='white' shape='circle'>
{t('无限制')}
</Tag>
);
}
const ips = text
.split('\n')
.map((ip) => ip.trim())
.filter(Boolean);
const displayIps = ips.slice(0, 1);
const extraCount = ips.length - displayIps.length;
const ipTags = displayIps.map((ip, idx) => (
<Tag key={idx} shape='circle'>
{ip}
</Tag>
));
if (extraCount > 0) {
ipTags.push(
<Tooltip
key='extra'
content={ips.slice(1).join(', ')}
position='top'
showArrow
>
<Tag shape='circle'>
{'+' + extraCount}
</Tag>
</Tooltip>
);
}
return <Space wrap>{ipTags}</Space>;
},
},
{
@@ -211,58 +365,6 @@ const TokensTable = () => {
}
}
// 创建更多操作的下拉菜单项
const moreMenuItems = [
{
node: 'item',
name: t('查看'),
onClick: () => {
Modal.info({
title: t('令牌详情'),
content: 'sk-' + record.key,
size: 'large',
});
},
},
{
node: 'item',
name: t('删除'),
type: 'danger',
onClick: () => {
Modal.confirm({
title: t('确定是否要删除此令牌?'),
content: t('此修改将不可逆'),
onOk: () => {
manageToken(record.id, 'delete', record).then(() => {
removeRecord(record.key);
});
},
});
},
}
];
// 动态添加启用/禁用按钮
if (record.status === 1) {
moreMenuItems.push({
node: 'item',
name: t('禁用'),
type: 'warning',
onClick: () => {
manageToken(record.id, 'disable', record);
},
});
} else {
moreMenuItems.push({
node: 'item',
name: t('启用'),
type: 'secondary',
onClick: () => {
manageToken(record.id, 'enable', record);
},
});
}
return (
<Space wrap>
<SplitButtonGroup
@@ -270,9 +372,8 @@ const TokensTable = () => {
aria-label={t('项目操作按钮组')}
>
<Button
theme='light'
size="small"
style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
type='tertiary'
onClick={() => {
if (chatsArray.length === 0) {
showError(t('请联系管理员配置聊天链接'));
@@ -293,11 +394,7 @@ const TokensTable = () => {
menu={chatsArray}
>
<Button
style={{
padding: '4px 4px',
color: 'rgba(var(--semi-teal-7), 1)',
}}
type='primary'
type='tertiary'
icon={<IconTreeTriangleDown />}
size="small"
></Button>
@@ -305,18 +402,6 @@ const TokensTable = () => {
</SplitButtonGroup>
<Button
theme='light'
type='secondary'
size="small"
onClick={async (text) => {
await copyText('sk-' + record.key);
}}
>
{t('复制')}
</Button>
<Button
theme='light'
type='tertiary'
size="small"
onClick={() => {
@@ -327,18 +412,24 @@ const TokensTable = () => {
{t('编辑')}
</Button>
<Dropdown
trigger='click'
position='bottomRight'
menu={moreMenuItems}
<Button
type='danger'
size="small"
onClick={() => {
Modal.confirm({
title: t('确定是否要删除此令牌?'),
content: t('此修改将不可逆'),
onOk: () => {
(async () => {
await manageToken(record.id, 'delete', record);
await refresh();
})();
},
});
}}
>
<Button
icon={<IconMore />}
theme='light'
type='tertiary'
size="small"
/>
</Dropdown>
{t('删除')}
</Button>
</Space>
);
},
@@ -357,6 +448,7 @@ const TokensTable = () => {
id: undefined,
});
const [compactMode, setCompactMode] = useTableCompactMode('tokens');
const [showKeys, setShowKeys] = useState({});
// Form 初始值
const formInitValues = {
@@ -405,8 +497,8 @@ const TokensTable = () => {
setLoading(false);
};
const refresh = async () => {
await loadTokens(1);
const refresh = async (page = activePage) => {
await loadTokens(page);
setSelectedKeys([]);
};
@@ -582,6 +674,11 @@ const TokensTable = () => {
const count = res.data.data || 0;
showSuccess(t('已删除 {{count}} 个令牌!', { count }));
await refresh();
setTimeout(() => {
if (tokens.length === 0 && activePage > 1) {
refresh(activePage - 1);
}
}, 100);
} else {
showError(res?.data?.message || t('删除失败'));
}
@@ -601,10 +698,10 @@ const TokensTable = () => {
<Text>{t('令牌用于API访问认证可以设置额度限制和模型权限。')}</Text>
</div>
<Button
theme="light"
type="secondary"
type="tertiary"
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
@@ -616,7 +713,6 @@ const TokensTable = () => {
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
<div className="flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1">
<Button
theme="light"
type="primary"
className="flex-1 md:flex-initial"
onClick={() => {
@@ -625,12 +721,12 @@ const TokensTable = () => {
});
setShowEdit(true);
}}
size="small"
>
{t('添加令牌')}
</Button>
<Button
theme="light"
type="warning"
type='tertiary'
className="flex-1 md:flex-initial"
onClick={() => {
if (selectedKeys.length === 0) {
@@ -644,8 +740,7 @@ const TokensTable = () => {
footer: (
<Space>
<Button
type="primary"
theme="solid"
type='tertiary'
onClick={async () => {
let content = '';
for (let i = 0; i < selectedKeys.length; i++) {
@@ -659,7 +754,6 @@ const TokensTable = () => {
{t('名称+密钥')}
</Button>
<Button
theme="light"
onClick={async () => {
let content = '';
for (let i = 0; i < selectedKeys.length; i++) {
@@ -675,12 +769,12 @@ const TokensTable = () => {
),
});
}}
size="small"
>
{t('复制所选令牌')}
</Button>
<Button
theme="light"
type="danger"
type='danger'
className="w-full md:w-auto"
onClick={() => {
if (selectedKeys.length === 0) {
@@ -697,6 +791,7 @@ const TokensTable = () => {
onOk: () => batchDeleteTokens(),
});
}}
size="small"
>
{t('删除所选令牌')}
</Button>
@@ -721,6 +816,7 @@ const TokensTable = () => {
placeholder={t('搜索关键字')}
showClear
pure
size="small"
/>
</div>
<div className="relative w-full md:w-56">
@@ -730,19 +826,21 @@ const TokensTable = () => {
placeholder={t('密钥')}
showClear
pure
size="small"
/>
</div>
<div className="flex gap-2 w-full md:w-auto">
<Button
type="primary"
type="tertiary"
htmlType="submit"
loading={loading || searching}
className="flex-1 md:flex-initial md:w-auto"
size="small"
>
{t('查询')}
</Button>
<Button
theme="light"
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
@@ -753,6 +851,7 @@ const TokensTable = () => {
}
}}
className="flex-1 md:flex-initial md:w-auto"
size="small"
>
{t('重置')}
</Button>

View File

@@ -54,25 +54,25 @@ const UsersTable = () => {
switch (role) {
case 1:
return (
<Tag size='large' color='blue' shape='circle' prefixIcon={<User size={14} />}>
<Tag color='blue' shape='circle' prefixIcon={<User size={14} />}>
{t('普通用户')}
</Tag>
);
case 10:
return (
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
<Tag color='yellow' shape='circle' prefixIcon={<Shield size={14} />}>
{t('管理员')}
</Tag>
);
case 100:
return (
<Tag color='orange' size='large' shape='circle' prefixIcon={<Crown size={14} />}>
<Tag color='orange' shape='circle' prefixIcon={<Crown size={14} />}>
{t('超级管理员')}
</Tag>
);
default:
return (
<Tag color='red' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知身份')}
</Tag>
);
@@ -82,16 +82,16 @@ const UsersTable = () => {
const renderStatus = (status) => {
switch (status) {
case 1:
return <Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
return <Tag color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
case 2:
return (
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('已封禁')}
</Tag>
);
default:
return (
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
<Tag color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知状态')}
</Tag>
);
@@ -117,7 +117,7 @@ const UsersTable = () => {
<Space spacing={2}>
<span>{text}</span>
<Tooltip content={remark} position="top" showArrow>
<Tag color='white' size='large' shape='circle' className="!text-xs">
<Tag color='white' shape='circle' className="!text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 flex-shrink-0 rounded-full" style={{ backgroundColor: '#10b981' }} />
{displayRemark}
@@ -142,13 +142,13 @@ const UsersTable = () => {
return (
<div>
<Space spacing={1}>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
{t('剩余')}: {renderQuota(record.quota)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
{t('已用')}: {renderQuota(record.used_quota)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
{t('调用')}: {renderNumber(record.request_count)}
</Tag>
</Space>
@@ -163,13 +163,13 @@ const UsersTable = () => {
return (
<div>
<Space spacing={1}>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Users size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<Users size={14} />}>
{t('邀请')}: {renderNumber(record.aff_count)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<DollarSign size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<DollarSign size={14} />}>
{t('收益')}: {renderQuota(record.aff_history_quota)}
</Tag>
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
<Tag color='white' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
{record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
</Tag>
</Space>
@@ -247,9 +247,15 @@ const UsersTable = () => {
title: t('确定是否要注销此用户?'),
content: t('相当于删除用户,此修改将不可逆'),
onOk: () => {
manageUser(record.id, 'delete', record).then(() => {
removeRecord(record.id);
});
(async () => {
await manageUser(record.id, 'delete', record);
await refresh();
setTimeout(() => {
if (users.length === 0 && activePage > 1) {
refresh(activePage - 1);
}
}, 100);
})();
},
});
},
@@ -281,7 +287,6 @@ const UsersTable = () => {
return (
<Space>
<Button
theme='light'
type='tertiary'
size="small"
onClick={() => {
@@ -297,7 +302,6 @@ const UsersTable = () => {
menu={moreMenuItems}
>
<Button
theme='light'
type='tertiary'
size="small"
icon={<IconMore />}
@@ -459,13 +463,12 @@ const UsersTable = () => {
});
};
const refresh = async () => {
setActivePage(1);
const refresh = async (page = activePage) => {
const { searchKeyword, searchGroup } = getFormValues();
if (searchKeyword === '' && searchGroup === '') {
await loadUsers(1, pageSize);
await loadUsers(page, pageSize);
} else {
await searchUsers(1, pageSize, searchKeyword, searchGroup);
await searchUsers(page, pageSize, searchKeyword, searchGroup);
}
};
@@ -520,10 +523,10 @@ const UsersTable = () => {
<Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
</div>
<Button
theme='light'
type='secondary'
type='tertiary'
className="w-full md:w-auto"
onClick={() => setCompactMode(!compactMode)}
size="small"
>
{compactMode ? t('自适应列表') : t('紧凑列表')}
</Button>
@@ -535,12 +538,11 @@ const UsersTable = () => {
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
<Button
theme='light'
type='primary'
className="w-full md:w-auto"
onClick={() => {
setShowAddUser(true);
}}
size="small"
>
{t('添加用户')}
</Button>
@@ -568,6 +570,7 @@ const UsersTable = () => {
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
showClear
pure
size="small"
/>
</div>
<div className="w-full md:w-48">
@@ -585,23 +588,24 @@ const UsersTable = () => {
className="w-full"
showClear
pure
size="small"
/>
</div>
<div className="flex gap-2 w-full md:w-auto">
<Button
type="primary"
type="tertiary"
htmlType="submit"
loading={loading || searching}
className="flex-1 md:flex-initial md:w-auto"
size="small"
>
{t('查询')}
</Button>
<Button
theme="light"
type='tertiary'
onClick={() => {
if (formApi) {
formApi.reset();
// 重置后立即查询使用setTimeout确保表单重置完成
setTimeout(() => {
setActivePage(1);
loadUsers(1, pageSize);
@@ -609,6 +613,7 @@ const UsersTable = () => {
}
}}
className="flex-1 md:flex-initial md:w-auto"
size="small"
>
{t('重置')}
</Button>