tokenFactory/web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx

762 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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>
);
}