Merge branch 'alpha' into feature/simple_stripe
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 登录,例如 Okta、Auth0 等兼容 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>
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
// contexts/Style/index.js
|
||||
|
||||
import React, { useReducer, useEffect, useMemo, createContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isMobile as getIsMobile } from '../../helpers';
|
||||
|
||||
// Action Types
|
||||
const ACTION_TYPES = {
|
||||
TOGGLE_SIDER: 'TOGGLE_SIDER',
|
||||
SET_SIDER: 'SET_SIDER',
|
||||
SET_MOBILE: 'SET_MOBILE',
|
||||
SET_SIDER_COLLAPSED: 'SET_SIDER_COLLAPSED',
|
||||
BATCH_UPDATE: 'BATCH_UPDATE',
|
||||
};
|
||||
|
||||
// Constants
|
||||
const STORAGE_KEYS = {
|
||||
SIDEBAR_COLLAPSED: 'default_collapse_sidebar',
|
||||
};
|
||||
|
||||
const ROUTE_PATTERNS = {
|
||||
CONSOLE: '/console',
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断路径是否为控制台路由
|
||||
* @param {string} pathname - 路由路径
|
||||
* @returns {boolean} 是否为控制台路由
|
||||
*/
|
||||
const isConsoleRoute = (pathname) => {
|
||||
return pathname === ROUTE_PATTERNS.CONSOLE ||
|
||||
pathname.startsWith(ROUTE_PATTERNS.CONSOLE + '/');
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取初始状态
|
||||
* @param {string} pathname - 当前路由路径
|
||||
* @returns {Object} 初始状态对象
|
||||
*/
|
||||
const getInitialState = (pathname) => {
|
||||
const isMobile = getIsMobile();
|
||||
const isConsole = isConsoleRoute(pathname);
|
||||
const isCollapsed = localStorage.getItem(STORAGE_KEYS.SIDEBAR_COLLAPSED) === 'true';
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
showSider: isConsole && !isMobile,
|
||||
siderCollapsed: isCollapsed,
|
||||
isManualSiderControl: false,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Style reducer
|
||||
* @param {Object} state - 当前状态
|
||||
* @param {Object} action - action 对象
|
||||
* @returns {Object} 新状态
|
||||
*/
|
||||
const styleReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.TOGGLE_SIDER:
|
||||
return {
|
||||
...state,
|
||||
showSider: !state.showSider,
|
||||
isManualSiderControl: true,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.SET_SIDER:
|
||||
return {
|
||||
...state,
|
||||
showSider: action.payload,
|
||||
isManualSiderControl: action.isManualControl ?? false,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.SET_MOBILE:
|
||||
return {
|
||||
...state,
|
||||
isMobile: action.payload,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.SET_SIDER_COLLAPSED:
|
||||
// 自动保存到 localStorage
|
||||
localStorage.setItem(STORAGE_KEYS.SIDEBAR_COLLAPSED, action.payload.toString());
|
||||
return {
|
||||
...state,
|
||||
siderCollapsed: action.payload,
|
||||
};
|
||||
|
||||
case ACTION_TYPES.BATCH_UPDATE:
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Context (内部使用,不导出)
|
||||
const StyleContext = createContext(null);
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 处理窗口大小变化
|
||||
* @param {Function} dispatch - dispatch 函数
|
||||
* @param {Object} state - 当前状态
|
||||
* @param {string} pathname - 当前路径
|
||||
*/
|
||||
const useWindowResize = (dispatch, state, pathname) => {
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const isMobile = getIsMobile();
|
||||
dispatch({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile });
|
||||
|
||||
// 只有在非手动控制的情况下,才根据屏幕大小自动调整侧边栏
|
||||
if (!state.isManualSiderControl && isConsoleRoute(pathname)) {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.SET_SIDER,
|
||||
payload: !isMobile,
|
||||
isManualControl: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let timeoutId;
|
||||
const debouncedResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(handleResize, 150);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', debouncedResize);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [dispatch, state.isManualSiderControl, pathname]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 处理路由变化
|
||||
* @param {Function} dispatch - dispatch 函数
|
||||
* @param {string} pathname - 当前路径
|
||||
*/
|
||||
const useRouteChange = (dispatch, pathname) => {
|
||||
useEffect(() => {
|
||||
const isMobile = getIsMobile();
|
||||
const isConsole = isConsoleRoute(pathname);
|
||||
|
||||
dispatch({
|
||||
type: ACTION_TYPES.BATCH_UPDATE,
|
||||
payload: {
|
||||
showSider: isConsole && !isMobile,
|
||||
isManualSiderControl: false,
|
||||
},
|
||||
});
|
||||
}, [pathname, dispatch]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 处理移动设备侧边栏自动收起
|
||||
* @param {Object} state - 当前状态
|
||||
* @param {Function} dispatch - dispatch 函数
|
||||
*/
|
||||
const useMobileSiderAutoHide = (state, dispatch) => {
|
||||
useEffect(() => {
|
||||
// 移动设备上,如果不是手动控制且侧边栏是打开的,则自动关闭
|
||||
if (state.isMobile && state.showSider && !state.isManualSiderControl) {
|
||||
dispatch({ type: ACTION_TYPES.SET_SIDER, payload: false });
|
||||
}
|
||||
}, [state.isMobile, state.showSider, state.isManualSiderControl, dispatch]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Style Provider 组件
|
||||
*/
|
||||
export const StyleProvider = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
styleReducer,
|
||||
pathname,
|
||||
getInitialState
|
||||
);
|
||||
|
||||
useWindowResize(dispatch, state, pathname);
|
||||
useRouteChange(dispatch, pathname);
|
||||
useMobileSiderAutoHide(state, dispatch);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({ state, dispatch }),
|
||||
[state]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyleContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</StyleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 自定义 Hook - 使用 StyleContext
|
||||
* @returns {{state: Object, dispatch: Function}} context value
|
||||
*/
|
||||
export const useStyle = () => {
|
||||
const context = React.useContext(StyleContext);
|
||||
if (!context) {
|
||||
throw new Error('useStyle must be used within StyleProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// 导出 action creators 以便外部使用
|
||||
export const styleActions = {
|
||||
toggleSider: () => ({ type: ACTION_TYPES.TOGGLE_SIDER }),
|
||||
setSider: (show, isManualControl = false) => ({
|
||||
type: ACTION_TYPES.SET_SIDER,
|
||||
payload: show,
|
||||
isManualControl
|
||||
}),
|
||||
setMobile: (isMobile) => ({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile }),
|
||||
setSiderCollapsed: (collapsed) => ({
|
||||
type: ACTION_TYPES.SET_SIDER_COLLAPSED,
|
||||
payload: collapsed
|
||||
}),
|
||||
};
|
||||
@@ -59,7 +59,12 @@ export function updateAPI() {
|
||||
API.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// 如果请求配置中显式要求跳过全局错误处理,则不弹出默认错误提示
|
||||
if (error.config && error.config.skipErrorHandler) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
showError(error);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -82,8 +87,9 @@ export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
group: inputs.group,
|
||||
messages: processedMessages,
|
||||
group: inputs.group,
|
||||
group: inputs.group,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
|
||||
10
web/src/helpers/boolean.js
Normal file
10
web/src/helpers/boolean.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const toBoolean = (value) => {
|
||||
// 兼容字符串、数字以及布尔原生类型
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'number') return value === 1;
|
||||
if (typeof value === 'string') {
|
||||
const v = value.toLowerCase();
|
||||
return v === 'true' || v === '1';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -6,3 +6,4 @@ export * from './render';
|
||||
export * from './log';
|
||||
export * from './data';
|
||||
export * from './token';
|
||||
export * from './boolean';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import i18next from 'i18next';
|
||||
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { copy, isMobile, showSuccess } from './utils';
|
||||
import { copy, showSuccess } from './utils';
|
||||
import { MOBILE_BREAKPOINT } from '../hooks/useIsMobile.js';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import {
|
||||
OpenAI,
|
||||
@@ -31,6 +32,8 @@ import {
|
||||
Coze,
|
||||
SiliconCloud,
|
||||
FastGPT,
|
||||
Kling,
|
||||
Jimeng,
|
||||
} from '@lobehub/icons';
|
||||
|
||||
import {
|
||||
@@ -51,18 +54,18 @@ import {
|
||||
|
||||
// 侧边栏图标颜色映射
|
||||
export const sidebarIconColors = {
|
||||
dashboard: '#4F46E5', // 紫蓝色
|
||||
dashboard: '#10B981', // 绿色
|
||||
terminal: '#10B981', // 绿色
|
||||
message: '#06B6D4', // 青色
|
||||
key: '#3B82F6', // 蓝色
|
||||
chart: '#8B5CF6', // 紫色
|
||||
chart: '#F59E0B', // 琥珀色
|
||||
image: '#EC4899', // 粉色
|
||||
check: '#F59E0B', // 琥珀色
|
||||
credit: '#F97316', // 橙色
|
||||
layers: '#EF4444', // 红色
|
||||
gift: '#F43F5E', // 玫红色
|
||||
user: '#6366F1', // 靛蓝色
|
||||
settings: '#6B7280', // 灰色
|
||||
user: '#10B981', // 绿色
|
||||
settings: '#F97316', // 橙色
|
||||
};
|
||||
|
||||
// 获取侧边栏Lucide图标组件
|
||||
@@ -386,6 +389,10 @@ export function getChannelIcon(channelType) {
|
||||
return <XAI size={iconSize} />;
|
||||
case 49: // Coze
|
||||
return <Coze size={iconSize} />;
|
||||
case 50: // 可灵 Kling
|
||||
return <Kling.Color size={iconSize} />;
|
||||
case 51: // 即梦 Jimeng
|
||||
return <Jimeng.Color size={iconSize} />;
|
||||
case 8: // 自定义渠道
|
||||
case 22: // 知识库:FastGPT
|
||||
return <FastGPT.Color size={iconSize} />;
|
||||
@@ -533,7 +540,7 @@ export function stringToColor(str) {
|
||||
export function renderModelTag(modelName, options = {}) {
|
||||
const {
|
||||
color,
|
||||
size = 'large',
|
||||
size = 'default',
|
||||
shape = 'circle',
|
||||
onClick,
|
||||
suffixIcon,
|
||||
@@ -578,7 +585,7 @@ export function renderText(text, limit) {
|
||||
export function renderGroup(group) {
|
||||
if (group === '') {
|
||||
return (
|
||||
<Tag size='large' key='default' color='orange' shape='circle'>
|
||||
<Tag key='default' color='white' shape='circle'>
|
||||
{i18next.t('用户分组')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -597,7 +604,6 @@ export function renderGroup(group) {
|
||||
<span key={group}>
|
||||
{groups.map((group) => (
|
||||
<Tag
|
||||
size='large'
|
||||
color={tagColors[group] || stringToColor(group)}
|
||||
key={group}
|
||||
shape='circle'
|
||||
@@ -664,7 +670,8 @@ const measureTextWidth = (
|
||||
};
|
||||
|
||||
export function truncateText(text, maxWidth = 200) {
|
||||
if (!isMobile()) {
|
||||
const isMobileScreen = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches;
|
||||
if (!isMobileScreen) {
|
||||
return text;
|
||||
}
|
||||
if (!text) return text;
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
import { TABLE_COMPACT_MODES_KEY } from '../constants';
|
||||
import { MOBILE_BREAKPOINT } from '../hooks/useIsMobile.js';
|
||||
|
||||
const HTMLToastContent = ({ htmlContent }) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||
@@ -67,9 +68,7 @@ export async function copy(text) {
|
||||
return okay;
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
// isMobile 函数已移除,请改用 useIsMobile Hook
|
||||
|
||||
let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT };
|
||||
let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT };
|
||||
@@ -77,7 +76,8 @@ let showSuccessOptions = { autoClose: toastConstants.SUCCESS_TIMEOUT };
|
||||
let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT };
|
||||
let showNoticeOptions = { autoClose: false };
|
||||
|
||||
if (isMobile()) {
|
||||
const isMobileScreen = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches;
|
||||
if (isMobileScreen) {
|
||||
showErrorOptions.position = 'top-center';
|
||||
// showErrorOptions.transition = 'flip';
|
||||
|
||||
|
||||
16
web/src/hooks/useIsMobile.js
Normal file
16
web/src/hooks/useIsMobile.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
export const useIsMobile = () => {
|
||||
const query = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
|
||||
return useSyncExternalStore(
|
||||
(callback) => {
|
||||
const mql = window.matchMedia(query);
|
||||
mql.addEventListener('change', callback);
|
||||
return () => mql.removeEventListener('change', callback);
|
||||
},
|
||||
() => window.matchMedia(query).matches,
|
||||
() => false,
|
||||
);
|
||||
};
|
||||
22
web/src/hooks/useSidebarCollapsed.js
Normal file
22
web/src/hooks/useSidebarCollapsed.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
const KEY = 'default_collapse_sidebar';
|
||||
|
||||
export const useSidebarCollapsed = () => {
|
||||
const [collapsed, setCollapsed] = useState(() => localStorage.getItem(KEY) === 'true');
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setCollapsed(prev => {
|
||||
const next = !prev;
|
||||
localStorage.setItem(KEY, next.toString());
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const set = useCallback((value) => {
|
||||
setCollapsed(value);
|
||||
localStorage.setItem(KEY, value.toString());
|
||||
}, []);
|
||||
|
||||
return [collapsed, toggle, set];
|
||||
};
|
||||
@@ -179,7 +179,6 @@
|
||||
"注销": "Logout",
|
||||
"登录": "Sign in",
|
||||
"注册": "Sign up",
|
||||
"加载{name}中...": "Loading {name}...",
|
||||
"未登录或登录已过期,请重新登录!": "Not logged in or session expired. Please login again!",
|
||||
"用户登录": "User Login",
|
||||
"密码": "Password",
|
||||
@@ -373,6 +372,9 @@
|
||||
"搜索令牌的名称 ...": "Search for the name of the token...",
|
||||
"已用额度": "Quota used",
|
||||
"剩余额度": "Remaining quota",
|
||||
"总额度": "Total quota",
|
||||
"智能熔断": "Smart fallback",
|
||||
"当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "The current group is auto, it will automatically select the optimal group, and automatically downgrade to the next group when a group is unavailable (breakage mechanism)",
|
||||
"过期时间": "Expiration time",
|
||||
"无": "None",
|
||||
"无限制": "Unlimited",
|
||||
@@ -421,6 +423,8 @@
|
||||
"微信身份验证": "WeChat Authentication",
|
||||
"Turnstile 用户校验": "Turnstile User Verification",
|
||||
"创建新的渠道": "Create New Channel",
|
||||
"是否自动禁用": "Whether to automatically disable",
|
||||
"仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "Only effective when automatic disabling is enabled, after closing, the channel will not be automatically disabled",
|
||||
"镜像": "Mirror",
|
||||
"请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used",
|
||||
"模型": "Model",
|
||||
@@ -507,6 +511,7 @@
|
||||
"创建新的兑换码": "Create a new redemption code",
|
||||
"未找到所请求的页面": "The requested page was not found",
|
||||
"过期时间格式错误!": "Expiration time format error!",
|
||||
"过期时间不能早于当前时间!": "Expiration time cannot be earlier than the current time!",
|
||||
"请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit",
|
||||
"此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:",
|
||||
"此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
|
||||
@@ -927,7 +932,6 @@
|
||||
"更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.",
|
||||
"一小时": "One hour",
|
||||
"新建数量": "New quantity",
|
||||
"加载失败,请稍后重试": "Loading failed, please try again later",
|
||||
"未设置": "Not set",
|
||||
"API文档": "API documentation",
|
||||
"不是合法的 JSON 字符串": "Not a valid JSON string",
|
||||
@@ -960,6 +964,7 @@
|
||||
"启用突发备用号池(建议勾选,极大降低故障率)": "Enable burst backup number pool (it is recommended to check this box to greatly reduce the failure rate)",
|
||||
"查看说明": "View instructions",
|
||||
"添加令牌": "Create token",
|
||||
"IP限制": "IP restrictions",
|
||||
"令牌纬度控制 Midjouney 配置,设置优先级:令牌 > 路径参数 > 系统默认": "Token latitude controls Midjouney configuration, setting priority: token > path parameter > system default",
|
||||
"启用速率限制": "Enable rate limiting",
|
||||
"复制BaseURL": "Copy BaseURL",
|
||||
@@ -1142,7 +1147,7 @@
|
||||
"鉴权json": "Authentication JSON",
|
||||
"请输入鉴权json": "Please enter authentication JSON",
|
||||
"组织": "Organization",
|
||||
"组织,可选,不填则为默认组织": "Organization (optional), default if empty",
|
||||
"组织,不填则为默认组织": "Organization, default if empty",
|
||||
"请输入组织org-xxx": "Please enter organization org-xxx",
|
||||
"默认测试模型": "Default Test Model",
|
||||
"不填则为模型列表第一个": "First model in list if empty",
|
||||
@@ -1623,6 +1628,7 @@
|
||||
"请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings.",
|
||||
"确定要删除此API信息吗?": "Are you sure you want to delete this API information?",
|
||||
"测速": "Speed Test",
|
||||
"跳转": "Jump",
|
||||
"批量删除": "Batch Delete",
|
||||
"常见问答": "FAQ",
|
||||
"进行中": "Ongoing",
|
||||
@@ -1754,7 +1760,23 @@
|
||||
"批量创建时会在名称后自动添加随机后缀": "When creating in batches, a random suffix will be automatically added to the name",
|
||||
"额度必须大于0": "Quota must be greater than 0",
|
||||
"生成数量必须大于0": "Generation quantity must be greater than 0",
|
||||
"创建后可在编辑渠道时获取上游模型列表": "After creation, you can get the upstream model list when editing the channel",
|
||||
"可用端点类型": "Supported endpoint types",
|
||||
"未登录,使用默认分组倍率:": "Not logged in, using default group ratio: "
|
||||
"未登录,使用默认分组倍率:": "Not logged in, using default group ratio: ",
|
||||
"该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "This server address will affect the payment callback address and the address displayed on the default homepage, please ensure correct configuration",
|
||||
"密钥聚合模式": "Key aggregation mode",
|
||||
"随机": "Random",
|
||||
"轮询": "Polling",
|
||||
"密钥文件 (.json)": "Key file (.json)",
|
||||
"点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here",
|
||||
"仅支持 JSON 文件": "Only JSON files are supported",
|
||||
"仅支持 JSON 文件,支持多文件": "Only JSON files are supported, multiple files are supported",
|
||||
"请上传密钥文件": "Please upload the key file",
|
||||
"请填写部署地区": "Please fill in the deployment region",
|
||||
"请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}",
|
||||
"其他": "Other",
|
||||
"未知渠道": "Unknown channel",
|
||||
"切换为单密钥模式": "Switch to single key mode",
|
||||
"将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Only the first key file will be retained, and the remaining files will be removed. Continue?",
|
||||
"自定义模型名称": "Custom model name",
|
||||
"启用全部密钥": "Enable all keys"
|
||||
}
|
||||
@@ -14,6 +14,22 @@
|
||||
}
|
||||
|
||||
/* ==================== 全局基础样式 ==================== */
|
||||
/* 侧边栏宽度相关的 CSS 变量,配合 .sidebar-collapsed 类和媒体查询实现响应式布局 */
|
||||
:root {
|
||||
--sidebar-width: 180px;
|
||||
/* 展开时宽度 */
|
||||
--sidebar-width-collapsed: 60px; /* 折叠后宽度,显示图标栏 */
|
||||
/* 折叠后宽度 */
|
||||
--sidebar-current-width: var(--sidebar-width);
|
||||
}
|
||||
|
||||
/* 当 body 上存在 .sidebar-collapsed 类时,使用折叠宽度 */
|
||||
body.sidebar-collapsed {
|
||||
--sidebar-current-width: var(--sidebar-width-collapsed);
|
||||
}
|
||||
|
||||
/* 移除了在移动端强制设为 0 的限制,改由 React 控制是否渲染侧边栏以实现显示/隐藏 */
|
||||
|
||||
body {
|
||||
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif;
|
||||
color: var(--semi-color-text-0);
|
||||
@@ -59,6 +75,11 @@ code {
|
||||
|
||||
.semi-navigation-item {
|
||||
margin-bottom: 4px !important;
|
||||
padding: 4px 12px !important;
|
||||
}
|
||||
|
||||
.semi-navigation-sub-title {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.semi-navigation-item-icon {
|
||||
@@ -70,6 +91,7 @@ code {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.semi-navigation-item,
|
||||
.semi-navigation-sub-title {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,18 @@ import { UserProvider } from './context/User';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { StatusProvider } from './context/Status';
|
||||
import { ThemeProvider } from './context/Theme';
|
||||
import { StyleProvider } from './context/Style/index.js';
|
||||
import PageLayout from './components/layout/PageLayout.js';
|
||||
import './i18n/i18n.js';
|
||||
import './index.css';
|
||||
|
||||
// 欢迎信息(二次开发者不准将此移除)
|
||||
// Welcome message (Secondary developers are not allowed to remove this)
|
||||
if (typeof window !== 'undefined') {
|
||||
console.log('%cWe ❤ NewAPI%c Github: https://github.com/QuantumNous/new-api',
|
||||
'color: #10b981; font-weight: bold; font-size: 24px;',
|
||||
'color: inherit; font-size: 14px;');
|
||||
}
|
||||
|
||||
// initialization
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
@@ -18,11 +25,14 @@ root.render(
|
||||
<React.StrictMode>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<BrowserRouter
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
}}
|
||||
>
|
||||
<ThemeProvider>
|
||||
<StyleProvider>
|
||||
<PageLayout />
|
||||
</StyleProvider>
|
||||
<PageLayout />
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
|
||||
@@ -105,7 +105,7 @@ const About = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
{aboutLoaded && about === '' ? (
|
||||
<div className="flex justify-center items-center h-screen p-8">
|
||||
<Empty
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
isMobile,
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
} from '../../helpers';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import { CHANNEL_OPTIONS } from '../../constants';
|
||||
import {
|
||||
SideSheet,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
Row,
|
||||
Col,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels, copy } from '../../helpers';
|
||||
import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers';
|
||||
import {
|
||||
IconSave,
|
||||
IconClose,
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
IconSetting,
|
||||
IconCode,
|
||||
IconGlobe,
|
||||
IconBolt,
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
@@ -80,6 +81,7 @@ const EditChannel = (props) => {
|
||||
const channelId = props.editingChannel.id;
|
||||
const isEdit = channelId !== undefined;
|
||||
const [loading, setLoading] = useState(isEdit);
|
||||
const isMobile = useIsMobile();
|
||||
const handleCancel = () => {
|
||||
props.handleClose();
|
||||
};
|
||||
@@ -100,10 +102,12 @@ const EditChannel = (props) => {
|
||||
priority: 0,
|
||||
weight: 0,
|
||||
tag: '',
|
||||
multi_key_mode: 'random',
|
||||
};
|
||||
const [batch, setBatch] = useState(false);
|
||||
const [multiToSingle, setMultiToSingle] = useState(false);
|
||||
const [multiKeyMode, setMultiKeyMode] = useState('random');
|
||||
const [autoBan, setAutoBan] = useState(true);
|
||||
// const [autoBan, setAutoBan] = useState(true);
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
const [originModelOptions, setOriginModelOptions] = useState([]);
|
||||
const [modelOptions, setModelOptions] = useState([]);
|
||||
@@ -114,6 +118,10 @@ const EditChannel = (props) => {
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const formApiRef = useRef(null);
|
||||
const [vertexKeys, setVertexKeys] = useState([]);
|
||||
const [vertexFileList, setVertexFileList] = useState([]);
|
||||
const vertexErroredNames = useRef(new Set()); // 避免重复报错
|
||||
const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
|
||||
const getInitValues = () => ({ ...originInputs });
|
||||
const handleInputChange = (name, value) => {
|
||||
if (formApiRef.current) {
|
||||
@@ -211,6 +219,19 @@ const EditChannel = (props) => {
|
||||
2,
|
||||
);
|
||||
}
|
||||
const chInfo = data.channel_info || {};
|
||||
const isMulti = chInfo.is_multi_key === true;
|
||||
setIsMultiKeyChannel(isMulti);
|
||||
if (isMulti) {
|
||||
setBatch(true);
|
||||
setMultiToSingle(true);
|
||||
const modeVal = chInfo.multi_key_mode || 'random';
|
||||
setMultiKeyMode(modeVal);
|
||||
data.multi_key_mode = modeVal;
|
||||
} else {
|
||||
setBatch(false);
|
||||
setMultiToSingle(false);
|
||||
}
|
||||
setInputs(data);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValues(data);
|
||||
@@ -238,9 +259,9 @@ const EditChannel = (props) => {
|
||||
let err = false;
|
||||
|
||||
if (isEdit) {
|
||||
// 如果是编辑模式,使用已有的channel id获取模型列表
|
||||
const res = await API.get('/api/channel/fetch_models/' + channelId);
|
||||
if (res.data && res.data.success) {
|
||||
// 如果是编辑模式,使用已有的 channelId 获取模型列表
|
||||
const res = await API.get('/api/channel/fetch_models/' + channelId, { skipErrorHandler: true });
|
||||
if (res && res.data && res.data.success) {
|
||||
models.push(...res.data.data);
|
||||
} else {
|
||||
err = true;
|
||||
@@ -252,13 +273,17 @@ const EditChannel = (props) => {
|
||||
err = true;
|
||||
} else {
|
||||
try {
|
||||
const res = await API.post('/api/channel/fetch_models', {
|
||||
base_url: inputs['base_url'],
|
||||
type: inputs['type'],
|
||||
key: inputs['key'],
|
||||
});
|
||||
const res = await API.post(
|
||||
'/api/channel/fetch_models',
|
||||
{
|
||||
base_url: inputs['base_url'],
|
||||
type: inputs['type'],
|
||||
key: inputs['key'],
|
||||
},
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
|
||||
if (res.data && res.data.success) {
|
||||
if (res && res.data && res.data.success) {
|
||||
models.push(...res.data.data);
|
||||
} else {
|
||||
err = true;
|
||||
@@ -324,14 +349,14 @@ const EditChannel = (props) => {
|
||||
useEffect(() => {
|
||||
const modelMap = new Map();
|
||||
|
||||
originModelOptions.forEach(option => {
|
||||
originModelOptions.forEach((option) => {
|
||||
const v = (option.value || '').trim();
|
||||
if (!modelMap.has(v)) {
|
||||
modelMap.set(v, option);
|
||||
}
|
||||
});
|
||||
|
||||
inputs.models.forEach(model => {
|
||||
inputs.models.forEach((model) => {
|
||||
const v = (model || '').trim();
|
||||
if (!modelMap.has(v)) {
|
||||
modelMap.set(v, {
|
||||
@@ -342,8 +367,29 @@ const EditChannel = (props) => {
|
||||
}
|
||||
});
|
||||
|
||||
setModelOptions(Array.from(modelMap.values()));
|
||||
}, [originModelOptions, inputs.models]);
|
||||
const categories = getModelCategories(t);
|
||||
const optionsWithIcon = Array.from(modelMap.values()).map((opt) => {
|
||||
const modelName = opt.value;
|
||||
let icon = null;
|
||||
for (const [key, category] of Object.entries(categories)) {
|
||||
if (key !== 'all' && category.filter({ model_name: modelName })) {
|
||||
icon = category.icon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...opt,
|
||||
label: (
|
||||
<span className="flex items-center gap-1">
|
||||
{icon}
|
||||
{modelName}
|
||||
</span>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
setModelOptions(optionsWithIcon);
|
||||
}, [originModelOptions, inputs.models, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels().then();
|
||||
@@ -377,10 +423,96 @@ const EditChannel = (props) => {
|
||||
}
|
||||
}, [props.visible, channelId]);
|
||||
|
||||
const handleVertexUploadChange = ({ fileList }) => {
|
||||
vertexErroredNames.current.clear();
|
||||
(async () => {
|
||||
let validFiles = [];
|
||||
let keys = [];
|
||||
const errorNames = [];
|
||||
for (const item of fileList) {
|
||||
const fileObj = item.fileInstance;
|
||||
if (!fileObj) continue;
|
||||
try {
|
||||
const txt = await fileObj.text();
|
||||
keys.push(JSON.parse(txt));
|
||||
validFiles.push(item);
|
||||
} catch (err) {
|
||||
if (!vertexErroredNames.current.has(item.name)) {
|
||||
errorNames.push(item.name);
|
||||
vertexErroredNames.current.add(item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非批量模式下只保留一个文件(最新选择的),避免重复叠加
|
||||
if (!batch && validFiles.length > 1) {
|
||||
validFiles = [validFiles[validFiles.length - 1]];
|
||||
keys = [keys[keys.length - 1]];
|
||||
}
|
||||
|
||||
setVertexKeys(keys);
|
||||
setVertexFileList(validFiles);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('vertex_files', validFiles);
|
||||
}
|
||||
setInputs((prev) => ({ ...prev, vertex_files: validFiles }));
|
||||
|
||||
if (errorNames.length > 0) {
|
||||
showError(t('以下文件解析失败,已忽略:{{list}}', { list: errorNames.join(', ') }));
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
|
||||
let localInputs = { ...formValues };
|
||||
|
||||
if (localInputs.type === 41) {
|
||||
let keys = vertexKeys;
|
||||
|
||||
// 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
|
||||
if (keys.length === 0 && vertexFileList.length > 0) {
|
||||
try {
|
||||
const parsed = await Promise.all(
|
||||
vertexFileList.map(async (item) => {
|
||||
const fileObj = item.fileInstance;
|
||||
if (!fileObj) return null;
|
||||
const txt = await fileObj.text();
|
||||
return JSON.parse(txt);
|
||||
})
|
||||
);
|
||||
keys = parsed.filter(Boolean);
|
||||
} catch (err) {
|
||||
showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建模式必须上传密钥;编辑模式可选
|
||||
if (keys.length === 0) {
|
||||
if (!isEdit) {
|
||||
showInfo(t('请上传密钥文件!'));
|
||||
return;
|
||||
} else {
|
||||
// 编辑模式且未上传新密钥,不修改 key
|
||||
delete localInputs.key;
|
||||
}
|
||||
} else {
|
||||
// 有新密钥,则覆盖
|
||||
if (batch) {
|
||||
localInputs.key = JSON.stringify(keys);
|
||||
} else {
|
||||
localInputs.key = JSON.stringify(keys[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是编辑模式且 key 为空字符串,避免提交空值覆盖旧密钥
|
||||
if (isEdit && (!localInputs.key || localInputs.key.trim() === '')) {
|
||||
delete localInputs.key;
|
||||
}
|
||||
delete localInputs.vertex_files;
|
||||
|
||||
if (!isEdit && (!localInputs.name || !localInputs.key)) {
|
||||
showInfo(t('请填写渠道名称和渠道密钥!'));
|
||||
return;
|
||||
@@ -406,13 +538,23 @@ const EditChannel = (props) => {
|
||||
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
|
||||
localInputs.models = localInputs.models.join(',');
|
||||
localInputs.group = (localInputs.groups || []).join(',');
|
||||
|
||||
let mode = 'single';
|
||||
if (batch) {
|
||||
mode = multiToSingle ? 'multi_to_single' : 'batch';
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
res = await API.put(`/api/channel/`, {
|
||||
...localInputs,
|
||||
id: parseInt(channelId),
|
||||
});
|
||||
} else {
|
||||
res = await API.post(`/api/channel/`, localInputs);
|
||||
res = await API.post(`/api/channel/`, {
|
||||
mode: mode,
|
||||
multi_key_mode: mode === 'multi_to_single' ? multiKeyMode : undefined,
|
||||
channel: localInputs,
|
||||
});
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@@ -465,11 +607,79 @@ const EditChannel = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const batchAllowed = !isEdit && inputs.type !== 41;
|
||||
const batchAllowed = !isEdit || isMultiKeyChannel;
|
||||
const batchExtra = batchAllowed ? (
|
||||
<Checkbox checked={batch} onChange={() => setBatch(!batch)}>{t('批量创建')}</Checkbox>
|
||||
<Space>
|
||||
<Checkbox
|
||||
disabled={isEdit}
|
||||
checked={batch}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
|
||||
if (!checked && vertexFileList.length > 1) {
|
||||
Modal.confirm({
|
||||
title: t('切换为单密钥模式'),
|
||||
content: t('将仅保留第一个密钥文件,其余文件将被移除,是否继续?'),
|
||||
onOk: () => {
|
||||
const firstFile = vertexFileList[0];
|
||||
const firstKey = vertexKeys[0] ? [vertexKeys[0]] : [];
|
||||
|
||||
setVertexFileList([firstFile]);
|
||||
setVertexKeys(firstKey);
|
||||
|
||||
formApiRef.current?.setValue('vertex_files', [firstFile]);
|
||||
setInputs((prev) => ({ ...prev, vertex_files: [firstFile] }));
|
||||
|
||||
setBatch(false);
|
||||
setMultiToSingle(false);
|
||||
setMultiKeyMode('random');
|
||||
},
|
||||
onCancel: () => {
|
||||
setBatch(true);
|
||||
},
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setBatch(checked);
|
||||
if (!checked) {
|
||||
setMultiToSingle(false);
|
||||
setMultiKeyMode('random');
|
||||
}
|
||||
}}
|
||||
>{t('批量创建')}</Checkbox>
|
||||
{batch && (
|
||||
<Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => {
|
||||
setMultiToSingle(prev => !prev);
|
||||
setInputs(prev => {
|
||||
const newInputs = { ...prev };
|
||||
if (!multiToSingle) {
|
||||
newInputs.multi_key_mode = multiKeyMode;
|
||||
} else {
|
||||
delete newInputs.multi_key_mode;
|
||||
}
|
||||
return newInputs;
|
||||
});
|
||||
}}>{t('密钥聚合模式')}</Checkbox>
|
||||
)}
|
||||
</Space>
|
||||
) : null;
|
||||
|
||||
const channelOptionList = useMemo(
|
||||
() =>
|
||||
CHANNEL_OPTIONS.map((opt) => ({
|
||||
...opt,
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
{getChannelIcon(opt.value)}
|
||||
{opt.label}
|
||||
</span>
|
||||
),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideSheet
|
||||
@@ -484,7 +694,7 @@ const EditChannel = (props) => {
|
||||
}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
visible={props.visible}
|
||||
width={isMobile() ? '100%' : 600}
|
||||
width={isMobile ? '100%' : 600}
|
||||
footer={
|
||||
<div className="flex justify-end bg-white">
|
||||
<Space>
|
||||
@@ -535,7 +745,7 @@ const EditChannel = (props) => {
|
||||
label={t('类型')}
|
||||
placeholder={t('请选择渠道类型')}
|
||||
rules={[{ required: true, message: t('请选择渠道类型') }]}
|
||||
optionList={CHANNEL_OPTIONS}
|
||||
optionList={channelOptionList}
|
||||
style={{ width: '100%' }}
|
||||
filter
|
||||
searchPosition='dropdown'
|
||||
@@ -553,56 +763,170 @@ const EditChannel = (props) => {
|
||||
/>
|
||||
|
||||
{batch ? (
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
placeholder={t('请输入密钥,一行一个')}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autosize={{ minRows: 6, maxRows: 6 }}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
extraText={batchExtra}
|
||||
/>
|
||||
inputs.type === 41 ? (
|
||||
<Form.Upload
|
||||
field='vertex_files'
|
||||
label={t('密钥文件 (.json)')}
|
||||
accept='.json'
|
||||
multiple
|
||||
draggable
|
||||
dragIcon={<IconBolt />}
|
||||
dragMainText={t('点击上传文件或拖拽文件到这里')}
|
||||
dragSubText={t('仅支持 JSON 文件,支持多文件')}
|
||||
style={{ marginTop: 10 }}
|
||||
uploadTrigger='custom'
|
||||
beforeUpload={() => false}
|
||||
onChange={handleVertexUploadChange}
|
||||
fileList={vertexFileList}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
|
||||
extraText={batchExtra}
|
||||
/>
|
||||
) : (
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
placeholder={t('请输入密钥,一行一个')}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autosize
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
extraText={batchExtra}
|
||||
showClear
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 41 ? (
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
placeholder={
|
||||
'{\n' +
|
||||
' "type": "service_account",\n' +
|
||||
' "project_id": "abc-bcd-123-456",\n' +
|
||||
' "private_key_id": "123xxxxx456",\n' +
|
||||
' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
|
||||
' "client_email": "xxx@developer.gserviceaccount.com",\n' +
|
||||
' "client_id": "111222333",\n' +
|
||||
' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
|
||||
' "token_uri": "https://oauth2.googleapis.com/token",\n' +
|
||||
' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
|
||||
' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
|
||||
' "universe_domain": "googleapis.com"\n' +
|
||||
'}'
|
||||
}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autosize={{ minRows: 10 }}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
<Form.Upload
|
||||
field='vertex_files'
|
||||
label={t('密钥文件 (.json)')}
|
||||
accept='.json'
|
||||
draggable
|
||||
dragIcon={<IconBolt />}
|
||||
dragMainText={t('点击上传文件或拖拽文件到这里')}
|
||||
dragSubText={t('仅支持 JSON 文件')}
|
||||
style={{ marginTop: 10 }}
|
||||
uploadTrigger='custom'
|
||||
beforeUpload={() => false}
|
||||
onChange={handleVertexUploadChange}
|
||||
fileList={vertexFileList}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
|
||||
extraText={batchExtra}
|
||||
/>
|
||||
) : (
|
||||
<Form.Input
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
label={isEdit ? t('密钥(编辑模式下,保存的密钥不会显示)') : t('密钥')}
|
||||
placeholder={t(type2secretPrompt(inputs.type))}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
extraText={batchExtra}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{batch && multiToSingle && (
|
||||
<>
|
||||
<Form.Select
|
||||
field='multi_key_mode'
|
||||
label={t('密钥聚合模式')}
|
||||
placeholder={t('请选择多密钥使用策略')}
|
||||
optionList={[
|
||||
{ label: t('随机'), value: 'random' },
|
||||
{ label: t('轮询'), value: 'polling' },
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
value={inputs.multi_key_mode || 'random'}
|
||||
onChange={(value) => {
|
||||
setMultiKeyMode(value);
|
||||
handleInputChange('multi_key_mode', value);
|
||||
}}
|
||||
/>
|
||||
{inputs.multi_key_mode === 'polling' && (
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t('轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能')}
|
||||
className='!rounded-lg mt-2'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{inputs.type === 18 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label={t('模型版本')}
|
||||
placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 41 && (
|
||||
<Form.TextArea
|
||||
field='other'
|
||||
label={t('部署地区')}
|
||||
placeholder={t(
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}'
|
||||
)}
|
||||
autosize
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
rules={[{ required: true, message: t('请填写部署地区') }]}
|
||||
extraText={
|
||||
<Text
|
||||
className="!text-semi-color-primary cursor-pointer"
|
||||
onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Text>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 21 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label={t('知识库 ID')}
|
||||
placeholder={'请输入知识库 ID,例如:123456'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 39 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label='Account ID'
|
||||
placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 49 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label={t('智能体ID')}
|
||||
placeholder={'请输入智能体ID,例如:7342866812345'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 1 && (
|
||||
<Form.Input
|
||||
field='openai_organization'
|
||||
label={t('组织')}
|
||||
placeholder={t('请输入组织org-xxx')}
|
||||
showClear
|
||||
helpText={t('组织,不填则为默认组织')}
|
||||
onChange={(value) => handleInputChange('openai_organization', value)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* API Configuration Card */}
|
||||
@@ -747,7 +1071,7 @@ const EditChannel = (props) => {
|
||||
<Form.Select
|
||||
field='models'
|
||||
label={t('模型')}
|
||||
placeholder={isEdit ? t('请选择该渠道所支持的模型') : t('创建后可在编辑渠道时获取上游模型列表')}
|
||||
placeholder={t('请选择该渠道所支持的模型')}
|
||||
rules={[{ required: true, message: t('请选择模型') }]}
|
||||
multiple
|
||||
filter
|
||||
@@ -763,11 +1087,9 @@ const EditChannel = (props) => {
|
||||
<Button size='small' type='secondary' onClick={() => handleInputChange('models', fullModels)}>
|
||||
{t('填入所有模型')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button size='small' type='tertiary' onClick={() => fetchUpstreamModelList('models')}>
|
||||
{t('获取模型列表')}
|
||||
</Button>
|
||||
)}
|
||||
<Button size='small' type='tertiary' onClick={() => fetchUpstreamModelList('models')}>
|
||||
{t('获取模型列表')}
|
||||
</Button>
|
||||
<Button size='small' type='warning' onClick={() => handleInputChange('models', [])}>
|
||||
{t('清除所有模型')}
|
||||
</Button>
|
||||
@@ -860,77 +1182,6 @@ const EditChannel = (props) => {
|
||||
onChange={(value) => handleInputChange('groups', value)}
|
||||
/>
|
||||
|
||||
{inputs.type === 18 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label={t('模型版本')}
|
||||
placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 41 && (
|
||||
<Form.TextArea
|
||||
field='other'
|
||||
label={t('部署地区')}
|
||||
placeholder={t(
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}'
|
||||
)}
|
||||
autosize={{ minRows: 2 }}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
extraText={
|
||||
<Text
|
||||
className="!text-semi-color-primary cursor-pointer"
|
||||
onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 21 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label={t('知识库 ID')}
|
||||
placeholder={'请输入知识库 ID,例如:123456'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 39 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label='Account ID'
|
||||
placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 49 && (
|
||||
<Form.Input
|
||||
field='other'
|
||||
label={t('智能体ID')}
|
||||
placeholder={'请输入智能体ID,例如:7342866812345'}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 1 && (
|
||||
<Form.Input
|
||||
field='openai_organization'
|
||||
label={t('组织')}
|
||||
placeholder={t('请输入组织org-xxx')}
|
||||
showClear
|
||||
helpText={t('组织,可选,不填则为默认组织')}
|
||||
onChange={(value) => handleInputChange('openai_organization', value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Input
|
||||
field='tag'
|
||||
label={t('渠道标签')}
|
||||
|
||||
@@ -3,7 +3,7 @@ import ChannelsTable from '../../components/table/ChannelsTable';
|
||||
|
||||
const File = () => {
|
||||
return (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<ChannelsTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ const chat2page = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<h3>正在加载,请稍候...</h3>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react';
|
||||
import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle, ExternalLink } from 'lucide-react';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
Spin,
|
||||
IconButton,
|
||||
Button,
|
||||
Modal,
|
||||
Avatar,
|
||||
Tabs,
|
||||
@@ -18,14 +18,14 @@ import {
|
||||
Timeline,
|
||||
Collapse,
|
||||
Progress,
|
||||
Divider
|
||||
Divider,
|
||||
Skeleton
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconMoneyExchangeStroked,
|
||||
IconHistogram,
|
||||
IconRotate,
|
||||
IconCoinMoneyStroked,
|
||||
IconTextStroked,
|
||||
IconPulse,
|
||||
@@ -33,15 +33,17 @@ import {
|
||||
IconTypograph,
|
||||
IconPieChart2Stroked,
|
||||
IconPlus,
|
||||
IconMinus
|
||||
IconMinus,
|
||||
IconSend
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
||||
import { VChart } from '@visactor/react-vchart';
|
||||
import {
|
||||
API,
|
||||
isAdmin,
|
||||
isMobile,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
timestamp2string,
|
||||
timestamp2string1,
|
||||
getQuotaWithUnit,
|
||||
@@ -50,9 +52,9 @@ import {
|
||||
renderQuota,
|
||||
modelToColor,
|
||||
copy,
|
||||
showSuccess,
|
||||
getRelativeTime
|
||||
} from '../../helpers';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -65,6 +67,7 @@ const Detail = (props) => {
|
||||
// ========== Hooks - Navigation & Translation ==========
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// ========== Hooks - Refs ==========
|
||||
const formRef = useRef();
|
||||
@@ -192,6 +195,7 @@ const Detail = (props) => {
|
||||
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(getDefaultTime());
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [greetingVisible, setGreetingVisible] = useState(false);
|
||||
const [quotaData, setQuotaData] = useState([]);
|
||||
const [consumeQuota, setConsumeQuota] = useState(0);
|
||||
const [consumeTokens, setConsumeTokens] = useState(0);
|
||||
@@ -449,7 +453,7 @@ const Detail = (props) => {
|
||||
// ========== Hooks - Memoized Values ==========
|
||||
const performanceMetrics = useMemo(() => {
|
||||
const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
|
||||
const avgRPM = (times / timeDiff).toFixed(3);
|
||||
const avgRPM = isNaN(times / timeDiff) ? '0' : (times / timeDiff).toFixed(3);
|
||||
const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3);
|
||||
|
||||
return { avgRPM, avgTPM, timeDiff };
|
||||
@@ -518,7 +522,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('当前余额'),
|
||||
value: renderQuota(userState?.user?.quota),
|
||||
icon: <IconMoneyExchangeStroked size="large" />,
|
||||
icon: <IconMoneyExchangeStroked />,
|
||||
avatarColor: 'blue',
|
||||
onClick: () => navigate('/console/topup'),
|
||||
trendData: [],
|
||||
@@ -527,7 +531,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('历史消耗'),
|
||||
value: renderQuota(userState?.user?.used_quota),
|
||||
icon: <IconHistogram size="large" />,
|
||||
icon: <IconHistogram />,
|
||||
avatarColor: 'purple',
|
||||
trendData: [],
|
||||
trendColor: '#8b5cf6'
|
||||
@@ -541,7 +545,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('请求次数'),
|
||||
value: userState.user?.request_count,
|
||||
icon: <IconRotate size="large" />,
|
||||
icon: <IconSend />,
|
||||
avatarColor: 'green',
|
||||
trendData: [],
|
||||
trendColor: '#10b981'
|
||||
@@ -549,7 +553,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('统计次数'),
|
||||
value: times,
|
||||
icon: <IconPulse size="large" />,
|
||||
icon: <IconPulse />,
|
||||
avatarColor: 'cyan',
|
||||
trendData: trendData.times,
|
||||
trendColor: '#06b6d4'
|
||||
@@ -563,7 +567,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('统计额度'),
|
||||
value: renderQuota(consumeQuota),
|
||||
icon: <IconCoinMoneyStroked size="large" />,
|
||||
icon: <IconCoinMoneyStroked />,
|
||||
avatarColor: 'yellow',
|
||||
trendData: trendData.consumeQuota,
|
||||
trendColor: '#f59e0b'
|
||||
@@ -571,7 +575,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('统计Tokens'),
|
||||
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
||||
icon: <IconTextStroked size="large" />,
|
||||
icon: <IconTextStroked />,
|
||||
avatarColor: 'pink',
|
||||
trendData: trendData.tokens,
|
||||
trendColor: '#ec4899'
|
||||
@@ -585,7 +589,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('平均RPM'),
|
||||
value: performanceMetrics.avgRPM,
|
||||
icon: <IconStopwatchStroked size="large" />,
|
||||
icon: <IconStopwatchStroked />,
|
||||
avatarColor: 'indigo',
|
||||
trendData: trendData.rpm,
|
||||
trendColor: '#6366f1'
|
||||
@@ -593,7 +597,7 @@ const Detail = (props) => {
|
||||
{
|
||||
title: t('平均TPM'),
|
||||
value: performanceMetrics.avgTPM,
|
||||
icon: <IconTypograph size="large" />,
|
||||
icon: <IconTypograph />,
|
||||
avatarColor: 'orange',
|
||||
trendData: trendData.tpm,
|
||||
trendColor: '#f97316'
|
||||
@@ -614,7 +618,7 @@ const Detail = (props) => {
|
||||
const handleSpeedTest = useCallback((apiUrl) => {
|
||||
const encodedUrl = encodeURIComponent(apiUrl);
|
||||
const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`;
|
||||
window.open(speedTestUrl, '_blank');
|
||||
window.open(speedTestUrl, '_blank', 'noopener,noreferrer');
|
||||
}, []);
|
||||
|
||||
const handleInputChange = useCallback((value, name) => {
|
||||
@@ -627,6 +631,7 @@ const Detail = (props) => {
|
||||
|
||||
const loadQuotaData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
@@ -654,7 +659,11 @@ const Detail = (props) => {
|
||||
showError(message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
const elapsed = Date.now() - startTime;
|
||||
const remainingTime = Math.max(0, 500 - elapsed);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, remainingTime);
|
||||
}
|
||||
}, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
|
||||
|
||||
@@ -746,6 +755,13 @@ const Detail = (props) => {
|
||||
return () => clearTimeout(timer);
|
||||
}, [uptimeData, activeUptimeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setGreetingVisible(true);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const getUserData = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -1104,16 +1120,23 @@ const Detail = (props) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 h-full mt-[64px]">
|
||||
<div className="bg-gray-50 h-full mt-[64px] px-2">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-2xl font-semibold text-gray-800">{getGreeting}</h2>
|
||||
<h2
|
||||
className="text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out"
|
||||
style={{ opacity: greetingVisible ? 1 : 0 }}
|
||||
>
|
||||
{getGreeting}
|
||||
</h2>
|
||||
<div className="flex gap-3">
|
||||
<IconButton
|
||||
<Button
|
||||
type='tertiary'
|
||||
icon={<IconSearch />}
|
||||
onClick={showSearchModal}
|
||||
className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`}
|
||||
/>
|
||||
<IconButton
|
||||
<Button
|
||||
type='tertiary'
|
||||
icon={<IconRefresh />}
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
@@ -1129,7 +1152,7 @@ const Detail = (props) => {
|
||||
onOk={handleSearchConfirm}
|
||||
onCancel={handleCloseModal}
|
||||
closeOnEsc={true}
|
||||
size={isMobile() ? 'full-width' : 'small'}
|
||||
size={isMobile ? 'full-width' : 'small'}
|
||||
centered
|
||||
>
|
||||
<Form ref={formRef} layout='vertical' className="w-full">
|
||||
@@ -1174,143 +1197,159 @@ const Detail = (props) => {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
<div className="mb-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{groupedStatsData.map((group, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
{...CARD_PROPS}
|
||||
className={`${group.color} border-0 !rounded-2xl w-full`}
|
||||
title={group.title}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{group.items.map((item, itemIdx) => (
|
||||
<div
|
||||
key={itemIdx}
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-3"
|
||||
size="small"
|
||||
color={item.avatarColor}
|
||||
>
|
||||
{item.icon}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">{item.title}</div>
|
||||
<div className="text-lg font-semibold">{item.value}</div>
|
||||
<div className="mb-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{groupedStatsData.map((group, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
{...CARD_PROPS}
|
||||
className={`${group.color} border-0 !rounded-2xl w-full`}
|
||||
title={group.title}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{group.items.map((item, itemIdx) => (
|
||||
<div
|
||||
key={itemIdx}
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-3"
|
||||
size="small"
|
||||
color={item.avatarColor}
|
||||
>
|
||||
{item.icon}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">{item.title}</div>
|
||||
<div className="text-lg font-semibold">
|
||||
<Skeleton
|
||||
loading={loading}
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Paragraph
|
||||
active
|
||||
rows={1}
|
||||
style={{ width: '65px', height: '24px', marginTop: '4px' }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{item.value}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
{item.trendData && item.trendData.length > 0 && (
|
||||
<div className="w-24 h-10">
|
||||
<VChart
|
||||
spec={getTrendSpec(item.trendData, item.trendColor)}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className={`grid grid-cols-1 gap-4 ${hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className={`shadow-sm !rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
|
||||
title={
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
|
||||
<div className={FLEX_CENTER_GAP2}>
|
||||
<PieChart size={16} />
|
||||
{t('模型数据分析')}
|
||||
{(loading || (item.trendData && item.trendData.length > 0)) && (
|
||||
<div className="w-24 h-10">
|
||||
<VChart
|
||||
spec={getTrendSpec(item.trendData, item.trendColor)}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tabs
|
||||
type="button"
|
||||
activeKey={activeChartTab}
|
||||
onChange={setActiveChartTab}
|
||||
>
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconHistogram />
|
||||
{t('消耗分布')}
|
||||
</span>
|
||||
} itemKey="1" />
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconPulse />
|
||||
{t('消耗趋势')}
|
||||
</span>
|
||||
} itemKey="2" />
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconPieChart2Stroked />
|
||||
{t('调用次数分布')}
|
||||
</span>
|
||||
} itemKey="3" />
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconHistogram />
|
||||
{t('调用次数排行')}
|
||||
</span>
|
||||
} itemKey="4" />
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ height: 400 }}>
|
||||
{activeChartTab === '1' && (
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
{activeChartTab === '2' && (
|
||||
<VChart
|
||||
spec={spec_model_line}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
{activeChartTab === '3' && (
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
{activeChartTab === '4' && (
|
||||
<VChart
|
||||
spec={spec_rank_bar}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasApiInfoPanel && (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className="bg-gray-50 border-0 !rounded-2xl"
|
||||
title={
|
||||
<div className={FLEX_CENTER_GAP2}>
|
||||
<Server size={16} />
|
||||
{t('API信息')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="card-content-container">
|
||||
<div
|
||||
ref={apiScrollRef}
|
||||
className="space-y-3 max-h-96 overflow-y-auto card-content-scroll"
|
||||
onScroll={handleApiScroll}
|
||||
>
|
||||
{apiInfoData.length > 0 ? (
|
||||
apiInfoData.map((api) => (
|
||||
<div className="mb-4">
|
||||
<div className={`grid grid-cols-1 gap-4 ${hasApiInfoPanel ? 'lg:grid-cols-4' : ''}`}>
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className={`shadow-sm !rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
|
||||
title={
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
|
||||
<div className={FLEX_CENTER_GAP2}>
|
||||
<PieChart size={16} />
|
||||
{t('模型数据分析')}
|
||||
</div>
|
||||
<Tabs
|
||||
type="button"
|
||||
activeKey={activeChartTab}
|
||||
onChange={setActiveChartTab}
|
||||
>
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconHistogram />
|
||||
{t('消耗分布')}
|
||||
</span>
|
||||
} itemKey="1" />
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconPulse />
|
||||
{t('消耗趋势')}
|
||||
</span>
|
||||
} itemKey="2" />
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconPieChart2Stroked />
|
||||
{t('调用次数分布')}
|
||||
</span>
|
||||
} itemKey="3" />
|
||||
<TabPane tab={
|
||||
<span>
|
||||
<IconHistogram />
|
||||
{t('调用次数排行')}
|
||||
</span>
|
||||
} itemKey="4" />
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div className="h-96 p-2">
|
||||
{activeChartTab === '1' && (
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
{activeChartTab === '2' && (
|
||||
<VChart
|
||||
spec={spec_model_line}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
{activeChartTab === '3' && (
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
{activeChartTab === '4' && (
|
||||
<VChart
|
||||
spec={spec_rank_bar}
|
||||
option={CHART_CONFIG}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{hasApiInfoPanel && (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className="bg-gray-50 border-0 !rounded-2xl"
|
||||
title={
|
||||
<div className={FLEX_CENTER_GAP2}>
|
||||
<Server size={16} />
|
||||
{t('API信息')}
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div className="card-content-container">
|
||||
<div
|
||||
ref={apiScrollRef}
|
||||
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
|
||||
onScroll={handleApiScroll}
|
||||
>
|
||||
{apiInfoData.length > 0 ? (
|
||||
apiInfoData.map((api) => (
|
||||
<>
|
||||
<div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
|
||||
<div className="flex-shrink-0 mr-3">
|
||||
<Avatar
|
||||
@@ -1321,18 +1360,32 @@ const Detail = (props) => {
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-900 mb-1 !font-bold flex items-center gap-2">
|
||||
<Tag
|
||||
prefixIcon={<Gauge size={12} />}
|
||||
size="small"
|
||||
color="white"
|
||||
shape='circle'
|
||||
onClick={() => handleSpeedTest(api.url)}
|
||||
className="cursor-pointer hover:opacity-80 text-xs"
|
||||
>
|
||||
{t('测速')}
|
||||
</Tag>
|
||||
{api.route}
|
||||
<div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 !font-bold break-all">
|
||||
{api.route}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 mt-1 lg:mt-0">
|
||||
<Tag
|
||||
prefixIcon={<Gauge size={12} />}
|
||||
size="small"
|
||||
color="white"
|
||||
shape='circle'
|
||||
onClick={() => handleSpeedTest(api.url)}
|
||||
className="cursor-pointer hover:opacity-80 text-xs"
|
||||
>
|
||||
{t('测速')}
|
||||
</Tag>
|
||||
<Tag
|
||||
prefixIcon={<ExternalLink size={12} />}
|
||||
size="small"
|
||||
color="white"
|
||||
shape='circle'
|
||||
onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
|
||||
className="cursor-pointer hover:opacity-80 text-xs"
|
||||
>
|
||||
{t('跳转')}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
|
||||
@@ -1345,30 +1398,33 @@ const Detail = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Empty
|
||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||
title={t('暂无API信息')}
|
||||
description={t('请联系管理员在系统设置中配置API信息')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="card-content-fade-indicator"
|
||||
style={{ opacity: showApiScrollHint ? 1 : 0 }}
|
||||
/>
|
||||
<Divider />
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Empty
|
||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||
title={t('暂无API信息')}
|
||||
description={t('请联系管理员在系统设置中配置API信息')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="card-content-fade-indicator"
|
||||
style={{ opacity: showApiScrollHint ? 1 : 0 }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统公告和常见问答卡片 */}
|
||||
{hasInfoPanels && (
|
||||
{/* 系统公告和常见问答卡片 */}
|
||||
{
|
||||
hasInfoPanels && (
|
||||
<div className="mb-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
{/* 公告卡片 */}
|
||||
@@ -1381,7 +1437,7 @@ const Detail = (props) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell size={16} />
|
||||
{t('系统公告')}
|
||||
<Tag size="small" color="grey" shape="circle">
|
||||
<Tag color="white" shape="circle">
|
||||
{t('显示最新20条')}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -1405,6 +1461,7 @@ const Detail = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div className="card-content-container">
|
||||
<div
|
||||
@@ -1513,19 +1570,20 @@ const Detail = (props) => {
|
||||
{uptimeEnabled && (
|
||||
<Card
|
||||
{...CARD_PROPS}
|
||||
className="shadow-sm !rounded-2xl lg:col-span-1 flex flex-col"
|
||||
className="shadow-sm !rounded-2xl lg:col-span-1"
|
||||
title={
|
||||
<div className="flex items-center justify-between w-full gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gauge size={16} />
|
||||
{t('服务可用性')}
|
||||
</div>
|
||||
<IconButton
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
onClick={loadUptimeData}
|
||||
loading={uptimeLoading}
|
||||
size="small"
|
||||
theme="borderless"
|
||||
type='tertiary'
|
||||
className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -1533,7 +1591,7 @@ const Detail = (props) => {
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
{/* 内容区域 */}
|
||||
<div className="flex-1 relative">
|
||||
<div className="relative">
|
||||
<Spin spinning={uptimeLoading}>
|
||||
{uptimeData.length > 0 ? (
|
||||
uptimeData.length === 1 ? (
|
||||
@@ -1613,9 +1671,9 @@ const Detail = (props) => {
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
{/* 固定在底部的图例 */}
|
||||
{/* 图例 */}
|
||||
{uptimeData.length > 0 && (
|
||||
<div className="p-3 mt-auto bg-gray-50 rounded-b-2xl">
|
||||
<div className="p-3 bg-gray-50 rounded-b-2xl">
|
||||
<div className="flex flex-wrap gap-3 text-xs justify-center">
|
||||
{uptimeLegendData.map((legend, index) => (
|
||||
<div key={index} className="flex items-center gap-1">
|
||||
@@ -1633,9 +1691,9 @@ const Detail = (props) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Typography, Tag, Input, ScrollList, ScrollItem } from '@douyinfe/semi-ui';
|
||||
import { API, showError, isMobile, copy, showSuccess } from '../../helpers';
|
||||
import { API, showError, copy, showSuccess } from '../../helpers';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import { API_ENDPOINTS } from '../../constants/common.constant';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
@@ -18,6 +19,7 @@ const Home = () => {
|
||||
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
|
||||
const [homePageContent, setHomePageContent] = useState('');
|
||||
const [noticeVisible, setNoticeVisible] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
||||
const docsLink = statusState?.status?.docs_link || '';
|
||||
const serverAddress = statusState?.status?.server_address || window.location.origin;
|
||||
@@ -98,7 +100,7 @@ const Home = () => {
|
||||
<NoticeModal
|
||||
visible={noticeVisible}
|
||||
onClose={() => setNoticeVisible(false)}
|
||||
isMobile={isMobile()}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
{homePageContentLoaded && homePageContent === '' ? (
|
||||
<div className="w-full overflow-x-hidden">
|
||||
@@ -133,7 +135,7 @@ const Home = () => {
|
||||
readonly
|
||||
value={serverAddress}
|
||||
className="flex-1 !rounded-full"
|
||||
size={isMobile() ? 'default' : 'large'}
|
||||
size={isMobile ? 'default' : 'large'}
|
||||
suffix={
|
||||
<div className="flex items-center gap-2">
|
||||
<ScrollList bodyHeight={32} style={{ border: 'unset', boxShadow: 'unset' }}>
|
||||
@@ -160,13 +162,13 @@ const Home = () => {
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex flex-row gap-4 justify-center items-center">
|
||||
<Link to="/console">
|
||||
<Button theme="solid" type="primary" size={isMobile() ? "default" : "large"} className="!rounded-3xl px-8 py-2" icon={<IconPlay />}>
|
||||
<Button theme="solid" type="primary" size={isMobile ? "default" : "large"} className="!rounded-3xl px-8 py-2" icon={<IconPlay />}>
|
||||
{t('获取密钥')}
|
||||
</Button>
|
||||
</Link>
|
||||
{isDemoSiteMode && statusState?.status?.version ? (
|
||||
<Button
|
||||
size={isMobile() ? "default" : "large"}
|
||||
size={isMobile ? "default" : "large"}
|
||||
className="flex items-center !rounded-3xl px-6 py-2"
|
||||
icon={<IconGithubLogo />}
|
||||
onClick={() => window.open('https://github.com/QuantumNous/new-api', '_blank')}
|
||||
@@ -176,7 +178,7 @@ const Home = () => {
|
||||
) : (
|
||||
docsLink && (
|
||||
<Button
|
||||
size={isMobile() ? "default" : "large"}
|
||||
size={isMobile ? "default" : "large"}
|
||||
className="flex items-center !rounded-3xl px-6 py-2"
|
||||
icon={<IconFile />}
|
||||
onClick={() => window.open(docsLink, '_blank')}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import LogsTable from '../../components/table/LogsTable';
|
||||
|
||||
const Token = () => (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<LogsTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import MjLogsTable from '../../components/table/MjLogsTable';
|
||||
|
||||
const Midjourney = () => (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<MjLogsTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Layout, Toast, Modal } from '@douyinfe/semi-ui';
|
||||
|
||||
// Context
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
|
||||
// hooks
|
||||
import { usePlaygroundState } from '../../hooks/usePlaygroundState.js';
|
||||
@@ -59,7 +59,8 @@ const generateAvatarDataUrl = (username) => {
|
||||
const Playground = () => {
|
||||
const { t } = useTranslation();
|
||||
const [userState] = useContext(UserContext);
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const isMobile = useIsMobile();
|
||||
const styleState = { isMobile };
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const state = usePlaygroundState();
|
||||
@@ -321,19 +322,7 @@ const Playground = () => {
|
||||
}
|
||||
}, [searchParams, t]);
|
||||
|
||||
// 处理窗口大小变化
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const mobile = window.innerWidth < 768;
|
||||
if (styleState.isMobile !== mobile) {
|
||||
styleDispatch(styleActions.setMobile(mobile));
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [styleState.isMobile, styleDispatch]);
|
||||
// Playground 组件无需再监听窗口变化,isMobile 由 useIsMobile Hook 自动更新
|
||||
|
||||
// 构建预览payload
|
||||
useEffect(() => {
|
||||
@@ -365,26 +354,26 @@ const Playground = () => {
|
||||
return (
|
||||
<div className="h-full bg-gray-50 mt-[64px]">
|
||||
<Layout style={{ height: '100%', background: 'transparent' }} className="flex flex-col md:flex-row">
|
||||
{(showSettings || !styleState.isMobile) && (
|
||||
{(showSettings || !isMobile) && (
|
||||
<Layout.Sider
|
||||
style={{
|
||||
background: 'transparent',
|
||||
borderRight: 'none',
|
||||
flexShrink: 0,
|
||||
minWidth: styleState.isMobile ? '100%' : 320,
|
||||
maxWidth: styleState.isMobile ? '100%' : 320,
|
||||
height: styleState.isMobile ? 'auto' : 'calc(100vh - 66px)',
|
||||
minWidth: isMobile ? '100%' : 320,
|
||||
maxWidth: isMobile ? '100%' : 320,
|
||||
height: isMobile ? 'auto' : 'calc(100vh - 66px)',
|
||||
overflow: 'auto',
|
||||
position: styleState.isMobile ? 'fixed' : 'relative',
|
||||
zIndex: styleState.isMobile ? 1000 : 1,
|
||||
position: isMobile ? 'fixed' : 'relative',
|
||||
zIndex: isMobile ? 1000 : 1,
|
||||
width: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
width={styleState.isMobile ? '100%' : 320}
|
||||
className={styleState.isMobile ? 'bg-white shadow-lg' : ''}
|
||||
width={isMobile ? '100%' : 320}
|
||||
className={isMobile ? 'bg-white shadow-lg' : ''}
|
||||
>
|
||||
<OptimizedSettingsPanel
|
||||
inputs={inputs}
|
||||
@@ -432,7 +421,7 @@ const Playground = () => {
|
||||
</div>
|
||||
|
||||
{/* 调试面板 - 桌面端 */}
|
||||
{showDebugPanel && !styleState.isMobile && (
|
||||
{showDebugPanel && !isMobile && (
|
||||
<div className="w-96 flex-shrink-0 h-full">
|
||||
<OptimizedDebugPanel
|
||||
debugData={debugData}
|
||||
@@ -446,7 +435,7 @@ const Playground = () => {
|
||||
</div>
|
||||
|
||||
{/* 调试面板 - 移动端覆盖层 */}
|
||||
{showDebugPanel && styleState.isMobile && (
|
||||
{showDebugPanel && isMobile && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import ModelPricing from '../../components/table/ModelPricing.js';
|
||||
|
||||
const Pricing = () => (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<ModelPricing />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
downloadTextAsFile,
|
||||
isMobile,
|
||||
showError,
|
||||
showSuccess,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
} from '../../helpers';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
@@ -36,6 +36,7 @@ const EditRedemption = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = props.editingRedemption.id !== undefined;
|
||||
const [loading, setLoading] = useState(isEdit);
|
||||
const isMobile = useIsMobile();
|
||||
const formApiRef = useRef(null);
|
||||
|
||||
const getInitValues = () => ({
|
||||
@@ -155,7 +156,7 @@ const EditRedemption = (props) => {
|
||||
}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
visible={props.visiable}
|
||||
width={isMobile() ? '100%' : 600}
|
||||
width={isMobile ? '100%' : 600}
|
||||
footer={
|
||||
<div className="flex justify-end bg-white">
|
||||
<Space>
|
||||
|
||||
@@ -3,7 +3,7 @@ import RedemptionsTable from '../../components/table/RedemptionsTable';
|
||||
|
||||
const Redemption = () => {
|
||||
return (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<RedemptionsTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -65,6 +65,7 @@ export default function SettingsGeneralPayment(props) {
|
||||
label={t('服务器地址')}
|
||||
placeholder={'https://yourdomain.com'}
|
||||
style={{ width: '100%' }}
|
||||
extraText={t('该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置')}
|
||||
/>
|
||||
<Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
|
||||
</Form.Section>
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
} from 'lucide-react';
|
||||
import { API, showError, showSuccess, showWarning, stringToColor, isMobile } from '../../../helpers';
|
||||
import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers';
|
||||
import { useIsMobile } from '../../../hooks/useIsMobile.js';
|
||||
import { DEFAULT_ENDPOINT } from '../../../constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
import ChannelSelectorModal from '../../../components/settings/ChannelSelectorModal';
|
||||
|
||||
function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
|
||||
const isMobile = useIsMobile();
|
||||
const columns = [
|
||||
{ title: t('渠道'), dataIndex: 'channel' },
|
||||
{ title: t('模型'), dataIndex: 'model' },
|
||||
@@ -49,7 +51,7 @@ function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
size={isMobile() ? 'full-width' : 'large'}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
>
|
||||
<Table columns={columns} dataSource={items} pagination={false} size="small" />
|
||||
</Modal>
|
||||
@@ -61,6 +63,7 @@ export default function UpstreamRatioSync(props) {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [syncLoading, setSyncLoading] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// 渠道选择相关
|
||||
const [allChannels, setAllChannels] = useState([]);
|
||||
|
||||
@@ -150,7 +150,7 @@ const Setting = () => {
|
||||
}
|
||||
}, [location.search]);
|
||||
return (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Tabs
|
||||
|
||||
@@ -183,7 +183,7 @@ const Setup = () => {
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{t('数据库警告')}</span>
|
||||
<Tag color='orange' size='small' className="ml-2 !rounded-full">
|
||||
<Tag color='orange' shape='circle' className="ml-2">
|
||||
SQLite
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -222,7 +222,7 @@ const Setup = () => {
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{t('数据库信息')}</span>
|
||||
<Tag color='blue' size='small' className="ml-2 !rounded-full">
|
||||
<Tag color='blue' shape='circle' className="ml-2">
|
||||
MySQL
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -256,7 +256,7 @@ const Setup = () => {
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{t('数据库信息')}</span>
|
||||
<Tag color='green' size='small' className="ml-2 !rounded-full">
|
||||
<Tag color='green' shape='circle' className="ml-2">
|
||||
PostgreSQL
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -425,7 +425,7 @@ const Setup = () => {
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 mb-1">{t('对外运营模式')}</div>
|
||||
<div className="text-sm text-gray-500">{t('适用于为多个用户提供服务的场景')}</div>
|
||||
<Tag color='blue' size='small' className="!rounded-full mt-2">
|
||||
<Tag color='blue' shape='circle' className="mt-2">
|
||||
{t('默认模式')}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -443,7 +443,7 @@ const Setup = () => {
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 mb-1">{t('自用模式')}</div>
|
||||
<div className="text-sm text-gray-500">{t('适用于个人使用的场景,不需要设置模型价格')}</div>
|
||||
<Tag color='green' size='small' className="!rounded-full mt-2">
|
||||
<Tag color='green' shape='circle' className="mt-2">
|
||||
{t('无需计费')}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -461,7 +461,7 @@ const Setup = () => {
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 mb-1">{t('演示站点模式')}</div>
|
||||
<div className="text-sm text-gray-500">{t('适用于展示系统功能的场景,提供基础功能演示')}</div>
|
||||
<Tag color='purple' size='small' className="!rounded-full mt-2">
|
||||
<Tag color='purple' shape='circle' className="mt-2">
|
||||
{t('演示体验')}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -522,8 +522,8 @@ const Setup = () => {
|
||||
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
|
||||
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
|
||||
<div className="mt-3">
|
||||
<Tag color='blue' className="!rounded-full mr-2">{t('计费模式')}</Tag>
|
||||
<Tag color='blue' className="!rounded-full">{t('多用户支持')}</Tag>
|
||||
<Tag color='blue' shape='circle' className="mr-2">{t('计费模式')}</Tag>
|
||||
<Tag color='blue' shape='circle'>{t('多用户支持')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -542,8 +542,8 @@ const Setup = () => {
|
||||
<p>{t('适用于个人使用的场景。')}</p>
|
||||
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
|
||||
<div className="mt-3">
|
||||
<Tag color='green' className="!rounded-full mr-2">{t('无需计费')}</Tag>
|
||||
<Tag color='green' className="!rounded-full">{t('个人使用')}</Tag>
|
||||
<Tag color='green' shape='circle' className="mr-2">{t('无需计费')}</Tag>
|
||||
<Tag color='green' shape='circle'>{t('个人使用')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -562,8 +562,8 @@ const Setup = () => {
|
||||
<p>{t('适用于展示系统功能的场景。')}</p>
|
||||
<p>{t('提供基础功能演示,方便用户了解系统特性。')}</p>
|
||||
<div className="mt-3">
|
||||
<Tag color='purple' className="!rounded-full mr-2">{t('功能演示')}</Tag>
|
||||
<Tag color='purple' className="!rounded-full">{t('体验试用')}</Tag>
|
||||
<Tag color='purple' shape='circle' className="mr-2">{t('功能演示')}</Tag>
|
||||
<Tag color='purple' shape='circle'>{t('体验试用')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import TaskLogsTable from '../../components/table/TaskLogsTable.js';
|
||||
|
||||
const Task = () => (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<TaskLogsTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useEffect, useState, useContext, useRef } from 'react';
|
||||
import {
|
||||
API,
|
||||
isMobile,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
renderGroupOption,
|
||||
renderQuotaWithPrompt,
|
||||
getModelCategories,
|
||||
} from '../../helpers';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import {
|
||||
Button,
|
||||
SideSheet,
|
||||
@@ -37,6 +38,7 @@ const EditToken = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const formApiRef = useRef(null);
|
||||
const [models, setModels] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
@@ -78,10 +80,25 @@ const EditToken = (props) => {
|
||||
let res = await API.get(`/api/user/models`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let localModelOptions = data.map((model) => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}));
|
||||
const categories = getModelCategories(t);
|
||||
let localModelOptions = data.map((model) => {
|
||||
let icon = null;
|
||||
for (const [key, category] of Object.entries(categories)) {
|
||||
if (key !== 'all' && category.filter({ model_name: model })) {
|
||||
icon = category.icon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: (
|
||||
<span className="flex items-center gap-1">
|
||||
{icon}
|
||||
{model}
|
||||
</span>
|
||||
),
|
||||
value: model,
|
||||
};
|
||||
});
|
||||
setModels(localModelOptions);
|
||||
} else {
|
||||
showError(t(message));
|
||||
@@ -261,7 +278,7 @@ const EditToken = (props) => {
|
||||
}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
visible={props.visiable}
|
||||
width={isMobile() ? '100%' : 600}
|
||||
width={isMobile ? '100%' : 600}
|
||||
footer={
|
||||
<div className='flex justify-end bg-white'>
|
||||
<Space>
|
||||
@@ -345,7 +362,23 @@ const EditToken = (props) => {
|
||||
label={t('过期时间')}
|
||||
type='dateTime'
|
||||
placeholder={t('请选择过期时间')}
|
||||
rules={[{ required: true, message: t('请选择过期时间') }]}
|
||||
rules={[
|
||||
{ required: true, message: t('请选择过期时间') },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
// 允许 -1 表示永不过期,也允许空值在必填校验时被拦截
|
||||
if (value === -1 || !value) return Promise.resolve();
|
||||
const time = Date.parse(value);
|
||||
if (isNaN(time)) {
|
||||
return Promise.reject(t('过期时间格式错误!'));
|
||||
}
|
||||
if (time <= Date.now()) {
|
||||
return Promise.reject(t('过期时间不能早于当前时间!'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
showClear
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
@@ -453,6 +486,20 @@ const EditToken = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Form.Select
|
||||
field='model_limits'
|
||||
label={t('模型限制列表')}
|
||||
placeholder={t('请选择该令牌支持的模型,留空支持所有模型')}
|
||||
multiple
|
||||
optionList={models}
|
||||
extraText={t('非必要,不建议启用模型限制')}
|
||||
filter
|
||||
searchPosition='dropdown'
|
||||
showClear
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.TextArea
|
||||
field='allow_ips'
|
||||
@@ -465,19 +512,6 @@ const EditToken = (props) => {
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Select
|
||||
field='model_limits'
|
||||
label={t('模型限制列表')}
|
||||
placeholder={t('请选择该令牌支持的模型,留空支持所有模型')}
|
||||
multiple
|
||||
optionList={models}
|
||||
maxTagCount={3}
|
||||
extraText={t('非必要,不建议启用模型限制')}
|
||||
showClear
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import TokensTable from '../../components/table/TokensTable';
|
||||
|
||||
const Token = () => {
|
||||
return (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<TokensTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { API, isMobile, showError, showSuccess } from '../../helpers';
|
||||
import { API, showError, showSuccess } from '../../helpers';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import {
|
||||
Button,
|
||||
SideSheet,
|
||||
@@ -26,6 +27,7 @@ const AddUser = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const formApiRef = useRef(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const getInitValues = () => ({
|
||||
username: '',
|
||||
@@ -67,7 +69,7 @@ const AddUser = (props) => {
|
||||
}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
visible={props.visible}
|
||||
width={isMobile() ? '100%' : 600}
|
||||
width={isMobile ? '100%' : 600}
|
||||
footer={
|
||||
<div className="flex justify-end bg-white">
|
||||
<Space>
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
isMobile,
|
||||
showError,
|
||||
showSuccess,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
} from '../../helpers';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile.js';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
@@ -41,6 +41,7 @@ const EditUser = (props) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [addQuotaModalOpen, setIsModalOpen] = useState(false);
|
||||
const [addQuotaLocal, setAddQuotaLocal] = useState('');
|
||||
const isMobile = useIsMobile();
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const formApiRef = useRef(null);
|
||||
|
||||
@@ -137,7 +138,7 @@ const EditUser = (props) => {
|
||||
}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
visible={props.visible}
|
||||
width={isMobile() ? '100%' : 600}
|
||||
width={isMobile ? '100%' : 600}
|
||||
footer={
|
||||
<div className='flex justify-end bg-white'>
|
||||
<Space>
|
||||
|
||||
@@ -3,7 +3,7 @@ import UsersTable from '../../components/table/UsersTable';
|
||||
|
||||
const User = () => {
|
||||
return (
|
||||
<div className="mt-[64px]">
|
||||
<div className="mt-[64px] px-2">
|
||||
<UsersTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user