762 lines
24 KiB
JavaScript
762 lines
24 KiB
JavaScript
/*
|
||
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, useState, useContext } from 'react';
|
||
import {
|
||
Button,
|
||
Card,
|
||
Col,
|
||
Form,
|
||
Row,
|
||
Switch,
|
||
Typography,
|
||
} from '@douyinfe/semi-ui';
|
||
import { API, showError, showSuccess } from '../../../helpers';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { StatusContext } from '../../../context/Status';
|
||
|
||
const { Text } = Typography;
|
||
|
||
export default function SettingsHeaderNavModules(props) {
|
||
const { t } = useTranslation();
|
||
const [loading, setLoading] = useState(false);
|
||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||
|
||
// 顶栏模块管理状态
|
||
const [headerNavModules, setHeaderNavModules] = useState({
|
||
home: {
|
||
enabled: true,
|
||
blurPricing: false,
|
||
showCostPrice: false,
|
||
},
|
||
pricing: {
|
||
enabled: true,
|
||
requireAuth: false,
|
||
blurPricing: false,
|
||
},
|
||
console: true,
|
||
benchmark: true,
|
||
docs: true,
|
||
});
|
||
const [showThemeToggle, setShowThemeToggle] = useState(true);
|
||
const [showLanguageSelector, setShowLanguageSelector] = useState(true);
|
||
|
||
// 处理顶栏模块配置变更
|
||
function handleHeaderNavModuleChange(moduleKey) {
|
||
return (checked) => {
|
||
const newModules = { ...headerNavModules };
|
||
if (moduleKey === 'pricing' || moduleKey === 'home') {
|
||
newModules[moduleKey] = {
|
||
...newModules[moduleKey],
|
||
enabled: checked,
|
||
};
|
||
} else {
|
||
newModules[moduleKey] = checked;
|
||
}
|
||
setHeaderNavModules(newModules);
|
||
};
|
||
}
|
||
|
||
// 处理模型广场权限控制变更
|
||
function handlePricingAuthChange(checked) {
|
||
const newModules = { ...headerNavModules };
|
||
newModules.pricing = {
|
||
...newModules.pricing,
|
||
requireAuth: checked,
|
||
};
|
||
setHeaderNavModules(newModules);
|
||
}
|
||
|
||
// 处理首页成本价展示开关变更(即时保存)
|
||
function handleShowCostPriceChange(checked) {
|
||
const newModules = { ...headerNavModules };
|
||
newModules.home = {
|
||
...newModules.home,
|
||
showCostPrice: checked,
|
||
};
|
||
setHeaderNavModules(newModules);
|
||
(async () => {
|
||
try {
|
||
const res = await API.put('/api/option/', {
|
||
key: 'HeaderNavModules',
|
||
value: JSON.stringify(newModules),
|
||
});
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('保存成功'));
|
||
statusDispatch({
|
||
type: 'set',
|
||
payload: {
|
||
...statusState.status,
|
||
HeaderNavModules: JSON.stringify(newModules),
|
||
},
|
||
});
|
||
if (props.refresh) {
|
||
await props.refresh();
|
||
}
|
||
} else {
|
||
showError(message);
|
||
newModules.home = {
|
||
...newModules.home,
|
||
showCostPrice: !checked,
|
||
};
|
||
setHeaderNavModules(newModules);
|
||
}
|
||
} catch {
|
||
showError(t('保存失败,请重试'));
|
||
newModules.home = {
|
||
...newModules.home,
|
||
showCostPrice: !checked,
|
||
};
|
||
setHeaderNavModules(newModules);
|
||
}
|
||
})();
|
||
}
|
||
|
||
// 处理主题切换开关变更(即时保存)
|
||
async function handleShowThemeToggleChange(checked) {
|
||
setShowThemeToggle(checked);
|
||
const key = 'HeaderShowThemeToggle';
|
||
const value = String(checked);
|
||
try {
|
||
const res = await API.put('/api/option/', { key, value });
|
||
if (res.data.success) {
|
||
showSuccess(t('保存成功'));
|
||
statusDispatch({
|
||
type: 'set',
|
||
payload: { ...statusState.status, [key]: value },
|
||
});
|
||
} else {
|
||
showError(res.data.message);
|
||
setShowThemeToggle(!checked);
|
||
}
|
||
} catch {
|
||
showError(t('保存失败,请重试'));
|
||
setShowThemeToggle(!checked);
|
||
}
|
||
}
|
||
|
||
// 处理语言选择器开关变更(即时保存)
|
||
async function handleShowLanguageSelectorChange(checked) {
|
||
setShowLanguageSelector(checked);
|
||
const key = 'HeaderShowLanguageSelector';
|
||
const value = String(checked);
|
||
try {
|
||
const res = await API.put('/api/option/', { key, value });
|
||
if (res.data.success) {
|
||
showSuccess(t('保存成功'));
|
||
statusDispatch({
|
||
type: 'set',
|
||
payload: { ...statusState.status, [key]: value },
|
||
});
|
||
} else {
|
||
showError(res.data.message);
|
||
setShowLanguageSelector(!checked);
|
||
}
|
||
} catch {
|
||
showError(t('保存失败,请重试'));
|
||
setShowLanguageSelector(!checked);
|
||
}
|
||
}
|
||
|
||
// 处理模糊价格开关变更(即时保存)
|
||
function handleBlurPricingChange(moduleKey) {
|
||
return async (checked) => {
|
||
const newModules = { ...headerNavModules };
|
||
newModules[moduleKey] = {
|
||
...newModules[moduleKey],
|
||
blurPricing: checked,
|
||
};
|
||
setHeaderNavModules(newModules);
|
||
try {
|
||
const res = await API.put('/api/option/', {
|
||
key: 'HeaderNavModules',
|
||
value: JSON.stringify(newModules),
|
||
});
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('保存成功'));
|
||
statusDispatch({
|
||
type: 'set',
|
||
payload: {
|
||
...statusState.status,
|
||
HeaderNavModules: JSON.stringify(newModules),
|
||
},
|
||
});
|
||
if (props.refresh) {
|
||
await props.refresh();
|
||
}
|
||
} else {
|
||
showError(message);
|
||
newModules[moduleKey] = { ...newModules[moduleKey], blurPricing: !checked };
|
||
setHeaderNavModules(newModules);
|
||
}
|
||
} catch {
|
||
showError(t('保存失败,请重试'));
|
||
newModules[moduleKey] = { ...newModules[moduleKey], blurPricing: !checked };
|
||
setHeaderNavModules(newModules);
|
||
}
|
||
};
|
||
}
|
||
|
||
// 重置顶栏模块为默认配置
|
||
function resetHeaderNavModules() {
|
||
const defaultModules = {
|
||
home: {
|
||
enabled: true,
|
||
blurPricing: false,
|
||
showCostPrice: false,
|
||
},
|
||
pricing: {
|
||
enabled: true,
|
||
requireAuth: false,
|
||
blurPricing: false,
|
||
},
|
||
console: true,
|
||
benchmark: true,
|
||
docs: true,
|
||
};
|
||
setHeaderNavModules(defaultModules);
|
||
showSuccess(t('已重置为默认配置'));
|
||
}
|
||
|
||
// 保存配置
|
||
async function onSubmit() {
|
||
setLoading(true);
|
||
try {
|
||
const res = await API.put('/api/option/', {
|
||
key: 'HeaderNavModules',
|
||
value: JSON.stringify(headerNavModules),
|
||
});
|
||
const { success, message } = res.data;
|
||
if (success) {
|
||
showSuccess(t('保存成功'));
|
||
|
||
// 立即更新StatusContext中的状态
|
||
statusDispatch({
|
||
type: 'set',
|
||
payload: {
|
||
...statusState.status,
|
||
HeaderNavModules: JSON.stringify(headerNavModules),
|
||
},
|
||
});
|
||
|
||
// 刷新父组件状态
|
||
if (props.refresh) {
|
||
await props.refresh();
|
||
}
|
||
} else {
|
||
showError(message);
|
||
}
|
||
} catch (error) {
|
||
showError(t('保存失败,请重试'));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
// 从 props.options 中获取主题切换和语言选择器设置
|
||
if (props.options) {
|
||
const themeVal = props.options.HeaderShowThemeToggle;
|
||
if (themeVal !== undefined && themeVal !== null) {
|
||
setShowThemeToggle(themeVal === 'true' || themeVal === true);
|
||
}
|
||
const langVal = props.options.HeaderShowLanguageSelector;
|
||
if (langVal !== undefined && langVal !== null) {
|
||
setShowLanguageSelector(langVal === 'true' || langVal === true);
|
||
}
|
||
}
|
||
// 从 props.options 中获取配置
|
||
if (props.options && props.options.HeaderNavModules) {
|
||
try {
|
||
const modules = JSON.parse(props.options.HeaderNavModules);
|
||
|
||
// 处理向后兼容性:如果home是boolean,转换为对象格式
|
||
if (typeof modules.home === 'boolean') {
|
||
modules.home = {
|
||
enabled: modules.home,
|
||
blurPricing: false,
|
||
showCostPrice: false,
|
||
};
|
||
}
|
||
|
||
// 处理向后兼容性:如果pricing是boolean,转换为对象格式
|
||
if (typeof modules.pricing === 'boolean') {
|
||
modules.pricing = {
|
||
enabled: modules.pricing,
|
||
requireAuth: false,
|
||
blurPricing: false,
|
||
};
|
||
}
|
||
|
||
// 确保 blurPricing 字段存在
|
||
if (typeof modules.home === 'object' && modules.home.blurPricing === undefined) {
|
||
modules.home.blurPricing = false;
|
||
}
|
||
if (typeof modules.home === 'object' && modules.home.showCostPrice === undefined) {
|
||
modules.home.showCostPrice = false;
|
||
}
|
||
if (typeof modules.pricing === 'object' && modules.pricing.blurPricing === undefined) {
|
||
modules.pricing.blurPricing = false;
|
||
}
|
||
|
||
setHeaderNavModules(modules);
|
||
} catch (error) {
|
||
// 使用默认配置
|
||
const defaultModules = {
|
||
home: {
|
||
enabled: true,
|
||
blurPricing: false,
|
||
showCostPrice: false,
|
||
},
|
||
pricing: {
|
||
enabled: true,
|
||
requireAuth: false,
|
||
blurPricing: false,
|
||
},
|
||
console: true,
|
||
benchmark: true,
|
||
docs: true,
|
||
};
|
||
setHeaderNavModules(defaultModules);
|
||
}
|
||
}
|
||
}, [props.options]);
|
||
|
||
// 获取模块是否启用
|
||
function getModuleEnabled(moduleKey) {
|
||
const val = headerNavModules[moduleKey];
|
||
if (typeof val === 'object' && val !== null) {
|
||
return val.enabled;
|
||
}
|
||
return !!val;
|
||
}
|
||
|
||
// 模块配置数据
|
||
const moduleConfigs = [
|
||
{
|
||
key: 'home',
|
||
title: t('首页'),
|
||
description: t('用户主页,展示系统信息'),
|
||
hasBlurPricing: true,
|
||
hasCostPrice: true,
|
||
},
|
||
{
|
||
key: 'pricing',
|
||
title: t('模型'),
|
||
description: t('模型定价,需要登录访问'),
|
||
hasSubConfig: true,
|
||
hasBlurPricing: true,
|
||
},
|
||
{
|
||
key: 'console',
|
||
title: t('控制台'),
|
||
description: t('用户控制面板,管理账户'),
|
||
},
|
||
{
|
||
key: 'benchmark',
|
||
title: t('评测'),
|
||
description: t('AI 模型评测与排行'),
|
||
},
|
||
{
|
||
key: 'docs',
|
||
title: t('文档'),
|
||
description: t('系统文档和帮助信息'),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<Card>
|
||
<Form.Section
|
||
text={t('顶栏管理')}
|
||
extraText={t('控制顶栏模块显示状态,全局生效')}
|
||
>
|
||
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
|
||
{moduleConfigs.map((module) => (
|
||
<Col key={module.key} xs={24} sm={12} md={6} lg={6} xl={6}>
|
||
<Card
|
||
className='transition-shadow hover:shadow-md'
|
||
style={{
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--semi-color-border)',
|
||
transition: 'all 0.2s ease',
|
||
background: 'var(--semi-color-bg-1)',
|
||
minHeight: '80px',
|
||
}}
|
||
bodyStyle={{ padding: '16px' }}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
height: '100%',
|
||
}}
|
||
>
|
||
<div style={{ flex: 1, textAlign: 'left' }}>
|
||
<div
|
||
style={{
|
||
fontWeight: '600',
|
||
fontSize: '14px',
|
||
color: 'var(--semi-color-text-0)',
|
||
marginBottom: '4px',
|
||
}}
|
||
>
|
||
{module.title}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{module.description}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginLeft: '16px' }}>
|
||
<Switch
|
||
checked={getModuleEnabled(module.key)}
|
||
onChange={handleHeaderNavModuleChange(module.key)}
|
||
size='default'
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 为模型广场添加权限控制子开关 */}
|
||
{module.key === 'pricing' && getModuleEnabled(module.key) && (
|
||
<div
|
||
style={{
|
||
borderTop: '1px solid var(--semi-color-border)',
|
||
marginTop: '12px',
|
||
paddingTop: '12px',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<div style={{ flex: 1, textAlign: 'left' }}>
|
||
<div
|
||
style={{
|
||
fontWeight: '500',
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-1)',
|
||
marginBottom: '2px',
|
||
}}
|
||
>
|
||
{t('需要登录访问')}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '11px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{t('开启后未登录用户无法访问模型广场')}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginLeft: '16px' }}>
|
||
<Switch
|
||
checked={
|
||
headerNavModules.pricing?.requireAuth || false
|
||
}
|
||
onChange={handlePricingAuthChange}
|
||
size='default'
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 未登录模糊价格子开关 */}
|
||
{module.hasBlurPricing && getModuleEnabled(module.key) && (
|
||
<div
|
||
style={{
|
||
borderTop: '1px solid var(--semi-color-border)',
|
||
marginTop: '12px',
|
||
paddingTop: '12px',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<div style={{ flex: 1, textAlign: 'left' }}>
|
||
<div
|
||
style={{
|
||
fontWeight: '500',
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-1)',
|
||
marginBottom: '2px',
|
||
}}
|
||
>
|
||
{t('未登录模糊价格')}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '11px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{t('开启后未登录用户看到的价格和供应商信息将模糊处理')}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginLeft: '16px' }}>
|
||
<Switch
|
||
checked={
|
||
headerNavModules[module.key]?.blurPricing || false
|
||
}
|
||
onChange={handleBlurPricingChange(module.key)}
|
||
size='default'
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 首页成本价展示子开关 */}
|
||
{module.hasCostPrice && getModuleEnabled(module.key) && (
|
||
<div
|
||
style={{
|
||
borderTop: '1px solid var(--semi-color-border)',
|
||
marginTop: '12px',
|
||
paddingTop: '12px',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<div style={{ flex: 1, textAlign: 'left' }}>
|
||
<div
|
||
style={{
|
||
fontWeight: '500',
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-1)',
|
||
marginBottom: '2px',
|
||
}}
|
||
>
|
||
{t('成本价')}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '11px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{t(
|
||
'开启后,供应商、代理商、管理员与超级管理员在首页模型详情侧栏「我的令牌」下方可查看各通道成本价',
|
||
)}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginLeft: '16px' }}>
|
||
<Switch
|
||
checked={
|
||
headerNavModules.home?.showCostPrice || false
|
||
}
|
||
onChange={handleShowCostPriceChange}
|
||
size='default'
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
</Col>
|
||
))}
|
||
|
||
{/* 主题切换开关 */}
|
||
<Col xs={24} sm={12} md={6} lg={6} xl={6}>
|
||
<Card
|
||
className='transition-shadow hover:shadow-md'
|
||
style={{
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--semi-color-border)',
|
||
transition: 'all 0.2s ease',
|
||
background: 'var(--semi-color-bg-1)',
|
||
minHeight: '80px',
|
||
}}
|
||
bodyStyle={{ padding: '16px' }}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
height: '100%',
|
||
}}
|
||
>
|
||
<div style={{ flex: 1, textAlign: 'left' }}>
|
||
<div
|
||
style={{
|
||
fontWeight: '600',
|
||
fontSize: '14px',
|
||
color: 'var(--semi-color-text-0)',
|
||
marginBottom: '4px',
|
||
}}
|
||
>
|
||
{t('主题切换')}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{t('顶栏显示主题切换按钮,允许用户切换亮色/暗色模式')}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginLeft: '16px' }}>
|
||
<Switch
|
||
checked={showThemeToggle}
|
||
onChange={handleShowThemeToggleChange}
|
||
size='default'
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
|
||
{/* 语言选择器开关 */}
|
||
<Col xs={24} sm={12} md={6} lg={6} xl={6}>
|
||
<Card
|
||
className='transition-shadow hover:shadow-md'
|
||
style={{
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--semi-color-border)',
|
||
transition: 'all 0.2s ease',
|
||
background: 'var(--semi-color-bg-1)',
|
||
minHeight: '80px',
|
||
}}
|
||
bodyStyle={{ padding: '16px' }}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
height: '100%',
|
||
}}
|
||
>
|
||
<div style={{ flex: 1, textAlign: 'left' }}>
|
||
<div
|
||
style={{
|
||
fontWeight: '600',
|
||
fontSize: '14px',
|
||
color: 'var(--semi-color-text-0)',
|
||
marginBottom: '4px',
|
||
}}
|
||
>
|
||
{t('语言选择器')}
|
||
</div>
|
||
<Text
|
||
type='secondary'
|
||
size='small'
|
||
style={{
|
||
fontSize: '12px',
|
||
color: 'var(--semi-color-text-2)',
|
||
lineHeight: '1.4',
|
||
display: 'block',
|
||
}}
|
||
>
|
||
{t('顶栏显示语言选择器,允许用户切换界面语言')}
|
||
</Text>
|
||
</div>
|
||
<div style={{ marginLeft: '16px' }}>
|
||
<Switch
|
||
checked={showLanguageSelector}
|
||
onChange={handleShowLanguageSelectorChange}
|
||
size='default'
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
gap: '12px',
|
||
justifyContent: 'flex-start',
|
||
alignItems: 'center',
|
||
paddingTop: '8px',
|
||
borderTop: '1px solid var(--semi-color-border)',
|
||
}}
|
||
>
|
||
<Button
|
||
size='default'
|
||
type='tertiary'
|
||
onClick={resetHeaderNavModules}
|
||
style={{
|
||
borderRadius: '6px',
|
||
fontWeight: '500',
|
||
}}
|
||
>
|
||
{t('重置为默认')}
|
||
</Button>
|
||
<Button
|
||
size='default'
|
||
type='primary'
|
||
onClick={onSubmit}
|
||
loading={loading}
|
||
style={{
|
||
borderRadius: '6px',
|
||
fontWeight: '500',
|
||
minWidth: '100px',
|
||
}}
|
||
>
|
||
{t('保存设置')}
|
||
</Button>
|
||
</div>
|
||
</Form.Section>
|
||
</Card>
|
||
);
|
||
}
|