feat(ratio-sync, ui): add built‑in “Official Ratio Preset” and harden upstream sync

Backend (controller/ratio_sync.go):
- Add built‑in official upstream to GetSyncableChannels (ID: -100, BaseURL: https://basellm.github.io)
- Support absolute endpoint URLs; otherwise join BaseURL + endpoint (defaults to /api/ratio_config)
- Harden HTTP client:
  - IPv4‑first with IPv6 fallback for github.io
  - Add ResponseHeaderTimeout
  - 3 attempts with exponential backoff (200/400/800ms)
- Validate Content-Type and limit response body to 10MB (safe decode via io.LimitReader)
- Robust parsing: support type1 ratio_config map and type2 pricing list
- Use net.SplitHostPort for host parsing
- Use float tolerance in differences comparison to avoid false mismatches
- Remove unused code (tryDirect) and improve warnings

Frontend:
- UpstreamRatioSync.jsx: auto-assign official endpoint to /llm-metadata/api/newapi/ratio_config-v1-base.json
- ChannelSelectorModal.jsx:
  - Pin the official source at the top of the list
  - Show a green “官方” tag next to the status
  - Refactor status renderer to accept the full record

Notes:
- Backward compatible; no API surface changes
- Official ratio_config reference: https://basellm.github.io/llm-metadata/api/newapi/ratio_config-v1-base.json
This commit is contained in:
t0ng7u
2025-09-01 23:43:39 +08:00
parent b601d8fd7c
commit 1f111a163a
18 changed files with 1023 additions and 694 deletions

View File

@@ -4,6 +4,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net"
"net/http" "net/http"
"one-api/logger" "one-api/logger"
"strings" "strings"
@@ -21,8 +23,26 @@ const (
defaultTimeoutSeconds = 10 defaultTimeoutSeconds = 10
defaultEndpoint = "/api/ratio_config" defaultEndpoint = "/api/ratio_config"
maxConcurrentFetches = 8 maxConcurrentFetches = 8
maxRatioConfigBytes = 10 << 20 // 10MB
floatEpsilon = 1e-9
) )
func nearlyEqual(a, b float64) bool {
if a > b {
return a-b < floatEpsilon
}
return b-a < floatEpsilon
}
func valuesEqual(a, b interface{}) bool {
af, aok := a.(float64)
bf, bok := b.(float64)
if aok && bok {
return nearlyEqual(af, bf)
}
return a == b
}
var ratioTypes = []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"} var ratioTypes = []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
type upstreamResult struct { type upstreamResult struct {
@@ -87,7 +107,23 @@ func FetchUpstreamRatios(c *gin.Context) {
sem := make(chan struct{}, maxConcurrentFetches) sem := make(chan struct{}, maxConcurrentFetches)
client := &http.Client{Transport: &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second}} dialer := &net.Dialer{Timeout: 10 * time.Second}
transport := &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, ResponseHeaderTimeout: 10 * time.Second}
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
// 对 github.io 优先尝试 IPv4失败则回退 IPv6
if strings.HasSuffix(host, "github.io") {
if conn, err := dialer.DialContext(ctx, "tcp4", addr); err == nil {
return conn, nil
}
return dialer.DialContext(ctx, "tcp6", addr)
}
return dialer.DialContext(ctx, network, addr)
}
client := &http.Client{Transport: transport}
for _, chn := range upstreams { for _, chn := range upstreams {
wg.Add(1) wg.Add(1)
@@ -98,12 +134,17 @@ func FetchUpstreamRatios(c *gin.Context) {
defer func() { <-sem }() defer func() { <-sem }()
endpoint := chItem.Endpoint endpoint := chItem.Endpoint
if endpoint == "" { var fullURL string
endpoint = defaultEndpoint if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") {
} else if !strings.HasPrefix(endpoint, "/") { fullURL = endpoint
endpoint = "/" + endpoint } else {
if endpoint == "" {
endpoint = defaultEndpoint
} else if !strings.HasPrefix(endpoint, "/") {
endpoint = "/" + endpoint
}
fullURL = chItem.BaseURL + endpoint
} }
fullURL := chItem.BaseURL + endpoint
uniqueName := chItem.Name uniqueName := chItem.Name
if chItem.ID != 0 { if chItem.ID != 0 {
@@ -120,10 +161,19 @@ func FetchUpstreamRatios(c *gin.Context) {
return return
} }
resp, err := client.Do(httpReq) // 简单重试:最多 3 次,指数退避
if err != nil { var resp *http.Response
logger.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error()) var lastErr error
ch <- upstreamResult{Name: uniqueName, Err: err.Error()} for attempt := 0; attempt < 3; attempt++ {
resp, lastErr = client.Do(httpReq)
if lastErr == nil {
break
}
time.Sleep(time.Duration(200*(1<<attempt)) * time.Millisecond)
}
if lastErr != nil {
logger.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+lastErr.Error())
ch <- upstreamResult{Name: uniqueName, Err: lastErr.Error()}
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -132,6 +182,12 @@ func FetchUpstreamRatios(c *gin.Context) {
ch <- upstreamResult{Name: uniqueName, Err: resp.Status} ch <- upstreamResult{Name: uniqueName, Err: resp.Status}
return return
} }
// Content-Type 和响应体大小校验
if ct := resp.Header.Get("Content-Type"); ct != "" && !strings.Contains(strings.ToLower(ct), "application/json") {
logger.LogWarn(c.Request.Context(), "unexpected content-type from "+chItem.Name+": "+ct)
}
limited := io.LimitReader(resp.Body, maxRatioConfigBytes)
// 兼容两种上游接口格式: // 兼容两种上游接口格式:
// type1: /api/ratio_config -> data 为 map[string]any包含 model_ratio/completion_ratio/cache_ratio/model_price // type1: /api/ratio_config -> data 为 map[string]any包含 model_ratio/completion_ratio/cache_ratio/model_price
// type2: /api/pricing -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式 // type2: /api/pricing -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式
@@ -141,7 +197,7 @@ func FetchUpstreamRatios(c *gin.Context) {
Message string `json:"message"` Message string `json:"message"`
} }
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { if err := json.NewDecoder(limited).Decode(&body); err != nil {
logger.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error()) logger.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error())
ch <- upstreamResult{Name: uniqueName, Err: err.Error()} ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
return return
@@ -152,6 +208,8 @@ func FetchUpstreamRatios(c *gin.Context) {
return return
} }
// 若 Data 为空,将继续按 type1 尝试解析(与多数静态 ratio_config 兼容)
// 尝试按 type1 解析 // 尝试按 type1 解析
var type1Data map[string]any var type1Data map[string]any
if err := json.Unmarshal(body.Data, &type1Data); err == nil { if err := json.Unmarshal(body.Data, &type1Data); err == nil {
@@ -357,9 +415,9 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
upstreamValue = val upstreamValue = val
hasUpstreamValue = true hasUpstreamValue = true
if localValue != nil && localValue != val { if localValue != nil && !valuesEqual(localValue, val) {
hasDifference = true hasDifference = true
} else if localValue == val { } else if valuesEqual(localValue, val) {
upstreamValue = "same" upstreamValue = "same"
} }
} }
@@ -466,6 +524,13 @@ func GetSyncableChannels(c *gin.Context) {
} }
} }
syncableChannels = append(syncableChannels, dto.SyncableChannel{
ID: -100,
Name: "官方倍率预设",
BaseURL: "https://basellm.github.io",
Status: 1,
})
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",

View File

@@ -279,7 +279,10 @@ function App() {
element={ element={
pricingRequireAuth ? ( pricingRequireAuth ? (
<PrivateRoute> <PrivateRoute>
<Suspense fallback={<Loading></Loading>} key={location.pathname}> <Suspense
fallback={<Loading></Loading>}
key={location.pathname}
>
<Pricing /> <Pricing />
</Suspense> </Suspense>
</PrivateRoute> </PrivateRoute>

View File

@@ -21,7 +21,13 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import SkeletonWrapper from './SkeletonWrapper'; import SkeletonWrapper from './SkeletonWrapper';
const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth }) => { const Navigation = ({
mainNavLinks,
isMobile,
isLoading,
userState,
pricingRequireAuth,
}) => {
const renderNavLinks = () => { const renderNavLinks = () => {
const baseClasses = const baseClasses =
'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out'; 'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';

View File

@@ -50,7 +50,11 @@ const routerMap = {
const SiderBar = ({ onNavigate = () => {} }) => { const SiderBar = ({ onNavigate = () => {} }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [collapsed, toggleCollapsed] = useSidebarCollapsed(); const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const { isModuleVisible, hasSectionVisibleModules, loading: sidebarLoading } = useSidebar(); const {
isModuleVisible,
hasSectionVisibleModules,
loading: sidebarLoading,
} = useSidebar();
const [selectedKeys, setSelectedKeys] = useState(['home']); const [selectedKeys, setSelectedKeys] = useState(['home']);
const [chatItems, setChatItems] = useState([]); const [chatItems, setChatItems] = useState([]);
@@ -58,160 +62,148 @@ const SiderBar = ({ onNavigate = () => {} }) => {
const location = useLocation(); const location = useLocation();
const [routerMapState, setRouterMapState] = useState(routerMap); const [routerMapState, setRouterMapState] = useState(routerMap);
const workspaceItems = useMemo( const workspaceItems = useMemo(() => {
() => { const items = [
const items = [ {
{ text: t('数据看板'),
text: t('数据看板'), itemKey: 'detail',
itemKey: 'detail', to: '/detail',
to: '/detail', className:
className: localStorage.getItem('enable_data_export') === 'true'
localStorage.getItem('enable_data_export') === 'true' ? ''
? '' : 'tableHiddle',
: 'tableHiddle', },
}, {
{ text: t('令牌管理'),
text: t('令牌管理'), itemKey: 'token',
itemKey: 'token', to: '/token',
to: '/token', },
}, {
{ text: t('使用日志'),
text: t('使用日志'), itemKey: 'log',
itemKey: 'log', to: '/log',
to: '/log', },
}, {
{ text: t('绘图日志'),
text: t('绘图日志'), itemKey: 'midjourney',
itemKey: 'midjourney', to: '/midjourney',
to: '/midjourney', className:
className: localStorage.getItem('enable_drawing') === 'true'
localStorage.getItem('enable_drawing') === 'true' ? ''
? '' : 'tableHiddle',
: 'tableHiddle', },
}, {
{ text: t('任务日志'),
text: t('任务日志'), itemKey: 'task',
itemKey: 'task', to: '/task',
to: '/task', className:
className: localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle', },
}, ];
];
// 根据配置过滤项目 // 根据配置过滤项目
const filteredItems = items.filter(item => { const filteredItems = items.filter((item) => {
const configVisible = isModuleVisible('console', item.itemKey); const configVisible = isModuleVisible('console', item.itemKey);
return configVisible; return configVisible;
}); });
return filteredItems; return filteredItems;
}, }, [
[ localStorage.getItem('enable_data_export'),
localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'),
localStorage.getItem('enable_drawing'), localStorage.getItem('enable_task'),
localStorage.getItem('enable_task'), t,
t, isModuleVisible,
isModuleVisible, ]);
],
);
const financeItems = useMemo( const financeItems = useMemo(() => {
() => { const items = [
const items = [ {
{ text: t('钱包管理'),
text: t('钱包管理'), itemKey: 'topup',
itemKey: 'topup', to: '/topup',
to: '/topup', },
}, {
{ text: t('个人设置'),
text: t('个人设置'), itemKey: 'personal',
itemKey: 'personal', to: '/personal',
to: '/personal', },
}, ];
];
// 根据配置过滤项目 // 根据配置过滤项目
const filteredItems = items.filter(item => { const filteredItems = items.filter((item) => {
const configVisible = isModuleVisible('personal', item.itemKey); const configVisible = isModuleVisible('personal', item.itemKey);
return configVisible; return configVisible;
}); });
return filteredItems; return filteredItems;
}, }, [t, isModuleVisible]);
[t, isModuleVisible],
);
const adminItems = useMemo( const adminItems = useMemo(() => {
() => { const items = [
const items = [ {
{ text: t('渠道管理'),
text: t('渠道管理'), itemKey: 'channel',
itemKey: 'channel', to: '/channel',
to: '/channel', className: isAdmin() ? '' : 'tableHiddle',
className: isAdmin() ? '' : 'tableHiddle', },
}, {
{ text: t('模型管理'),
text: t('模型管理'), itemKey: 'models',
itemKey: 'models', to: '/console/models',
to: '/console/models', className: isAdmin() ? '' : 'tableHiddle',
className: isAdmin() ? '' : 'tableHiddle', },
}, {
{ text: t('兑换码管理'),
text: t('兑换码管理'), itemKey: 'redemption',
itemKey: 'redemption', to: '/redemption',
to: '/redemption', className: isAdmin() ? '' : 'tableHiddle',
className: isAdmin() ? '' : 'tableHiddle', },
}, {
{ text: t('用户管理'),
text: t('用户管理'), itemKey: 'user',
itemKey: 'user', to: '/user',
to: '/user', className: isAdmin() ? '' : 'tableHiddle',
className: isAdmin() ? '' : 'tableHiddle', },
}, {
{ text: t('系统设置'),
text: t('系统设置'), itemKey: 'setting',
itemKey: 'setting', to: '/setting',
to: '/setting', className: isRoot() ? '' : 'tableHiddle',
className: isRoot() ? '' : 'tableHiddle', },
}, ];
];
// 根据配置过滤项目 // 根据配置过滤项目
const filteredItems = items.filter(item => { const filteredItems = items.filter((item) => {
const configVisible = isModuleVisible('admin', item.itemKey); const configVisible = isModuleVisible('admin', item.itemKey);
return configVisible; return configVisible;
}); });
return filteredItems; return filteredItems;
}, }, [isAdmin(), isRoot(), t, isModuleVisible]);
[isAdmin(), isRoot(), t, isModuleVisible],
);
const chatMenuItems = useMemo( const chatMenuItems = useMemo(() => {
() => { const items = [
const items = [ {
{ text: t('操练场'),
text: t('操练场'), itemKey: 'playground',
itemKey: 'playground', to: '/playground',
to: '/playground', },
}, {
{ text: t('聊天'),
text: t('聊天'), itemKey: 'chat',
itemKey: 'chat', items: chatItems,
items: chatItems, },
}, ];
];
// 根据配置过滤项目 // 根据配置过滤项目
const filteredItems = items.filter(item => { const filteredItems = items.filter((item) => {
const configVisible = isModuleVisible('chat', item.itemKey); const configVisible = isModuleVisible('chat', item.itemKey);
return configVisible; return configVisible;
}); });
return filteredItems; return filteredItems;
}, }, [chatItems, t, isModuleVisible]);
[chatItems, t, isModuleVisible],
);
// 更新路由映射,添加聊天路由 // 更新路由映射,添加聊天路由
const updateRouterMapWithChats = (chats) => { const updateRouterMapWithChats = (chats) => {
@@ -426,7 +418,9 @@ const SiderBar = ({ onNavigate = () => {} }) => {
{/* 聊天区域 */} {/* 聊天区域 */}
{hasSectionVisibleModules('chat') && ( {hasSectionVisibleModules('chat') && (
<div className='sidebar-section'> <div className='sidebar-section'>
{!collapsed && <div className='sidebar-group-label'>{t('聊天')}</div>} {!collapsed && (
<div className='sidebar-group-label'>{t('聊天')}</div>
)}
{chatMenuItems.map((item) => renderSubItem(item))} {chatMenuItems.map((item) => renderSubItem(item))}
</div> </div>
)} )}

View File

@@ -34,7 +34,6 @@ import {
Tag, Tag,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons'; import { IconSearch } from '@douyinfe/semi-icons';
import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
const ChannelSelectorModal = forwardRef( const ChannelSelectorModal = forwardRef(
( (
@@ -65,6 +64,18 @@ const ChannelSelectorModal = forwardRef(
}, },
})); }));
// 官方渠道识别
const isOfficialChannel = (record) => {
const id = record?.key ?? record?.value ?? record?._originalData?.id;
const base = record?._originalData?.base_url || '';
const name = record?.label || '';
return (
id === -100 ||
base === 'https://basellm.github.io' ||
name === '官方倍率预设'
);
};
useEffect(() => { useEffect(() => {
if (!allChannels) return; if (!allChannels) return;
@@ -77,7 +88,13 @@ const ChannelSelectorModal = forwardRef(
}) })
: allChannels; : allChannels;
setFilteredData(matched); const sorted = [...matched].sort((a, b) => {
const wa = isOfficialChannel(a) ? 0 : 1;
const wb = isOfficialChannel(b) ? 0 : 1;
return wa - wb;
});
setFilteredData(sorted);
}, [allChannels, searchText]); }, [allChannels, searchText]);
const total = filteredData.length; const total = filteredData.length;
@@ -143,45 +160,49 @@ const ChannelSelectorModal = forwardRef(
); );
}; };
const renderStatusCell = (status) => { const renderStatusCell = (record) => {
const status = record?._originalData?.status || 0;
const official = isOfficialChannel(record);
let statusTag = null;
switch (status) { switch (status) {
case 1: case 1:
return ( statusTag = (
<Tag <Tag color='green' shape='circle'>
color='green'
shape='circle'
prefixIcon={<CheckCircle size={14} />}
>
{t('已启用')} {t('已启用')}
</Tag> </Tag>
); );
break;
case 2: case 2:
return ( statusTag = (
<Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}> <Tag color='red' shape='circle'>
{t('已禁用')} {t('已禁用')}
</Tag> </Tag>
); );
break;
case 3: case 3:
return ( statusTag = (
<Tag <Tag color='yellow' shape='circle'>
color='yellow'
shape='circle'
prefixIcon={<AlertCircle size={14} />}
>
{t('自动禁用')} {t('自动禁用')}
</Tag> </Tag>
); );
break;
default: default:
return ( statusTag = (
<Tag <Tag color='grey' shape='circle'>
color='grey'
shape='circle'
prefixIcon={<HelpCircle size={14} />}
>
{t('未知状态')} {t('未知状态')}
</Tag> </Tag>
); );
} }
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{statusTag}
{official && (
<Tag color='green' shape='circle' type='light'>
{t('官方')}
</Tag>
)}
</div>
);
}; };
const renderNameCell = (text) => ( const renderNameCell = (text) => (
@@ -207,8 +228,7 @@ const ChannelSelectorModal = forwardRef(
{ {
title: t('状态'), title: t('状态'),
dataIndex: '_originalData.status', dataIndex: '_originalData.status',
render: (_, record) => render: (_, record) => renderStatusCell(record),
renderStatusCell(record._originalData?.status || 0),
}, },
{ {
title: t('同步接口'), title: t('同步接口'),

View File

@@ -38,8 +38,6 @@ const PersonalSetting = () => {
let navigate = useNavigate(); let navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
wechat_verification_code: '', wechat_verification_code: '',
email_verification_code: '', email_verification_code: '',
@@ -335,8 +333,6 @@ const PersonalSetting = () => {
saveNotificationSettings={saveNotificationSettings} saveNotificationSettings={saveNotificationSettings}
/> />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -34,7 +34,12 @@ import {
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons'; import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react'; import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react';
import { renderQuotaWithPrompt, API, showSuccess, showError } from '../../../../helpers'; import {
renderQuotaWithPrompt,
API,
showSuccess,
showError,
} from '../../../../helpers';
import CodeViewer from '../../../playground/CodeViewer'; import CodeViewer from '../../../playground/CodeViewer';
import { StatusContext } from '../../../../context/Status'; import { StatusContext } from '../../../../context/Status';
import { UserContext } from '../../../../context/User'; import { UserContext } from '../../../../context/User';
@@ -57,7 +62,7 @@ const NotificationSettings = ({
chat: { chat: {
enabled: true, enabled: true,
playground: true, playground: true,
chat: true chat: true,
}, },
console: { console: {
enabled: true, enabled: true,
@@ -65,12 +70,12 @@ const NotificationSettings = ({
token: true, token: true,
log: true, log: true,
midjourney: true, midjourney: true,
task: true task: true,
}, },
personal: { personal: {
enabled: true, enabled: true,
topup: true, topup: true,
personal: true personal: true,
}, },
admin: { admin: {
enabled: true, enabled: true,
@@ -78,8 +83,8 @@ const NotificationSettings = ({
models: true, models: true,
redemption: true, redemption: true,
user: true, user: true,
setting: true setting: true,
} },
}); });
const [adminConfig, setAdminConfig] = useState(null); const [adminConfig, setAdminConfig] = useState(null);
@@ -99,8 +104,8 @@ const NotificationSettings = ({
...sidebarModulesUser, ...sidebarModulesUser,
[sectionKey]: { [sectionKey]: {
...sidebarModulesUser[sectionKey], ...sidebarModulesUser[sectionKey],
enabled: checked enabled: checked,
} },
}; };
setSidebarModulesUser(newModules); setSidebarModulesUser(newModules);
}; };
@@ -112,8 +117,8 @@ const NotificationSettings = ({
...sidebarModulesUser, ...sidebarModulesUser,
[sectionKey]: { [sectionKey]: {
...sidebarModulesUser[sectionKey], ...sidebarModulesUser[sectionKey],
[moduleKey]: checked [moduleKey]: checked,
} },
}; };
setSidebarModulesUser(newModules); setSidebarModulesUser(newModules);
}; };
@@ -123,7 +128,7 @@ const NotificationSettings = ({
setSidebarLoading(true); setSidebarLoading(true);
try { try {
const res = await API.put('/api/user/self', { const res = await API.put('/api/user/self', {
sidebar_modules: JSON.stringify(sidebarModulesUser) sidebar_modules: JSON.stringify(sidebarModulesUser),
}); });
if (res.data.success) { if (res.data.success) {
showSuccess(t('侧边栏设置保存成功')); showSuccess(t('侧边栏设置保存成功'));
@@ -139,9 +144,23 @@ const NotificationSettings = ({
const resetSidebarModules = () => { const resetSidebarModules = () => {
const defaultConfig = { const defaultConfig = {
chat: { enabled: true, playground: true, chat: true }, chat: { enabled: true, playground: true, chat: true },
console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true }, console: {
enabled: true,
detail: true,
token: true,
log: true,
midjourney: true,
task: true,
},
personal: { enabled: true, topup: true, personal: true }, personal: { enabled: true, topup: true, personal: true },
admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true } admin: {
enabled: true,
channel: true,
models: true,
redemption: true,
user: true,
setting: true,
},
}; };
setSidebarModulesUser(defaultConfig); setSidebarModulesUser(defaultConfig);
}; };
@@ -187,7 +206,9 @@ const NotificationSettings = ({
if (!adminConfig) return true; if (!adminConfig) return true;
if (moduleKey) { if (moduleKey) {
return adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]; return (
adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]
);
} else { } else {
return adminConfig[sectionKey]?.enabled; return adminConfig[sectionKey]?.enabled;
} }
@@ -200,9 +221,13 @@ const NotificationSettings = ({
title: t('聊天区域'), title: t('聊天区域'),
description: t('操练场和聊天功能'), description: t('操练场和聊天功能'),
modules: [ modules: [
{ key: 'playground', title: t('操练场'), description: t('AI模型测试环境') }, {
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') } key: 'playground',
] title: t('操练场'),
description: t('AI模型测试环境'),
},
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
],
}, },
{ {
key: 'console', key: 'console',
@@ -212,9 +237,13 @@ const NotificationSettings = ({
{ key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
{ key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, { key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
{ key: 'log', 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: 'midjourney',
] title: t('绘图日志'),
description: t('绘图任务记录'),
},
{ key: 'task', title: t('任务日志'), description: t('系统任务记录') },
],
}, },
{ {
key: 'personal', key: 'personal',
@@ -222,8 +251,12 @@ const NotificationSettings = ({
description: t('用户个人功能'), description: t('用户个人功能'),
modules: [ modules: [
{ key: 'topup', title: t('钱包管理'), description: t('余额充值管理') }, { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
{ key: 'personal', title: t('个人设置'), description: t('个人信息设置') } {
] key: 'personal',
title: t('个人设置'),
description: t('个人信息设置'),
},
],
}, },
// 管理员区域:根据后端权限控制显示 // 管理员区域:根据后端权限控制显示
{ {
@@ -233,23 +266,35 @@ const NotificationSettings = ({
modules: [ modules: [
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') }, { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
{ key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') }, {
key: 'redemption',
title: t('兑换码管理'),
description: t('兑换码生成管理'),
},
{ key: 'user', title: t('用户管理'), description: t('用户账户管理') }, { key: 'user', title: t('用户管理'), description: t('用户账户管理') },
{ key: 'setting', title: t('系统设置'), description: t('系统参数配置') } {
] key: 'setting',
} title: t('系统设置'),
].filter(section => { description: t('系统参数配置'),
// 使用后端权限验证替代前端角色判断 },
return isSidebarSectionAllowed(section.key); ],
}).map(section => ({ },
...section, ]
modules: section.modules.filter(module => .filter((section) => {
isSidebarModuleAllowed(section.key, module.key) // 使用后端权限验证替代前端角色判断
) return isSidebarSectionAllowed(section.key);
})).filter(section => })
// 过滤掉没有可用模块的区域 .map((section) => ({
section.modules.length > 0 && isAllowedByAdmin(section.key) ...section,
); modules: section.modules.filter((module) =>
isSidebarModuleAllowed(section.key, module.key),
),
}))
.filter(
(section) =>
// 过滤掉没有可用模块的区域
section.modules.length > 0 && isAllowedByAdmin(section.key),
);
// 表单提交 // 表单提交
const handleSubmit = () => { const handleSubmit = () => {
@@ -491,7 +536,9 @@ const NotificationSettings = ({
<Form.Input <Form.Input
field='barkUrl' field='barkUrl'
label={t('Bark推送URL')} label={t('Bark推送URL')}
placeholder={t('请输入Bark推送URL例如: https://api.day.app/yourkey/{{title}}/{{content}}')} placeholder={t(
'请输入Bark推送URL例如: https://api.day.app/yourkey/{{title}}/{{content}}',
)}
onChange={(val) => handleFormChange('barkUrl', val)} onChange={(val) => handleFormChange('barkUrl', val)}
prefix={<IconLink />} prefix={<IconLink />}
extraText={t( extraText={t(
@@ -500,8 +547,7 @@ const NotificationSettings = ({
showClear showClear
rules={[ rules={[
{ {
required: required: notificationSettings.warningType === 'bark',
notificationSettings.warningType === 'bark',
message: t('请输入Bark推送URL'), message: t('请输入Bark推送URL'),
}, },
{ {
@@ -516,16 +562,23 @@ const NotificationSettings = ({
<strong>{t('模板示例')}</strong> <strong>{t('模板示例')}</strong>
</div> </div>
<div className='text-xs text-gray-600 font-mono bg-white p-3 rounded-lg shadow-sm mb-4'> <div className='text-xs text-gray-600 font-mono bg-white p-3 rounded-lg shadow-sm mb-4'>
https://api.day.app/yourkey/{'{{title}}'}/{'{{content}}'}?sound=alarm&group=quota https://api.day.app/yourkey/{'{{title}}'}/
{'{{content}}'}?sound=alarm&group=quota
</div> </div>
<div className='text-xs text-gray-500 space-y-2'> <div className='text-xs text-gray-500 space-y-2'>
<div> <strong>{'title'}:</strong> {t('通知标题')}</div> <div>
<div> <strong>{'content'}:</strong> {t('通知内容')}</div> <strong>{'title'}:</strong> {t('通知标题')}
</div>
<div>
<strong>{'content'}:</strong> {t('通知内容')}
</div>
<div className='mt-3 pt-3 border-t border-gray-200'> <div className='mt-3 pt-3 border-t border-gray-200'>
<span className='text-gray-400'>{t('更多参数请参考')}</span>{' '} <span className='text-gray-400'>
<a {t('更多参数请参考')}
href='https://github.com/Finb/Bark' </span>{' '}
target='_blank' <a
href='https://github.com/Finb/Bark'
target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
className='text-blue-500 hover:text-blue-600 font-medium' className='text-blue-500 hover:text-blue-600 font-medium'
> >
@@ -603,27 +656,25 @@ const NotificationSettings = ({
<div className='py-4'> <div className='py-4'>
<div className='mb-4'> <div className='mb-4'>
<Typography.Text <Typography.Text
type="secondary" type='secondary'
size="small" size='small'
style={{ style={{
fontSize: '12px', fontSize: '12px',
lineHeight: '1.5', lineHeight: '1.5',
color: 'var(--semi-color-text-2)' color: 'var(--semi-color-text-2)',
}} }}
> >
{t('您可以个性化设置侧边栏的要显示功能')} {t('您可以个性化设置侧边栏的要显示功能')}
</Typography.Text> </Typography.Text>
</div> </div>
{/* 边栏设置功能区域容器 */} {/* 边栏设置功能区域容器 */}
<div <div
className='border rounded-xl p-4' className='border rounded-xl p-4'
style={{ style={{
borderColor: 'var(--semi-color-border)', borderColor: 'var(--semi-color-border)',
backgroundColor: 'var(--semi-color-bg-1)' backgroundColor: 'var(--semi-color-bg-1)',
}} }}
> >
{sectionConfigs.map((section) => ( {sectionConfigs.map((section) => (
<div key={section.key} className='mb-6'> <div key={section.key} className='mb-6'>
{/* 区域标题和总开关 */} {/* 区域标题和总开关 */}
@@ -632,80 +683,102 @@ const NotificationSettings = ({
style={{ style={{
backgroundColor: 'var(--semi-color-fill-0)', backgroundColor: 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-border-light)', border: '1px solid var(--semi-color-border-light)',
borderColor: 'var(--semi-color-fill-1)' borderColor: 'var(--semi-color-fill-1)',
}} }}
> >
<div> <div>
<div className='font-semibold text-base text-gray-900 mb-1'> <div className='font-semibold text-base text-gray-900 mb-1'>
{section.title} {section.title}
</div> </div>
<Typography.Text <Typography.Text
type="secondary" type='secondary'
size="small" size='small'
style={{ style={{
fontSize: '12px', fontSize: '12px',
lineHeight: '1.5', lineHeight: '1.5',
color: 'var(--semi-color-text-2)' color: 'var(--semi-color-text-2)',
}} }}
>
{section.description}
</Typography.Text>
</div>
<Switch
checked={sidebarModulesUser[section.key]?.enabled}
onChange={handleSectionChange(section.key)}
size="default"
/>
</div>
{/* 功能模块网格 */}
<Row gutter={[12, 12]}>
{section.modules
.filter(module => isAllowedByAdmin(section.key, module.key))
.map((module) => (
<Col key={module.key} xs={24} sm={24} md={12} lg={8} xl={8}>
<Card
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled ? '' : 'opacity-50'
}`}
bodyStyle={{ padding: '16px' }}
hoverable
> >
<div className='flex justify-between items-center h-full'> {section.description}
<div className='flex-1 text-left'> </Typography.Text>
<div className='font-semibold text-sm text-gray-900 mb-1'> </div>
{module.title} <Switch
checked={sidebarModulesUser[section.key]?.enabled}
onChange={handleSectionChange(section.key)}
size='default'
/>
</div>
{/* 功能模块网格 */}
<Row gutter={[12, 12]}>
{section.modules
.filter((module) =>
isAllowedByAdmin(section.key, module.key),
)
.map((module) => (
<Col
key={module.key}
xs={24}
sm={24}
md={12}
lg={8}
xl={8}
>
<Card
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled
? ''
: '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>
<Typography.Text
type='secondary'
size='small'
className='block'
style={{
fontSize: '12px',
lineHeight: '1.5',
color: 'var(--semi-color-text-2)',
marginTop: '4px',
}}
>
{module.description}
</Typography.Text>
</div>
<div className='ml-4'>
<Switch
checked={
sidebarModulesUser[section.key]?.[
module.key
]
}
onChange={handleModuleChange(
section.key,
module.key,
)}
size='default'
disabled={
!sidebarModulesUser[section.key]
?.enabled
}
/>
</div>
</div> </div>
<Typography.Text </Card>
type="secondary" </Col>
size="small" ))}
className='block' </Row>
style={{
fontSize: '12px',
lineHeight: '1.5',
color: 'var(--semi-color-text-2)',
marginTop: '4px'
}}
>
{module.description}
</Typography.Text>
</div>
<div className='ml-4'>
<Switch
checked={sidebarModulesUser[section.key]?.[module.key]}
onChange={handleModuleChange(section.key, module.key)}
size="default"
disabled={!sidebarModulesUser[section.key]?.enabled}
/>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div> </div>
))} ))}
</div> {/* 关闭边栏设置功能区域容器 */} </div>{' '}
{/* 关闭边栏设置功能区域容器 */}
</div> </div>
</TabPane> </TabPane>
)} )}

View File

@@ -231,11 +231,14 @@ export const getChannelsColumns = ({
theme='outline' theme='outline'
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigator.clipboard.writeText(record.remark).then(() => { navigator.clipboard
showSuccess(t('复制成功')); .writeText(record.remark)
}).catch(() => { .then(() => {
showError(t('复制失败')); showSuccess(t('复制成功'));
}); })
.catch(() => {
showError(t('复制失败'));
});
}} }}
> >
{t('复制')} {t('复制')}

View File

@@ -64,7 +64,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
if (typeof modules.pricing === 'boolean') { if (typeof modules.pricing === 'boolean') {
modules.pricing = { modules.pricing = {
enabled: modules.pricing, enabled: modules.pricing,
requireAuth: false // 默认不需要登录鉴权 requireAuth: false, // 默认不需要登录鉴权
}; };
} }

View File

@@ -20,67 +20,66 @@ For commercial licensing, please contact support@quantumnous.com
import { useMemo } from 'react'; import { useMemo } from 'react';
export const useNavigation = (t, docsLink, headerNavModules) => { export const useNavigation = (t, docsLink, headerNavModules) => {
const mainNavLinks = useMemo( const mainNavLinks = useMemo(() => {
() => { // 默认配置,如果没有传入配置则显示所有模块
// 默认配置,如果没有传入配置则显示所有模块 const defaultModules = {
const defaultModules = { home: true,
home: true, console: true,
console: true, pricing: true,
pricing: true, docs: true,
docs: true, about: true,
about: true, };
};
// 使用传入的配置或默认配置 // 使用传入的配置或默认配置
const modules = headerNavModules || defaultModules; const modules = headerNavModules || defaultModules;
const allLinks = [ const allLinks = [
{ {
text: t('首页'), text: t('首页'),
itemKey: 'home', itemKey: 'home',
to: '/', to: '/',
}, },
{ {
text: t('控制台'), text: t('控制台'),
itemKey: 'console', itemKey: 'console',
to: '/console', to: '/console',
}, },
{ {
text: t('模型广场'), text: t('模型广场'),
itemKey: 'pricing', itemKey: 'pricing',
to: '/pricing', to: '/pricing',
}, },
...(docsLink ...(docsLink
? [ ? [
{ {
text: t('文档'), text: t('文档'),
itemKey: 'docs', itemKey: 'docs',
isExternal: true, isExternal: true,
externalLink: docsLink, externalLink: docsLink,
}, },
] ]
: []), : []),
{ {
text: t('关于'), text: t('关于'),
itemKey: 'about', itemKey: 'about',
to: '/about', to: '/about',
}, },
]; ];
// 根据配置过滤导航链接 // 根据配置过滤导航链接
return allLinks.filter(link => { return allLinks.filter((link) => {
if (link.itemKey === 'docs') { if (link.itemKey === 'docs') {
return docsLink && modules.docs; return docsLink && modules.docs;
} }
if (link.itemKey === 'pricing') { if (link.itemKey === 'pricing') {
// 支持新的pricing配置格式 // 支持新的pricing配置格式
return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing; return typeof modules.pricing === 'object'
} ? modules.pricing.enabled
return modules[link.itemKey] === true; : modules.pricing;
}); }
}, return modules[link.itemKey] === true;
[t, docsLink, headerNavModules], });
); }, [t, docsLink, headerNavModules]);
return { return {
mainNavLinks, mainNavLinks,

View File

@@ -31,7 +31,7 @@ export const useSidebar = () => {
chat: { chat: {
enabled: true, enabled: true,
playground: true, playground: true,
chat: true chat: true,
}, },
console: { console: {
enabled: true, enabled: true,
@@ -39,12 +39,12 @@ export const useSidebar = () => {
token: true, token: true,
log: true, log: true,
midjourney: true, midjourney: true,
task: true task: true,
}, },
personal: { personal: {
enabled: true, enabled: true,
topup: true, topup: true,
personal: true personal: true,
}, },
admin: { admin: {
enabled: true, enabled: true,
@@ -52,8 +52,8 @@ export const useSidebar = () => {
models: true, models: true,
redemption: true, redemption: true,
user: true, user: true,
setting: true setting: true,
} },
}; };
// 获取管理员配置 // 获取管理员配置
@@ -87,12 +87,15 @@ export const useSidebar = () => {
// 当用户没有配置时,生成一个基于管理员配置的默认用户配置 // 当用户没有配置时,生成一个基于管理员配置的默认用户配置
// 这样可以确保权限控制正确生效 // 这样可以确保权限控制正确生效
const defaultUserConfig = {}; const defaultUserConfig = {};
Object.keys(adminConfig).forEach(sectionKey => { Object.keys(adminConfig).forEach((sectionKey) => {
if (adminConfig[sectionKey]?.enabled) { if (adminConfig[sectionKey]?.enabled) {
defaultUserConfig[sectionKey] = { enabled: true }; defaultUserConfig[sectionKey] = { enabled: true };
// 为每个管理员允许的模块设置默认值为true // 为每个管理员允许的模块设置默认值为true
Object.keys(adminConfig[sectionKey]).forEach(moduleKey => { Object.keys(adminConfig[sectionKey]).forEach((moduleKey) => {
if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) { if (
moduleKey !== 'enabled' &&
adminConfig[sectionKey][moduleKey]
) {
defaultUserConfig[sectionKey][moduleKey] = true; defaultUserConfig[sectionKey][moduleKey] = true;
} }
}); });
@@ -103,10 +106,10 @@ export const useSidebar = () => {
} catch (error) { } catch (error) {
// 出错时也生成默认配置,而不是设置为空对象 // 出错时也生成默认配置,而不是设置为空对象
const defaultUserConfig = {}; const defaultUserConfig = {};
Object.keys(adminConfig).forEach(sectionKey => { Object.keys(adminConfig).forEach((sectionKey) => {
if (adminConfig[sectionKey]?.enabled) { if (adminConfig[sectionKey]?.enabled) {
defaultUserConfig[sectionKey] = { enabled: true }; defaultUserConfig[sectionKey] = { enabled: true };
Object.keys(adminConfig[sectionKey]).forEach(moduleKey => { Object.keys(adminConfig[sectionKey]).forEach((moduleKey) => {
if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) { if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) {
defaultUserConfig[sectionKey][moduleKey] = true; defaultUserConfig[sectionKey][moduleKey] = true;
} }
@@ -149,7 +152,7 @@ export const useSidebar = () => {
} }
// 遍历所有区域 // 遍历所有区域
Object.keys(adminConfig).forEach(sectionKey => { Object.keys(adminConfig).forEach((sectionKey) => {
const adminSection = adminConfig[sectionKey]; const adminSection = adminConfig[sectionKey];
const userSection = userConfig[sectionKey]; const userSection = userConfig[sectionKey];
@@ -161,18 +164,21 @@ export const useSidebar = () => {
// 区域级别:用户可以选择隐藏管理员允许的区域 // 区域级别:用户可以选择隐藏管理员允许的区域
// 当userSection存在时检查enabled状态否则默认为true // 当userSection存在时检查enabled状态否则默认为true
const sectionEnabled = userSection ? (userSection.enabled !== false) : true; const sectionEnabled = userSection ? userSection.enabled !== false : true;
result[sectionKey] = { enabled: sectionEnabled }; result[sectionKey] = { enabled: sectionEnabled };
// 功能级别:只有管理员和用户都允许的功能才显示 // 功能级别:只有管理员和用户都允许的功能才显示
Object.keys(adminSection).forEach(moduleKey => { Object.keys(adminSection).forEach((moduleKey) => {
if (moduleKey === 'enabled') return; if (moduleKey === 'enabled') return;
const adminAllowed = adminSection[moduleKey]; const adminAllowed = adminSection[moduleKey];
// 当userSection存在时检查模块状态否则默认为true // 当userSection存在时检查模块状态否则默认为true
const userAllowed = userSection ? (userSection[moduleKey] !== false) : true; const userAllowed = userSection
? userSection[moduleKey] !== false
: true;
result[sectionKey][moduleKey] = adminAllowed && userAllowed && sectionEnabled; result[sectionKey][moduleKey] =
adminAllowed && userAllowed && sectionEnabled;
}); });
}); });
@@ -192,9 +198,9 @@ export const useSidebar = () => {
const hasSectionVisibleModules = (sectionKey) => { const hasSectionVisibleModules = (sectionKey) => {
const section = finalConfig[sectionKey]; const section = finalConfig[sectionKey];
if (!section?.enabled) return false; if (!section?.enabled) return false;
return Object.keys(section).some(key => return Object.keys(section).some(
key !== 'enabled' && section[key] === true (key) => key !== 'enabled' && section[key] === true,
); );
}; };
@@ -202,9 +208,10 @@ export const useSidebar = () => {
const getVisibleModules = (sectionKey) => { const getVisibleModules = (sectionKey) => {
const section = finalConfig[sectionKey]; const section = finalConfig[sectionKey];
if (!section?.enabled) return []; if (!section?.enabled) return [];
return Object.keys(section) return Object.keys(section).filter(
.filter(key => key !== 'enabled' && section[key] === true); (key) => key !== 'enabled' && section[key] === true,
);
}; };
return { return {
@@ -215,6 +222,6 @@ export const useSidebar = () => {
isModuleVisible, isModuleVisible,
hasSectionVisibleModules, hasSectionVisibleModules,
getVisibleModules, getVisibleModules,
refreshUserConfig refreshUserConfig,
}; };
}; };

View File

@@ -1,3 +1,21 @@
/*
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 } from 'react'; import { useState, useEffect } from 'react';
import { API } from '../../helpers'; import { API } from '../../helpers';
@@ -52,22 +70,22 @@ export const useUserPermissions = () => {
const isSidebarModuleAllowed = (sectionKey, moduleKey) => { const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
if (!permissions?.sidebar_modules) return true; if (!permissions?.sidebar_modules) return true;
const sectionPerms = permissions.sidebar_modules[sectionKey]; const sectionPerms = permissions.sidebar_modules[sectionKey];
// 如果整个区域被禁用 // 如果整个区域被禁用
if (sectionPerms === false) return false; if (sectionPerms === false) return false;
// 如果区域存在但模块被禁用 // 如果区域存在但模块被禁用
if (sectionPerms && sectionPerms[moduleKey] === false) return false; if (sectionPerms && sectionPerms[moduleKey] === false) return false;
return true; return true;
}; };
// 获取允许的边栏区域列表 // 获取允许的边栏区域列表
const getAllowedSidebarSections = () => { const getAllowedSidebarSections = () => {
if (!permissions?.sidebar_modules) return []; if (!permissions?.sidebar_modules) return [];
return Object.keys(permissions.sidebar_modules).filter(sectionKey => return Object.keys(permissions.sidebar_modules).filter((sectionKey) =>
isSidebarSectionAllowed(sectionKey) isSidebarSectionAllowed(sectionKey),
); );
}; };
@@ -75,12 +93,13 @@ export const useUserPermissions = () => {
const getAllowedSidebarModules = (sectionKey) => { const getAllowedSidebarModules = (sectionKey) => {
if (!permissions?.sidebar_modules) return []; if (!permissions?.sidebar_modules) return [];
const sectionPerms = permissions.sidebar_modules[sectionKey]; const sectionPerms = permissions.sidebar_modules[sectionKey];
if (sectionPerms === false) return []; if (sectionPerms === false) return [];
if (!sectionPerms || typeof sectionPerms !== 'object') return []; if (!sectionPerms || typeof sectionPerms !== 'object') return [];
return Object.keys(sectionPerms).filter(moduleKey => return Object.keys(sectionPerms).filter(
moduleKey !== 'enabled' && sectionPerms[moduleKey] === true (moduleKey) =>
moduleKey !== 'enabled' && sectionPerms[moduleKey] === true,
); );
}; };

View File

@@ -18,7 +18,15 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useEffect, useState, useContext } from 'react'; import React, { useEffect, useState, useContext } from 'react';
import { Button, Card, Col, Form, Row, Switch, Typography } from '@douyinfe/semi-ui'; import {
Button,
Card,
Col,
Form,
Row,
Switch,
Typography,
} from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../../../helpers'; import { API, showError, showSuccess } from '../../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { StatusContext } from '../../../context/Status'; import { StatusContext } from '../../../context/Status';
@@ -29,14 +37,14 @@ export default function SettingsHeaderNavModules(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
// 顶栏模块管理状态 // 顶栏模块管理状态
const [headerNavModules, setHeaderNavModules] = useState({ const [headerNavModules, setHeaderNavModules] = useState({
home: true, home: true,
console: true, console: true,
pricing: { pricing: {
enabled: true, enabled: true,
requireAuth: false // 默认不需要登录鉴权 requireAuth: false, // 默认不需要登录鉴权
}, },
docs: true, docs: true,
about: true, about: true,
@@ -50,7 +58,7 @@ export default function SettingsHeaderNavModules(props) {
// 对于pricing模块只更新enabled属性 // 对于pricing模块只更新enabled属性
newModules[moduleKey] = { newModules[moduleKey] = {
...newModules[moduleKey], ...newModules[moduleKey],
enabled: checked enabled: checked,
}; };
} else { } else {
newModules[moduleKey] = checked; newModules[moduleKey] = checked;
@@ -64,7 +72,7 @@ export default function SettingsHeaderNavModules(props) {
const newModules = { ...headerNavModules }; const newModules = { ...headerNavModules };
newModules.pricing = { newModules.pricing = {
...newModules.pricing, ...newModules.pricing,
requireAuth: checked requireAuth: checked,
}; };
setHeaderNavModules(newModules); setHeaderNavModules(newModules);
} }
@@ -76,7 +84,7 @@ export default function SettingsHeaderNavModules(props) {
console: true, console: true,
pricing: { pricing: {
enabled: true, enabled: true,
requireAuth: false requireAuth: false,
}, },
docs: true, docs: true,
about: true, about: true,
@@ -102,8 +110,8 @@ export default function SettingsHeaderNavModules(props) {
type: 'set', type: 'set',
payload: { payload: {
...statusState.status, ...statusState.status,
HeaderNavModules: JSON.stringify(headerNavModules) HeaderNavModules: JSON.stringify(headerNavModules),
} },
}); });
// 刷新父组件状态 // 刷新父组件状态
@@ -130,7 +138,7 @@ export default function SettingsHeaderNavModules(props) {
if (typeof modules.pricing === 'boolean') { if (typeof modules.pricing === 'boolean') {
modules.pricing = { modules.pricing = {
enabled: modules.pricing, enabled: modules.pricing,
requireAuth: false // 默认不需要登录鉴权 requireAuth: false, // 默认不需要登录鉴权
}; };
} }
@@ -142,7 +150,7 @@ export default function SettingsHeaderNavModules(props) {
console: true, console: true,
pricing: { pricing: {
enabled: true, enabled: true,
requireAuth: false requireAuth: false,
}, },
docs: true, docs: true,
about: true, about: true,
@@ -157,35 +165,37 @@ export default function SettingsHeaderNavModules(props) {
{ {
key: 'home', key: 'home',
title: t('首页'), title: t('首页'),
description: t('用户主页,展示系统信息') description: t('用户主页,展示系统信息'),
}, },
{ {
key: 'console', key: 'console',
title: t('控制台'), title: t('控制台'),
description: t('用户控制面板,管理账户') description: t('用户控制面板,管理账户'),
}, },
{ {
key: 'pricing', key: 'pricing',
title: t('模型广场'), title: t('模型广场'),
description: t('模型定价,需要登录访问'), description: t('模型定价,需要登录访问'),
hasSubConfig: true // 标识该模块有子配置 hasSubConfig: true, // 标识该模块有子配置
}, },
{ {
key: 'docs', key: 'docs',
title: t('文档'), title: t('文档'),
description: t('系统文档和帮助信息') description: t('系统文档和帮助信息'),
}, },
{ {
key: 'about', key: 'about',
title: t('关于'), title: t('关于'),
description: t('关于系统的详细信息') description: t('关于系统的详细信息'),
} },
]; ];
return ( return (
<Card> <Card>
<Form.Section text={t('顶栏管理')} extraText={t('控制顶栏模块显示状态,全局生效')}> <Form.Section
text={t('顶栏管理')}
extraText={t('控制顶栏模块显示状态,全局生效')}
>
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}> <Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
{moduleConfigs.map((module) => ( {moduleConfigs.map((module) => (
<Col key={module.key} xs={24} sm={12} md={6} lg={6} xl={6}> <Col key={module.key} xs={24} sm={12} md={6} lg={6} xl={6}>
@@ -195,34 +205,38 @@ export default function SettingsHeaderNavModules(props) {
border: '1px solid var(--semi-color-border)', border: '1px solid var(--semi-color-border)',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
background: 'var(--semi-color-bg-1)', background: 'var(--semi-color-bg-1)',
minHeight: '80px' minHeight: '80px',
}} }}
bodyStyle={{ padding: '16px' }} bodyStyle={{ padding: '16px' }}
hoverable hoverable
> >
<div style={{ <div
display: 'flex', style={{
justifyContent: 'space-between', display: 'flex',
alignItems: 'center', justifyContent: 'space-between',
height: '100%' alignItems: 'center',
}}> height: '100%',
}}
>
<div style={{ flex: 1, textAlign: 'left' }}> <div style={{ flex: 1, textAlign: 'left' }}>
<div style={{ <div
fontWeight: '600', style={{
fontSize: '14px', fontWeight: '600',
color: 'var(--semi-color-text-0)', fontSize: '14px',
marginBottom: '4px' color: 'var(--semi-color-text-0)',
}}> marginBottom: '4px',
}}
>
{module.title} {module.title}
</div> </div>
<Text <Text
type="secondary" type='secondary'
size="small" size='small'
style={{ style={{
fontSize: '12px', fontSize: '12px',
color: 'var(--semi-color-text-2)', color: 'var(--semi-color-text-2)',
lineHeight: '1.4', lineHeight: '1.4',
display: 'block' display: 'block',
}} }}
> >
{module.description} {module.description}
@@ -230,78 +244,94 @@ export default function SettingsHeaderNavModules(props) {
</div> </div>
<div style={{ marginLeft: '16px' }}> <div style={{ marginLeft: '16px' }}>
<Switch <Switch
checked={module.key === 'pricing' ? headerNavModules[module.key]?.enabled : headerNavModules[module.key]} checked={
module.key === 'pricing'
? headerNavModules[module.key]?.enabled
: headerNavModules[module.key]
}
onChange={handleHeaderNavModuleChange(module.key)} onChange={handleHeaderNavModuleChange(module.key)}
size="default" size='default'
/> />
</div> </div>
</div> </div>
{/* 为模型广场添加权限控制子开关 */} {/* 为模型广场添加权限控制子开关 */}
{module.key === 'pricing' && (module.key === 'pricing' ? headerNavModules[module.key]?.enabled : headerNavModules[module.key]) && ( {module.key === 'pricing' &&
<div style={{ (module.key === 'pricing'
borderTop: '1px solid var(--semi-color-border)', ? headerNavModules[module.key]?.enabled
marginTop: '12px', : headerNavModules[module.key]) && (
paddingTop: '12px' <div
}}> style={{
<div style={{ borderTop: '1px solid var(--semi-color-border)',
display: 'flex', marginTop: '12px',
justifyContent: 'space-between', paddingTop: '12px',
alignItems: 'center' }}
}}> >
<div style={{ flex: 1, textAlign: 'left' }}> <div
<div style={{ style={{
fontWeight: '500', display: 'flex',
fontSize: '12px', justifyContent: 'space-between',
color: 'var(--semi-color-text-1)', alignItems: 'center',
marginBottom: '2px' }}
}}> >
{t('需要登录访问')} <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='small'
/>
</div> </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="small"
/>
</div> </div>
</div> </div>
</div> )}
)}
</Card> </Card>
</Col> </Col>
))} ))}
</Row> </Row>
<div style={{ <div
display: 'flex', style={{
gap: '12px', display: 'flex',
justifyContent: 'flex-start', gap: '12px',
alignItems: 'center', justifyContent: 'flex-start',
paddingTop: '8px', alignItems: 'center',
borderTop: '1px solid var(--semi-color-border)' paddingTop: '8px',
}}> borderTop: '1px solid var(--semi-color-border)',
}}
>
<Button <Button
size='default' size='default'
type='tertiary' type='tertiary'
onClick={resetHeaderNavModules} onClick={resetHeaderNavModules}
style={{ style={{
borderRadius: '6px', borderRadius: '6px',
fontWeight: '500' fontWeight: '500',
}} }}
> >
{t('重置为默认')} {t('重置为默认')}
@@ -314,7 +344,7 @@ export default function SettingsHeaderNavModules(props) {
style={{ style={{
borderRadius: '6px', borderRadius: '6px',
fontWeight: '500', fontWeight: '500',
minWidth: '100px' minWidth: '100px',
}} }}
> >
{t('保存设置')} {t('保存设置')}

View File

@@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card, Form, Button, Switch, Row, Col, Typography } from '@douyinfe/semi-ui'; import {
Card,
Form,
Button,
Switch,
Row,
Col,
Typography,
} from '@douyinfe/semi-ui';
import { API, showSuccess, showError } from '../../../helpers'; import { API, showSuccess, showError } from '../../../helpers';
import { StatusContext } from '../../../context/Status'; import { StatusContext } from '../../../context/Status';
@@ -29,13 +37,13 @@ export default function SettingsSidebarModulesAdmin(props) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
// 左侧边栏模块管理状态(管理员全局控制) // 左侧边栏模块管理状态(管理员全局控制)
const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState({ const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState({
chat: { chat: {
enabled: true, enabled: true,
playground: true, playground: true,
chat: true chat: true,
}, },
console: { console: {
enabled: true, enabled: true,
@@ -43,12 +51,12 @@ export default function SettingsSidebarModulesAdmin(props) {
token: true, token: true,
log: true, log: true,
midjourney: true, midjourney: true,
task: true task: true,
}, },
personal: { personal: {
enabled: true, enabled: true,
topup: true, topup: true,
personal: true personal: true,
}, },
admin: { admin: {
enabled: true, enabled: true,
@@ -56,19 +64,19 @@ export default function SettingsSidebarModulesAdmin(props) {
models: true, models: true,
redemption: true, redemption: true,
user: true, user: true,
setting: true setting: true,
} },
}); });
// 处理区域级别开关变更 // 处理区域级别开关变更
function handleSectionChange(sectionKey) { function handleSectionChange(sectionKey) {
return (checked) => { return (checked) => {
const newModules = { const newModules = {
...sidebarModulesAdmin, ...sidebarModulesAdmin,
[sectionKey]: { [sectionKey]: {
...sidebarModulesAdmin[sectionKey], ...sidebarModulesAdmin[sectionKey],
enabled: checked enabled: checked,
} },
}; };
setSidebarModulesAdmin(newModules); setSidebarModulesAdmin(newModules);
}; };
@@ -77,12 +85,12 @@ export default function SettingsSidebarModulesAdmin(props) {
// 处理功能级别开关变更 // 处理功能级别开关变更
function handleModuleChange(sectionKey, moduleKey) { function handleModuleChange(sectionKey, moduleKey) {
return (checked) => { return (checked) => {
const newModules = { const newModules = {
...sidebarModulesAdmin, ...sidebarModulesAdmin,
[sectionKey]: { [sectionKey]: {
...sidebarModulesAdmin[sectionKey], ...sidebarModulesAdmin[sectionKey],
[moduleKey]: checked [moduleKey]: checked,
} },
}; };
setSidebarModulesAdmin(newModules); setSidebarModulesAdmin(newModules);
}; };
@@ -94,7 +102,7 @@ export default function SettingsSidebarModulesAdmin(props) {
chat: { chat: {
enabled: true, enabled: true,
playground: true, playground: true,
chat: true chat: true,
}, },
console: { console: {
enabled: true, enabled: true,
@@ -102,12 +110,12 @@ export default function SettingsSidebarModulesAdmin(props) {
token: true, token: true,
log: true, log: true,
midjourney: true, midjourney: true,
task: true task: true,
}, },
personal: { personal: {
enabled: true, enabled: true,
topup: true, topup: true,
personal: true personal: true,
}, },
admin: { admin: {
enabled: true, enabled: true,
@@ -115,8 +123,8 @@ export default function SettingsSidebarModulesAdmin(props) {
models: true, models: true,
redemption: true, redemption: true,
user: true, user: true,
setting: true setting: true,
} },
}; };
setSidebarModulesAdmin(defaultModules); setSidebarModulesAdmin(defaultModules);
showSuccess(t('已重置为默认配置')); showSuccess(t('已重置为默认配置'));
@@ -139,8 +147,8 @@ export default function SettingsSidebarModulesAdmin(props) {
type: 'set', type: 'set',
payload: { payload: {
...statusState.status, ...statusState.status,
SidebarModulesAdmin: JSON.stringify(sidebarModulesAdmin) SidebarModulesAdmin: JSON.stringify(sidebarModulesAdmin),
} },
}); });
// 刷新父组件状态 // 刷新父组件状态
@@ -167,9 +175,23 @@ export default function SettingsSidebarModulesAdmin(props) {
// 使用默认配置 // 使用默认配置
const defaultModules = { const defaultModules = {
chat: { enabled: true, playground: true, chat: true }, chat: { enabled: true, playground: true, chat: true },
console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true }, console: {
enabled: true,
detail: true,
token: true,
log: true,
midjourney: true,
task: true,
},
personal: { enabled: true, topup: true, personal: true }, personal: { enabled: true, topup: true, personal: true },
admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true } admin: {
enabled: true,
channel: true,
models: true,
redemption: true,
user: true,
setting: true,
},
}; };
setSidebarModulesAdmin(defaultModules); setSidebarModulesAdmin(defaultModules);
} }
@@ -183,9 +205,13 @@ export default function SettingsSidebarModulesAdmin(props) {
title: t('聊天区域'), title: t('聊天区域'),
description: t('操练场和聊天功能'), description: t('操练场和聊天功能'),
modules: [ modules: [
{ key: 'playground', title: t('操练场'), description: t('AI模型测试环境') }, {
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') } key: 'playground',
] title: t('操练场'),
description: t('AI模型测试环境'),
},
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
],
}, },
{ {
key: 'console', key: 'console',
@@ -195,9 +221,13 @@ export default function SettingsSidebarModulesAdmin(props) {
{ key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
{ key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, { key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
{ key: 'log', 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: 'midjourney',
] title: t('绘图日志'),
description: t('绘图任务记录'),
},
{ key: 'task', title: t('任务日志'), description: t('系统任务记录') },
],
}, },
{ {
key: 'personal', key: 'personal',
@@ -205,8 +235,12 @@ export default function SettingsSidebarModulesAdmin(props) {
description: t('用户个人功能'), description: t('用户个人功能'),
modules: [ modules: [
{ key: 'topup', title: t('钱包管理'), description: t('余额充值管理') }, { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
{ key: 'personal', title: t('个人设置'), description: t('个人信息设置') } {
] key: 'personal',
title: t('个人设置'),
description: t('个人信息设置'),
},
],
}, },
{ {
key: 'admin', key: 'admin',
@@ -215,46 +249,62 @@ export default function SettingsSidebarModulesAdmin(props) {
modules: [ modules: [
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') }, { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
{ key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') }, {
key: 'redemption',
title: t('兑换码管理'),
description: t('兑换码生成管理'),
},
{ key: 'user', title: t('用户管理'), description: t('用户账户管理') }, { key: 'user', title: t('用户管理'), description: t('用户账户管理') },
{ key: 'setting', title: t('系统设置'), description: t('系统参数配置') } {
] key: 'setting',
} title: t('系统设置'),
description: t('系统参数配置'),
},
],
},
]; ];
return ( return (
<Card> <Card>
<Form.Section text={t('侧边栏管理(全局控制)')} extraText={t('全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用')}> <Form.Section
text={t('侧边栏管理(全局控制)')}
extraText={t(
'全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用',
)}
>
{sectionConfigs.map((section) => ( {sectionConfigs.map((section) => (
<div key={section.key} style={{ marginBottom: '32px' }}> <div key={section.key} style={{ marginBottom: '32px' }}>
{/* 区域标题和总开关 */} {/* 区域标题和总开关 */}
<div style={{ <div
display: 'flex', style={{
justifyContent: 'space-between', display: 'flex',
alignItems: 'center', justifyContent: 'space-between',
marginBottom: '16px', alignItems: 'center',
padding: '12px 16px', marginBottom: '16px',
backgroundColor: 'var(--semi-color-fill-0)', padding: '12px 16px',
borderRadius: '8px', backgroundColor: 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-border)' borderRadius: '8px',
}}> border: '1px solid var(--semi-color-border)',
}}
>
<div> <div>
<div style={{ <div
fontWeight: '600', style={{
fontSize: '16px', fontWeight: '600',
color: 'var(--semi-color-text-0)', fontSize: '16px',
marginBottom: '4px' color: 'var(--semi-color-text-0)',
}}> marginBottom: '4px',
}}
>
{section.title} {section.title}
</div> </div>
<Text <Text
type="secondary" type='secondary'
size="small" size='small'
style={{ style={{
fontSize: '12px', fontSize: '12px',
color: 'var(--semi-color-text-2)', color: 'var(--semi-color-text-2)',
lineHeight: '1.4' lineHeight: '1.4',
}} }}
> >
{section.description} {section.description}
@@ -263,7 +313,7 @@ export default function SettingsSidebarModulesAdmin(props) {
<Switch <Switch
checked={sidebarModulesAdmin[section.key]?.enabled} checked={sidebarModulesAdmin[section.key]?.enabled}
onChange={handleSectionChange(section.key)} onChange={handleSectionChange(section.key)}
size="default" size='default'
/> />
</div> </div>
@@ -275,33 +325,39 @@ export default function SettingsSidebarModulesAdmin(props) {
bodyStyle={{ padding: '16px' }} bodyStyle={{ padding: '16px' }}
hoverable hoverable
style={{ style={{
opacity: sidebarModulesAdmin[section.key]?.enabled ? 1 : 0.5, opacity: sidebarModulesAdmin[section.key]?.enabled
transition: 'opacity 0.2s' ? 1
: 0.5,
transition: 'opacity 0.2s',
}} }}
> >
<div style={{ <div
display: 'flex', style={{
justifyContent: 'space-between', display: 'flex',
alignItems: 'center', justifyContent: 'space-between',
height: '100%' alignItems: 'center',
}}> height: '100%',
}}
>
<div style={{ flex: 1, textAlign: 'left' }}> <div style={{ flex: 1, textAlign: 'left' }}>
<div style={{ <div
fontWeight: '600', style={{
fontSize: '14px', fontWeight: '600',
color: 'var(--semi-color-text-0)', fontSize: '14px',
marginBottom: '4px' color: 'var(--semi-color-text-0)',
}}> marginBottom: '4px',
}}
>
{module.title} {module.title}
</div> </div>
<Text <Text
type="secondary" type='secondary'
size="small" size='small'
style={{ style={{
fontSize: '12px', fontSize: '12px',
color: 'var(--semi-color-text-2)', color: 'var(--semi-color-text-2)',
lineHeight: '1.4', lineHeight: '1.4',
display: 'block' display: 'block',
}} }}
> >
{module.description} {module.description}
@@ -309,9 +365,11 @@ export default function SettingsSidebarModulesAdmin(props) {
</div> </div>
<div style={{ marginLeft: '16px' }}> <div style={{ marginLeft: '16px' }}>
<Switch <Switch
checked={sidebarModulesAdmin[section.key]?.[module.key]} checked={
sidebarModulesAdmin[section.key]?.[module.key]
}
onChange={handleModuleChange(section.key, module.key)} onChange={handleModuleChange(section.key, module.key)}
size="default" size='default'
disabled={!sidebarModulesAdmin[section.key]?.enabled} disabled={!sidebarModulesAdmin[section.key]?.enabled}
/> />
</div> </div>
@@ -323,21 +381,23 @@ export default function SettingsSidebarModulesAdmin(props) {
</div> </div>
))} ))}
<div style={{ <div
display: 'flex', style={{
gap: '12px', display: 'flex',
justifyContent: 'flex-start', gap: '12px',
alignItems: 'center', justifyContent: 'flex-start',
paddingTop: '8px', alignItems: 'center',
borderTop: '1px solid var(--semi-color-border)' paddingTop: '8px',
}}> borderTop: '1px solid var(--semi-color-border)',
}}
>
<Button <Button
size='default' size='default'
type='tertiary' type='tertiary'
onClick={resetSidebarModules} onClick={resetSidebarModules}
style={{ style={{
borderRadius: '6px', borderRadius: '6px',
fontWeight: '500' fontWeight: '500',
}} }}
> >
{t('重置为默认')} {t('重置为默认')}
@@ -350,7 +410,7 @@ export default function SettingsSidebarModulesAdmin(props) {
style={{ style={{
borderRadius: '6px', borderRadius: '6px',
fontWeight: '500', fontWeight: '500',
minWidth: '100px' minWidth: '100px',
}} }}
> >
{t('保存设置')} {t('保存设置')}

View File

@@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
import { useState, useEffect, useContext } from 'react'; import { useState, useEffect, useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card, Button, Switch, Typography, Row, Col, Avatar } from '@douyinfe/semi-ui'; import {
Card,
Button,
Switch,
Typography,
Row,
Col,
Avatar,
} from '@douyinfe/semi-ui';
import { API, showSuccess, showError } from '../../../helpers'; import { API, showSuccess, showError } from '../../../helpers';
import { StatusContext } from '../../../context/Status'; import { StatusContext } from '../../../context/Status';
import { UserContext } from '../../../context/User'; import { UserContext } from '../../../context/User';
@@ -55,7 +63,7 @@ export default function SettingsSidebarModulesUser() {
if (permissionsLoading) { if (permissionsLoading) {
return null; return null;
} }
// 根据用户权限生成默认配置 // 根据用户权限生成默认配置
const generateDefaultConfig = () => { const generateDefaultConfig = () => {
const defaultConfig = {}; const defaultConfig = {};
@@ -65,7 +73,7 @@ export default function SettingsSidebarModulesUser() {
defaultConfig.chat = { defaultConfig.chat = {
enabled: true, enabled: true,
playground: isSidebarModuleAllowed('chat', 'playground'), playground: isSidebarModuleAllowed('chat', 'playground'),
chat: isSidebarModuleAllowed('chat', 'chat') chat: isSidebarModuleAllowed('chat', 'chat'),
}; };
} }
@@ -77,7 +85,7 @@ export default function SettingsSidebarModulesUser() {
token: isSidebarModuleAllowed('console', 'token'), token: isSidebarModuleAllowed('console', 'token'),
log: isSidebarModuleAllowed('console', 'log'), log: isSidebarModuleAllowed('console', 'log'),
midjourney: isSidebarModuleAllowed('console', 'midjourney'), midjourney: isSidebarModuleAllowed('console', 'midjourney'),
task: isSidebarModuleAllowed('console', 'task') task: isSidebarModuleAllowed('console', 'task'),
}; };
} }
@@ -86,7 +94,7 @@ export default function SettingsSidebarModulesUser() {
defaultConfig.personal = { defaultConfig.personal = {
enabled: true, enabled: true,
topup: isSidebarModuleAllowed('personal', 'topup'), topup: isSidebarModuleAllowed('personal', 'topup'),
personal: isSidebarModuleAllowed('personal', 'personal') personal: isSidebarModuleAllowed('personal', 'personal'),
}; };
} }
@@ -98,7 +106,7 @@ export default function SettingsSidebarModulesUser() {
models: isSidebarModuleAllowed('admin', 'models'), models: isSidebarModuleAllowed('admin', 'models'),
redemption: isSidebarModuleAllowed('admin', 'redemption'), redemption: isSidebarModuleAllowed('admin', 'redemption'),
user: isSidebarModuleAllowed('admin', 'user'), user: isSidebarModuleAllowed('admin', 'user'),
setting: isSidebarModuleAllowed('admin', 'setting') setting: isSidebarModuleAllowed('admin', 'setting'),
}; };
} }
@@ -114,12 +122,12 @@ export default function SettingsSidebarModulesUser() {
// 处理区域级别开关变更 // 处理区域级别开关变更
function handleSectionChange(sectionKey) { function handleSectionChange(sectionKey) {
return (checked) => { return (checked) => {
const newModules = { const newModules = {
...sidebarModulesUser, ...sidebarModulesUser,
[sectionKey]: { [sectionKey]: {
...sidebarModulesUser[sectionKey], ...sidebarModulesUser[sectionKey],
enabled: checked enabled: checked,
} },
}; };
setSidebarModulesUser(newModules); setSidebarModulesUser(newModules);
console.log('用户边栏区域配置变更:', sectionKey, checked, newModules); console.log('用户边栏区域配置变更:', sectionKey, checked, newModules);
@@ -129,15 +137,21 @@ export default function SettingsSidebarModulesUser() {
// 处理功能级别开关变更 // 处理功能级别开关变更
function handleModuleChange(sectionKey, moduleKey) { function handleModuleChange(sectionKey, moduleKey) {
return (checked) => { return (checked) => {
const newModules = { const newModules = {
...sidebarModulesUser, ...sidebarModulesUser,
[sectionKey]: { [sectionKey]: {
...sidebarModulesUser[sectionKey], ...sidebarModulesUser[sectionKey],
[moduleKey]: checked [moduleKey]: checked,
} },
}; };
setSidebarModulesUser(newModules); setSidebarModulesUser(newModules);
console.log('用户边栏功能配置变更:', sectionKey, moduleKey, checked, newModules); console.log(
'用户边栏功能配置变更:',
sectionKey,
moduleKey,
checked,
newModules,
);
}; };
} }
@@ -202,12 +216,15 @@ export default function SettingsSidebarModulesUser() {
// 确保用户配置也经过权限过滤 // 确保用户配置也经过权限过滤
const filteredUserConf = {}; const filteredUserConf = {};
Object.keys(userConf).forEach(sectionKey => { Object.keys(userConf).forEach((sectionKey) => {
if (isSidebarSectionAllowed(sectionKey)) { if (isSidebarSectionAllowed(sectionKey)) {
filteredUserConf[sectionKey] = { ...userConf[sectionKey] }; filteredUserConf[sectionKey] = { ...userConf[sectionKey] };
// 过滤不允许的模块 // 过滤不允许的模块
Object.keys(userConf[sectionKey]).forEach(moduleKey => { Object.keys(userConf[sectionKey]).forEach((moduleKey) => {
if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) { if (
moduleKey !== 'enabled' &&
!isSidebarModuleAllowed(sectionKey, moduleKey)
) {
delete filteredUserConf[sectionKey][moduleKey]; delete filteredUserConf[sectionKey][moduleKey];
} }
}); });
@@ -233,14 +250,22 @@ export default function SettingsSidebarModulesUser() {
if (!permissionsLoading && hasSidebarSettingsPermission()) { if (!permissionsLoading && hasSidebarSettingsPermission()) {
loadConfigs(); loadConfigs();
} }
}, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]); }, [
statusState,
permissionsLoading,
hasSidebarSettingsPermission,
isSidebarSectionAllowed,
isSidebarModuleAllowed,
]);
// 检查功能是否被管理员允许 // 检查功能是否被管理员允许
const isAllowedByAdmin = (sectionKey, moduleKey = null) => { const isAllowedByAdmin = (sectionKey, moduleKey = null) => {
if (!adminConfig) return true; if (!adminConfig) return true;
if (moduleKey) { if (moduleKey) {
return adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]; return (
adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]
);
} else { } else {
return adminConfig[sectionKey]?.enabled; return adminConfig[sectionKey]?.enabled;
} }
@@ -253,9 +278,13 @@ export default function SettingsSidebarModulesUser() {
title: t('聊天区域'), title: t('聊天区域'),
description: t('操练场和聊天功能'), description: t('操练场和聊天功能'),
modules: [ modules: [
{ key: 'playground', title: t('操练场'), description: t('AI模型测试环境') }, {
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') } key: 'playground',
] title: t('操练场'),
description: t('AI模型测试环境'),
},
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
],
}, },
{ {
key: 'console', key: 'console',
@@ -265,9 +294,13 @@ export default function SettingsSidebarModulesUser() {
{ key: 'detail', title: t('数据看板'), description: t('系统数据统计') }, { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
{ key: 'token', title: t('令牌管理'), description: t('API令牌管理') }, { key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
{ key: 'log', 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: 'midjourney',
] title: t('绘图日志'),
description: t('绘图任务记录'),
},
{ key: 'task', title: t('任务日志'), description: t('系统任务记录') },
],
}, },
{ {
key: 'personal', key: 'personal',
@@ -275,8 +308,12 @@ export default function SettingsSidebarModulesUser() {
description: t('用户个人功能'), description: t('用户个人功能'),
modules: [ modules: [
{ key: 'topup', title: t('钱包管理'), description: t('余额充值管理') }, { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
{ key: 'personal', title: t('个人设置'), description: t('个人信息设置') } {
] key: 'personal',
title: t('个人设置'),
description: t('个人信息设置'),
},
],
}, },
{ {
key: 'admin', key: 'admin',
@@ -285,23 +322,35 @@ export default function SettingsSidebarModulesUser() {
modules: [ modules: [
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') }, { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') }, { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
{ key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') }, {
key: 'redemption',
title: t('兑换码管理'),
description: t('兑换码生成管理'),
},
{ key: 'user', title: t('用户管理'), description: t('用户账户管理') }, { key: 'user', title: t('用户管理'), description: t('用户账户管理') },
{ key: 'setting', title: t('系统设置'), description: t('系统参数配置') } {
] key: 'setting',
} title: t('系统设置'),
].filter(section => { description: t('系统参数配置'),
// 使用后端权限验证替代前端角色判断 },
return isSidebarSectionAllowed(section.key); ],
}).map(section => ({ },
...section, ]
modules: section.modules.filter(module => .filter((section) => {
isSidebarModuleAllowed(section.key, module.key) // 使用后端权限验证替代前端角色判断
) return isSidebarSectionAllowed(section.key);
})).filter(section => })
// 过滤掉没有可用模块的区域 .map((section) => ({
section.modules.length > 0 && isAllowedByAdmin(section.key) ...section,
); modules: section.modules.filter((module) =>
isSidebarModuleAllowed(section.key, module.key),
),
}))
.filter(
(section) =>
// 过滤掉没有可用模块的区域
section.modules.length > 0 && isAllowedByAdmin(section.key),
);
return ( return (
<Card className='!rounded-2xl shadow-sm border-0'> <Card className='!rounded-2xl shadow-sm border-0'>
@@ -321,65 +370,65 @@ export default function SettingsSidebarModulesUser() {
</div> </div>
<div className='mb-4'> <div className='mb-4'>
<Text type="secondary" className='text-sm text-gray-600'> <Text type='secondary' className='text-sm text-gray-600'>
{t('您可以个性化设置侧边栏的要显示功能')} {t('您可以个性化设置侧边栏的要显示功能')}
</Text> </Text>
</div> </div>
{sectionConfigs.map((section) => ( {sectionConfigs.map((section) => (
<div key={section.key} className='mb-6'> <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 className='flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-xl border border-gray-200'>
<div> <div>
<div className='font-semibold text-base text-gray-900 mb-1'> <div className='font-semibold text-base text-gray-900 mb-1'>
{section.title} {section.title}
</div>
<Text className='text-xs text-gray-600'>
{section.description}
</Text>
</div> </div>
<Switch <Text className='text-xs text-gray-600'>
checked={sidebarModulesUser[section.key]?.enabled} {section.description}
onChange={handleSectionChange(section.key)} </Text>
size="default"
/>
</div> </div>
<Switch
{/* 功能模块网格 */} checked={sidebarModulesUser[section.key]?.enabled}
<Row gutter={[12, 12]}> onChange={handleSectionChange(section.key)}
{section.modules.map((module) => ( size='default'
<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 ? '' : '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]}
onChange={handleModuleChange(section.key, module.key)}
size="default"
disabled={!sidebarModulesUser[section.key]?.enabled}
/>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div> </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 ? '' : '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]}
onChange={handleModuleChange(section.key, module.key)}
size='default'
disabled={!sidebarModulesUser[section.key]?.enabled}
/>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
))}
{/* 底部按钮 */} {/* 底部按钮 */}
<div className='flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200'> <div className='flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200'>

View File

@@ -130,9 +130,7 @@ export default function ModelRatioNotSetEditor(props) {
// 在 return 语句之前,先处理过滤和分页逻辑 // 在 return 语句之前,先处理过滤和分页逻辑
const filteredModels = models.filter((model) => const filteredModels = models.filter((model) =>
searchText searchText ? model.name.includes(searchText) : true,
? model.name.includes(searchText)
: true,
); );
// 然后基于过滤后的数据计算分页数据 // 然后基于过滤后的数据计算分页数据

View File

@@ -99,9 +99,7 @@ export default function ModelSettingsVisualEditor(props) {
// 在 return 语句之前,先处理过滤和分页逻辑 // 在 return 语句之前,先处理过滤和分页逻辑
const filteredModels = models.filter((model) => { const filteredModels = models.filter((model) => {
const keywordMatch = searchText const keywordMatch = searchText ? model.name.includes(searchText) : true;
? model.name.includes(searchText)
: true;
const conflictMatch = conflictOnly ? model.hasConflict : true; const conflictMatch = conflictOnly ? model.hasConflict : true;
return keywordMatch && conflictMatch; return keywordMatch && conflictMatch;
}); });

View File

@@ -151,8 +151,17 @@ export default function UpstreamRatioSync(props) {
setChannelEndpoints((prev) => { setChannelEndpoints((prev) => {
const merged = { ...prev }; const merged = { ...prev };
transferData.forEach((channel) => { transferData.forEach((channel) => {
if (!merged[channel.key]) { const id = channel.key;
merged[channel.key] = DEFAULT_ENDPOINT; const base = channel._originalData?.base_url || '';
const name = channel.label || '';
const isOfficial =
id === -100 ||
base === 'https://basellm.github.io' ||
name === '官方倍率预设';
if (!merged[id]) {
merged[id] = isOfficial
? '/llm-metadata/api/newapi/ratio_config-v1-base.json'
: DEFAULT_ENDPOINT;
} }
}); });
return merged; return merged;