mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 02:05:21 +00:00
Remove deprecated components and hooks
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -35,5 +35,4 @@
|
||||
# GitHub Linguist - Language Detection
|
||||
# ============================================
|
||||
# Mark web frontend as vendored so GitHub recognizes this as a Go project
|
||||
web/** linguist-vendored
|
||||
electron/** linguist-vendored
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Modal } from '@douyinfe/semi-ui';
|
||||
import { useSecureVerification } from '../../../hooks/common/useSecureVerification';
|
||||
import { createApiCalls } from '../../../services/secureVerification';
|
||||
import SecureVerificationModal from '../modals/SecureVerificationModal';
|
||||
import ChannelKeyDisplay from '../ui/ChannelKeyDisplay';
|
||||
|
||||
/**
|
||||
* 渠道密钥查看组件使用示例
|
||||
* 展示如何使用通用安全验证系统
|
||||
*/
|
||||
const ChannelKeyViewExample = ({ channelId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [keyData, setKeyData] = useState('');
|
||||
const [showKeyModal, setShowKeyModal] = useState(false);
|
||||
|
||||
// 使用通用安全验证 Hook
|
||||
const {
|
||||
isModalVisible,
|
||||
verificationMethods,
|
||||
verificationState,
|
||||
startVerification,
|
||||
executeVerification,
|
||||
cancelVerification,
|
||||
setVerificationCode,
|
||||
switchVerificationMethod,
|
||||
} = useSecureVerification({
|
||||
onSuccess: (result) => {
|
||||
// 验证成功后处理结果
|
||||
if (result.success && result.data?.key) {
|
||||
setKeyData(result.data.key);
|
||||
setShowKeyModal(true);
|
||||
}
|
||||
},
|
||||
successMessage: t('密钥获取成功'),
|
||||
});
|
||||
|
||||
// 开始查看密钥流程
|
||||
const handleViewKey = async () => {
|
||||
const apiCall = createApiCalls.viewChannelKey(channelId);
|
||||
|
||||
await startVerification(apiCall, {
|
||||
title: t('查看渠道密钥'),
|
||||
description: t('为了保护账户安全,请验证您的身份。'),
|
||||
preferredMethod: 'passkey', // 可以指定首选验证方式
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 查看密钥按钮 */}
|
||||
<Button type='primary' theme='outline' onClick={handleViewKey}>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
|
||||
{/* 安全验证模态框 */}
|
||||
<SecureVerificationModal
|
||||
visible={isModalVisible}
|
||||
verificationMethods={verificationMethods}
|
||||
verificationState={verificationState}
|
||||
onVerify={executeVerification}
|
||||
onCancel={cancelVerification}
|
||||
onCodeChange={setVerificationCode}
|
||||
onMethodSwitch={switchVerificationMethod}
|
||||
title={verificationState.title}
|
||||
description={verificationState.description}
|
||||
/>
|
||||
|
||||
{/* 密钥显示模态框 */}
|
||||
<Modal
|
||||
title={t('渠道密钥信息')}
|
||||
visible={showKeyModal}
|
||||
onCancel={() => setShowKeyModal(false)}
|
||||
footer={
|
||||
<Button type='primary' onClick={() => setShowKeyModal(false)}>
|
||||
{t('完成')}
|
||||
</Button>
|
||||
}
|
||||
width={700}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<ChannelKeyDisplay
|
||||
keyData={keyData}
|
||||
showSuccessIcon={true}
|
||||
successText={t('密钥获取成功')}
|
||||
showWarning={true}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelKeyViewExample;
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal, Button, Input, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
/**
|
||||
* 可复用的两步验证模态框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.visible - 是否显示模态框
|
||||
* @param {string} props.code - 验证码值
|
||||
* @param {boolean} props.loading - 是否正在验证
|
||||
* @param {Function} props.onCodeChange - 验证码变化回调
|
||||
* @param {Function} props.onVerify - 验证回调
|
||||
* @param {Function} props.onCancel - 取消回调
|
||||
* @param {string} props.title - 模态框标题
|
||||
* @param {string} props.description - 验证描述文本
|
||||
* @param {string} props.placeholder - 输入框占位文本
|
||||
*/
|
||||
const TwoFactorAuthModal = ({
|
||||
visible,
|
||||
code,
|
||||
loading,
|
||||
onCodeChange,
|
||||
onVerify,
|
||||
onCancel,
|
||||
title,
|
||||
description,
|
||||
placeholder,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && code && !loading) {
|
||||
onVerify();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className='flex items-center'>
|
||||
<div className='w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3'>
|
||||
<svg
|
||||
className='w-4 h-4 text-blue-600 dark:text-blue-400'
|
||||
fill='currentColor'
|
||||
viewBox='0 0 20 20'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{title || t('安全验证')}
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onCancel}>{t('取消')}</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
loading={loading}
|
||||
disabled={!code || loading}
|
||||
onClick={onVerify}
|
||||
>
|
||||
{t('验证')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={500}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{/* 安全提示 */}
|
||||
<div className='bg-blue-50 dark:bg-blue-900 rounded-lg p-4'>
|
||||
<div className='flex items-start'>
|
||||
<svg
|
||||
className='w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0'
|
||||
fill='currentColor'
|
||||
viewBox='0 0 20 20'
|
||||
>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
d='M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
|
||||
clipRule='evenodd'
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<Typography.Text
|
||||
strong
|
||||
className='text-blue-800 dark:text-blue-200'
|
||||
>
|
||||
{t('安全验证')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className='block text-blue-700 dark:text-blue-300 text-sm mt-1'>
|
||||
{description || t('为了保护账户安全,请验证您的两步验证码。')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 验证码输入 */}
|
||||
<div>
|
||||
<Typography.Text strong className='block mb-2'>
|
||||
{t('验证身份')}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
placeholder={placeholder || t('请输入认证器验证码或备用码')}
|
||||
value={code}
|
||||
onChange={onCodeChange}
|
||||
size='large'
|
||||
maxLength={8}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
<Typography.Text type='tertiary' size='small' className='mt-2 block'>
|
||||
{t(
|
||||
'支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。',
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoFactorAuthModal;
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
export { default as SettingsPanel } from './SettingsPanel';
|
||||
export { default as ChatArea } from './ChatArea';
|
||||
export { default as DebugPanel } from './DebugPanel';
|
||||
export { default as MessageContent } from './MessageContent';
|
||||
export { default as MessageActions } from './MessageActions';
|
||||
export { default as CustomInputRender } from './CustomInputRender';
|
||||
export { default as SSEViewer } from './SSEViewer';
|
||||
export { default as ParameterControl } from './ParameterControl';
|
||||
export { default as ImageUrlInput } from './ImageUrlInput';
|
||||
export { default as FloatingButtons } from './FloatingButtons';
|
||||
export { default as ConfigManager } from './ConfigManager';
|
||||
|
||||
export {
|
||||
saveConfig,
|
||||
loadConfig,
|
||||
clearConfig,
|
||||
hasStoredConfig,
|
||||
getConfigTimestamp,
|
||||
exportConfig,
|
||||
importConfig,
|
||||
} from './configStorage';
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Empty,
|
||||
Skeleton,
|
||||
Space,
|
||||
Tag,
|
||||
Collapsible,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Typography,
|
||||
Avatar,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoContent,
|
||||
IllustrationNoContentDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { renderModelTag, getModelCategories } from '../../../../helpers';
|
||||
|
||||
const ModelsList = ({ t, models, modelsLoading, copyText }) => {
|
||||
const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
|
||||
// Initialize from localStorage if available
|
||||
const savedState = localStorage.getItem('modelsExpanded');
|
||||
return savedState ? JSON.parse(savedState) : false;
|
||||
});
|
||||
const [activeModelCategory, setActiveModelCategory] = useState('all');
|
||||
const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
|
||||
|
||||
// Save models expanded state to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded));
|
||||
}, [isModelsExpanded]);
|
||||
|
||||
return (
|
||||
<div className='py-4'>
|
||||
{/* 卡片头部 */}
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='green' className='mr-3 shadow-md'>
|
||||
<Settings size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('可用模型')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600'>
|
||||
{t('查看当前可用的所有模型')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可用模型部分 */}
|
||||
<div className='bg-gray-50 dark:bg-gray-800 rounded-xl'>
|
||||
{modelsLoading ? (
|
||||
// 骨架屏加载状态 - 模拟实际加载后的布局
|
||||
<div className='space-y-4'>
|
||||
{/* 模拟分类标签 */}
|
||||
<div
|
||||
className='mb-4'
|
||||
style={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
>
|
||||
<div className='flex overflow-x-auto py-2 gap-2'>
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<Skeleton.Button
|
||||
key={`cat-${index}`}
|
||||
style={{
|
||||
width: index === 0 ? 130 : 100 + Math.random() * 50,
|
||||
height: 36,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模拟模型标签列表 */}
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{Array.from({ length: 20 }).map((_, index) => (
|
||||
<Skeleton.Button
|
||||
key={`model-${index}`}
|
||||
style={{
|
||||
width: 100 + Math.random() * 100,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
margin: '4px',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : models.length === 0 ? (
|
||||
<div className='py-8'>
|
||||
<Empty
|
||||
image={
|
||||
<IllustrationNoContent style={{ width: 150, height: 150 }} />
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoContentDark
|
||||
style={{ width: 150, height: 150 }}
|
||||
/>
|
||||
}
|
||||
description={t('没有可用模型')}
|
||||
style={{ padding: '24px 0' }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 模型分类标签页 */}
|
||||
<div className='mb-4'>
|
||||
<Tabs
|
||||
type='card'
|
||||
activeKey={activeModelCategory}
|
||||
onChange={(key) => setActiveModelCategory(key)}
|
||||
className='mt-2'
|
||||
collapsible
|
||||
>
|
||||
{Object.entries(getModelCategories(t)).map(
|
||||
([key, category]) => {
|
||||
// 计算该分类下的模型数量
|
||||
const modelCount =
|
||||
key === 'all'
|
||||
? models.length
|
||||
: models.filter((model) =>
|
||||
category.filter({ model_name: model }),
|
||||
).length;
|
||||
|
||||
if (modelCount === 0 && key !== 'all') return null;
|
||||
|
||||
return (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className='flex items-center gap-2'>
|
||||
{category.icon && (
|
||||
<span className='w-4 h-4'>{category.icon}</span>
|
||||
)}
|
||||
{category.label}
|
||||
<Tag
|
||||
color={
|
||||
activeModelCategory === key ? 'red' : 'grey'
|
||||
}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{modelCount}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
itemKey={key}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className='bg-white dark:bg-gray-700 rounded-lg p-3'>
|
||||
{(() => {
|
||||
// 根据当前选中的分类过滤模型
|
||||
const categories = getModelCategories(t);
|
||||
const filteredModels =
|
||||
activeModelCategory === 'all'
|
||||
? models
|
||||
: models.filter((model) =>
|
||||
categories[activeModelCategory].filter({
|
||||
model_name: model,
|
||||
}),
|
||||
);
|
||||
|
||||
// 如果过滤后没有模型,显示空状态
|
||||
if (filteredModels.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
image={
|
||||
<IllustrationNoContent
|
||||
style={{ width: 120, height: 120 }}
|
||||
/>
|
||||
}
|
||||
darkModeImage={
|
||||
<IllustrationNoContentDark
|
||||
style={{ width: 120, height: 120 }}
|
||||
/>
|
||||
}
|
||||
description={t('该分类下没有可用模型')}
|
||||
style={{ padding: '16px 0' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
|
||||
return (
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) =>
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
}),
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Collapsible isOpen={isModelsExpanded}>
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) =>
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
}),
|
||||
)}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className='cursor-pointer !rounded-lg'
|
||||
onClick={() => setIsModelsExpanded(false)}
|
||||
icon={<IconChevronUp />}
|
||||
>
|
||||
{t('收起')}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Collapsible>
|
||||
{!isModelsExpanded && (
|
||||
<Space wrap>
|
||||
{filteredModels
|
||||
.slice(0, MODELS_DISPLAY_COUNT)
|
||||
.map((model) =>
|
||||
renderModelTag(model, {
|
||||
size: 'small',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
}),
|
||||
)}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className='cursor-pointer !rounded-lg'
|
||||
onClick={() => setIsModelsExpanded(true)}
|
||||
icon={<IconChevronDown />}
|
||||
>
|
||||
{t('更多')}{' '}
|
||||
{filteredModels.length - MODELS_DISPLAY_COUNT}{' '}
|
||||
{t('个模型')}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsList;
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { Layers } from 'lucide-react';
|
||||
import CompactModeToggle from '../../common/ui/CompactModeToggle';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ModelsDescription = ({ compactMode, setCompactMode, t }) => {
|
||||
return (
|
||||
<div className='flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full'>
|
||||
<div className='flex items-center text-green-500'>
|
||||
<Layers size={16} className='mr-2' />
|
||||
<Text>{t('模型管理')}</Text>
|
||||
</div>
|
||||
|
||||
<CompactModeToggle
|
||||
compactMode={compactMode}
|
||||
setCompactMode={setCompactMode}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsDescription;
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Modal, Select, Space, Typography } from '@douyinfe/semi-ui';
|
||||
import { API, showError, showSuccess } from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const BindSubscriptionModal = ({ visible, onCancel, user, t, onSuccess }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [selectedPlanId, setSelectedPlanId] = useState(null);
|
||||
|
||||
const loadPlans = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.get('/api/subscription/admin/plans');
|
||||
if (res.data?.success) {
|
||||
setPlans(res.data.data || []);
|
||||
} else {
|
||||
showError(res.data?.message || t('加载失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
showError(t('请求失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setSelectedPlanId(null);
|
||||
loadPlans();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const planOptions = useMemo(() => {
|
||||
return (plans || []).map((p) => ({
|
||||
label: `${p?.plan?.title || ''} (${p?.plan?.currency || 'USD'} ${Number(p?.plan?.price_amount || 0)})`,
|
||||
value: p?.plan?.id,
|
||||
}));
|
||||
}, [plans]);
|
||||
|
||||
const bind = async () => {
|
||||
if (!user?.id) {
|
||||
showError(t('用户信息缺失'));
|
||||
return;
|
||||
}
|
||||
if (!selectedPlanId) {
|
||||
showError(t('请选择订阅套餐'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/subscription/admin/bind', {
|
||||
user_id: user.id,
|
||||
plan_id: selectedPlanId,
|
||||
});
|
||||
if (res.data?.success) {
|
||||
showSuccess(t('绑定成功'));
|
||||
onSuccess?.();
|
||||
onCancel?.();
|
||||
} else {
|
||||
showError(res.data?.message || t('绑定失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
showError(t('请求失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('绑定订阅套餐')}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={bind}
|
||||
confirmLoading={loading}
|
||||
maskClosable={false}
|
||||
centered
|
||||
>
|
||||
<Space vertical style={{ width: '100%' }} spacing='medium'>
|
||||
<div className='text-sm'>
|
||||
<Text strong>{t('用户')}:</Text>
|
||||
<Text>{user?.username}</Text>
|
||||
<Text type='tertiary'> (ID: {user?.id})</Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={t('选择订阅套餐')}
|
||||
optionList={planOptions}
|
||||
value={selectedPlanId}
|
||||
onChange={setSelectedPlanId}
|
||||
loading={loading}
|
||||
filter
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<div className='text-xs text-gray-500'>
|
||||
{t('绑定后会立即生成用户订阅(无需支付),有效期按套餐配置计算。')}
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default BindSubscriptionModal;
|
||||
@@ -1,312 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { API } from '../../helpers';
|
||||
import { showError } from '../../helpers';
|
||||
|
||||
export const useDeploymentResources = () => {
|
||||
const [hardwareTypes, setHardwareTypes] = useState([]);
|
||||
const [hardwareTotalAvailable, setHardwareTotalAvailable] = useState(0);
|
||||
const [locations, setLocations] = useState([]);
|
||||
const [locationsTotalAvailable, setLocationsTotalAvailable] = useState(0);
|
||||
const [availableReplicas, setAvailableReplicas] = useState([]);
|
||||
const [priceEstimation, setPriceEstimation] = useState(null);
|
||||
|
||||
const [loadingHardware, setLoadingHardware] = useState(false);
|
||||
const [loadingLocations, setLoadingLocations] = useState(false);
|
||||
const [loadingReplicas, setLoadingReplicas] = useState(false);
|
||||
const [loadingPrice, setLoadingPrice] = useState(false);
|
||||
|
||||
const fetchHardwareTypes = useCallback(async () => {
|
||||
try {
|
||||
setLoadingHardware(true);
|
||||
const response = await API.get('/api/deployments/hardware-types');
|
||||
if (response.data.success) {
|
||||
const { hardware_types: hardwareList = [], total_available } =
|
||||
response.data.data || {};
|
||||
const normalizedHardware = hardwareList.map((hardware) => {
|
||||
const availableCountValue = Number(hardware.available_count);
|
||||
const availableCount = Number.isNaN(availableCountValue)
|
||||
? 0
|
||||
: availableCountValue;
|
||||
const availableBool =
|
||||
typeof hardware.available === 'boolean'
|
||||
? hardware.available
|
||||
: availableCount > 0;
|
||||
|
||||
return {
|
||||
...hardware,
|
||||
available: availableBool,
|
||||
available_count: availableCount,
|
||||
};
|
||||
});
|
||||
|
||||
const providedTotal = Number(total_available);
|
||||
const fallbackTotal = normalizedHardware.reduce(
|
||||
(acc, item) =>
|
||||
acc +
|
||||
(Number.isNaN(item.available_count) ? 0 : item.available_count),
|
||||
0,
|
||||
);
|
||||
const hasProvidedTotal =
|
||||
total_available !== undefined &&
|
||||
total_available !== null &&
|
||||
total_available !== '' &&
|
||||
!Number.isNaN(providedTotal);
|
||||
|
||||
setHardwareTypes(normalizedHardware);
|
||||
setHardwareTotalAvailable(
|
||||
hasProvidedTotal ? providedTotal : fallbackTotal,
|
||||
);
|
||||
return normalizedHardware;
|
||||
} else {
|
||||
showError('获取硬件类型失败: ' + response.data.message);
|
||||
setHardwareTotalAvailable(0);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
showError('获取硬件类型失败: ' + error.message);
|
||||
setHardwareTotalAvailable(0);
|
||||
return [];
|
||||
} finally {
|
||||
setLoadingHardware(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchLocations = useCallback(async (hardwareId, gpuCount = 1) => {
|
||||
if (!hardwareId) {
|
||||
setLocations([]);
|
||||
setLocationsTotalAvailable(0);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
setLoadingLocations(true);
|
||||
const response = await API.get(
|
||||
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
|
||||
);
|
||||
if (response.data.success) {
|
||||
const replicas = response.data.data?.replicas || [];
|
||||
const nextLocationsMap = new Map();
|
||||
replicas.forEach((replica) => {
|
||||
const rawId = replica?.location_id ?? replica?.location?.id;
|
||||
if (rawId === null || rawId === undefined) return;
|
||||
|
||||
const mapKey = String(rawId);
|
||||
if (nextLocationsMap.has(mapKey)) return;
|
||||
|
||||
const rawIso2 =
|
||||
replica?.iso2 ?? replica?.location_iso2 ?? replica?.location?.iso2;
|
||||
const iso2 = rawIso2 ? String(rawIso2).toUpperCase() : '';
|
||||
const name =
|
||||
replica?.location_name ??
|
||||
replica?.location?.name ??
|
||||
replica?.name ??
|
||||
String(rawId);
|
||||
|
||||
nextLocationsMap.set(mapKey, {
|
||||
id: rawId,
|
||||
name: String(name),
|
||||
iso2,
|
||||
region:
|
||||
replica?.region ??
|
||||
replica?.location_region ??
|
||||
replica?.location?.region,
|
||||
country:
|
||||
replica?.country ??
|
||||
replica?.location_country ??
|
||||
replica?.location?.country,
|
||||
code:
|
||||
replica?.code ??
|
||||
replica?.location_code ??
|
||||
replica?.location?.code,
|
||||
available: Number(replica?.available_count) || 0,
|
||||
});
|
||||
});
|
||||
|
||||
const normalizedLocations = Array.from(nextLocationsMap.values());
|
||||
setLocations(normalizedLocations);
|
||||
setLocationsTotalAvailable(
|
||||
normalizedLocations.reduce(
|
||||
(acc, item) => acc + (item.available || 0),
|
||||
0,
|
||||
),
|
||||
);
|
||||
return normalizedLocations;
|
||||
} else {
|
||||
showError('获取部署位置失败: ' + response.data.message);
|
||||
setLocationsTotalAvailable(0);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
showError('获取部署位置失败: ' + error.message);
|
||||
setLocationsTotalAvailable(0);
|
||||
return [];
|
||||
} finally {
|
||||
setLoadingLocations(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchAvailableReplicas = useCallback(
|
||||
async (hardwareId, gpuCount = 1) => {
|
||||
if (!hardwareId) {
|
||||
setAvailableReplicas([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
setLoadingReplicas(true);
|
||||
const response = await API.get(
|
||||
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
|
||||
);
|
||||
if (response.data.success) {
|
||||
const replicas = response.data.data.replicas || [];
|
||||
setAvailableReplicas(replicas);
|
||||
return replicas;
|
||||
} else {
|
||||
showError('获取可用资源失败: ' + response.data.message);
|
||||
setAvailableReplicas([]);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load available replicas error:', error);
|
||||
setAvailableReplicas([]);
|
||||
return [];
|
||||
} finally {
|
||||
setLoadingReplicas(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const calculatePrice = useCallback(async (params) => {
|
||||
const {
|
||||
locationIds,
|
||||
hardwareId,
|
||||
gpusPerContainer,
|
||||
durationHours,
|
||||
replicaCount,
|
||||
} = params;
|
||||
|
||||
if (
|
||||
!locationIds?.length ||
|
||||
!hardwareId ||
|
||||
!gpusPerContainer ||
|
||||
!durationHours ||
|
||||
!replicaCount
|
||||
) {
|
||||
setPriceEstimation(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoadingPrice(true);
|
||||
const requestData = {
|
||||
location_ids: locationIds,
|
||||
hardware_id: hardwareId,
|
||||
gpus_per_container: gpusPerContainer,
|
||||
duration_hours: durationHours,
|
||||
replica_count: replicaCount,
|
||||
};
|
||||
|
||||
const response = await API.post(
|
||||
'/api/deployments/price-estimation',
|
||||
requestData,
|
||||
);
|
||||
if (response.data.success) {
|
||||
const estimation = response.data.data;
|
||||
setPriceEstimation(estimation);
|
||||
return estimation;
|
||||
} else {
|
||||
showError('价格计算失败: ' + response.data.message);
|
||||
setPriceEstimation(null);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Price calculation error:', error);
|
||||
setPriceEstimation(null);
|
||||
return null;
|
||||
} finally {
|
||||
setLoadingPrice(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkClusterNameAvailability = useCallback(async (name) => {
|
||||
if (!name?.trim()) return false;
|
||||
|
||||
try {
|
||||
const response = await API.get(
|
||||
`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`,
|
||||
);
|
||||
if (response.data.success) {
|
||||
return response.data.data.available;
|
||||
} else {
|
||||
showError('检查名称可用性失败: ' + response.data.message);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Check cluster name availability error:', error);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const createDeployment = useCallback(async (deploymentData) => {
|
||||
try {
|
||||
const response = await API.post('/api/deployments', deploymentData);
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data.message || '创建部署失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// Data
|
||||
hardwareTypes,
|
||||
hardwareTotalAvailable,
|
||||
locations,
|
||||
locationsTotalAvailable,
|
||||
availableReplicas,
|
||||
priceEstimation,
|
||||
|
||||
// Loading states
|
||||
loadingHardware,
|
||||
loadingLocations,
|
||||
loadingReplicas,
|
||||
loadingPrice,
|
||||
|
||||
// Functions
|
||||
fetchHardwareTypes,
|
||||
fetchLocations,
|
||||
fetchAvailableReplicas,
|
||||
calculatePrice,
|
||||
checkClusterNameAvailability,
|
||||
createDeployment,
|
||||
|
||||
// Clear functions
|
||||
clearPriceEstimation: () => setPriceEstimation(null),
|
||||
clearAvailableReplicas: () => setAvailableReplicas([]),
|
||||
};
|
||||
};
|
||||
|
||||
export default useDeploymentResources;
|
||||
@@ -1,286 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { API, showError, showSuccess } from '../../helpers';
|
||||
|
||||
export const useEnhancedDeploymentActions = (t) => {
|
||||
const [loading, setLoading] = useState({});
|
||||
|
||||
// Set loading state for specific operation
|
||||
const setOperationLoading = (operation, deploymentId, isLoading) => {
|
||||
setLoading((prev) => ({
|
||||
...prev,
|
||||
[`${operation}_${deploymentId}`]: isLoading,
|
||||
}));
|
||||
};
|
||||
|
||||
// Get loading state for specific operation
|
||||
const isOperationLoading = (operation, deploymentId) => {
|
||||
return loading[`${operation}_${deploymentId}`] || false;
|
||||
};
|
||||
|
||||
// Extend deployment duration
|
||||
const extendDeployment = async (deploymentId, durationHours) => {
|
||||
try {
|
||||
setOperationLoading('extend', deploymentId, true);
|
||||
|
||||
const response = await API.post(
|
||||
`/api/deployments/${deploymentId}/extend`,
|
||||
{
|
||||
duration_hours: durationHours,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
showSuccess(t('容器时长延长成功'));
|
||||
return response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
showError(
|
||||
t('延长时长失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('extend', deploymentId, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get deployment details
|
||||
const getDeploymentDetails = async (deploymentId) => {
|
||||
try {
|
||||
setOperationLoading('details', deploymentId, true);
|
||||
|
||||
const response = await API.get(`/api/deployments/${deploymentId}`);
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
showError(
|
||||
t('获取详情失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('details', deploymentId, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get deployment logs
|
||||
const getDeploymentLogs = async (deploymentId, options = {}) => {
|
||||
try {
|
||||
setOperationLoading('logs', deploymentId, true);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options.containerId)
|
||||
params.append('container_id', options.containerId);
|
||||
if (options.level) params.append('level', options.level);
|
||||
if (options.limit) params.append('limit', options.limit.toString());
|
||||
if (options.cursor) params.append('cursor', options.cursor);
|
||||
if (options.follow) params.append('follow', 'true');
|
||||
if (options.startTime) params.append('start_time', options.startTime);
|
||||
if (options.endTime) params.append('end_time', options.endTime);
|
||||
|
||||
const response = await API.get(
|
||||
`/api/deployments/${deploymentId}/logs?${params}`,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
showError(
|
||||
t('获取日志失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('logs', deploymentId, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Update deployment configuration
|
||||
const updateDeploymentConfig = async (deploymentId, config) => {
|
||||
try {
|
||||
setOperationLoading('config', deploymentId, true);
|
||||
|
||||
const response = await API.put(
|
||||
`/api/deployments/${deploymentId}`,
|
||||
config,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
showSuccess(t('容器配置更新成功'));
|
||||
return response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
showError(
|
||||
t('更新配置失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('config', deploymentId, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete (destroy) deployment
|
||||
const deleteDeployment = async (deploymentId) => {
|
||||
try {
|
||||
setOperationLoading('delete', deploymentId, true);
|
||||
|
||||
const response = await API.delete(`/api/deployments/${deploymentId}`);
|
||||
|
||||
if (response.data.success) {
|
||||
showSuccess(t('容器销毁请求已提交'));
|
||||
return response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
showError(
|
||||
t('销毁容器失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('delete', deploymentId, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Update deployment name
|
||||
const updateDeploymentName = async (deploymentId, newName) => {
|
||||
try {
|
||||
setOperationLoading('rename', deploymentId, true);
|
||||
|
||||
const response = await API.put(`/api/deployments/${deploymentId}/name`, {
|
||||
name: newName,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
showSuccess(t('容器名称更新成功'));
|
||||
return response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
showError(
|
||||
t('更新名称失败') +
|
||||
': ' +
|
||||
(error.response?.data?.message || error.message),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('rename', deploymentId, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Batch operations
|
||||
const batchDelete = async (deploymentIds) => {
|
||||
try {
|
||||
setOperationLoading('batch_delete', 'all', true);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
deploymentIds.map((id) => deleteDeployment(id)),
|
||||
);
|
||||
|
||||
const successful = results.filter((r) => r.status === 'fulfilled').length;
|
||||
const failed = results.filter((r) => r.status === 'rejected').length;
|
||||
|
||||
if (successful > 0) {
|
||||
showSuccess(
|
||||
t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
|
||||
success: successful,
|
||||
failed: failed,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return { successful, failed };
|
||||
} catch (error) {
|
||||
showError(t('批量操作失败') + ': ' + error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('batch_delete', 'all', false);
|
||||
}
|
||||
};
|
||||
|
||||
// Export logs
|
||||
const exportLogs = async (deploymentId, options = {}) => {
|
||||
try {
|
||||
setOperationLoading('export_logs', deploymentId, true);
|
||||
|
||||
const logs = await getDeploymentLogs(deploymentId, {
|
||||
...options,
|
||||
limit: 10000, // Get more logs for export
|
||||
});
|
||||
|
||||
if (logs && logs.logs) {
|
||||
const logText = logs.logs
|
||||
.map(
|
||||
(log) =>
|
||||
`[${new Date(log.timestamp).toISOString()}] [${log.level}] ${log.source ? `[${log.source}] ` : ''}${log.message}`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([logText], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `deployment-${deploymentId}-logs-${new Date().toISOString().split('T')[0]}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showSuccess(t('日志导出成功'));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('导出日志失败') + ': ' + error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
setOperationLoading('export_logs', deploymentId, false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// Actions
|
||||
extendDeployment,
|
||||
getDeploymentDetails,
|
||||
getDeploymentLogs,
|
||||
updateDeploymentConfig,
|
||||
deleteDeployment,
|
||||
updateDeploymentName,
|
||||
batchDelete,
|
||||
exportLogs,
|
||||
|
||||
// Loading states
|
||||
isOperationLoading,
|
||||
loading,
|
||||
|
||||
// Utility
|
||||
setOperationLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEnhancedDeploymentActions;
|
||||
@@ -1,488 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Switch,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Avatar,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showSuccess, showError } from '../../../helpers';
|
||||
import { StatusContext } from '../../../context/Status';
|
||||
import { UserContext } from '../../../context/User';
|
||||
import { useUserPermissions } from '../../../hooks/common/useUserPermissions';
|
||||
import { mergeAdminConfig, useSidebar } from '../../../hooks/common/useSidebar';
|
||||
import { Settings } from 'lucide-react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function SettingsSidebarModulesUser() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [statusState] = useContext(StatusContext);
|
||||
|
||||
// 使用后端权限验证替代前端角色判断
|
||||
const {
|
||||
permissions,
|
||||
loading: permissionsLoading,
|
||||
hasSidebarSettingsPermission,
|
||||
isSidebarSectionAllowed,
|
||||
isSidebarModuleAllowed,
|
||||
} = useUserPermissions();
|
||||
|
||||
// 使用useSidebar钩子获取刷新方法
|
||||
const { refreshUserConfig } = useSidebar();
|
||||
|
||||
// 如果没有边栏设置权限,不显示此组件
|
||||
if (!permissionsLoading && !hasSidebarSettingsPermission()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 权限加载中,显示加载状态
|
||||
if (permissionsLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 根据用户权限生成默认配置
|
||||
const generateDefaultConfig = () => {
|
||||
const defaultConfig = {};
|
||||
|
||||
// 聊天区域 - 所有用户都可以访问
|
||||
if (isSidebarSectionAllowed('chat')) {
|
||||
defaultConfig.chat = {
|
||||
enabled: true,
|
||||
playground: isSidebarModuleAllowed('chat', 'playground'),
|
||||
chat: isSidebarModuleAllowed('chat', 'chat'),
|
||||
};
|
||||
}
|
||||
|
||||
// 控制台区域 - 所有用户都可以访问
|
||||
if (isSidebarSectionAllowed('console')) {
|
||||
defaultConfig.console = {
|
||||
enabled: true,
|
||||
detail: isSidebarModuleAllowed('console', 'detail'),
|
||||
token: isSidebarModuleAllowed('console', 'token'),
|
||||
log: isSidebarModuleAllowed('console', 'log'),
|
||||
midjourney: isSidebarModuleAllowed('console', 'midjourney'),
|
||||
task: isSidebarModuleAllowed('console', 'task'),
|
||||
};
|
||||
}
|
||||
|
||||
// 个人中心区域 - 所有用户都可以访问
|
||||
if (isSidebarSectionAllowed('personal')) {
|
||||
defaultConfig.personal = {
|
||||
enabled: true,
|
||||
topup: isSidebarModuleAllowed('personal', 'topup'),
|
||||
personal: isSidebarModuleAllowed('personal', 'personal'),
|
||||
};
|
||||
}
|
||||
|
||||
// 管理员区域 - 只有管理员可以访问
|
||||
if (isSidebarSectionAllowed('admin')) {
|
||||
defaultConfig.admin = {
|
||||
enabled: true,
|
||||
channel: isSidebarModuleAllowed('admin', 'channel'),
|
||||
models: isSidebarModuleAllowed('admin', 'models'),
|
||||
deployment: isSidebarModuleAllowed('admin', 'deployment'),
|
||||
redemption: isSidebarModuleAllowed('admin', 'redemption'),
|
||||
user: isSidebarModuleAllowed('admin', 'user'),
|
||||
subscription: isSidebarModuleAllowed('admin', 'subscription'),
|
||||
setting: isSidebarModuleAllowed('admin', 'setting'),
|
||||
};
|
||||
}
|
||||
|
||||
return defaultConfig;
|
||||
};
|
||||
|
||||
// 用户个人左侧边栏模块设置
|
||||
const [sidebarModulesUser, setSidebarModulesUser] = useState({});
|
||||
|
||||
// 管理员全局配置
|
||||
const [adminConfig, setAdminConfig] = useState(null);
|
||||
|
||||
// 处理区域级别开关变更
|
||||
function handleSectionChange(sectionKey) {
|
||||
return (checked) => {
|
||||
const newModules = {
|
||||
...sidebarModulesUser,
|
||||
[sectionKey]: {
|
||||
...sidebarModulesUser[sectionKey],
|
||||
enabled: checked,
|
||||
},
|
||||
};
|
||||
setSidebarModulesUser(newModules);
|
||||
console.log('用户边栏区域配置变更:', sectionKey, checked, newModules);
|
||||
};
|
||||
}
|
||||
|
||||
// 处理功能级别开关变更
|
||||
function handleModuleChange(sectionKey, moduleKey) {
|
||||
return (checked) => {
|
||||
const newModules = {
|
||||
...sidebarModulesUser,
|
||||
[sectionKey]: {
|
||||
...sidebarModulesUser[sectionKey],
|
||||
[moduleKey]: checked,
|
||||
},
|
||||
};
|
||||
setSidebarModulesUser(newModules);
|
||||
console.log(
|
||||
'用户边栏功能配置变更:',
|
||||
sectionKey,
|
||||
moduleKey,
|
||||
checked,
|
||||
newModules,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 重置为默认配置(基于权限过滤)
|
||||
function resetSidebarModules() {
|
||||
const defaultConfig = generateDefaultConfig();
|
||||
setSidebarModulesUser(defaultConfig);
|
||||
showSuccess(t('已重置为默认配置'));
|
||||
console.log('用户边栏配置重置为默认:', defaultConfig);
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function onSubmit() {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('保存用户边栏配置:', sidebarModulesUser);
|
||||
const res = await API.put('/api/user/self', {
|
||||
sidebar_modules: JSON.stringify(sidebarModulesUser),
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('保存成功'));
|
||||
console.log('用户边栏配置保存成功');
|
||||
|
||||
// 刷新useSidebar钩子中的用户配置,实现实时更新
|
||||
await refreshUserConfig();
|
||||
console.log('用户边栏配置已刷新,边栏将立即更新');
|
||||
} else {
|
||||
showError(message);
|
||||
console.error('用户边栏配置保存失败:', message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('保存失败,请重试'));
|
||||
console.error('用户边栏配置保存异常:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的配置加载逻辑
|
||||
useEffect(() => {
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
// 获取管理员全局配置
|
||||
if (statusState?.status?.SidebarModulesAdmin) {
|
||||
try {
|
||||
const adminConf = JSON.parse(
|
||||
statusState.status.SidebarModulesAdmin,
|
||||
);
|
||||
const mergedAdminConf = mergeAdminConfig(adminConf);
|
||||
setAdminConfig(mergedAdminConf);
|
||||
console.log('加载管理员边栏配置:', mergedAdminConf);
|
||||
} catch (error) {
|
||||
const mergedAdminConf = mergeAdminConfig(null);
|
||||
setAdminConfig(mergedAdminConf);
|
||||
console.log(
|
||||
'加载管理员边栏配置失败,使用默认配置:',
|
||||
mergedAdminConf,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const mergedAdminConf = mergeAdminConfig(null);
|
||||
setAdminConfig(mergedAdminConf);
|
||||
console.log('管理员边栏配置缺失,使用默认配置:', mergedAdminConf);
|
||||
}
|
||||
|
||||
// 获取用户个人配置
|
||||
const userRes = await API.get('/api/user/self');
|
||||
if (userRes.data.success && userRes.data.data.sidebar_modules) {
|
||||
let userConf;
|
||||
// 检查sidebar_modules是字符串还是对象
|
||||
if (typeof userRes.data.data.sidebar_modules === 'string') {
|
||||
userConf = JSON.parse(userRes.data.data.sidebar_modules);
|
||||
} else {
|
||||
userConf = userRes.data.data.sidebar_modules;
|
||||
}
|
||||
console.log('从API加载的用户配置:', userConf);
|
||||
|
||||
// 确保用户配置也经过权限过滤
|
||||
const filteredUserConf = {};
|
||||
Object.keys(userConf).forEach((sectionKey) => {
|
||||
if (isSidebarSectionAllowed(sectionKey)) {
|
||||
filteredUserConf[sectionKey] = { ...userConf[sectionKey] };
|
||||
// 过滤不允许的模块
|
||||
Object.keys(userConf[sectionKey]).forEach((moduleKey) => {
|
||||
if (
|
||||
moduleKey !== 'enabled' &&
|
||||
!isSidebarModuleAllowed(sectionKey, moduleKey)
|
||||
) {
|
||||
delete filteredUserConf[sectionKey][moduleKey];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
setSidebarModulesUser(filteredUserConf);
|
||||
console.log('权限过滤后的用户配置:', filteredUserConf);
|
||||
} else {
|
||||
// 如果用户没有配置,使用权限过滤后的默认配置
|
||||
const defaultConfig = generateDefaultConfig();
|
||||
setSidebarModulesUser(defaultConfig);
|
||||
console.log('用户无配置,使用默认配置:', defaultConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载边栏配置失败:', error);
|
||||
// 出错时也使用默认配置
|
||||
const defaultConfig = generateDefaultConfig();
|
||||
setSidebarModulesUser(defaultConfig);
|
||||
}
|
||||
};
|
||||
|
||||
// 只有权限加载完成且有边栏设置权限时才加载配置
|
||||
if (!permissionsLoading && hasSidebarSettingsPermission()) {
|
||||
loadConfigs();
|
||||
}
|
||||
}, [
|
||||
statusState,
|
||||
permissionsLoading,
|
||||
hasSidebarSettingsPermission,
|
||||
isSidebarSectionAllowed,
|
||||
isSidebarModuleAllowed,
|
||||
]);
|
||||
|
||||
// 检查功能是否被管理员允许
|
||||
const isAllowedByAdmin = (sectionKey, moduleKey = null) => {
|
||||
if (!adminConfig) return true;
|
||||
|
||||
if (moduleKey) {
|
||||
return (
|
||||
adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]
|
||||
);
|
||||
} else {
|
||||
return adminConfig[sectionKey]?.enabled;
|
||||
}
|
||||
};
|
||||
|
||||
// 区域配置数据(根据后端权限过滤)
|
||||
const sectionConfigs = [
|
||||
{
|
||||
key: 'chat',
|
||||
title: t('聊天区域'),
|
||||
description: t('操练场和聊天功能'),
|
||||
modules: [
|
||||
{
|
||||
key: 'playground',
|
||||
title: t('操练场'),
|
||||
description: t('AI模型测试环境'),
|
||||
},
|
||||
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'console',
|
||||
title: t('控制台区域'),
|
||||
description: t('数据管理和日志查看'),
|
||||
modules: [
|
||||
{ key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
|
||||
{ key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
|
||||
{ key: 'log', title: t('使用日志'), description: t('API使用记录') },
|
||||
{
|
||||
key: 'midjourney',
|
||||
title: t('绘图日志'),
|
||||
description: t('绘图任务记录'),
|
||||
},
|
||||
{ key: 'task', title: t('任务日志'), description: t('系统任务记录') },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'personal',
|
||||
title: t('个人中心区域'),
|
||||
description: t('用户个人功能'),
|
||||
modules: [
|
||||
{ key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
|
||||
{
|
||||
key: 'personal',
|
||||
title: t('个人设置'),
|
||||
description: t('个人信息设置'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
title: t('管理员区域'),
|
||||
description: t('系统管理功能'),
|
||||
modules: [
|
||||
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
|
||||
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
|
||||
{
|
||||
key: 'deployment',
|
||||
title: t('模型部署'),
|
||||
description: t('模型部署管理'),
|
||||
},
|
||||
{
|
||||
key: 'subscription',
|
||||
title: t('订阅管理'),
|
||||
description: t('订阅套餐管理'),
|
||||
},
|
||||
{
|
||||
key: 'redemption',
|
||||
title: t('兑换码管理'),
|
||||
description: t('兑换码生成管理'),
|
||||
},
|
||||
{ key: 'user', title: t('用户管理'), description: t('用户账户管理') },
|
||||
{
|
||||
key: 'setting',
|
||||
title: t('系统设置'),
|
||||
description: t('系统参数配置'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
.filter((section) => {
|
||||
// 使用后端权限验证替代前端角色判断
|
||||
return isSidebarSectionAllowed(section.key);
|
||||
})
|
||||
.map((section) => ({
|
||||
...section,
|
||||
modules: section.modules.filter((module) =>
|
||||
isSidebarModuleAllowed(section.key, module.key),
|
||||
),
|
||||
}))
|
||||
.filter(
|
||||
(section) =>
|
||||
// 过滤掉没有可用模块的区域
|
||||
section.modules.length > 0 && isAllowedByAdmin(section.key),
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
{/* 卡片头部 */}
|
||||
<div className='flex items-center mb-4'>
|
||||
<Avatar size='small' color='purple' className='mr-3 shadow-md'>
|
||||
<Settings size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography.Text className='text-lg font-medium'>
|
||||
{t('左侧边栏个人设置')}
|
||||
</Typography.Text>
|
||||
<div className='text-xs text-gray-600'>
|
||||
{t('个性化设置左侧边栏的显示内容')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<Text type='secondary' className='text-sm text-gray-600'>
|
||||
{t('您可以个性化设置侧边栏的要显示功能')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{sectionConfigs.map((section) => (
|
||||
<div key={section.key} className='mb-6'>
|
||||
{/* 区域标题和总开关 */}
|
||||
<div className='flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-xl border border-gray-200'>
|
||||
<div>
|
||||
<div className='font-semibold text-base text-gray-900 mb-1'>
|
||||
{section.title}
|
||||
</div>
|
||||
<Text className='text-xs text-gray-600'>
|
||||
{section.description}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={sidebarModulesUser[section.key]?.enabled !== false}
|
||||
onChange={handleSectionChange(section.key)}
|
||||
size='default'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 功能模块网格 */}
|
||||
<Row gutter={[12, 12]}>
|
||||
{section.modules.map((module) => (
|
||||
<Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
|
||||
<Card
|
||||
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
|
||||
sidebarModulesUser[section.key]?.enabled !== false
|
||||
? ''
|
||||
: 'opacity-50'
|
||||
}`}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
hoverable
|
||||
>
|
||||
<div className='flex justify-between items-center h-full'>
|
||||
<div className='flex-1 text-left'>
|
||||
<div className='font-semibold text-sm text-gray-900 mb-1'>
|
||||
{module.title}
|
||||
</div>
|
||||
<Text className='text-xs text-gray-600 leading-relaxed block'>
|
||||
{module.description}
|
||||
</Text>
|
||||
</div>
|
||||
<div className='ml-4'>
|
||||
<Switch
|
||||
checked={
|
||||
sidebarModulesUser[section.key]?.[module.key] !==
|
||||
false
|
||||
}
|
||||
onChange={handleModuleChange(section.key, module.key)}
|
||||
size='default'
|
||||
disabled={
|
||||
sidebarModulesUser[section.key]?.enabled === false
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className='flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200'>
|
||||
<Button
|
||||
type='tertiary'
|
||||
onClick={resetSidebarModules}
|
||||
className='!rounded-lg'
|
||||
>
|
||||
{t('重置为默认')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={onSubmit}
|
||||
loading={loading}
|
||||
className='!rounded-lg'
|
||||
>
|
||||
{t('保存设置')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user