Merge branch 'alpha' into fix-balance-unit-sync
This commit is contained in:
@@ -32,7 +32,6 @@ import OIDCIcon from '../common/logo/OIDCIcon.js';
|
||||
import WeChatIcon from '../common/logo/WeChatIcon.js';
|
||||
import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '/example.png';
|
||||
|
||||
const LoginForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -266,7 +265,7 @@ const LoginForm = () => {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="flex items-center justify-center mb-6 gap-2">
|
||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
||||
<Title heading={3} className='!text-white'>{systemName}</Title>
|
||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
|
||||
@@ -500,19 +499,8 @@ const LoginForm = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
{/* 背景图片容器 - 放大并保持居中 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
style={{
|
||||
backgroundImage: `url(${Background})`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* 半透明遮罩层 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
|
||||
|
||||
<div className="w-full max-w-sm relative z-10">
|
||||
<div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-sm">
|
||||
{showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
||||
? renderEmailLoginForm()
|
||||
: renderOAuthOptions()}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
|
||||
import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '/example.png';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -79,24 +78,13 @@ const PasswordResetConfirm = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
{/* 背景图片容器 - 放大并保持居中 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
style={{
|
||||
backgroundImage: `url(${Background})`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* 半透明遮罩层 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
|
||||
|
||||
<div className="w-full max-w-sm relative z-10">
|
||||
<div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="flex items-center justify-center mb-6 gap-2">
|
||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
||||
<Title heading={3} className='!text-white'>{systemName}</Title>
|
||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconMail } from '@douyinfe/semi-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '/example.png';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -79,24 +78,13 @@ const PasswordResetForm = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
{/* 背景图片容器 - 放大并保持居中 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
style={{
|
||||
backgroundImage: `url(${Background})`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* 半透明遮罩层 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
|
||||
|
||||
<div className="w-full max-w-sm relative z-10">
|
||||
<div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="flex items-center justify-center mb-6 gap-2">
|
||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
||||
<Title heading={3} className='!text-white'>{systemName}</Title>
|
||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
|
||||
|
||||
@@ -33,7 +33,6 @@ import WeChatIcon from '../common/logo/WeChatIcon.js';
|
||||
import TelegramLoginButton from 'react-telegram-login/src';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '/example.png';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -272,7 +271,7 @@ const RegisterForm = () => {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="flex items-center justify-center mb-6 gap-2">
|
||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
||||
<Title heading={3} className='!text-white'>{systemName}</Title>
|
||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
|
||||
@@ -379,7 +378,7 @@ const RegisterForm = () => {
|
||||
<div className="w-full max-w-md">
|
||||
<div className="flex items-center justify-center mb-6 gap-2">
|
||||
<img src={logo} alt="Logo" className="h-10 rounded-full" />
|
||||
<Title heading={3} className='!text-white'>{systemName}</Title>
|
||||
<Title heading={3} className='!text-gray-800'>{systemName}</Title>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
|
||||
@@ -542,17 +541,8 @@ const RegisterForm = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
style={{
|
||||
backgroundImage: `url(${Background})`
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
|
||||
|
||||
<div className="w-full max-w-sm relative z-10">
|
||||
<div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-sm">
|
||||
{showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
|
||||
? renderEmailRegisterForm()
|
||||
: renderOAuthOptions()}
|
||||
|
||||
57
web/src/components/settings/DashboardSetting.js
Normal file
57
web/src/components/settings/DashboardSetting.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import { API, showError } from '../../helpers';
|
||||
import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
|
||||
|
||||
const DashboardSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
ApiInfo: '',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key in inputs) {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
});
|
||||
setInputs(newInputs);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
async function onRefresh() {
|
||||
try {
|
||||
setLoading(true);
|
||||
await getOptions();
|
||||
} catch (error) {
|
||||
showError('刷新失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onRefresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading} size='large'>
|
||||
{/* API信息管理 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsAPIInfo options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSetting;
|
||||
@@ -104,6 +104,33 @@ const PersonalSetting = () => {
|
||||
});
|
||||
const [modelsLoading, setModelsLoading] = useState(true);
|
||||
const [showWebhookDocs, setShowWebhookDocs] = useState(true);
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
// 检测暗色模式
|
||||
useEffect(() => {
|
||||
const checkDarkMode = () => {
|
||||
const isDark = document.documentElement.classList.contains('dark') ||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setIsDarkMode(isDark);
|
||||
};
|
||||
|
||||
checkDarkMode();
|
||||
|
||||
// 监听主题变化
|
||||
const observer = new MutationObserver(checkDarkMode);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addListener(checkDarkMode);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
mediaQuery.removeListener(checkDarkMode);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
@@ -384,107 +411,81 @@ const PersonalSetting = () => {
|
||||
<Card className="!rounded-2xl shadow-lg border-0">
|
||||
{/* 顶部用户信息区域 */}
|
||||
<Card
|
||||
className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden"
|
||||
className="!rounded-2xl !border-0 !shadow-lg overflow-hidden"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 25%, #a855f7 50%, #c084fc 75%, #d8b4fe 100%)',
|
||||
background: isDarkMode
|
||||
? 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)'
|
||||
: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
||||
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-3 rounded-full"></div>
|
||||
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div>
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-slate-400 dark:bg-slate-500 opacity-5 rounded-full"></div>
|
||||
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-slate-300 dark:bg-slate-400 opacity-8 rounded-full"></div>
|
||||
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-slate-400 dark:bg-slate-500 opacity-6 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative p-4 sm:p-6 md:p-8" style={{ color: 'white' }}>
|
||||
<div className="relative p-4 sm:p-6 md:p-8 text-gray-600 dark:text-gray-300">
|
||||
<div className="flex justify-between items-start mb-4 sm:mb-6">
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<Avatar
|
||||
size='large'
|
||||
color={stringToColor(getUsername())}
|
||||
border={{ motion: true }}
|
||||
contentMotion={true}
|
||||
className="mr-3 sm:mr-4 shadow-lg flex-shrink-0"
|
||||
className="mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400"
|
||||
>
|
||||
{getAvatarText()}
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base sm:text-lg font-semibold truncate" style={{ color: 'white' }}>
|
||||
<div className="text-base sm:text-lg font-semibold truncate text-gray-800 dark:text-gray-100">
|
||||
{getUsername()}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap gap-1 sm:gap-2">
|
||||
{isRoot() ? (
|
||||
<Tag
|
||||
color='red'
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
color: '#dc2626',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
className="!rounded-full"
|
||||
className="!rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-600"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
{t('超级管理员')}
|
||||
</Tag>
|
||||
) : isAdmin() ? (
|
||||
<Tag
|
||||
color='orange'
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
color: '#ea580c',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
className="!rounded-full"
|
||||
className="!rounded-full bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-600"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
{t('管理员')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
color: '#2563eb',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
className="!rounded-full"
|
||||
className="!rounded-full bg-slate-50 dark:bg-slate-700 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-600"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
{t('普通用户')}
|
||||
</Tag>
|
||||
)}
|
||||
<Tag
|
||||
color='green'
|
||||
size='small'
|
||||
className="!rounded-full"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
color: '#16a34a',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
className="!rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-600"
|
||||
style={{ fontWeight: '500' }}
|
||||
>
|
||||
ID: {userState?.user?.id}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-lg flex-shrink-0 ml-2"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${stringToColor(getUsername())} 0%, #f59e0b 100%)`
|
||||
}}
|
||||
>
|
||||
<IconUser size="default" style={{ color: 'white' }} />
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-lg flex items-center justify-center shadow-md flex-shrink-0 ml-2 bg-slate-400 dark:bg-slate-500">
|
||||
<IconUser size="default" className="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<div className="text-xs sm:text-sm mb-1 sm:mb-2" style={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
<div className="text-xs sm:text-sm mb-1 sm:mb-2 text-gray-500 dark:text-gray-400">
|
||||
{t('当前余额')}
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide" style={{ color: 'white' }}>
|
||||
<div className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-wide text-gray-900 dark:text-gray-100">
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -492,33 +493,33 @@ const PersonalSetting = () => {
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-end">
|
||||
<div className="grid grid-cols-3 gap-2 sm:flex sm:space-x-6 lg:space-x-8 mb-3 sm:mb-0">
|
||||
<div className="text-center sm:text-left">
|
||||
<div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{t('历史消耗')}
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}>
|
||||
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center sm:text-left">
|
||||
<div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{t('请求次数')}
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}>
|
||||
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
|
||||
{userState.user?.request_count || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center sm:text-left">
|
||||
<div className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{t('用户分组')}
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm font-medium truncate" style={{ color: 'white' }}>
|
||||
<div className="text-xs sm:text-sm font-medium truncate text-gray-600 dark:text-gray-300">
|
||||
{userState?.user?.group || t('默认')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-yellow-400 via-orange-400 to-red-400" style={{ opacity: 0.6 }}></div>
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"></div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -537,10 +538,10 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="gap-6 py-4">
|
||||
{/* 可用模型部分 */}
|
||||
<div className="bg-gray-50 rounded-xl">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-3">
|
||||
<Settings size={20} className="text-purple-500" />
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<Settings size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title>
|
||||
@@ -629,7 +630,7 @@ const PersonalSetting = () => {
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-3">
|
||||
<div className="bg-white dark:bg-gray-700 rounded-lg p-3">
|
||||
{(() => {
|
||||
// 根据当前选中的分类过滤模型
|
||||
const categories = getModelCategories(t);
|
||||
@@ -736,9 +737,9 @@ const PersonalSetting = () => {
|
||||
shadows='hover'
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center mr-3">
|
||||
<IconMail size="default" className="text-red-500" />
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<IconMail size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('邮箱')}</div>
|
||||
@@ -771,8 +772,8 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-3">
|
||||
<SiWechat size={20} className="text-green-500" />
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('微信')}</div>
|
||||
@@ -808,8 +809,8 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center mr-3">
|
||||
<IconGithubLogo size="default" className="text-gray-700" />
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('GitHub')}</div>
|
||||
@@ -844,8 +845,8 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-indigo-50 flex items-center justify-center mr-3">
|
||||
<IconShield size="default" className="text-indigo-500" />
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<IconShield size="default" className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('OIDC')}</div>
|
||||
@@ -883,8 +884,8 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-3">
|
||||
<SiTelegram size={20} className="text-blue-500" />
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('Telegram')}</div>
|
||||
@@ -926,8 +927,8 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-3">
|
||||
<SiLinux size={20} className="text-orange-500" />
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
|
||||
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-gray-900">{t('LinuxDO')}</div>
|
||||
@@ -978,8 +979,8 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-blue-50 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconKey size="large" className="text-blue-500" />
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconKey size="large" className="text-slate-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Typography.Title heading={6} className="mb-1">
|
||||
@@ -1006,7 +1007,7 @@ const PersonalSetting = () => {
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={generateAccessToken}
|
||||
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 w-full sm:w-auto"
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||
icon={<IconKey />}
|
||||
>
|
||||
{systemToken ? t('重新生成') : t('生成令牌')}
|
||||
@@ -1022,8 +1023,8 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-orange-50 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconLock size="large" className="text-orange-500" />
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconLock size="large" className="text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-1">
|
||||
@@ -1038,7 +1039,7 @@ const PersonalSetting = () => {
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={() => setShowChangePasswordModal(true)}
|
||||
className="!rounded-lg !bg-orange-500 hover:!bg-orange-600 w-full sm:w-auto"
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
|
||||
icon={<IconLock />}
|
||||
>
|
||||
{t('修改密码')}
|
||||
@@ -1054,11 +1055,11 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start w-full sm:w-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-red-50 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconDelete size="large" className="text-red-500" />
|
||||
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mr-4 flex-shrink-0">
|
||||
<IconDelete size="large" className="text-slate-600" />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-1 text-red-600">
|
||||
<Typography.Title heading={6} className="mb-1 text-slate-700">
|
||||
{t('删除账户')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="tertiary" className="text-sm">
|
||||
@@ -1070,7 +1071,7 @@ const PersonalSetting = () => {
|
||||
type="danger"
|
||||
theme="solid"
|
||||
onClick={() => setShowAccountDeleteModal(true)}
|
||||
className="!rounded-lg w-full sm:w-auto"
|
||||
className="!rounded-lg w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
|
||||
icon={<IconDelete />}
|
||||
>
|
||||
{t('删除账户')}
|
||||
@@ -1111,7 +1112,7 @@ const PersonalSetting = () => {
|
||||
>
|
||||
<Radio value='email' className="!p-4 !rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<IconMail className="mr-2 text-blue-500" />
|
||||
<IconMail className="mr-2 text-slate-600" />
|
||||
<div>
|
||||
<div className="font-medium">{t('邮件通知')}</div>
|
||||
<div className="text-sm text-gray-500">{t('通过邮件接收通知')}</div>
|
||||
@@ -1120,7 +1121,7 @@ const PersonalSetting = () => {
|
||||
</Radio>
|
||||
<Radio value='webhook' className="!p-4 !rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<Webhook size={16} className="mr-2 text-green-500" />
|
||||
<Webhook size={16} className="mr-2 text-slate-600" />
|
||||
<div>
|
||||
<div className="font-medium">{t('Webhook通知')}</div>
|
||||
<div className="text-sm text-gray-500">{t('通过HTTP请求接收通知')}</div>
|
||||
@@ -1167,11 +1168,11 @@ const PersonalSetting = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 rounded-xl">
|
||||
<div className="bg-slate-50 rounded-xl">
|
||||
<div className="flex items-center justify-between cursor-pointer" onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
|
||||
<div className="flex items-center">
|
||||
<Globe size={16} className="mr-2 text-yellow-600" />
|
||||
<Typography.Text strong className="text-yellow-800">
|
||||
<Globe size={16} className="mr-2 text-slate-600" />
|
||||
<Typography.Text strong className="text-slate-700">
|
||||
{t('Webhook请求结构')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
@@ -1254,11 +1255,11 @@ const PersonalSetting = () => {
|
||||
itemKey='price'
|
||||
>
|
||||
<div className="py-4">
|
||||
<div className="bg-white rounded-xl">
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mt-1">
|
||||
<Shield size={20} className="text-orange-500" />
|
||||
</div>
|
||||
<div className="bg-white rounded-xl">
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center mt-1">
|
||||
<Shield size={20} className="text-slate-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -1292,7 +1293,7 @@ const PersonalSetting = () => {
|
||||
type='primary'
|
||||
onClick={saveNotificationSettings}
|
||||
size="large"
|
||||
className="!rounded-lg !bg-purple-500 hover:!bg-purple-600"
|
||||
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700"
|
||||
icon={<IconSetting />}
|
||||
>
|
||||
{t('保存设置')}
|
||||
@@ -1408,7 +1409,7 @@ const PersonalSetting = () => {
|
||||
theme="solid"
|
||||
size='large'
|
||||
onClick={bindWeChat}
|
||||
className="!rounded-lg w-full !bg-green-500 hover:!bg-green-600"
|
||||
className="!rounded-lg w-full !bg-slate-600 hover:!bg-slate-700"
|
||||
icon={<SiWechat size={16} />}
|
||||
>
|
||||
{t('绑定')}
|
||||
|
||||
@@ -6,15 +6,31 @@ import {
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
renderGroup,
|
||||
renderQuotaWithAmount,
|
||||
renderQuota
|
||||
renderNumberWithPoint,
|
||||
renderQuota,
|
||||
getChannelIcon
|
||||
} from '../../helpers/index.js';
|
||||
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
HelpCircle,
|
||||
TestTube,
|
||||
Zap,
|
||||
Timer,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
Coins,
|
||||
Tags
|
||||
} from 'lucide-react';
|
||||
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
@@ -25,13 +41,15 @@ import {
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Checkbox,
|
||||
Card,
|
||||
Select
|
||||
Form
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import EditChannel from '../../pages/Channel/EditChannel.js';
|
||||
import {
|
||||
IconList,
|
||||
IconTreeTriangleDown,
|
||||
IconFilter,
|
||||
IconPlus,
|
||||
@@ -64,7 +82,12 @@ const ChannelsTable = () => {
|
||||
type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
|
||||
}
|
||||
return (
|
||||
<Tag size='large' color={type2label[type]?.color} shape='circle'>
|
||||
<Tag
|
||||
size='large'
|
||||
color={type2label[type]?.color}
|
||||
shape='circle'
|
||||
prefixIcon={getChannelIcon(type)}
|
||||
>
|
||||
{type2label[type]?.label}
|
||||
</Tag>
|
||||
);
|
||||
@@ -74,7 +97,7 @@ const ChannelsTable = () => {
|
||||
return (
|
||||
<Tag
|
||||
color='light-blue'
|
||||
prefixIcon={<IconList />}
|
||||
prefixIcon={<Tags size={14} />}
|
||||
size='large'
|
||||
shape='circle'
|
||||
type='light'
|
||||
@@ -88,25 +111,25 @@ const ChannelsTable = () => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag size='large' color='green' shape='circle'>
|
||||
<Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag size='large' color='yellow' shape='circle'>
|
||||
<Tag size='large' color='yellow' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag size='large' color='yellow' shape='circle'>
|
||||
<Tag size='large' color='yellow' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
||||
{t('自动禁用')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag size='large' color='grey' shape='circle'>
|
||||
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -118,139 +141,48 @@ const ChannelsTable = () => {
|
||||
time = time.toFixed(2) + t(' 秒');
|
||||
if (responseTime === 0) {
|
||||
return (
|
||||
<Tag size='large' color='grey' shape='circle'>
|
||||
<Tag size='large' color='grey' shape='circle' prefixIcon={<TestTube size={14} />}>
|
||||
{t('未测试')}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 1000) {
|
||||
return (
|
||||
<Tag size='large' color='green' shape='circle'>
|
||||
<Tag size='large' color='green' shape='circle' prefixIcon={<Zap size={14} />}>
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 3000) {
|
||||
return (
|
||||
<Tag size='large' color='lime' shape='circle'>
|
||||
<Tag size='large' color='lime' shape='circle' prefixIcon={<Timer size={14} />}>
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 5000) {
|
||||
return (
|
||||
<Tag size='large' color='yellow' shape='circle'>
|
||||
<Tag size='large' color='yellow' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag size='large' color='red' shape='circle'>
|
||||
<Tag size='large' color='red' shape='circle' prefixIcon={<AlertTriangle size={14} />}>
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Define column keys for selection
|
||||
const COLUMN_KEYS = {
|
||||
ID: 'id',
|
||||
NAME: 'name',
|
||||
GROUP: 'group',
|
||||
TYPE: 'type',
|
||||
STATUS: 'status',
|
||||
RESPONSE_TIME: 'response_time',
|
||||
BALANCE: 'balance',
|
||||
PRIORITY: 'priority',
|
||||
WEIGHT: 'weight',
|
||||
OPERATE: 'operate',
|
||||
};
|
||||
|
||||
// State for column visibility
|
||||
const [visibleColumns, setVisibleColumns] = useState({});
|
||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
||||
|
||||
// Load saved column preferences from localStorage
|
||||
useEffect(() => {
|
||||
const savedColumns = localStorage.getItem('channels-table-columns');
|
||||
if (savedColumns) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedColumns);
|
||||
// Make sure all columns are accounted for
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
const merged = { ...defaults, ...parsed };
|
||||
setVisibleColumns(merged);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved column preferences', e);
|
||||
initDefaultColumns();
|
||||
}
|
||||
} else {
|
||||
initDefaultColumns();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update table when column visibility changes
|
||||
useEffect(() => {
|
||||
if (Object.keys(visibleColumns).length > 0) {
|
||||
// Save to localStorage
|
||||
localStorage.setItem(
|
||||
'channels-table-columns',
|
||||
JSON.stringify(visibleColumns),
|
||||
);
|
||||
}
|
||||
}, [visibleColumns]);
|
||||
|
||||
// Get default column visibility
|
||||
const getDefaultColumnVisibility = () => {
|
||||
return {
|
||||
[COLUMN_KEYS.ID]: true,
|
||||
[COLUMN_KEYS.NAME]: true,
|
||||
[COLUMN_KEYS.GROUP]: true,
|
||||
[COLUMN_KEYS.TYPE]: true,
|
||||
[COLUMN_KEYS.STATUS]: true,
|
||||
[COLUMN_KEYS.RESPONSE_TIME]: true,
|
||||
[COLUMN_KEYS.BALANCE]: true,
|
||||
[COLUMN_KEYS.PRIORITY]: true,
|
||||
[COLUMN_KEYS.WEIGHT]: true,
|
||||
[COLUMN_KEYS.OPERATE]: true,
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize default column visibility
|
||||
const initDefaultColumns = () => {
|
||||
const defaults = getDefaultColumnVisibility();
|
||||
setVisibleColumns(defaults);
|
||||
};
|
||||
|
||||
// Handle column visibility change
|
||||
const handleColumnVisibilityChange = (columnKey, checked) => {
|
||||
const updatedColumns = { ...visibleColumns, [columnKey]: checked };
|
||||
setVisibleColumns(updatedColumns);
|
||||
};
|
||||
|
||||
// Handle "Select All" checkbox
|
||||
const handleSelectAll = (checked) => {
|
||||
const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
|
||||
const updatedColumns = {};
|
||||
|
||||
allKeys.forEach((key) => {
|
||||
updatedColumns[key] = checked;
|
||||
});
|
||||
|
||||
setVisibleColumns(updatedColumns);
|
||||
};
|
||||
|
||||
// Define all columns with keys
|
||||
const allColumns = [
|
||||
// Define all columns
|
||||
const columns = [
|
||||
{
|
||||
key: COLUMN_KEYS.ID,
|
||||
title: t('ID'),
|
||||
dataIndex: 'id',
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.NAME,
|
||||
title: t('名称'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.GROUP,
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
render: (text, record, index) => (
|
||||
@@ -269,7 +201,6 @@ const ChannelsTable = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.TYPE,
|
||||
title: t('类型'),
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
@@ -281,7 +212,6 @@ const ChannelsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.STATUS,
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
render: (text, record, index) => {
|
||||
@@ -307,7 +237,6 @@ const ChannelsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.RESPONSE_TIME,
|
||||
title: t('响应时间'),
|
||||
dataIndex: 'response_time',
|
||||
render: (text, record, index) => (
|
||||
@@ -315,7 +244,6 @@ const ChannelsTable = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.BALANCE,
|
||||
title: t('已用/剩余'),
|
||||
dataIndex: 'expired_time',
|
||||
render: (text, record, index) => {
|
||||
@@ -324,7 +252,7 @@ const ChannelsTable = () => {
|
||||
<div>
|
||||
<Space spacing={1}>
|
||||
<Tooltip content={t('已用额度')}>
|
||||
<Tag color='white' type='ghost' size='large' shape='circle'>
|
||||
<Tag color='white' type='ghost' size='large' shape='circle' prefixIcon={<Coins size={14} />}>
|
||||
{renderQuota(record.used_quota)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
@@ -334,6 +262,7 @@ const ChannelsTable = () => {
|
||||
type='ghost'
|
||||
size='large'
|
||||
shape='circle'
|
||||
prefixIcon={<Coins size={14} />}
|
||||
onClick={() => updateChannelBalance(record)}
|
||||
>
|
||||
{renderQuotaWithAmount(record.balance)}
|
||||
@@ -345,7 +274,7 @@ const ChannelsTable = () => {
|
||||
} else {
|
||||
return (
|
||||
<Tooltip content={t('已用额度')}>
|
||||
<Tag color='white' type='ghost' size='large' shape='circle'>
|
||||
<Tag color='white' type='ghost' size='large' shape='circle' prefixIcon={<Coins size={14} />}>
|
||||
{renderQuota(record.used_quota)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
@@ -354,7 +283,6 @@ const ChannelsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.PRIORITY,
|
||||
title: t('优先级'),
|
||||
dataIndex: 'priority',
|
||||
render: (text, record, index) => {
|
||||
@@ -406,7 +334,6 @@ const ChannelsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.WEIGHT,
|
||||
title: t('权重'),
|
||||
dataIndex: 'weight',
|
||||
render: (text, record, index) => {
|
||||
@@ -458,7 +385,6 @@ const ChannelsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.OPERATE,
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
fixed: 'right',
|
||||
@@ -631,96 +557,10 @@ const ChannelsTable = () => {
|
||||
},
|
||||
];
|
||||
|
||||
// Filter columns based on visibility settings
|
||||
const getVisibleColumns = () => {
|
||||
return allColumns.filter((column) => visibleColumns[column.key]);
|
||||
};
|
||||
|
||||
// Column selector modal
|
||||
const renderColumnSelector = () => {
|
||||
return (
|
||||
<Modal
|
||||
title={t('列设置')}
|
||||
visible={showColumnSelector}
|
||||
onCancel={() => setShowColumnSelector(false)}
|
||||
footer={
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
theme="light"
|
||||
onClick={() => initDefaultColumns()}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
onClick={() => setShowColumnSelector(false)}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => setShowColumnSelector(false)}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('确定')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
size="middle"
|
||||
centered={true}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Checkbox
|
||||
checked={Object.values(visibleColumns).every((v) => v === true)}
|
||||
indeterminate={
|
||||
Object.values(visibleColumns).some((v) => v === true) &&
|
||||
!Object.values(visibleColumns).every((v) => v === true)
|
||||
}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
>
|
||||
{t('全选')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
|
||||
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||
>
|
||||
{allColumns.map((column) => {
|
||||
// Skip columns without title
|
||||
if (!column.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={column.key}
|
||||
className="w-1/2 mb-4 pr-2"
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!visibleColumns[column.key]}
|
||||
onChange={(e) =>
|
||||
handleColumnVisibilityChange(column.key, e.target.checked)
|
||||
}
|
||||
>
|
||||
{column.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [idSort, setIdSort] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searchGroup, setSearchGroup] = useState('');
|
||||
const [searchModel, setSearchModel] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
||||
const [channelCount, setChannelCount] = useState(pageSize);
|
||||
@@ -745,6 +585,16 @@ const ChannelsTable = () => {
|
||||
const [testQueue, setTestQueue] = useState([]);
|
||||
const [isProcessingQueue, setIsProcessingQueue] = useState(false);
|
||||
|
||||
// Form API 引用
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
|
||||
// Form 初始值
|
||||
const formInitValues = {
|
||||
searchKeyword: '',
|
||||
searchGroup: '',
|
||||
searchModel: '',
|
||||
};
|
||||
|
||||
const removeRecord = (record) => {
|
||||
let newDataSource = [...channels];
|
||||
if (record.id != null) {
|
||||
@@ -896,15 +746,11 @@ const ChannelsTable = () => {
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
const { searchKeyword, searchGroup, searchModel } = getFormValues();
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
|
||||
} else {
|
||||
await searchChannels(
|
||||
searchKeyword,
|
||||
searchGroup,
|
||||
searchModel,
|
||||
enableTagMode,
|
||||
);
|
||||
await searchChannels(enableTagMode);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1010,29 +856,40 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const searchChannels = async (
|
||||
searchKeyword,
|
||||
searchGroup,
|
||||
searchModel,
|
||||
enableTagMode,
|
||||
) => {
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
|
||||
// setActivePage(1);
|
||||
return;
|
||||
}
|
||||
// 获取表单值的辅助函数,确保所有值都是字符串
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
return {
|
||||
searchKeyword: formValues.searchKeyword || '',
|
||||
searchGroup: formValues.searchGroup || '',
|
||||
searchModel: formValues.searchModel || '',
|
||||
};
|
||||
};
|
||||
|
||||
const searchChannels = async (enableTagMode) => {
|
||||
const { searchKeyword, searchGroup, searchModel } = getFormValues();
|
||||
|
||||
setSearching(true);
|
||||
const res = await API.get(
|
||||
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
|
||||
);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setChannelFormat(data, enableTagMode);
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
try {
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
await loadChannels(activePage - 1, pageSize, idSort, enableTagMode);
|
||||
// setActivePage(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await API.get(
|
||||
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`,
|
||||
);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setChannelFormat(data, enableTagMode);
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const updateChannelProperty = (channelId, updateFn) => {
|
||||
@@ -1540,71 +1397,83 @@ const ChannelsTable = () => {
|
||||
>
|
||||
{t('刷新')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className="!rounded-full w-full md:w-auto"
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
|
||||
<div className="relative w-full md:w-64">
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={(v) => {
|
||||
setSearchKeyword(v.trim());
|
||||
}}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-48">
|
||||
<Input
|
||||
prefix={<IconFilter />}
|
||||
placeholder={t('模型关键字')}
|
||||
value={searchModel}
|
||||
loading={searching}
|
||||
onChange={(v) => {
|
||||
setSearchModel(v.trim());
|
||||
}}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-48">
|
||||
<Select
|
||||
placeholder={t('选择分组')}
|
||||
optionList={[
|
||||
{ label: t('选择分组'), value: null },
|
||||
...groupOptions,
|
||||
]}
|
||||
value={searchGroup}
|
||||
onChange={(v) => {
|
||||
setSearchGroup(v);
|
||||
searchChannels(searchKeyword, v, searchModel, enableTagMode);
|
||||
}}
|
||||
className="!rounded-full w-full"
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
searchChannels(searchKeyword, searchGroup, searchModel, enableTagMode);
|
||||
}}
|
||||
loading={searching}
|
||||
className="!rounded-full w-full md:w-auto"
|
||||
<Form
|
||||
initValues={formInitValues}
|
||||
getFormApi={(api) => setFormApi(api)}
|
||||
onSubmit={() => searchChannels(enableTagMode)}
|
||||
allowEmpty={true}
|
||||
autoComplete="off"
|
||||
layout="horizontal"
|
||||
trigger="change"
|
||||
stopValidateWithError={false}
|
||||
className="flex flex-col md:flex-row items-center gap-4 w-full"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<div className="relative w-full md:w-64">
|
||||
<Form.Input
|
||||
field="searchKeyword"
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-48">
|
||||
<Form.Input
|
||||
field="searchModel"
|
||||
prefix={<IconFilter />}
|
||||
placeholder={t('模型关键字')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-48">
|
||||
<Form.Select
|
||||
field="searchGroup"
|
||||
placeholder={t('选择分组')}
|
||||
optionList={[
|
||||
{ label: t('选择分组'), value: null },
|
||||
...groupOptions,
|
||||
]}
|
||||
className="!rounded-full w-full"
|
||||
showClear
|
||||
pure
|
||||
onChange={() => {
|
||||
// 延迟执行搜索,让表单值先更新
|
||||
setTimeout(() => {
|
||||
searchChannels(enableTagMode);
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading || searching}
|
||||
className="!rounded-full w-full md:w-auto"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
onClick={() => {
|
||||
if (formApi) {
|
||||
formApi.reset();
|
||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="!rounded-full w-full md:w-auto"
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1612,7 +1481,6 @@ const ChannelsTable = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderColumnSelector()}
|
||||
<EditTagModal
|
||||
visible={showEditTag}
|
||||
tag={editingTag}
|
||||
@@ -1633,7 +1501,7 @@ const ChannelsTable = () => {
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={getVisibleColumns()}
|
||||
columns={columns}
|
||||
dataSource={pageData}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
@@ -1663,6 +1531,14 @@ const ChannelsTable = () => {
|
||||
}
|
||||
: null
|
||||
}
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
loading={loading}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
CreditCard,
|
||||
ShoppingCart,
|
||||
Settings,
|
||||
Server,
|
||||
AlertTriangle,
|
||||
HelpCircle,
|
||||
Zap,
|
||||
Play,
|
||||
Clock,
|
||||
Hash,
|
||||
Key
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
@@ -20,16 +33,16 @@ import {
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
getLogOther,
|
||||
renderModelTag,
|
||||
renderModelTag
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Descriptions,
|
||||
Empty,
|
||||
Modal,
|
||||
Popover,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
@@ -39,24 +52,18 @@ import {
|
||||
Card,
|
||||
Typography,
|
||||
Divider,
|
||||
Input,
|
||||
DatePicker,
|
||||
Form
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||
import { IconSetting, IconSearch, IconForward } from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ key: 'all', text: 'all', value: 'all' },
|
||||
{ key: 'self', text: 'current user', value: 'self' },
|
||||
];
|
||||
|
||||
const colors = [
|
||||
'amber',
|
||||
'blue',
|
||||
@@ -238,11 +245,6 @@ const LogsTable = () => {
|
||||
onClick: (event) => {
|
||||
copyText(event, record.model_name).then((r) => { });
|
||||
},
|
||||
suffixIcon: (
|
||||
<IconForward
|
||||
style={{ width: '0.9em', height: '0.9em', opacity: 0.75 }}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
</Popover>
|
||||
</Space>
|
||||
@@ -737,39 +739,67 @@ const LogsTable = () => {
|
||||
const [logType, setLogType] = useState(0);
|
||||
const isAdminUser = isAdmin();
|
||||
let now = new Date();
|
||||
// 初始化start_timestamp为今天0点
|
||||
const [inputs, setInputs] = useState({
|
||||
|
||||
// Form 初始值
|
||||
const formInitValues = {
|
||||
username: '',
|
||||
token_name: '',
|
||||
model_name: '',
|
||||
start_timestamp: timestamp2string(getTodayStartTimestamp()),
|
||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
||||
channel: '',
|
||||
group: '',
|
||||
});
|
||||
const {
|
||||
username,
|
||||
token_name,
|
||||
model_name,
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
channel,
|
||||
group,
|
||||
} = inputs;
|
||||
dateRange: [
|
||||
timestamp2string(getTodayStartTimestamp()),
|
||||
timestamp2string(now.getTime() / 1000 + 3600)
|
||||
],
|
||||
logType: '0',
|
||||
};
|
||||
|
||||
const [stat, setStat] = useState({
|
||||
quota: 0,
|
||||
token: 0,
|
||||
});
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
// Form API 引用
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
|
||||
// 获取表单值的辅助函数,确保所有值都是字符串
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
|
||||
// 处理时间范围
|
||||
let start_timestamp = timestamp2string(getTodayStartTimestamp());
|
||||
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
||||
|
||||
if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
|
||||
start_timestamp = formValues.dateRange[0];
|
||||
end_timestamp = formValues.dateRange[1];
|
||||
}
|
||||
|
||||
return {
|
||||
username: formValues.username || '',
|
||||
token_name: formValues.token_name || '',
|
||||
model_name: formValues.model_name || '',
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
channel: formValues.channel || '',
|
||||
group: formValues.group || '',
|
||||
logType: formValues.logType ? parseInt(formValues.logType) : 0,
|
||||
};
|
||||
};
|
||||
|
||||
const getLogSelfStat = async () => {
|
||||
const {
|
||||
token_name,
|
||||
model_name,
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
group,
|
||||
logType: formLogType,
|
||||
} = getFormValues();
|
||||
const currentLogType = formLogType !== undefined ? formLogType : logType;
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
||||
let url = `/api/log/self/stat?type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
||||
url = encodeURI(url);
|
||||
let res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -781,9 +811,20 @@ const LogsTable = () => {
|
||||
};
|
||||
|
||||
const getLogStat = async () => {
|
||||
const {
|
||||
username,
|
||||
token_name,
|
||||
model_name,
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
channel,
|
||||
group,
|
||||
logType: formLogType,
|
||||
} = getFormValues();
|
||||
const currentLogType = formLogType !== undefined ? formLogType : logType;
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
||||
let url = `/api/log/stat?type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
||||
url = encodeURI(url);
|
||||
let res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -1016,16 +1057,30 @@ const LogsTable = () => {
|
||||
setLogs(logs);
|
||||
};
|
||||
|
||||
const loadLogs = async (startIdx, pageSize, logType = 0) => {
|
||||
const loadLogs = async (startIdx, pageSize, customLogType = null) => {
|
||||
setLoading(true);
|
||||
|
||||
let url = '';
|
||||
const {
|
||||
username,
|
||||
token_name,
|
||||
model_name,
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
channel,
|
||||
group,
|
||||
logType: formLogType,
|
||||
} = getFormValues();
|
||||
|
||||
// 使用传入的 logType 或者表单中的 logType 或者状态中的 logType
|
||||
const currentLogType = customLogType !== null ? customLogType : formLogType !== undefined ? formLogType : logType;
|
||||
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
||||
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
||||
} else {
|
||||
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
||||
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${currentLogType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
||||
}
|
||||
url = encodeURI(url);
|
||||
const res = await API.get(url);
|
||||
@@ -1045,7 +1100,7 @@ const LogsTable = () => {
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setActivePage(page);
|
||||
loadLogs(page, pageSize, logType).then((r) => { });
|
||||
loadLogs(page, pageSize).then((r) => { }); // 不传入logType,让其从表单获取最新值
|
||||
};
|
||||
|
||||
const handlePageSizeChange = async (size) => {
|
||||
@@ -1062,7 +1117,7 @@ const LogsTable = () => {
|
||||
const refresh = async () => {
|
||||
setActivePage(1);
|
||||
handleEyeClick();
|
||||
await loadLogs(activePage, pageSize, logType);
|
||||
await loadLogs(1, pageSize); // 不传入logType,让其从表单获取最新值
|
||||
};
|
||||
|
||||
const copyText = async (e, text) => {
|
||||
@@ -1083,9 +1138,15 @@ const LogsTable = () => {
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
handleEyeClick();
|
||||
}, []);
|
||||
|
||||
// 当 formApi 可用时,初始化统计
|
||||
useEffect(() => {
|
||||
if (formApi) {
|
||||
handleEyeClick();
|
||||
}
|
||||
}, [formApi]);
|
||||
|
||||
const expandRowRender = (record, index) => {
|
||||
return <Descriptions data={expandData[record.key]} />;
|
||||
};
|
||||
@@ -1149,115 +1210,144 @@ const LogsTable = () => {
|
||||
<Divider margin='12px' />
|
||||
|
||||
{/* 搜索表单区域 */}
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
||||
{/* 时间选择器 */}
|
||||
<div className='col-span-1 lg:col-span-2'>
|
||||
<DatePicker
|
||||
className='w-full'
|
||||
value={[start_timestamp, end_timestamp]}
|
||||
type='dateTimeRange'
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value) && value.length === 2) {
|
||||
handleInputChange(value[0], 'start_timestamp');
|
||||
handleInputChange(value[1], 'end_timestamp');
|
||||
}
|
||||
}}
|
||||
<Form
|
||||
initValues={formInitValues}
|
||||
getFormApi={(api) => setFormApi(api)}
|
||||
onSubmit={refresh}
|
||||
allowEmpty={true}
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
trigger="change"
|
||||
stopValidateWithError={false}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
||||
{/* 时间选择器 */}
|
||||
<div className='col-span-1 lg:col-span-2'>
|
||||
<Form.DatePicker
|
||||
field='dateRange'
|
||||
className='w-full'
|
||||
type='dateTimeRange'
|
||||
placeholder={[t('开始时间'), t('结束时间')]}
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 其他搜索字段 */}
|
||||
<Form.Input
|
||||
field='token_name'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('令牌名称')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field='model_name'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('模型名称')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field='group'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('分组')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
|
||||
{isAdminUser && (
|
||||
<>
|
||||
<Form.Input
|
||||
field='channel'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('渠道 ID')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
<Form.Input
|
||||
field='username'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('用户名称')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 日志类型选择器 */}
|
||||
<Select
|
||||
value={logType.toString()}
|
||||
placeholder={t('日志类型')}
|
||||
className='!rounded-full'
|
||||
onChange={(value) => {
|
||||
setLogType(parseInt(value));
|
||||
loadLogs(0, pageSize, parseInt(value));
|
||||
}}
|
||||
>
|
||||
<Select.Option value='0'>{t('全部')}</Select.Option>
|
||||
<Select.Option value='1'>{t('充值')}</Select.Option>
|
||||
<Select.Option value='2'>{t('消费')}</Select.Option>
|
||||
<Select.Option value='3'>{t('管理')}</Select.Option>
|
||||
<Select.Option value='4'>{t('系统')}</Select.Option>
|
||||
<Select.Option value='5'>{t('错误')}</Select.Option>
|
||||
</Select>
|
||||
|
||||
{/* 其他搜索字段 */}
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('令牌名称')}
|
||||
value={token_name}
|
||||
onChange={(value) => handleInputChange(value, 'token_name')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('模型名称')}
|
||||
value={model_name}
|
||||
onChange={(value) => handleInputChange(value, 'model_name')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('分组')}
|
||||
value={group}
|
||||
onChange={(value) => handleInputChange(value, 'group')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
|
||||
{isAdminUser && (
|
||||
<>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('渠道 ID')}
|
||||
value={channel}
|
||||
onChange={(value) => handleInputChange(value, 'channel')}
|
||||
className='!rounded-full'
|
||||
{/* 操作按钮区域 */}
|
||||
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3'>
|
||||
{/* 日志类型选择器 */}
|
||||
<div className='w-full sm:w-auto'>
|
||||
<Form.Select
|
||||
field='logType'
|
||||
placeholder={t('日志类型')}
|
||||
className='!rounded-full w-full sm:w-auto min-w-[120px]'
|
||||
showClear
|
||||
/>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('用户名称')}
|
||||
value={username}
|
||||
onChange={(value) => handleInputChange(value, 'username')}
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
pure
|
||||
onChange={() => {
|
||||
// 延迟执行搜索,让表单值先更新
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<Form.Select.Option value='0'>{t('全部')}</Form.Select.Option>
|
||||
<Form.Select.Option value='1'>{t('充值')}</Form.Select.Option>
|
||||
<Form.Select.Option value='2'>{t('消费')}</Form.Select.Option>
|
||||
<Form.Select.Option value='3'>{t('管理')}</Form.Select.Option>
|
||||
<Form.Select.Option value='4'>{t('系统')}</Form.Select.Option>
|
||||
<Form.Select.Option value='5'>{t('错误')}</Form.Select.Option>
|
||||
</Form.Select>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className='flex justify-between items-center pt-2'>
|
||||
<div></div>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
<div className='flex gap-2 w-full sm:w-auto justify-end'>
|
||||
<Button
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
loading={loading}
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
onClick={() => {
|
||||
if (formApi) {
|
||||
formApi.reset();
|
||||
setLogType(0);
|
||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
shadows='always'
|
||||
@@ -1276,6 +1366,14 @@ const LogsTable = () => {
|
||||
scroll={{ x: 'max-content' }}
|
||||
className='rounded-xl overflow-hidden'
|
||||
size='middle'
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
pagination={{
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
|
||||
@@ -1,35 +1,65 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Palette,
|
||||
ZoomIn,
|
||||
Shuffle,
|
||||
Move,
|
||||
FileText,
|
||||
Blend,
|
||||
Upload,
|
||||
Minimize2,
|
||||
RotateCcw,
|
||||
PaintBucket,
|
||||
Focus,
|
||||
Move3D,
|
||||
Monitor,
|
||||
UserCheck,
|
||||
HelpCircle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Copy,
|
||||
FileX,
|
||||
Pause,
|
||||
XCircle,
|
||||
Loader,
|
||||
AlertCircle,
|
||||
Hash
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
isAdmin,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
timestamp2string
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Empty,
|
||||
Form,
|
||||
ImagePreview,
|
||||
Input,
|
||||
Layout,
|
||||
Modal,
|
||||
Progress,
|
||||
Skeleton,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Typography
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
IconEyeOpened,
|
||||
IconSearch,
|
||||
IconSetting,
|
||||
IconSetting
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -154,103 +184,103 @@ const LogsTable = () => {
|
||||
switch (type) {
|
||||
case 'IMAGINE':
|
||||
return (
|
||||
<Tag color='blue' size='large' shape='circle'>
|
||||
<Tag color='blue' size='large' shape='circle' prefixIcon={<Palette size={14} />}>
|
||||
{t('绘图')}
|
||||
</Tag>
|
||||
);
|
||||
case 'UPSCALE':
|
||||
return (
|
||||
<Tag color='orange' size='large' shape='circle'>
|
||||
<Tag color='orange' size='large' shape='circle' prefixIcon={<ZoomIn size={14} />}>
|
||||
{t('放大')}
|
||||
</Tag>
|
||||
);
|
||||
case 'VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large' shape='circle'>
|
||||
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
||||
{t('变换')}
|
||||
</Tag>
|
||||
);
|
||||
case 'HIGH_VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large' shape='circle'>
|
||||
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
||||
{t('强变换')}
|
||||
</Tag>
|
||||
);
|
||||
case 'LOW_VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large' shape='circle'>
|
||||
<Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
|
||||
{t('弱变换')}
|
||||
</Tag>
|
||||
);
|
||||
case 'PAN':
|
||||
return (
|
||||
<Tag color='cyan' size='large' shape='circle'>
|
||||
<Tag color='cyan' size='large' shape='circle' prefixIcon={<Move size={14} />}>
|
||||
{t('平移')}
|
||||
</Tag>
|
||||
);
|
||||
case 'DESCRIBE':
|
||||
return (
|
||||
<Tag color='yellow' size='large' shape='circle'>
|
||||
<Tag color='yellow' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
|
||||
{t('图生文')}
|
||||
</Tag>
|
||||
);
|
||||
case 'BLEND':
|
||||
return (
|
||||
<Tag color='lime' size='large' shape='circle'>
|
||||
<Tag color='lime' size='large' shape='circle' prefixIcon={<Blend size={14} />}>
|
||||
{t('图混合')}
|
||||
</Tag>
|
||||
);
|
||||
case 'UPLOAD':
|
||||
return (
|
||||
<Tag color='blue' size='large' shape='circle'>
|
||||
<Tag color='blue' size='large' shape='circle' prefixIcon={<Upload size={14} />}>
|
||||
上传文件
|
||||
</Tag>
|
||||
);
|
||||
case 'SHORTEN':
|
||||
return (
|
||||
<Tag color='pink' size='large' shape='circle'>
|
||||
<Tag color='pink' size='large' shape='circle' prefixIcon={<Minimize2 size={14} />}>
|
||||
{t('缩词')}
|
||||
</Tag>
|
||||
);
|
||||
case 'REROLL':
|
||||
return (
|
||||
<Tag color='indigo' size='large' shape='circle'>
|
||||
<Tag color='indigo' size='large' shape='circle' prefixIcon={<RotateCcw size={14} />}>
|
||||
{t('重绘')}
|
||||
</Tag>
|
||||
);
|
||||
case 'INPAINT':
|
||||
return (
|
||||
<Tag color='violet' size='large' shape='circle'>
|
||||
<Tag color='violet' size='large' shape='circle' prefixIcon={<PaintBucket size={14} />}>
|
||||
{t('局部重绘-提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 'ZOOM':
|
||||
return (
|
||||
<Tag color='teal' size='large' shape='circle'>
|
||||
<Tag color='teal' size='large' shape='circle' prefixIcon={<Focus size={14} />}>
|
||||
{t('变焦')}
|
||||
</Tag>
|
||||
);
|
||||
case 'CUSTOM_ZOOM':
|
||||
return (
|
||||
<Tag color='teal' size='large' shape='circle'>
|
||||
<Tag color='teal' size='large' shape='circle' prefixIcon={<Move3D size={14} />}>
|
||||
{t('自定义变焦-提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 'MODAL':
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<Monitor size={14} />}>
|
||||
{t('窗口处理')}
|
||||
</Tag>
|
||||
);
|
||||
case 'SWAP_FACE':
|
||||
return (
|
||||
<Tag color='light-green' size='large' shape='circle'>
|
||||
<Tag color='light-green' size='large' shape='circle' prefixIcon={<UserCheck size={14} />}>
|
||||
{t('换脸')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large' shape='circle'>
|
||||
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -261,31 +291,31 @@ const LogsTable = () => {
|
||||
switch (code) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('已提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 21:
|
||||
return (
|
||||
<Tag color='lime' size='large' shape='circle'>
|
||||
<Tag color='lime' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||
{t('等待中')}
|
||||
</Tag>
|
||||
);
|
||||
case 22:
|
||||
return (
|
||||
<Tag color='orange' size='large' shape='circle'>
|
||||
<Tag color='orange' size='large' shape='circle' prefixIcon={<Copy size={14} />}>
|
||||
{t('重复提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 0:
|
||||
return (
|
||||
<Tag color='yellow' size='large' shape='circle'>
|
||||
<Tag color='yellow' size='large' shape='circle' prefixIcon={<FileX size={14} />}>
|
||||
{t('未提交')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large' shape='circle'>
|
||||
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -296,43 +326,43 @@ const LogsTable = () => {
|
||||
switch (type) {
|
||||
case 'SUCCESS':
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('成功')}
|
||||
</Tag>
|
||||
);
|
||||
case 'NOT_START':
|
||||
return (
|
||||
<Tag color='grey' size='large' shape='circle'>
|
||||
<Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
|
||||
{t('未启动')}
|
||||
</Tag>
|
||||
);
|
||||
case 'SUBMITTED':
|
||||
return (
|
||||
<Tag color='yellow' size='large' shape='circle'>
|
||||
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||
{t('队列中')}
|
||||
</Tag>
|
||||
);
|
||||
case 'IN_PROGRESS':
|
||||
return (
|
||||
<Tag color='blue' size='large' shape='circle'>
|
||||
<Tag color='blue' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
|
||||
{t('执行中')}
|
||||
</Tag>
|
||||
);
|
||||
case 'FAILURE':
|
||||
return (
|
||||
<Tag color='red' size='large' shape='circle'>
|
||||
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('失败')}
|
||||
</Tag>
|
||||
);
|
||||
case 'MODAL':
|
||||
return (
|
||||
<Tag color='yellow' size='large' shape='circle'>
|
||||
<Tag color='yellow' size='large' shape='circle' prefixIcon={<AlertCircle size={14} />}>
|
||||
{t('窗口等待')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large' shape='circle'>
|
||||
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -362,7 +392,7 @@ const LogsTable = () => {
|
||||
const color = durationSec > 60 ? 'red' : 'green';
|
||||
|
||||
return (
|
||||
<Tag color={color} size='large' shape='circle'>
|
||||
<Tag color={color} size='large' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||
{durationSec} {t('秒')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -398,6 +428,7 @@ const LogsTable = () => {
|
||||
color={colors[parseInt(text) % colors.length]}
|
||||
size='large'
|
||||
shape='circle'
|
||||
prefixIcon={<Hash size={14} />}
|
||||
onClick={() => {
|
||||
copyText(text);
|
||||
}}
|
||||
@@ -462,7 +493,7 @@ const LogsTable = () => {
|
||||
percent={text ? parseInt(text.replace('%', '')) : 0}
|
||||
showInfo={true}
|
||||
aria-label='drawing progress'
|
||||
style={{ minWidth: '200px' }}
|
||||
style={{ minWidth: '160px' }}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -483,6 +514,7 @@ const LogsTable = () => {
|
||||
setModalImageUrl(text);
|
||||
setIsModalOpenurl(true);
|
||||
}}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('查看图片')}
|
||||
</Button>
|
||||
@@ -570,7 +602,6 @@ const LogsTable = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
|
||||
const [logType, setLogType] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const [showBanner, setShowBanner] = useState(false);
|
||||
@@ -578,22 +609,44 @@ const LogsTable = () => {
|
||||
// 定义模态框图片URL的状态和更新函数
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
let now = new Date();
|
||||
// 初始化start_timestamp为前一天
|
||||
const [inputs, setInputs] = useState({
|
||||
|
||||
// Form 初始值
|
||||
const formInitValues = {
|
||||
channel_id: '',
|
||||
mj_id: '',
|
||||
start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000),
|
||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
||||
});
|
||||
const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs;
|
||||
dateRange: [
|
||||
timestamp2string(now.getTime() / 1000 - 2592000),
|
||||
timestamp2string(now.getTime() / 1000 + 3600)
|
||||
],
|
||||
};
|
||||
|
||||
// Form API 引用
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
|
||||
const [stat, setStat] = useState({
|
||||
quota: 0,
|
||||
token: 0,
|
||||
});
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
// 获取表单值的辅助函数
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
|
||||
// 处理时间范围
|
||||
let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
|
||||
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
||||
|
||||
if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
|
||||
start_timestamp = formValues.dateRange[0];
|
||||
end_timestamp = formValues.dateRange[1];
|
||||
}
|
||||
|
||||
return {
|
||||
channel_id: formValues.channel_id || '',
|
||||
mj_id: formValues.mj_id || '',
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
};
|
||||
};
|
||||
|
||||
const setLogsFormat = (logs) => {
|
||||
@@ -611,6 +664,7 @@ const LogsTable = () => {
|
||||
setLoading(true);
|
||||
|
||||
let url = '';
|
||||
const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
|
||||
let localStartTimestamp = Date.parse(start_timestamp);
|
||||
let localEndTimestamp = Date.parse(end_timestamp);
|
||||
if (isAdminUser) {
|
||||
@@ -673,7 +727,7 @@ const LogsTable = () => {
|
||||
const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
|
||||
setPageSize(localPageSize);
|
||||
loadLogs(0, localPageSize).then();
|
||||
}, [logType]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
|
||||
@@ -788,70 +842,93 @@ const LogsTable = () => {
|
||||
<Divider margin="12px" />
|
||||
|
||||
{/* 搜索表单区域 */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 时间选择器 */}
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
value={[start_timestamp, end_timestamp]}
|
||||
type='dateTimeRange'
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value) && value.length === 2) {
|
||||
handleInputChange(value[0], 'start_timestamp');
|
||||
handleInputChange(value[1], 'end_timestamp');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form
|
||||
initValues={formInitValues}
|
||||
getFormApi={(api) => setFormApi(api)}
|
||||
onSubmit={refresh}
|
||||
allowEmpty={true}
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
trigger="change"
|
||||
stopValidateWithError={false}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 时间选择器 */}
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<Form.DatePicker
|
||||
field='dateRange'
|
||||
className="w-full"
|
||||
type='dateTimeRange'
|
||||
placeholder={[t('开始时间'), t('结束时间')]}
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 任务 ID */}
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('任务 ID')}
|
||||
value={mj_id}
|
||||
onChange={(value) => handleInputChange(value, 'mj_id')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
|
||||
{/* 渠道 ID - 仅管理员可见 */}
|
||||
{isAdminUser && (
|
||||
<Input
|
||||
{/* 任务 ID */}
|
||||
<Form.Input
|
||||
field='mj_id'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('渠道 ID')}
|
||||
value={channel_id}
|
||||
onChange={(value) => handleInputChange(value, 'channel_id')}
|
||||
placeholder={t('任务 ID')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<div></div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
{/* 渠道 ID - 仅管理员可见 */}
|
||||
{isAdminUser && (
|
||||
<Form.Input
|
||||
field='channel_id'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('渠道 ID')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div></div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
loading={loading}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
onClick={() => {
|
||||
if (formApi) {
|
||||
formApi.reset();
|
||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
shadows='always'
|
||||
@@ -865,6 +942,14 @@ const LogsTable = () => {
|
||||
scroll={{ x: 'max-content' }}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
pagination={{
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
|
||||
@@ -17,14 +17,19 @@ import {
|
||||
Tabs,
|
||||
TabPane,
|
||||
Dropdown,
|
||||
Empty
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
IconVerify,
|
||||
IconHelpCircle,
|
||||
IconSearch,
|
||||
IconCopy,
|
||||
IconInfoCircle,
|
||||
IconLayers,
|
||||
IconLayers
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
@@ -489,6 +494,14 @@ const ModelPricing = () => {
|
||||
loading={loading}
|
||||
rowSelection={rowSelection}
|
||||
className="custom-table"
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
pageSize: pageSize,
|
||||
|
||||
@@ -8,20 +8,33 @@ import {
|
||||
renderQuota
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Minus,
|
||||
HelpCircle,
|
||||
Coins
|
||||
} from 'lucide-react';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Input,
|
||||
Empty,
|
||||
Form,
|
||||
Modal,
|
||||
Popover,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Typography
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
IconPlus,
|
||||
IconCopy,
|
||||
@@ -31,7 +44,7 @@ import {
|
||||
IconDelete,
|
||||
IconStop,
|
||||
IconPlay,
|
||||
IconMore,
|
||||
IconMore
|
||||
} from '@douyinfe/semi-icons';
|
||||
import EditRedemption from '../../pages/Redemption/EditRedemption';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -49,25 +62,25 @@ const RedemptionsTable = () => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('未使用')}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' size='large' shape='circle'>
|
||||
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='grey' size='large' shape='circle'>
|
||||
<Tag color='grey' size='large' shape='circle' prefixIcon={<Minus size={14} />}>
|
||||
{t('已使用')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='black' size='large' shape='circle'>
|
||||
<Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -95,7 +108,13 @@ const RedemptionsTable = () => {
|
||||
title: t('额度'),
|
||||
dataIndex: 'quota',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderQuota(parseInt(text))}</div>;
|
||||
return (
|
||||
<div>
|
||||
<Tag size={'large'} color={'grey'} shape='circle' prefixIcon={<Coins size={14} />}>
|
||||
{renderQuota(parseInt(text))}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -223,7 +242,6 @@ const RedemptionsTable = () => {
|
||||
const [redemptions, setRedemptions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
|
||||
const [selectedKeys, setSelectedKeys] = useState([]);
|
||||
@@ -233,6 +251,22 @@ const RedemptionsTable = () => {
|
||||
});
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
|
||||
// Form 初始值
|
||||
const formInitValues = {
|
||||
searchKeyword: '',
|
||||
};
|
||||
|
||||
// Form API 引用
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
|
||||
// 获取表单值的辅助函数
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
return {
|
||||
searchKeyword: formValues.searchKeyword || '',
|
||||
};
|
||||
};
|
||||
|
||||
const closeEdit = () => {
|
||||
setShowEdit(false);
|
||||
setTimeout(() => {
|
||||
@@ -340,8 +374,14 @@ const RedemptionsTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const searchRedemptions = async (keyword, page, pageSize) => {
|
||||
if (searchKeyword === '') {
|
||||
const searchRedemptions = async (keyword = null, page, pageSize) => {
|
||||
// 如果没有传递keyword参数,从表单获取值
|
||||
if (keyword === null) {
|
||||
const formValues = getFormValues();
|
||||
keyword = formValues.searchKeyword;
|
||||
}
|
||||
|
||||
if (keyword === '') {
|
||||
await loadRedemptions(page, pageSize);
|
||||
return;
|
||||
}
|
||||
@@ -361,10 +401,6 @@ const RedemptionsTable = () => {
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (value) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const sortRedemption = (key) => {
|
||||
if (redemptions.length === 0) return;
|
||||
setLoading(true);
|
||||
@@ -381,6 +417,7 @@ const RedemptionsTable = () => {
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setActivePage(page);
|
||||
const { searchKeyword } = getFormValues();
|
||||
if (searchKeyword === '') {
|
||||
loadRedemptions(page, pageSize).then();
|
||||
} else {
|
||||
@@ -457,28 +494,59 @@ const RedemptionsTable = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
|
||||
<div className="relative w-full md:w-64">
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('关键字(id或者名称)')}
|
||||
value={searchKeyword}
|
||||
onChange={handleKeywordChange}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
<Form
|
||||
initValues={formInitValues}
|
||||
getFormApi={(api) => setFormApi(api)}
|
||||
onSubmit={() => {
|
||||
setActivePage(1);
|
||||
searchRedemptions(null, 1, pageSize);
|
||||
}}
|
||||
allowEmpty={true}
|
||||
autoComplete="off"
|
||||
layout="horizontal"
|
||||
trigger="change"
|
||||
stopValidateWithError={false}
|
||||
className="w-full md:w-auto order-1 md:order-2"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
||||
<div className="relative w-full md:w-64">
|
||||
<Form.Input
|
||||
field="searchKeyword"
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('关键字(id或者名称)')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading || searching}
|
||||
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
onClick={() => {
|
||||
if (formApi) {
|
||||
formApi.reset();
|
||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||
setTimeout(() => {
|
||||
setActivePage(1);
|
||||
loadRedemptions(1, pageSize);
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
searchRedemptions(searchKeyword, 1, pageSize).then();
|
||||
}}
|
||||
loading={searching}
|
||||
className="!rounded-full w-full md:w-auto"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -517,6 +585,7 @@ const RedemptionsTable = () => {
|
||||
onPageSizeChange: (size) => {
|
||||
setPageSize(size);
|
||||
setActivePage(1);
|
||||
const { searchKeyword } = getFormValues();
|
||||
if (searchKeyword === '') {
|
||||
loadRedemptions(1, size).then();
|
||||
} else {
|
||||
@@ -528,6 +597,14 @@ const RedemptionsTable = () => {
|
||||
loading={loading}
|
||||
rowSelection={rowSelection}
|
||||
onRow={handleRow}
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
></Table>
|
||||
|
||||
@@ -1,34 +1,51 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Music,
|
||||
FileText,
|
||||
HelpCircle,
|
||||
CheckCircle,
|
||||
Pause,
|
||||
Clock,
|
||||
Play,
|
||||
XCircle,
|
||||
Loader,
|
||||
List,
|
||||
Hash
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
isAdmin,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
timestamp2string
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
Empty,
|
||||
Form,
|
||||
Layout,
|
||||
Modal,
|
||||
Progress,
|
||||
Skeleton,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Typography
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
IconEyeOpened,
|
||||
IconSearch,
|
||||
IconSetting,
|
||||
IconSetting
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -97,7 +114,7 @@ function renderDuration(submit_time, finishTime) {
|
||||
|
||||
// 返回带有样式的颜色标签
|
||||
return (
|
||||
<Tag color={color} size='large'>
|
||||
<Tag color={color} size='large' prefixIcon={<Clock size={14} />}>
|
||||
{durationSec} 秒
|
||||
</Tag>
|
||||
);
|
||||
@@ -188,19 +205,19 @@ const LogsTable = () => {
|
||||
switch (type) {
|
||||
case 'MUSIC':
|
||||
return (
|
||||
<Tag color='grey' size='large' shape='circle'>
|
||||
<Tag color='grey' size='large' shape='circle' prefixIcon={<Music size={14} />}>
|
||||
{t('生成音乐')}
|
||||
</Tag>
|
||||
);
|
||||
case 'LYRICS':
|
||||
return (
|
||||
<Tag color='pink' size='large' shape='circle'>
|
||||
<Tag color='pink' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
|
||||
{t('生成歌词')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large' shape='circle'>
|
||||
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -211,13 +228,13 @@ const LogsTable = () => {
|
||||
switch (type) {
|
||||
case 'suno':
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<Music size={14} />}>
|
||||
Suno
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large' shape='circle'>
|
||||
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -228,55 +245,55 @@ const LogsTable = () => {
|
||||
switch (type) {
|
||||
case 'SUCCESS':
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('成功')}
|
||||
</Tag>
|
||||
);
|
||||
case 'NOT_START':
|
||||
return (
|
||||
<Tag color='grey' size='large' shape='circle'>
|
||||
<Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
|
||||
{t('未启动')}
|
||||
</Tag>
|
||||
);
|
||||
case 'SUBMITTED':
|
||||
return (
|
||||
<Tag color='yellow' size='large' shape='circle'>
|
||||
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||
{t('队列中')}
|
||||
</Tag>
|
||||
);
|
||||
case 'IN_PROGRESS':
|
||||
return (
|
||||
<Tag color='blue' size='large' shape='circle'>
|
||||
<Tag color='blue' size='large' shape='circle' prefixIcon={<Play size={14} />}>
|
||||
{t('执行中')}
|
||||
</Tag>
|
||||
);
|
||||
case 'FAILURE':
|
||||
return (
|
||||
<Tag color='red' size='large' shape='circle'>
|
||||
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('失败')}
|
||||
</Tag>
|
||||
);
|
||||
case 'QUEUED':
|
||||
return (
|
||||
<Tag color='orange' size='large' shape='circle'>
|
||||
<Tag color='orange' size='large' shape='circle' prefixIcon={<List size={14} />}>
|
||||
{t('排队中')}
|
||||
</Tag>
|
||||
);
|
||||
case 'UNKNOWN':
|
||||
return (
|
||||
<Tag color='white' size='large' shape='circle'>
|
||||
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
case '':
|
||||
return (
|
||||
<Tag color='grey' size='large' shape='circle'>
|
||||
<Tag color='grey' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
|
||||
{t('正在提交')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large' shape='circle'>
|
||||
<Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -321,6 +338,7 @@ const LogsTable = () => {
|
||||
color={colors[parseInt(text) % colors.length]}
|
||||
size='large'
|
||||
shape='circle'
|
||||
prefixIcon={<Hash size={14} />}
|
||||
onClick={() => {
|
||||
copyText(text);
|
||||
}}
|
||||
@@ -395,7 +413,7 @@ const LogsTable = () => {
|
||||
percent={text ? parseInt(text.replace('%', '')) : 0}
|
||||
showInfo={true}
|
||||
aria-label='task progress'
|
||||
style={{ minWidth: '200px' }}
|
||||
style={{ minWidth: '160px' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -437,21 +455,43 @@ const LogsTable = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
|
||||
const [logType] = useState(0);
|
||||
|
||||
let now = new Date();
|
||||
// 初始化start_timestamp为前一天
|
||||
let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const [inputs, setInputs] = useState({
|
||||
|
||||
// Form 初始值
|
||||
const formInitValues = {
|
||||
channel_id: '',
|
||||
task_id: '',
|
||||
start_timestamp: timestamp2string(zeroNow.getTime() / 1000),
|
||||
end_timestamp: '',
|
||||
});
|
||||
const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
|
||||
dateRange: [
|
||||
timestamp2string(zeroNow.getTime() / 1000),
|
||||
timestamp2string(now.getTime() / 1000 + 3600)
|
||||
],
|
||||
};
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
// Form API 引用
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
|
||||
// 获取表单值的辅助函数
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
|
||||
// 处理时间范围
|
||||
let start_timestamp = timestamp2string(zeroNow.getTime() / 1000);
|
||||
let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
|
||||
|
||||
if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
|
||||
start_timestamp = formValues.dateRange[0];
|
||||
end_timestamp = formValues.dateRange[1];
|
||||
}
|
||||
|
||||
return {
|
||||
channel_id: formValues.channel_id || '',
|
||||
task_id: formValues.task_id || '',
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
};
|
||||
};
|
||||
|
||||
const setLogsFormat = (logs) => {
|
||||
@@ -469,6 +509,7 @@ const LogsTable = () => {
|
||||
setLoading(true);
|
||||
|
||||
let url = '';
|
||||
const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues();
|
||||
let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
|
||||
let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
|
||||
if (isAdminUser) {
|
||||
@@ -528,7 +569,7 @@ const LogsTable = () => {
|
||||
const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
|
||||
setPageSize(localPageSize);
|
||||
loadLogs(0, localPageSize).then();
|
||||
}, [logType]);
|
||||
}, []);
|
||||
|
||||
// 列选择器模态框
|
||||
const renderColumnSelector = () => {
|
||||
@@ -628,70 +669,93 @@ const LogsTable = () => {
|
||||
<Divider margin="12px" />
|
||||
|
||||
{/* 搜索表单区域 */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 时间选择器 */}
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
value={[start_timestamp, end_timestamp]}
|
||||
type='dateTimeRange'
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value) && value.length === 2) {
|
||||
handleInputChange(value[0], 'start_timestamp');
|
||||
handleInputChange(value[1], 'end_timestamp');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form
|
||||
initValues={formInitValues}
|
||||
getFormApi={(api) => setFormApi(api)}
|
||||
onSubmit={refresh}
|
||||
allowEmpty={true}
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
trigger="change"
|
||||
stopValidateWithError={false}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 时间选择器 */}
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<Form.DatePicker
|
||||
field='dateRange'
|
||||
className="w-full"
|
||||
type='dateTimeRange'
|
||||
placeholder={[t('开始时间'), t('结束时间')]}
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 任务 ID */}
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('任务 ID')}
|
||||
value={task_id}
|
||||
onChange={(value) => handleInputChange(value, 'task_id')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
|
||||
{/* 渠道 ID - 仅管理员可见 */}
|
||||
{isAdminUser && (
|
||||
<Input
|
||||
{/* 任务 ID */}
|
||||
<Form.Input
|
||||
field='task_id'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('渠道 ID')}
|
||||
value={channel_id}
|
||||
onChange={(value) => handleInputChange(value, 'channel_id')}
|
||||
placeholder={t('任务 ID')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<div></div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
{/* 渠道 ID - 仅管理员可见 */}
|
||||
{isAdminUser && (
|
||||
<Form.Input
|
||||
field='channel_id'
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('渠道 ID')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div></div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
loading={loading}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
onClick={() => {
|
||||
if (formApi) {
|
||||
formApi.reset();
|
||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className="!rounded-full"
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
shadows='always'
|
||||
@@ -705,6 +769,14 @@ const LogsTable = () => {
|
||||
scroll={{ x: 'max-content' }}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
pagination={{
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
renderGroup,
|
||||
renderQuota
|
||||
renderQuota,
|
||||
getQuotaPerUnit
|
||||
} from '../../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
@@ -14,13 +15,29 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Form,
|
||||
Modal,
|
||||
Space,
|
||||
SplitButtonGroup,
|
||||
Table,
|
||||
Tag,
|
||||
Input,
|
||||
Tag
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
|
||||
import {
|
||||
CheckCircle,
|
||||
Shield,
|
||||
XCircle,
|
||||
Clock,
|
||||
Gauge,
|
||||
HelpCircle,
|
||||
Infinity,
|
||||
Coins
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
IconPlus,
|
||||
@@ -32,7 +49,7 @@ import {
|
||||
IconDelete,
|
||||
IconStop,
|
||||
IconPlay,
|
||||
IconMore,
|
||||
IconMore
|
||||
} from '@douyinfe/semi-icons';
|
||||
import EditToken from '../../pages/Token/EditToken';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -49,38 +66,38 @@ const TokensTable = () => {
|
||||
case 1:
|
||||
if (model_limits_enabled) {
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
|
||||
{t('已启用:限制模型')}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color='green' size='large' shape='circle'>
|
||||
<Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' size='large' shape='circle'>
|
||||
<Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='yellow' size='large' shape='circle'>
|
||||
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
|
||||
{t('已过期')}
|
||||
</Tag>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<Tag color='grey' size='large' shape='circle'>
|
||||
<Tag color='grey' size='large' shape='circle' prefixIcon={<Gauge size={14} />}>
|
||||
{t('已耗尽')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='black' size='large' shape='circle'>
|
||||
<Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -111,21 +128,45 @@ const TokensTable = () => {
|
||||
title: t('已用额度'),
|
||||
dataIndex: 'used_quota',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderQuota(parseInt(text))}</div>;
|
||||
return (
|
||||
<div>
|
||||
<Tag size={'large'} color={'grey'} shape='circle' prefixIcon={<Coins size={14} />}>
|
||||
{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';
|
||||
} else {
|
||||
return 'green';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{record.unlimited_quota ? (
|
||||
<Tag size={'large'} color={'white'} shape='circle'>
|
||||
<Tag size={'large'} color={'white'} shape='circle' prefixIcon={<Infinity size={14} />}>
|
||||
{t('无限制')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag size={'large'} color={'light-blue'} shape='circle'>
|
||||
<Tag
|
||||
size={'large'}
|
||||
color={getQuotaColor(parseInt(text))}
|
||||
shape='circle'
|
||||
prefixIcon={<Coins size={14} />}
|
||||
>
|
||||
{renderQuota(parseInt(text))}
|
||||
</Tag>
|
||||
)}
|
||||
@@ -335,14 +376,29 @@ const TokensTable = () => {
|
||||
const [tokenCount, setTokenCount] = useState(pageSize);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searchToken, setSearchToken] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [chats, setChats] = useState([]);
|
||||
const [editingToken, setEditingToken] = useState({
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
// Form 初始值
|
||||
const formInitValues = {
|
||||
searchKeyword: '',
|
||||
searchToken: '',
|
||||
};
|
||||
|
||||
// Form API 引用
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
|
||||
// 获取表单值的辅助函数
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
return {
|
||||
searchKeyword: formValues.searchKeyword || '',
|
||||
searchToken: formValues.searchToken || '',
|
||||
};
|
||||
};
|
||||
|
||||
const closeEdit = () => {
|
||||
setShowEdit(false);
|
||||
setTimeout(() => {
|
||||
@@ -416,8 +472,6 @@ const TokensTable = () => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadTokens(0)
|
||||
.then()
|
||||
@@ -472,6 +526,7 @@ const TokensTable = () => {
|
||||
};
|
||||
|
||||
const searchTokens = async () => {
|
||||
const { searchKeyword, searchToken } = getFormValues();
|
||||
if (searchKeyword === '' && searchToken === '') {
|
||||
await loadTokens(0);
|
||||
setActivePage(1);
|
||||
@@ -491,14 +546,6 @@ const TokensTable = () => {
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (value) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const handleSearchTokenChange = async (value) => {
|
||||
setSearchToken(value.trim());
|
||||
};
|
||||
|
||||
const sortToken = (key) => {
|
||||
if (tokens.length === 0) return;
|
||||
setLoading(true);
|
||||
@@ -580,36 +627,65 @@ const TokensTable = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
|
||||
<div className="relative w-full md:w-56">
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('搜索关键字')}
|
||||
value={searchKeyword}
|
||||
onChange={handleKeywordChange}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
<Form
|
||||
initValues={formInitValues}
|
||||
getFormApi={(api) => setFormApi(api)}
|
||||
onSubmit={searchTokens}
|
||||
allowEmpty={true}
|
||||
autoComplete="off"
|
||||
layout="horizontal"
|
||||
trigger="change"
|
||||
stopValidateWithError={false}
|
||||
className="w-full md:w-auto order-1 md:order-2"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
||||
<div className="relative w-full md:w-56">
|
||||
<Form.Input
|
||||
field="searchKeyword"
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('搜索关键字')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full md:w-56">
|
||||
<Form.Input
|
||||
field="searchToken"
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('密钥')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={searching}
|
||||
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
onClick={() => {
|
||||
if (formApi) {
|
||||
formApi.reset();
|
||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||
setTimeout(() => {
|
||||
searchTokens();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full md:w-56">
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('密钥')}
|
||||
value={searchToken}
|
||||
onChange={handleSearchTokenChange}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={searchTokens}
|
||||
loading={searching}
|
||||
className="!rounded-full w-full md:w-auto"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -654,6 +730,14 @@ const TokensTable = () => {
|
||||
loading={loading}
|
||||
rowSelection={rowSelection}
|
||||
onRow={handleRow}
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
></Table>
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../../helpers';
|
||||
|
||||
import {
|
||||
User,
|
||||
Shield,
|
||||
Crown,
|
||||
HelpCircle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Minus,
|
||||
Coins,
|
||||
Activity,
|
||||
Users,
|
||||
DollarSign,
|
||||
UserPlus
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Input,
|
||||
Empty,
|
||||
Form,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Typography
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
@@ -23,7 +42,7 @@ import {
|
||||
IconMore,
|
||||
IconUserAdd,
|
||||
IconArrowUp,
|
||||
IconArrowDown,
|
||||
IconArrowDown
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import AddUser from '../../pages/User/AddUser';
|
||||
@@ -39,25 +58,25 @@ const UsersTable = () => {
|
||||
switch (role) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag size='large' color='blue' shape='circle'>
|
||||
<Tag size='large' color='blue' shape='circle' prefixIcon={<User size={14} />}>
|
||||
{t('普通用户')}
|
||||
</Tag>
|
||||
);
|
||||
case 10:
|
||||
return (
|
||||
<Tag color='yellow' size='large' shape='circle'>
|
||||
<Tag color='yellow' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
|
||||
{t('管理员')}
|
||||
</Tag>
|
||||
);
|
||||
case 100:
|
||||
return (
|
||||
<Tag color='orange' size='large' shape='circle'>
|
||||
<Tag color='orange' size='large' shape='circle' prefixIcon={<Crown size={14} />}>
|
||||
{t('超级管理员')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='red' size='large' shape='circle'>
|
||||
<Tag color='red' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知身份')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -67,16 +86,16 @@ const UsersTable = () => {
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return <Tag size='large' color='green' shape='circle'>{t('已激活')}</Tag>;
|
||||
return <Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>{t('已激活')}</Tag>;
|
||||
case 2:
|
||||
return (
|
||||
<Tag size='large' color='red' shape='circle'>
|
||||
<Tag size='large' color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
|
||||
{t('已封禁')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag size='large' color='grey' shape='circle'>
|
||||
<Tag size='large' color='grey' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
@@ -106,13 +125,13 @@ const UsersTable = () => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={1}>
|
||||
<Tag color='white' size='large' shape='circle' className="!text-xs">
|
||||
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Coins size={14} />}>
|
||||
{t('剩余')}: {renderQuota(record.quota)}
|
||||
</Tag>
|
||||
<Tag color='white' size='large' shape='circle' className="!text-xs">
|
||||
<Tag color='white' size='large' 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">
|
||||
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<Activity size={14} />}>
|
||||
{t('调用')}: {renderNumber(record.request_count)}
|
||||
</Tag>
|
||||
</Space>
|
||||
@@ -127,13 +146,13 @@ const UsersTable = () => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={1}>
|
||||
<Tag color='white' size='large' shape='circle' className="!text-xs">
|
||||
<Tag color='white' size='large' 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">
|
||||
<Tag color='white' size='large' 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">
|
||||
<Tag color='white' size='large' shape='circle' className="!text-xs" prefixIcon={<UserPlus size={14} />}>
|
||||
{record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
|
||||
</Tag>
|
||||
</Space>
|
||||
@@ -155,7 +174,7 @@ const UsersTable = () => {
|
||||
return (
|
||||
<div>
|
||||
{record.DeletedAt !== null ? (
|
||||
<Tag color='red' shape='circle'>{t('已注销')}</Tag>
|
||||
<Tag color='red' shape='circle' prefixIcon={<Minus size={14} />}>{t('已注销')}</Tag>
|
||||
) : (
|
||||
renderStatus(text)
|
||||
)}
|
||||
@@ -285,9 +304,7 @@ const UsersTable = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchGroup, setSearchGroup] = useState('');
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
|
||||
const [showAddUser, setShowAddUser] = useState(false);
|
||||
@@ -296,6 +313,24 @@ const UsersTable = () => {
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
// Form 初始值
|
||||
const formInitValues = {
|
||||
searchKeyword: '',
|
||||
searchGroup: '',
|
||||
};
|
||||
|
||||
// Form API 引用
|
||||
const [formApi, setFormApi] = useState(null);
|
||||
|
||||
// 获取表单值的辅助函数
|
||||
const getFormValues = () => {
|
||||
const formValues = formApi ? formApi.getValues() : {};
|
||||
return {
|
||||
searchKeyword: formValues.searchKeyword || '',
|
||||
searchGroup: formValues.searchGroup || '',
|
||||
};
|
||||
};
|
||||
|
||||
const removeRecord = (key) => {
|
||||
let newDataSource = [...users];
|
||||
if (key != null) {
|
||||
@@ -363,9 +398,16 @@ const UsersTable = () => {
|
||||
const searchUsers = async (
|
||||
startIdx,
|
||||
pageSize,
|
||||
searchKeyword,
|
||||
searchGroup,
|
||||
searchKeyword = null,
|
||||
searchGroup = null,
|
||||
) => {
|
||||
// 如果没有传递参数,从表单获取值
|
||||
if (searchKeyword === null || searchGroup === null) {
|
||||
const formValues = getFormValues();
|
||||
searchKeyword = formValues.searchKeyword;
|
||||
searchGroup = formValues.searchGroup;
|
||||
}
|
||||
|
||||
if (searchKeyword === '' && searchGroup === '') {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadUsers(startIdx, pageSize);
|
||||
@@ -387,12 +429,9 @@ const UsersTable = () => {
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (value) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setActivePage(page);
|
||||
const { searchKeyword, searchGroup } = getFormValues();
|
||||
if (searchKeyword === '' && searchGroup === '') {
|
||||
loadUsers(page, pageSize).then();
|
||||
} else {
|
||||
@@ -413,10 +452,11 @@ const UsersTable = () => {
|
||||
|
||||
const refresh = async () => {
|
||||
setActivePage(1);
|
||||
if (searchKeyword === '') {
|
||||
await loadUsers(activePage, pageSize);
|
||||
const { searchKeyword, searchGroup } = getFormValues();
|
||||
if (searchKeyword === '' && searchGroup === '') {
|
||||
await loadUsers(1, pageSize);
|
||||
} else {
|
||||
await searchUsers(activePage, pageSize, searchKeyword, searchGroup);
|
||||
await searchUsers(1, pageSize, searchKeyword, searchGroup);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -488,41 +528,76 @@ const UsersTable = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
|
||||
<div className="relative w-full md:w-64">
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
|
||||
value={searchKeyword}
|
||||
onChange={handleKeywordChange}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
/>
|
||||
<Form
|
||||
initValues={formInitValues}
|
||||
getFormApi={(api) => setFormApi(api)}
|
||||
onSubmit={() => {
|
||||
setActivePage(1);
|
||||
searchUsers(1, pageSize);
|
||||
}}
|
||||
allowEmpty={true}
|
||||
autoComplete="off"
|
||||
layout="horizontal"
|
||||
trigger="change"
|
||||
stopValidateWithError={false}
|
||||
className="w-full md:w-auto order-1 md:order-2"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
|
||||
<div className="relative w-full md:w-64">
|
||||
<Form.Input
|
||||
field="searchKeyword"
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
|
||||
className="!rounded-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-48">
|
||||
<Form.Select
|
||||
field="searchGroup"
|
||||
placeholder={t('选择分组')}
|
||||
optionList={groupOptions}
|
||||
onChange={(value) => {
|
||||
// 分组变化时自动搜索
|
||||
setTimeout(() => {
|
||||
setActivePage(1);
|
||||
searchUsers(1, pageSize);
|
||||
}, 100);
|
||||
}}
|
||||
className="!rounded-full w-full"
|
||||
showClear
|
||||
pure
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading || searching}
|
||||
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
onClick={() => {
|
||||
if (formApi) {
|
||||
formApi.reset();
|
||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
||||
setTimeout(() => {
|
||||
setActivePage(1);
|
||||
loadUsers(1, pageSize);
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="!rounded-full flex-1 md:flex-initial md:w-auto"
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:w-48">
|
||||
<Select
|
||||
placeholder={t('选择分组')}
|
||||
optionList={groupOptions}
|
||||
value={searchGroup}
|
||||
onChange={(value) => {
|
||||
setSearchGroup(value);
|
||||
searchUsers(activePage, pageSize, searchKeyword, value);
|
||||
}}
|
||||
className="!rounded-full w-full"
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
searchUsers(activePage, pageSize, searchKeyword, searchGroup);
|
||||
}}
|
||||
loading={searching}
|
||||
className="!rounded-full w-full md:w-auto"
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -570,6 +645,14 @@ const UsersTable = () => {
|
||||
}}
|
||||
loading={loading}
|
||||
onRow={handleRow}
|
||||
empty={
|
||||
<Empty
|
||||
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('搜索无结果')}
|
||||
style={{ padding: 30 }}
|
||||
/>
|
||||
}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user