Merge pull request #1278 from QuantumNous/alpha
feat: conditionally set Gemini ThinkingBudget based on MaxOutputTokens
This commit is contained in:
@@ -24,8 +24,7 @@ RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-
|
||||
|
||||
FROM alpine
|
||||
|
||||
RUN apk update \
|
||||
&& apk upgrade \
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
||||
&& update-ca-certificates
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shopspring/decimal"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -304,6 +306,40 @@ func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) {
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) {
|
||||
url := "https://api.moonshot.cn/v1/users/me/balance"
|
||||
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
type MoonshotBalanceData struct {
|
||||
AvailableBalance float64 `json:"available_balance"`
|
||||
VoucherBalance float64 `json:"voucher_balance"`
|
||||
CashBalance float64 `json:"cash_balance"`
|
||||
}
|
||||
|
||||
type MoonshotBalanceResponse struct {
|
||||
Code int `json:"code"`
|
||||
Data MoonshotBalanceData `json:"data"`
|
||||
Scode string `json:"scode"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
response := MoonshotBalanceResponse{}
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !response.Status || response.Code != 0 {
|
||||
return 0, fmt.Errorf("failed to update moonshot balance, status: %v, code: %d, scode: %s", response.Status, response.Code, response.Scode)
|
||||
}
|
||||
availableBalanceCny := response.Data.AvailableBalance
|
||||
availableBalanceUsd := decimal.NewFromFloat(availableBalanceCny).Div(decimal.NewFromFloat(setting.Price)).InexactFloat64()
|
||||
channel.UpdateBalance(availableBalanceUsd)
|
||||
return availableBalanceUsd, nil
|
||||
}
|
||||
|
||||
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
||||
if channel.GetBaseURL() == "" {
|
||||
@@ -332,6 +368,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||
return updateChannelDeepSeekBalance(channel)
|
||||
case common.ChannelTypeOpenRouter:
|
||||
return updateChannelOpenRouterBalance(channel)
|
||||
case common.ChannelTypeMoonshot:
|
||||
return updateChannelMoonshotBalance(channel)
|
||||
default:
|
||||
return 0, errors.New("尚未实现")
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
||||
isNew25Pro := strings.HasPrefix(modelName, "gemini-2.5-pro") &&
|
||||
!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-05-06") &&
|
||||
!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-03-25")
|
||||
is25FlashLite := strings.HasPrefix(modelName, "gemini-2.5-flash-lite")
|
||||
|
||||
if strings.Contains(modelName, "-thinking-") {
|
||||
parts := strings.SplitN(modelName, "-thinking-", 2)
|
||||
@@ -134,15 +133,17 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
||||
IncludeThoughts: true,
|
||||
}
|
||||
} else {
|
||||
budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
|
||||
clampedBudget := clampThinkingBudget(modelName, int(budgetTokens))
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
ThinkingBudget: common.GetPointer(clampedBudget),
|
||||
IncludeThoughts: true,
|
||||
}
|
||||
if geminiRequest.GenerationConfig.MaxOutputTokens > 0 {
|
||||
budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
|
||||
clampedBudget := clampThinkingBudget(modelName, int(budgetTokens))
|
||||
geminiRequest.GenerationConfig.ThinkingConfig.ThinkingBudget = common.GetPointer(clampedBudget)
|
||||
}
|
||||
}
|
||||
} else if strings.HasSuffix(modelName, "-nothinking") {
|
||||
if !isNew25Pro && !is25FlashLite {
|
||||
if !isNew25Pro {
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
ThinkingBudget: common.GetPointer(0),
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ const (
|
||||
const (
|
||||
// Gemini Audio Input Price
|
||||
Gemini25FlashPreviewInputAudioPrice = 1.00
|
||||
Gemini25FlashProductionInputAudioPrice = 1.00 // for `gemini-2.5-flash`
|
||||
Gemini25FlashLitePreviewInputAudioPrice = 0.50
|
||||
Gemini25FlashNativeAudioInputAudioPrice = 3.00
|
||||
Gemini20FlashInputAudioPrice = 0.70
|
||||
)
|
||||
@@ -64,10 +66,14 @@ func GetFileSearchPricePerThousand() float64 {
|
||||
}
|
||||
|
||||
func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
|
||||
if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
|
||||
return Gemini25FlashPreviewInputAudioPrice
|
||||
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
|
||||
if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
|
||||
return Gemini25FlashNativeAudioInputAudioPrice
|
||||
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-lite") {
|
||||
return Gemini25FlashLitePreviewInputAudioPrice
|
||||
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
|
||||
return Gemini25FlashPreviewInputAudioPrice
|
||||
} else if strings.HasPrefix(modelName, "gemini-2.5-flash") {
|
||||
return Gemini25FlashProductionInputAudioPrice
|
||||
} else if strings.HasPrefix(modelName, "gemini-2.0-flash") {
|
||||
return Gemini20FlashInputAudioPrice
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ var defaultModelRatio = map[string]float64{
|
||||
"gemini-2.0-flash": 0.05,
|
||||
"gemini-2.5-pro-exp-03-25": 0.625,
|
||||
"gemini-2.5-pro-preview-03-25": 0.625,
|
||||
"gemini-2.5-pro": 0.625,
|
||||
"gemini-2.5-flash-preview-04-17": 0.075,
|
||||
"gemini-2.5-flash-preview-04-17-thinking": 0.075,
|
||||
"gemini-2.5-flash-preview-04-17-nothinking": 0.075,
|
||||
@@ -148,6 +149,8 @@ var defaultModelRatio = map[string]float64{
|
||||
"gemini-2.5-flash-preview-05-20-nothinking": 0.075,
|
||||
"gemini-2.5-flash-thinking-*": 0.075, // 用于为后续所有2.5 flash thinking budget 模型设置默认倍率
|
||||
"gemini-2.5-pro-thinking-*": 0.625, // 用于为后续所有2.5 pro thinking budget 模型设置默认倍率
|
||||
"gemini-2.5-flash-lite-preview-06-17": 0.05,
|
||||
"gemini-2.5-flash": 0.15,
|
||||
"text-embedding-004": 0.001,
|
||||
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
||||
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
||||
@@ -423,7 +426,12 @@ func UpdateCompletionRatioByJSONString(jsonStr string) error {
|
||||
func GetCompletionRatio(name string) float64 {
|
||||
CompletionRatioMutex.RLock()
|
||||
defer CompletionRatioMutex.RUnlock()
|
||||
|
||||
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
||||
name = "gpt-4-gizmo-*"
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-4o-gizmo") {
|
||||
name = "gpt-4o-gizmo-*"
|
||||
}
|
||||
if strings.Contains(name, "/") {
|
||||
if ratio, ok := CompletionRatio[name]; ok {
|
||||
return ratio
|
||||
@@ -441,12 +449,6 @@ func GetCompletionRatio(name string) float64 {
|
||||
|
||||
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
||||
lowercaseName := strings.ToLower(name)
|
||||
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
||||
name = "gpt-4-gizmo-*"
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-4o-gizmo") {
|
||||
name = "gpt-4o-gizmo-*"
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
|
||||
if strings.HasPrefix(name, "gpt-4o") {
|
||||
if name == "gpt-4o-2024-05-13" {
|
||||
@@ -500,12 +502,17 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
||||
return 4, true
|
||||
} else if strings.HasPrefix(name, "gemini-2.5-pro") { // 移除preview来增加兼容性,这里假设正式版的倍率和preview一致
|
||||
return 8, true
|
||||
} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 同上
|
||||
if strings.HasSuffix(name, "-nothinking") {
|
||||
return 4, false
|
||||
} else {
|
||||
return 3.5 / 0.6, false
|
||||
} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 处理不同的flash模型倍率
|
||||
if strings.HasPrefix(name, "gemini-2.5-flash-preview") {
|
||||
if strings.HasSuffix(name, "-nothinking") {
|
||||
return 4, true
|
||||
}
|
||||
return 3.5 / 0.15, true
|
||||
}
|
||||
if strings.HasPrefix(name, "gemini-2.5-flash-lite-preview") {
|
||||
return 4, true
|
||||
}
|
||||
return 2.5 / 0.3, true
|
||||
}
|
||||
return 4, false
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Badge,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
@@ -43,6 +44,7 @@ const HeaderBar = () => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const [noticeVisible, setNoticeVisible] = useState(false);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
@@ -53,9 +55,44 @@ const HeaderBar = () => {
|
||||
const docsLink = statusState?.status?.docs_link || '';
|
||||
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
||||
|
||||
const isConsoleRoute = location.pathname.startsWith('/console');
|
||||
|
||||
const theme = useTheme();
|
||||
const setTheme = useSetTheme();
|
||||
|
||||
const announcements = statusState?.status?.announcements || [];
|
||||
|
||||
const getAnnouncementKey = (a) => `${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`;
|
||||
|
||||
const calculateUnreadCount = () => {
|
||||
if (!announcements.length) return 0;
|
||||
let readKeys = [];
|
||||
try {
|
||||
readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
|
||||
} catch (_) {
|
||||
readKeys = [];
|
||||
}
|
||||
const readSet = new Set(readKeys);
|
||||
return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).length;
|
||||
};
|
||||
|
||||
const getUnreadKeys = () => {
|
||||
if (!announcements.length) return [];
|
||||
let readKeys = [];
|
||||
try {
|
||||
readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
|
||||
} catch (_) {
|
||||
readKeys = [];
|
||||
}
|
||||
const readSet = new Set(readKeys);
|
||||
return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).map(getAnnouncementKey);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setUnreadCount(calculateUnreadCount());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [announcements]);
|
||||
|
||||
const mainNavLinks = [
|
||||
{
|
||||
text: t('首页'),
|
||||
@@ -106,6 +143,25 @@ const HeaderBar = () => {
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const handleNoticeOpen = () => {
|
||||
setNoticeVisible(true);
|
||||
};
|
||||
|
||||
const handleNoticeClose = () => {
|
||||
setNoticeVisible(false);
|
||||
if (announcements.length) {
|
||||
let readKeys = [];
|
||||
try {
|
||||
readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
|
||||
} catch (_) {
|
||||
readKeys = [];
|
||||
}
|
||||
const mergedKeys = Array.from(new Set([...readKeys, ...announcements.map(getAnnouncementKey)]));
|
||||
localStorage.setItem('notice_read_keys', JSON.stringify(mergedKeys));
|
||||
}
|
||||
setUnreadCount(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === 'dark') {
|
||||
document.body.setAttribute('theme-mode', 'dark');
|
||||
@@ -353,15 +409,14 @@ const HeaderBar = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查当前路由是否以/console开头
|
||||
const isConsoleRoute = location.pathname.startsWith('/console');
|
||||
|
||||
return (
|
||||
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg">
|
||||
<NoticeModal
|
||||
visible={noticeVisible}
|
||||
onClose={() => setNoticeVisible(false)}
|
||||
onClose={handleNoticeClose}
|
||||
isMobile={styleState.isMobile}
|
||||
defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
|
||||
unreadKeys={getUnreadKeys()}
|
||||
/>
|
||||
<div className="w-full px-2">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
@@ -462,14 +517,27 @@ const HeaderBar = () => {
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
<Button
|
||||
icon={<IconBell className="text-lg" />}
|
||||
aria-label={t('系统公告')}
|
||||
onClick={() => setNoticeVisible(true)}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
||||
/>
|
||||
{unreadCount > 0 ? (
|
||||
<Badge count={unreadCount} type="danger" overflowCount={99}>
|
||||
<Button
|
||||
icon={<IconBell className="text-lg" />}
|
||||
aria-label={t('系统公告')}
|
||||
onClick={handleNoticeOpen}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
||||
/>
|
||||
</Badge>
|
||||
) : (
|
||||
<Button
|
||||
icon={<IconBell className="text-lg" />}
|
||||
aria-label={t('系统公告')}
|
||||
onClick={handleNoticeOpen}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Modal, Empty } from '@douyinfe/semi-ui';
|
||||
import React, { useEffect, useState, useContext, useMemo } from 'react';
|
||||
import { Button, Modal, Empty, Tabs, TabPane, Timeline } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showError } from '../../helpers';
|
||||
import { API, showError, getRelativeTime } from '../../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { Bell, Megaphone } from 'lucide-react';
|
||||
|
||||
const NoticeModal = ({ visible, onClose, isMobile }) => {
|
||||
const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadKeys = [] }) => {
|
||||
const { t } = useTranslation();
|
||||
const [noticeContent, setNoticeContent] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(defaultTab);
|
||||
|
||||
const [statusState] = useContext(StatusContext);
|
||||
|
||||
const announcements = statusState?.status?.announcements || [];
|
||||
|
||||
const unreadSet = useMemo(() => new Set(unreadKeys), [unreadKeys]);
|
||||
|
||||
const getKeyForItem = (item) => `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`;
|
||||
|
||||
const processedAnnouncements = useMemo(() => {
|
||||
return (announcements || []).slice(0, 20).map(item => ({
|
||||
key: getKeyForItem(item),
|
||||
type: item.type || 'default',
|
||||
time: getRelativeTime(item.publishDate),
|
||||
content: item.content,
|
||||
extra: item.extra,
|
||||
isUnread: unreadSet.has(getKeyForItem(item))
|
||||
}));
|
||||
}, [announcements, unreadSet]);
|
||||
|
||||
const handleCloseTodayNotice = () => {
|
||||
const today = new Date().toDateString();
|
||||
@@ -44,7 +66,13 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const renderContent = () => {
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setActiveTab(defaultTab);
|
||||
}
|
||||
}, [defaultTab, visible]);
|
||||
|
||||
const renderMarkdownNotice = () => {
|
||||
if (loading) {
|
||||
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
||||
}
|
||||
@@ -64,14 +92,74 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
||||
className="notice-content-scroll max-h-[60vh] overflow-y-auto pr-2"
|
||||
className="notice-content-scroll max-h-[55vh] overflow-y-auto pr-2"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAnnouncementTimeline = () => {
|
||||
if (processedAnnouncements.length === 0) {
|
||||
return (
|
||||
<div className="py-12">
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('暂无系统公告')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-h-[55vh] overflow-y-auto pr-2 card-content-scroll">
|
||||
<Timeline mode="alternate">
|
||||
{processedAnnouncements.map((item, idx) => (
|
||||
<Timeline.Item
|
||||
key={idx}
|
||||
type={item.type}
|
||||
time={item.time}
|
||||
className={item.isUnread ? '' : ''}
|
||||
>
|
||||
<div>
|
||||
{item.isUnread ? (
|
||||
<span className="shine-text">
|
||||
{item.content}
|
||||
</span>
|
||||
) : (
|
||||
item.content
|
||||
)}
|
||||
{item.extra && <div className="text-xs text-gray-500">{item.extra}</div>}
|
||||
</div>
|
||||
</Timeline.Item>
|
||||
))}
|
||||
</Timeline>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBody = () => {
|
||||
if (activeTab === 'inApp') {
|
||||
return renderMarkdownNotice();
|
||||
}
|
||||
return renderAnnouncementTimeline();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('系统公告')}
|
||||
title={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{t('系统公告')}</span>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
type='card'
|
||||
size='small'
|
||||
>
|
||||
<TabPane tab={<span className="flex items-center gap-1"><Bell size={14} /> {t('通知')}</span>} itemKey='inApp' />
|
||||
<TabPane tab={<span className="flex items-center gap-1"><Megaphone size={14} /> {t('系统公告')}</span>} itemKey='system' />
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
footer={(
|
||||
@@ -82,7 +170,7 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
||||
)}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
>
|
||||
{renderContent()}
|
||||
{renderBody()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
63
web/src/components/settings/ChatsSetting.js
Normal file
63
web/src/components/settings/ChatsSetting.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import SettingsChats from '../../pages/Setting/Chat/SettingsChats.js';
|
||||
import { API, showError } from '../../helpers';
|
||||
|
||||
const ChatsSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
/* 聊天设置 */
|
||||
Chats: '[]',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (
|
||||
item.key.endsWith('Enabled') ||
|
||||
['DefaultCollapseSidebar'].includes(item.key)
|
||||
) {
|
||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||
} else {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
});
|
||||
|
||||
setInputs(newInputs);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
async function onRefresh() {
|
||||
try {
|
||||
setLoading(true);
|
||||
await getOptions();
|
||||
} catch (error) {
|
||||
showError('刷新失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onRefresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading} size='large'>
|
||||
{/* 聊天设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsChats options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatsSetting;
|
||||
@@ -5,6 +5,7 @@ import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
|
||||
import SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js';
|
||||
import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
|
||||
import SettingsUptimeKuma from '../../pages/Setting/Dashboard/SettingsUptimeKuma.js';
|
||||
import SettingsDataDashboard from '../../pages/Setting/Dashboard/SettingsDataDashboard.js';
|
||||
|
||||
const DashboardSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
@@ -23,6 +24,11 @@ const DashboardSetting = () => {
|
||||
FAQ: '',
|
||||
UptimeKumaUrl: '',
|
||||
UptimeKumaSlug: '',
|
||||
|
||||
/* 数据看板 */
|
||||
DataExportEnabled: false,
|
||||
DataExportDefaultTime: 'hour',
|
||||
DataExportInterval: 5,
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -37,6 +43,10 @@ const DashboardSetting = () => {
|
||||
if (item.key in inputs) {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
if (item.key.endsWith('Enabled') &&
|
||||
(item.key === 'DataExportEnabled')) {
|
||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||
}
|
||||
});
|
||||
setInputs(newInputs);
|
||||
} else {
|
||||
@@ -106,9 +116,9 @@ const DashboardSetting = () => {
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
{/* API信息管理 */}
|
||||
{/* 数据看板设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsAPIInfo options={inputs} refresh={onRefresh} />
|
||||
<SettingsDataDashboard options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
|
||||
{/* 系统公告管理 */}
|
||||
@@ -116,6 +126,11 @@ const DashboardSetting = () => {
|
||||
<SettingsAnnouncements options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
|
||||
{/* API信息管理 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsAPIInfo options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
|
||||
{/* 常见问答管理 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsFAQ options={inputs} refresh={onRefresh} />
|
||||
|
||||
65
web/src/components/settings/DrawingSetting.js
Normal file
65
web/src/components/settings/DrawingSetting.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import SettingsDrawing from '../../pages/Setting/Drawing/SettingsDrawing.js';
|
||||
import { API, showError } from '../../helpers';
|
||||
|
||||
const DrawingSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
/* 绘图设置 */
|
||||
DrawingEnabled: false,
|
||||
MjNotifyEnabled: false,
|
||||
MjAccountFilterEnabled: false,
|
||||
MjForwardUrlEnabled: false,
|
||||
MjModeClearEnabled: false,
|
||||
MjActionCheckSuccessEnabled: false,
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key.endsWith('Enabled')) {
|
||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||
} else {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
});
|
||||
|
||||
setInputs(newInputs);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
async function onRefresh() {
|
||||
try {
|
||||
setLoading(true);
|
||||
await getOptions();
|
||||
} catch (error) {
|
||||
showError('刷新失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onRefresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading} size='large'>
|
||||
{/* 绘图设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsDrawing options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawingSetting;
|
||||
@@ -1,13 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js';
|
||||
import SettingsDrawing from '../../pages/Setting/Operation/SettingsDrawing.js';
|
||||
import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||
import SettingsLog from '../../pages/Setting/Operation/SettingsLog.js';
|
||||
import SettingsDataDashboard from '../../pages/Setting/Operation/SettingsDataDashboard.js';
|
||||
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring.js';
|
||||
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
||||
import { API, showError } from '../../helpers';
|
||||
|
||||
const OperationSetting = () => {
|
||||
@@ -29,14 +26,6 @@ const OperationSetting = () => {
|
||||
DemoSiteEnabled: false,
|
||||
SelfUseModeEnabled: false,
|
||||
|
||||
/* 绘图设置 */
|
||||
DrawingEnabled: false,
|
||||
MjNotifyEnabled: false,
|
||||
MjAccountFilterEnabled: false,
|
||||
MjForwardUrlEnabled: false,
|
||||
MjModeClearEnabled: false,
|
||||
MjActionCheckSuccessEnabled: false,
|
||||
|
||||
/* 敏感词设置 */
|
||||
CheckSensitiveEnabled: false,
|
||||
CheckSensitiveOnPromptEnabled: false,
|
||||
@@ -45,20 +34,12 @@ const OperationSetting = () => {
|
||||
/* 日志设置 */
|
||||
LogConsumeEnabled: false,
|
||||
|
||||
/* 数据看板 */
|
||||
DataExportEnabled: false,
|
||||
DataExportDefaultTime: 'hour',
|
||||
DataExportInterval: 5,
|
||||
|
||||
/* 监控设置 */
|
||||
ChannelDisableThreshold: 0,
|
||||
QuotaRemindThreshold: 0,
|
||||
AutomaticDisableChannelEnabled: false,
|
||||
AutomaticEnableChannelEnabled: false,
|
||||
AutomaticDisableKeywords: '',
|
||||
|
||||
/* 聊天设置 */
|
||||
Chats: '[]',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -107,10 +88,6 @@ const OperationSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsGeneral options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 绘图设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsDrawing options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 屏蔽词过滤设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsSensitiveWords options={inputs} refresh={onRefresh} />
|
||||
@@ -119,10 +96,6 @@ const OperationSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsLog options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 数据看板 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsDataDashboard options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 监控设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsMonitoring options={inputs} refresh={onRefresh} />
|
||||
@@ -131,10 +104,6 @@ const OperationSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 聊天设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsChats options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
|
||||
88
web/src/components/settings/PaymentSetting.js
Normal file
88
web/src/components/settings/PaymentSetting.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment.js';
|
||||
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway.js';
|
||||
import { API, showError } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const PaymentSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let [inputs, setInputs] = useState({
|
||||
ServerAddress: '',
|
||||
PayAddress: '',
|
||||
EpayId: '',
|
||||
EpayKey: '',
|
||||
Price: 7.3,
|
||||
MinTopUp: 1,
|
||||
TopupGroupRatio: '',
|
||||
CustomCallbackAddress: '',
|
||||
PayMethods: '',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
switch (item.key) {
|
||||
case 'TopupGroupRatio':
|
||||
try {
|
||||
newInputs[item.key] = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
} catch (error) {
|
||||
console.error('解析TopupGroupRatio出错:', error);
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
break;
|
||||
case 'Price':
|
||||
case 'MinTopUp':
|
||||
newInputs[item.key] = parseFloat(item.value);
|
||||
break;
|
||||
default:
|
||||
if (item.key.endsWith('Enabled')) {
|
||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||
} else {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
setInputs(newInputs);
|
||||
} else {
|
||||
showError(t(message));
|
||||
}
|
||||
};
|
||||
|
||||
async function onRefresh() {
|
||||
try {
|
||||
setLoading(true);
|
||||
await getOptions();
|
||||
} catch (error) {
|
||||
showError(t('刷新失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onRefresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading} size='large'>
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsGeneralPayment options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGateway options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentSetting;
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
|
||||
import { API, showError, showSuccess } from '../../helpers/index.js';
|
||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
||||
import { API, showError } from '../../helpers/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RequestRateLimit from '../../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
|
||||
|
||||
@@ -24,14 +23,14 @@ const RateLimitSetting = () => {
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key === 'ModelRequestRateLimitGroup') {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
if (item.key === 'ModelRequestRateLimitGroup') {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
|
||||
if (item.key.endsWith('Enabled')) {
|
||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||
} else {
|
||||
newInputs[item.key] = item.value;
|
||||
if (item.key.endsWith('Enabled')) {
|
||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||
} else {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const RatioSetting = () => {
|
||||
<Spin spinning={loading} size='large'>
|
||||
{/* 模型倍率设置以及可视化编辑器 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<Tabs type='line'>
|
||||
<Tabs type='card'>
|
||||
<Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
|
||||
<ModelRatioSettings options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
removeTrailingSlash,
|
||||
showError,
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
} from '../../helpers';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -42,17 +41,9 @@ const SystemSetting = () => {
|
||||
SMTPAccount: '',
|
||||
SMTPFrom: '',
|
||||
SMTPToken: '',
|
||||
ServerAddress: '',
|
||||
WorkerUrl: '',
|
||||
WorkerValidKey: '',
|
||||
WorkerAllowHttpImageRequestEnabled: '',
|
||||
EpayId: '',
|
||||
EpayKey: '',
|
||||
Price: 7.3,
|
||||
MinTopUp: 1,
|
||||
TopupGroupRatio: '',
|
||||
PayAddress: '',
|
||||
CustomCallbackAddress: '',
|
||||
Footer: '',
|
||||
WeChatAuthEnabled: '',
|
||||
WeChatServerAddress: '',
|
||||
@@ -73,7 +64,6 @@ const SystemSetting = () => {
|
||||
LinuxDOOAuthEnabled: '',
|
||||
LinuxDOClientId: '',
|
||||
LinuxDOClientSecret: '',
|
||||
PayMethods: '',
|
||||
});
|
||||
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
@@ -200,11 +190,6 @@ const SystemSetting = () => {
|
||||
setInputs(values);
|
||||
};
|
||||
|
||||
const submitServerAddress = async () => {
|
||||
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
|
||||
await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
|
||||
};
|
||||
|
||||
const submitWorker = async () => {
|
||||
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
||||
const options = [
|
||||
@@ -220,56 +205,6 @@ const SystemSetting = () => {
|
||||
await updateOptions(options);
|
||||
};
|
||||
|
||||
const submitPayAddress = async () => {
|
||||
if (inputs.ServerAddress === '') {
|
||||
showError('请先填写服务器地址');
|
||||
return;
|
||||
}
|
||||
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
|
||||
if (!verifyJSON(inputs.TopupGroupRatio)) {
|
||||
showError('充值分组倍率不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (originInputs['PayMethods'] !== inputs.PayMethods) {
|
||||
if (!verifyJSON(inputs.PayMethods)) {
|
||||
showError('充值方式设置不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
|
||||
];
|
||||
|
||||
if (inputs.EpayId !== '') {
|
||||
options.push({ key: 'EpayId', value: inputs.EpayId });
|
||||
}
|
||||
if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
|
||||
options.push({ key: 'EpayKey', value: inputs.EpayKey });
|
||||
}
|
||||
if (inputs.Price !== '') {
|
||||
options.push({ key: 'Price', value: inputs.Price.toString() });
|
||||
}
|
||||
if (inputs.MinTopUp !== '') {
|
||||
options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
|
||||
}
|
||||
if (inputs.CustomCallbackAddress !== '') {
|
||||
options.push({
|
||||
key: 'CustomCallbackAddress',
|
||||
value: inputs.CustomCallbackAddress,
|
||||
});
|
||||
}
|
||||
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
|
||||
options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
|
||||
}
|
||||
if (originInputs['PayMethods'] !== inputs.PayMethods) {
|
||||
options.push({ key: 'PayMethods', value: inputs.PayMethods });
|
||||
}
|
||||
|
||||
await updateOptions(options);
|
||||
};
|
||||
|
||||
const submitSMTP = async () => {
|
||||
const options = [];
|
||||
|
||||
@@ -551,17 +486,6 @@ const SystemSetting = () => {
|
||||
marginTop: '10px',
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<Form.Section text='通用设置'>
|
||||
<Form.Input
|
||||
field='ServerAddress'
|
||||
label='服务器地址'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Button onClick={submitServerAddress}>更新服务器地址</Button>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
<Card>
|
||||
<Form.Section text='代理设置'>
|
||||
<Text>
|
||||
@@ -604,80 +528,6 @@ const SystemSetting = () => {
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Form.Section text='支付设置'>
|
||||
<Text>
|
||||
(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
|
||||
</Text>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='PayAddress'
|
||||
label='支付地址'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='EpayId'
|
||||
label='易支付商户ID'
|
||||
placeholder='例如:0001'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='EpayKey'
|
||||
label='易支付商户密钥'
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
type='password'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='CustomCallbackAddress'
|
||||
label='回调地址'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='Price'
|
||||
precision={2}
|
||||
label='充值价格(x元/美金)'
|
||||
placeholder='例如:7,就是7元/美金'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='MinTopUp'
|
||||
label='最低充值美元数量'
|
||||
placeholder='例如:2,就是最低充值2$'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.TextArea
|
||||
field='TopupGroupRatio'
|
||||
label='充值分组倍率'
|
||||
placeholder='为一个 JSON 文本,键为组名称,值为倍率'
|
||||
autosize
|
||||
/>
|
||||
<Form.TextArea
|
||||
field='PayMethods'
|
||||
label='充值方式设置'
|
||||
placeholder='为一个 JSON 文本'
|
||||
autosize
|
||||
/>
|
||||
<Button onClick={submitPayAddress}>更新支付设置</Button>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Form.Section text='配置登录注册'>
|
||||
<Row
|
||||
|
||||
@@ -1206,7 +1206,7 @@
|
||||
"默认折叠侧边栏": "Default collapse sidebar",
|
||||
"聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below",
|
||||
"你似乎并没有修改什么": "You seem to have not modified anything",
|
||||
"令牌聊天设置": "Chat settings",
|
||||
"聊天设置": "Chat settings",
|
||||
"必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Must set all chat links above to empty to use the chat settings below",
|
||||
"链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1",
|
||||
"聊天配置": "Chat configuration",
|
||||
@@ -1672,7 +1672,7 @@
|
||||
"获取倍率失败:": "Failed to get ratios: ",
|
||||
"后端请求失败": "Backend request failed",
|
||||
"部分渠道测试失败:": "Some channels failed to test: ",
|
||||
"已与上游倍率完全一致,无需同步": "The upstream ratio is completely consistent, no synchronization is required",
|
||||
"未找到差异化倍率,无需同步": "No differential ratio found, no synchronization is required",
|
||||
"请求后端接口失败:": "Failed to request the backend interface: ",
|
||||
"同步成功": "Synchronization successful",
|
||||
"部分保存失败": "Some settings failed to save",
|
||||
@@ -1688,5 +1688,18 @@
|
||||
"暂无差异化倍率显示": "No differential ratio display",
|
||||
"请先选择同步渠道": "Please select the synchronization channel first",
|
||||
"与本地相同": "Same as local",
|
||||
"未找到匹配的模型": "No matching model found"
|
||||
"未找到匹配的模型": "No matching model found",
|
||||
"暴露倍率接口": "Expose ratio API",
|
||||
"支付设置": "Payment Settings",
|
||||
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)",
|
||||
"支付地址": "Payment address",
|
||||
"易支付商户ID": "Epay merchant ID",
|
||||
"易支付商户密钥": "Epay merchant key",
|
||||
"回调地址": "Callback address",
|
||||
"充值价格(x元/美金)": "Recharge price (x yuan/dollar)",
|
||||
"最低充值美元数量": "Minimum recharge dollar amount",
|
||||
"充值分组倍率": "Recharge group ratio",
|
||||
"充值方式设置": "Recharge method settings",
|
||||
"更新支付设置": "Update payment settings",
|
||||
"通知": "Notice"
|
||||
}
|
||||
@@ -500,4 +500,32 @@ code {
|
||||
|
||||
.components-transfer-selected-item .semi-icon-close:hover {
|
||||
color: var(--semi-color-text-0);
|
||||
}
|
||||
|
||||
/* ==================== 未读通知闪光效果 ==================== */
|
||||
@keyframes sweep-shine {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shine-text {
|
||||
background: linear-gradient(90deg, currentColor 0%, currentColor 40%, rgba(255, 255, 255, 0.9) 50%, currentColor 60%, currentColor 100%);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: sweep-shine 4s linear infinite;
|
||||
}
|
||||
|
||||
.dark .shine-text {
|
||||
background: linear-gradient(90deg, currentColor 0%, currentColor 40%, #facc15 50%, currentColor 60%, currentColor 100%);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
@@ -2,10 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
@@ -16,7 +13,6 @@ import {
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
verifyJSONPromise,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -80,21 +76,6 @@ export default function SettingsChats(props) {
|
||||
}
|
||||
}
|
||||
|
||||
async function resetModelRatio() {
|
||||
try {
|
||||
let res = await API.post(`/api/option/rest_model_ratio`);
|
||||
// return {success, message}
|
||||
if (res.data.success) {
|
||||
showSuccess(res.data.message);
|
||||
props.refresh();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentInputs = {};
|
||||
for (let key in props.options) {
|
||||
@@ -119,13 +100,7 @@ export default function SettingsChats(props) {
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
style={{ marginBottom: 15 }}
|
||||
>
|
||||
<Form.Section text={t('令牌聊天设置')}>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t(
|
||||
'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能',
|
||||
)}
|
||||
/>
|
||||
<Form.Section text={t('聊天设置')}>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t(
|
||||
@@ -388,11 +388,17 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
// 计算当前页显示的数据
|
||||
// 计算当前页显示的数据(按发布时间倒序排序,最新优先显示)
|
||||
const getCurrentPageData = () => {
|
||||
const sortedList = [...announcementsList].sort((a, b) => {
|
||||
const dateA = new Date(a.publishDate).getTime();
|
||||
const dateB = new Date(b.publishDate).getTime();
|
||||
return dateB - dateA; // 倒序,最新的排在前面
|
||||
});
|
||||
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
return announcementsList.slice(startIndex, endIndex);
|
||||
return sortedList.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
const rowSelection = {
|
||||
|
||||
@@ -209,8 +209,8 @@ export default function SettingGeminiModel(props) {
|
||||
label={t('思考预算占比')}
|
||||
field={'gemini.thinking_adapter_budget_tokens_percentage'}
|
||||
initValue={''}
|
||||
extraText={t('0.1-1之间的小数')}
|
||||
min={0.1}
|
||||
extraText={t('0.002-1之间的小数')}
|
||||
min={0.002}
|
||||
max={1}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Form,
|
||||
Row,
|
||||
Spin,
|
||||
Collapse,
|
||||
Modal,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
@@ -92,10 +91,6 @@ export default function GeneralSettings(props) {
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading}>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t('聊天链接功能已经弃用,请使用下方聊天设置功能')}
|
||||
/>
|
||||
<Form
|
||||
values={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
|
||||
74
web/src/pages/Setting/Payment/SettingsGeneralPayment.js
Normal file
74
web/src/pages/Setting/Payment/SettingsGeneralPayment.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
API,
|
||||
removeTrailingSlash,
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SettingsGeneralPayment(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
ServerAddress: '',
|
||||
});
|
||||
const formApiRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.options && formApiRef.current) {
|
||||
const currentInputs = { ServerAddress: props.options.ServerAddress || '' };
|
||||
setInputs(currentInputs);
|
||||
formApiRef.current.setValues(currentInputs);
|
||||
}
|
||||
}, [props.options]);
|
||||
|
||||
const handleFormChange = (values) => {
|
||||
setInputs(values);
|
||||
};
|
||||
|
||||
const submitServerAddress = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
|
||||
const res = await API.put('/api/option/', {
|
||||
key: 'ServerAddress',
|
||||
value: ServerAddress,
|
||||
});
|
||||
if (res.data.success) {
|
||||
showSuccess(t('更新成功'));
|
||||
props.refresh && props.refresh();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('更新失败'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
onValueChange={handleFormChange}
|
||||
getFormApi={(api) => (formApiRef.current = api)}
|
||||
>
|
||||
<Form.Section text={t('通用设置')}>
|
||||
<Form.Input
|
||||
field='ServerAddress'
|
||||
label={t('服务器地址')}
|
||||
placeholder={'https://yourdomain.com'}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
218
web/src/pages/Setting/Payment/SettingsPaymentGateway.js
Normal file
218
web/src/pages/Setting/Payment/SettingsPaymentGateway.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
API,
|
||||
removeTrailingSlash,
|
||||
showError,
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SettingsPaymentGateway(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
PayAddress: '',
|
||||
EpayId: '',
|
||||
EpayKey: '',
|
||||
Price: 7.3,
|
||||
MinTopUp: 1,
|
||||
TopupGroupRatio: '',
|
||||
CustomCallbackAddress: '',
|
||||
PayMethods: '',
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
const formApiRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.options && formApiRef.current) {
|
||||
const currentInputs = {
|
||||
PayAddress: props.options.PayAddress || '',
|
||||
EpayId: props.options.EpayId || '',
|
||||
EpayKey: props.options.EpayKey || '',
|
||||
Price: props.options.Price !== undefined ? parseFloat(props.options.Price) : 7.3,
|
||||
MinTopUp: props.options.MinTopUp !== undefined ? parseFloat(props.options.MinTopUp) : 1,
|
||||
TopupGroupRatio: props.options.TopupGroupRatio || '',
|
||||
CustomCallbackAddress: props.options.CustomCallbackAddress || '',
|
||||
PayMethods: props.options.PayMethods || '',
|
||||
};
|
||||
setInputs(currentInputs);
|
||||
setOriginInputs({ ...currentInputs });
|
||||
formApiRef.current.setValues(currentInputs);
|
||||
}
|
||||
}, [props.options]);
|
||||
|
||||
const handleFormChange = (values) => {
|
||||
setInputs(values);
|
||||
};
|
||||
|
||||
const submitPayAddress = async () => {
|
||||
if (props.options.ServerAddress === '') {
|
||||
showError(t('请先填写服务器地址'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
|
||||
if (!verifyJSON(inputs.TopupGroupRatio)) {
|
||||
showError(t('充值分组倍率不是合法的 JSON 字符串'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (originInputs['PayMethods'] !== inputs.PayMethods) {
|
||||
if (!verifyJSON(inputs.PayMethods)) {
|
||||
showError(t('充值方式设置不是合法的 JSON 字符串'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const options = [
|
||||
{ key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
|
||||
];
|
||||
|
||||
if (inputs.EpayId !== '') {
|
||||
options.push({ key: 'EpayId', value: inputs.EpayId });
|
||||
}
|
||||
if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
|
||||
options.push({ key: 'EpayKey', value: inputs.EpayKey });
|
||||
}
|
||||
if (inputs.Price !== '') {
|
||||
options.push({ key: 'Price', value: inputs.Price.toString() });
|
||||
}
|
||||
if (inputs.MinTopUp !== '') {
|
||||
options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
|
||||
}
|
||||
if (inputs.CustomCallbackAddress !== '') {
|
||||
options.push({
|
||||
key: 'CustomCallbackAddress',
|
||||
value: inputs.CustomCallbackAddress,
|
||||
});
|
||||
}
|
||||
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
|
||||
options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
|
||||
}
|
||||
if (originInputs['PayMethods'] !== inputs.PayMethods) {
|
||||
options.push({ key: 'PayMethods', value: inputs.PayMethods });
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const requestQueue = options.map(opt =>
|
||||
API.put('/api/option/', {
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(requestQueue);
|
||||
|
||||
// 检查所有请求是否成功
|
||||
const errorResults = results.filter(res => !res.data.success);
|
||||
if (errorResults.length > 0) {
|
||||
errorResults.forEach(res => {
|
||||
showError(res.data.message);
|
||||
});
|
||||
} else {
|
||||
showSuccess(t('更新成功'));
|
||||
// 更新本地存储的原始值
|
||||
setOriginInputs({ ...inputs });
|
||||
props.refresh && props.refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('更新失败'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
onValueChange={handleFormChange}
|
||||
getFormApi={(api) => (formApiRef.current = api)}
|
||||
>
|
||||
<Form.Section text={t('支付设置')}>
|
||||
<Text>
|
||||
{t('(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)')}
|
||||
</Text>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='PayAddress'
|
||||
label={t('支付地址')}
|
||||
placeholder={t('例如:https://yourdomain.com')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='EpayId'
|
||||
label={t('易支付商户ID')}
|
||||
placeholder={t('例如:0001')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='EpayKey'
|
||||
label={t('易支付商户密钥')}
|
||||
placeholder={t('敏感信息不会发送到前端显示')}
|
||||
type='password'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row
|
||||
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='CustomCallbackAddress'
|
||||
label={t('回调地址')}
|
||||
placeholder={t('例如:https://yourdomain.com')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='Price'
|
||||
precision={2}
|
||||
label={t('充值价格(x元/美金)')}
|
||||
placeholder={t('例如:7,就是7元/美金')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.InputNumber
|
||||
field='MinTopUp'
|
||||
label={t('最低充值美元数量')}
|
||||
placeholder={t('例如:2,就是最低充值2$')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.TextArea
|
||||
field='TopupGroupRatio'
|
||||
label={t('充值分组倍率')}
|
||||
placeholder={t('为一个 JSON 文本,键为组名称,值为倍率')}
|
||||
autosize
|
||||
/>
|
||||
<Form.TextArea
|
||||
field='PayMethods'
|
||||
label={t('充值方式设置')}
|
||||
placeholder={t('为一个 JSON 文本')}
|
||||
autosize
|
||||
/>
|
||||
<Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
@@ -372,7 +372,7 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
return (
|
||||
<>
|
||||
<Space vertical align='start' style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Space className='mt-2'>
|
||||
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
|
||||
{t('添加模型')}
|
||||
</Button>
|
||||
|
||||
@@ -404,7 +404,7 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
return (
|
||||
<>
|
||||
<Space vertical align='start' style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Space className='mt-2'>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function UpstreamRatioSync(props) {
|
||||
setHasSynced(true);
|
||||
|
||||
if (Object.keys(differences).length === 0) {
|
||||
showSuccess(t('已与上游倍率完全一致,无需同步'));
|
||||
showSuccess(t('未找到差异化倍率,无需同步'));
|
||||
}
|
||||
} catch (e) {
|
||||
showError(t('请求后端接口失败:') + e.message);
|
||||
|
||||
@@ -2,6 +2,18 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Settings,
|
||||
Calculator,
|
||||
Gauge,
|
||||
Shapes,
|
||||
Cog,
|
||||
MoreHorizontal,
|
||||
LayoutDashboard,
|
||||
MessageSquare,
|
||||
Palette,
|
||||
CreditCard
|
||||
} from 'lucide-react';
|
||||
|
||||
import SystemSetting from '../../components/settings/SystemSetting.js';
|
||||
import { isRoot } from '../../helpers';
|
||||
@@ -11,6 +23,9 @@ import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
|
||||
import ModelSetting from '../../components/settings/ModelSetting.js';
|
||||
import DashboardSetting from '../../components/settings/DashboardSetting.js';
|
||||
import RatioSetting from '../../components/settings/RatioSetting.js';
|
||||
import ChatsSetting from '../../components/settings/ChatsSetting.js';
|
||||
import DrawingSetting from '../../components/settings/DrawingSetting.js';
|
||||
import PaymentSetting from '../../components/settings/PaymentSetting.js';
|
||||
|
||||
const Setting = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -21,40 +36,105 @@ const Setting = () => {
|
||||
|
||||
if (isRoot()) {
|
||||
panes.push({
|
||||
tab: t('运营设置'),
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<Settings size={18} />
|
||||
{t('运营设置')}
|
||||
</span>
|
||||
),
|
||||
content: <OperationSetting />,
|
||||
itemKey: 'operation',
|
||||
});
|
||||
panes.push({
|
||||
tab: t('倍率设置'),
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<MessageSquare size={18} />
|
||||
{t('聊天设置')}
|
||||
</span>
|
||||
),
|
||||
content: <ChatsSetting />,
|
||||
itemKey: 'chats',
|
||||
});
|
||||
panes.push({
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<Palette size={18} />
|
||||
{t('绘图设置')}
|
||||
</span>
|
||||
),
|
||||
content: <DrawingSetting />,
|
||||
itemKey: 'drawing',
|
||||
});
|
||||
panes.push({
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<CreditCard size={18} />
|
||||
{t('支付设置')}
|
||||
</span>
|
||||
),
|
||||
content: <PaymentSetting />,
|
||||
itemKey: 'payment',
|
||||
});
|
||||
panes.push({
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<Calculator size={18} />
|
||||
{t('倍率设置')}
|
||||
</span>
|
||||
),
|
||||
content: <RatioSetting />,
|
||||
itemKey: 'ratio',
|
||||
});
|
||||
panes.push({
|
||||
tab: t('速率限制设置'),
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<Gauge size={18} />
|
||||
{t('速率限制设置')}
|
||||
</span>
|
||||
),
|
||||
content: <RateLimitSetting />,
|
||||
itemKey: 'ratelimit',
|
||||
});
|
||||
panes.push({
|
||||
tab: t('模型相关设置'),
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<Shapes size={18} />
|
||||
{t('模型相关设置')}
|
||||
</span>
|
||||
),
|
||||
content: <ModelSetting />,
|
||||
itemKey: 'models',
|
||||
});
|
||||
panes.push({
|
||||
tab: t('系统设置'),
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<Cog size={18} />
|
||||
{t('系统设置')}
|
||||
</span>
|
||||
),
|
||||
content: <SystemSetting />,
|
||||
itemKey: 'system',
|
||||
});
|
||||
panes.push({
|
||||
tab: t('其他设置'),
|
||||
content: <OtherSetting />,
|
||||
itemKey: 'other',
|
||||
});
|
||||
panes.push({
|
||||
tab: t('仪表盘设置'),
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<LayoutDashboard size={18} />
|
||||
{t('仪表盘设置')}
|
||||
</span>
|
||||
),
|
||||
content: <DashboardSetting />,
|
||||
itemKey: 'dashboard',
|
||||
});
|
||||
panes.push({
|
||||
tab: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<MoreHorizontal size={18} />
|
||||
{t('其他设置')}
|
||||
</span>
|
||||
),
|
||||
content: <OtherSetting />,
|
||||
itemKey: 'other',
|
||||
});
|
||||
}
|
||||
const onChangeTab = (key) => {
|
||||
setTabActiveKey(key);
|
||||
@@ -74,7 +154,8 @@ const Setting = () => {
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Tabs
|
||||
type='line'
|
||||
type='card'
|
||||
collapsible
|
||||
activeKey={tabActiveKey}
|
||||
onChange={(key) => onChangeTab(key)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user