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
|
FROM alpine
|
||||||
|
|
||||||
RUN apk update \
|
RUN apk upgrade --no-cache \
|
||||||
&& apk upgrade \
|
|
||||||
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
||||||
&& update-ca-certificates
|
&& update-ca-certificates
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/service"
|
"one-api/service"
|
||||||
|
"one-api/setting"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -304,6 +306,40 @@ func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) {
|
|||||||
return balance, nil
|
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) {
|
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
baseURL := common.ChannelBaseURLs[channel.Type]
|
||||||
if channel.GetBaseURL() == "" {
|
if channel.GetBaseURL() == "" {
|
||||||
@@ -332,6 +368,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
|
|||||||
return updateChannelDeepSeekBalance(channel)
|
return updateChannelDeepSeekBalance(channel)
|
||||||
case common.ChannelTypeOpenRouter:
|
case common.ChannelTypeOpenRouter:
|
||||||
return updateChannelOpenRouterBalance(channel)
|
return updateChannelOpenRouterBalance(channel)
|
||||||
|
case common.ChannelTypeMoonshot:
|
||||||
|
return updateChannelMoonshotBalance(channel)
|
||||||
default:
|
default:
|
||||||
return 0, errors.New("尚未实现")
|
return 0, errors.New("尚未实现")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|||||||
isNew25Pro := strings.HasPrefix(modelName, "gemini-2.5-pro") &&
|
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-05-06") &&
|
||||||
!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-03-25")
|
!strings.HasPrefix(modelName, "gemini-2.5-pro-preview-03-25")
|
||||||
is25FlashLite := strings.HasPrefix(modelName, "gemini-2.5-flash-lite")
|
|
||||||
|
|
||||||
if strings.Contains(modelName, "-thinking-") {
|
if strings.Contains(modelName, "-thinking-") {
|
||||||
parts := strings.SplitN(modelName, "-thinking-", 2)
|
parts := strings.SplitN(modelName, "-thinking-", 2)
|
||||||
@@ -134,15 +133,17 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
|||||||
IncludeThoughts: true,
|
IncludeThoughts: true,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
|
|
||||||
clampedBudget := clampThinkingBudget(modelName, int(budgetTokens))
|
|
||||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||||
ThinkingBudget: common.GetPointer(clampedBudget),
|
|
||||||
IncludeThoughts: true,
|
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") {
|
} else if strings.HasSuffix(modelName, "-nothinking") {
|
||||||
if !isNew25Pro && !is25FlashLite {
|
if !isNew25Pro {
|
||||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||||
ThinkingBudget: common.GetPointer(0),
|
ThinkingBudget: common.GetPointer(0),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const (
|
|||||||
const (
|
const (
|
||||||
// Gemini Audio Input Price
|
// Gemini Audio Input Price
|
||||||
Gemini25FlashPreviewInputAudioPrice = 1.00
|
Gemini25FlashPreviewInputAudioPrice = 1.00
|
||||||
|
Gemini25FlashProductionInputAudioPrice = 1.00 // for `gemini-2.5-flash`
|
||||||
|
Gemini25FlashLitePreviewInputAudioPrice = 0.50
|
||||||
Gemini25FlashNativeAudioInputAudioPrice = 3.00
|
Gemini25FlashNativeAudioInputAudioPrice = 3.00
|
||||||
Gemini20FlashInputAudioPrice = 0.70
|
Gemini20FlashInputAudioPrice = 0.70
|
||||||
)
|
)
|
||||||
@@ -64,10 +66,14 @@ func GetFileSearchPricePerThousand() float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
|
func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
|
||||||
if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
|
if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
|
||||||
return Gemini25FlashPreviewInputAudioPrice
|
|
||||||
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
|
|
||||||
return Gemini25FlashNativeAudioInputAudioPrice
|
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") {
|
} else if strings.HasPrefix(modelName, "gemini-2.0-flash") {
|
||||||
return Gemini20FlashInputAudioPrice
|
return Gemini20FlashInputAudioPrice
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ var defaultModelRatio = map[string]float64{
|
|||||||
"gemini-2.0-flash": 0.05,
|
"gemini-2.0-flash": 0.05,
|
||||||
"gemini-2.5-pro-exp-03-25": 0.625,
|
"gemini-2.5-pro-exp-03-25": 0.625,
|
||||||
"gemini-2.5-pro-preview-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": 0.075,
|
||||||
"gemini-2.5-flash-preview-04-17-thinking": 0.075,
|
"gemini-2.5-flash-preview-04-17-thinking": 0.075,
|
||||||
"gemini-2.5-flash-preview-04-17-nothinking": 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-preview-05-20-nothinking": 0.075,
|
||||||
"gemini-2.5-flash-thinking-*": 0.075, // 用于为后续所有2.5 flash thinking budget 模型设置默认倍率
|
"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-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,
|
"text-embedding-004": 0.001,
|
||||||
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
||||||
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
||||||
@@ -423,7 +426,12 @@ func UpdateCompletionRatioByJSONString(jsonStr string) error {
|
|||||||
func GetCompletionRatio(name string) float64 {
|
func GetCompletionRatio(name string) float64 {
|
||||||
CompletionRatioMutex.RLock()
|
CompletionRatioMutex.RLock()
|
||||||
defer CompletionRatioMutex.RUnlock()
|
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 strings.Contains(name, "/") {
|
||||||
if ratio, ok := CompletionRatio[name]; ok {
|
if ratio, ok := CompletionRatio[name]; ok {
|
||||||
return ratio
|
return ratio
|
||||||
@@ -441,12 +449,6 @@ func GetCompletionRatio(name string) float64 {
|
|||||||
|
|
||||||
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
||||||
lowercaseName := strings.ToLower(name)
|
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-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
|
||||||
if strings.HasPrefix(name, "gpt-4o") {
|
if strings.HasPrefix(name, "gpt-4o") {
|
||||||
if name == "gpt-4o-2024-05-13" {
|
if name == "gpt-4o-2024-05-13" {
|
||||||
@@ -500,12 +502,17 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
|||||||
return 4, true
|
return 4, true
|
||||||
} else if strings.HasPrefix(name, "gemini-2.5-pro") { // 移除preview来增加兼容性,这里假设正式版的倍率和preview一致
|
} else if strings.HasPrefix(name, "gemini-2.5-pro") { // 移除preview来增加兼容性,这里假设正式版的倍率和preview一致
|
||||||
return 8, true
|
return 8, true
|
||||||
} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 同上
|
} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 处理不同的flash模型倍率
|
||||||
if strings.HasSuffix(name, "-nothinking") {
|
if strings.HasPrefix(name, "gemini-2.5-flash-preview") {
|
||||||
return 4, false
|
if strings.HasSuffix(name, "-nothinking") {
|
||||||
} else {
|
return 4, true
|
||||||
return 3.5 / 0.6, false
|
}
|
||||||
|
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
|
return 4, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
Badge,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { StatusContext } from '../../context/Status/index.js';
|
import { StatusContext } from '../../context/Status/index.js';
|
||||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||||
@@ -43,6 +44,7 @@ const HeaderBar = () => {
|
|||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [noticeVisible, setNoticeVisible] = useState(false);
|
const [noticeVisible, setNoticeVisible] = useState(false);
|
||||||
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
|
|
||||||
const systemName = getSystemName();
|
const systemName = getSystemName();
|
||||||
const logo = getLogo();
|
const logo = getLogo();
|
||||||
@@ -53,9 +55,44 @@ const HeaderBar = () => {
|
|||||||
const docsLink = statusState?.status?.docs_link || '';
|
const docsLink = statusState?.status?.docs_link || '';
|
||||||
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
||||||
|
|
||||||
|
const isConsoleRoute = location.pathname.startsWith('/console');
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const setTheme = useSetTheme();
|
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 = [
|
const mainNavLinks = [
|
||||||
{
|
{
|
||||||
text: t('首页'),
|
text: t('首页'),
|
||||||
@@ -106,6 +143,25 @@ const HeaderBar = () => {
|
|||||||
}, 3000);
|
}, 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(() => {
|
useEffect(() => {
|
||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
document.body.setAttribute('theme-mode', 'dark');
|
document.body.setAttribute('theme-mode', 'dark');
|
||||||
@@ -353,15 +409,14 @@ const HeaderBar = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查当前路由是否以/console开头
|
|
||||||
const isConsoleRoute = location.pathname.startsWith('/console');
|
|
||||||
|
|
||||||
return (
|
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">
|
<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
|
<NoticeModal
|
||||||
visible={noticeVisible}
|
visible={noticeVisible}
|
||||||
onClose={() => setNoticeVisible(false)}
|
onClose={handleNoticeClose}
|
||||||
isMobile={styleState.isMobile}
|
isMobile={styleState.isMobile}
|
||||||
|
defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
|
||||||
|
unreadKeys={getUnreadKeys()}
|
||||||
/>
|
/>
|
||||||
<div className="w-full px-2">
|
<div className="w-full px-2">
|
||||||
<div className="flex items-center justify-between h-16">
|
<div className="flex items-center justify-between h-16">
|
||||||
@@ -462,14 +517,27 @@ const HeaderBar = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
{unreadCount > 0 ? (
|
||||||
icon={<IconBell className="text-lg" />}
|
<Badge count={unreadCount} type="danger" overflowCount={99}>
|
||||||
aria-label={t('系统公告')}
|
<Button
|
||||||
onClick={() => setNoticeVisible(true)}
|
icon={<IconBell className="text-lg" />}
|
||||||
theme="borderless"
|
aria-label={t('系统公告')}
|
||||||
type="tertiary"
|
onClick={handleNoticeOpen}
|
||||||
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"
|
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
|
<Button
|
||||||
icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
|
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 React, { useEffect, useState, useContext, useMemo } from 'react';
|
||||||
import { Button, Modal, Empty } from '@douyinfe/semi-ui';
|
import { Button, Modal, Empty, Tabs, TabPane, Timeline } from '@douyinfe/semi-ui';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { API, showError } from '../../helpers';
|
import { API, showError, getRelativeTime } from '../../helpers';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
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 { t } = useTranslation();
|
||||||
const [noticeContent, setNoticeContent] = useState('');
|
const [noticeContent, setNoticeContent] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
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 handleCloseTodayNotice = () => {
|
||||||
const today = new Date().toDateString();
|
const today = new Date().toDateString();
|
||||||
@@ -44,7 +66,13 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
|||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
const renderContent = () => {
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setActiveTab(defaultTab);
|
||||||
|
}
|
||||||
|
}, [defaultTab, visible]);
|
||||||
|
|
||||||
|
const renderMarkdownNotice = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
||||||
}
|
}
|
||||||
@@ -64,14 +92,74 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
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 (
|
return (
|
||||||
<Modal
|
<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}
|
visible={visible}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={(
|
footer={(
|
||||||
@@ -82,7 +170,7 @@ const NoticeModal = ({ visible, onClose, isMobile }) => {
|
|||||||
)}
|
)}
|
||||||
size={isMobile ? 'full-width' : 'large'}
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderBody()}
|
||||||
</Modal>
|
</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 SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js';
|
||||||
import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
|
import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
|
||||||
import SettingsUptimeKuma from '../../pages/Setting/Dashboard/SettingsUptimeKuma.js';
|
import SettingsUptimeKuma from '../../pages/Setting/Dashboard/SettingsUptimeKuma.js';
|
||||||
|
import SettingsDataDashboard from '../../pages/Setting/Dashboard/SettingsDataDashboard.js';
|
||||||
|
|
||||||
const DashboardSetting = () => {
|
const DashboardSetting = () => {
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
@@ -23,6 +24,11 @@ const DashboardSetting = () => {
|
|||||||
FAQ: '',
|
FAQ: '',
|
||||||
UptimeKumaUrl: '',
|
UptimeKumaUrl: '',
|
||||||
UptimeKumaSlug: '',
|
UptimeKumaSlug: '',
|
||||||
|
|
||||||
|
/* 数据看板 */
|
||||||
|
DataExportEnabled: false,
|
||||||
|
DataExportDefaultTime: 'hour',
|
||||||
|
DataExportInterval: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@@ -37,6 +43,10 @@ const DashboardSetting = () => {
|
|||||||
if (item.key in inputs) {
|
if (item.key in inputs) {
|
||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
}
|
}
|
||||||
|
if (item.key.endsWith('Enabled') &&
|
||||||
|
(item.key === 'DataExportEnabled')) {
|
||||||
|
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setInputs(newInputs);
|
setInputs(newInputs);
|
||||||
} else {
|
} else {
|
||||||
@@ -106,9 +116,9 @@ const DashboardSetting = () => {
|
|||||||
</p>
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* API信息管理 */}
|
{/* 数据看板设置 */}
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsAPIInfo options={inputs} refresh={onRefresh} />
|
<SettingsDataDashboard options={inputs} refresh={onRefresh} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 系统公告管理 */}
|
{/* 系统公告管理 */}
|
||||||
@@ -116,6 +126,11 @@ const DashboardSetting = () => {
|
|||||||
<SettingsAnnouncements options={inputs} refresh={onRefresh} />
|
<SettingsAnnouncements options={inputs} refresh={onRefresh} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* API信息管理 */}
|
||||||
|
<Card style={{ marginTop: '10px' }}>
|
||||||
|
<SettingsAPIInfo options={inputs} refresh={onRefresh} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 常见问答管理 */}
|
{/* 常见问答管理 */}
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsFAQ options={inputs} refresh={onRefresh} />
|
<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 React, { useEffect, useState } from 'react';
|
||||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||||
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js';
|
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 SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||||
import SettingsLog from '../../pages/Setting/Operation/SettingsLog.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 SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring.js';
|
||||||
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js';
|
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
|
||||||
import { API, showError } from '../../helpers';
|
import { API, showError } from '../../helpers';
|
||||||
|
|
||||||
const OperationSetting = () => {
|
const OperationSetting = () => {
|
||||||
@@ -29,14 +26,6 @@ const OperationSetting = () => {
|
|||||||
DemoSiteEnabled: false,
|
DemoSiteEnabled: false,
|
||||||
SelfUseModeEnabled: false,
|
SelfUseModeEnabled: false,
|
||||||
|
|
||||||
/* 绘图设置 */
|
|
||||||
DrawingEnabled: false,
|
|
||||||
MjNotifyEnabled: false,
|
|
||||||
MjAccountFilterEnabled: false,
|
|
||||||
MjForwardUrlEnabled: false,
|
|
||||||
MjModeClearEnabled: false,
|
|
||||||
MjActionCheckSuccessEnabled: false,
|
|
||||||
|
|
||||||
/* 敏感词设置 */
|
/* 敏感词设置 */
|
||||||
CheckSensitiveEnabled: false,
|
CheckSensitiveEnabled: false,
|
||||||
CheckSensitiveOnPromptEnabled: false,
|
CheckSensitiveOnPromptEnabled: false,
|
||||||
@@ -45,20 +34,12 @@ const OperationSetting = () => {
|
|||||||
/* 日志设置 */
|
/* 日志设置 */
|
||||||
LogConsumeEnabled: false,
|
LogConsumeEnabled: false,
|
||||||
|
|
||||||
/* 数据看板 */
|
|
||||||
DataExportEnabled: false,
|
|
||||||
DataExportDefaultTime: 'hour',
|
|
||||||
DataExportInterval: 5,
|
|
||||||
|
|
||||||
/* 监控设置 */
|
/* 监控设置 */
|
||||||
ChannelDisableThreshold: 0,
|
ChannelDisableThreshold: 0,
|
||||||
QuotaRemindThreshold: 0,
|
QuotaRemindThreshold: 0,
|
||||||
AutomaticDisableChannelEnabled: false,
|
AutomaticDisableChannelEnabled: false,
|
||||||
AutomaticEnableChannelEnabled: false,
|
AutomaticEnableChannelEnabled: false,
|
||||||
AutomaticDisableKeywords: '',
|
AutomaticDisableKeywords: '',
|
||||||
|
|
||||||
/* 聊天设置 */
|
|
||||||
Chats: '[]',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@@ -107,10 +88,6 @@ const OperationSetting = () => {
|
|||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsGeneral options={inputs} refresh={onRefresh} />
|
<SettingsGeneral options={inputs} refresh={onRefresh} />
|
||||||
</Card>
|
</Card>
|
||||||
{/* 绘图设置 */}
|
|
||||||
<Card style={{ marginTop: '10px' }}>
|
|
||||||
<SettingsDrawing options={inputs} refresh={onRefresh} />
|
|
||||||
</Card>
|
|
||||||
{/* 屏蔽词过滤设置 */}
|
{/* 屏蔽词过滤设置 */}
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsSensitiveWords options={inputs} refresh={onRefresh} />
|
<SettingsSensitiveWords options={inputs} refresh={onRefresh} />
|
||||||
@@ -119,10 +96,6 @@ const OperationSetting = () => {
|
|||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsLog options={inputs} refresh={onRefresh} />
|
<SettingsLog options={inputs} refresh={onRefresh} />
|
||||||
</Card>
|
</Card>
|
||||||
{/* 数据看板 */}
|
|
||||||
<Card style={{ marginTop: '10px' }}>
|
|
||||||
<SettingsDataDashboard options={inputs} refresh={onRefresh} />
|
|
||||||
</Card>
|
|
||||||
{/* 监控设置 */}
|
{/* 监控设置 */}
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsMonitoring options={inputs} refresh={onRefresh} />
|
<SettingsMonitoring options={inputs} refresh={onRefresh} />
|
||||||
@@ -131,10 +104,6 @@ const OperationSetting = () => {
|
|||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
|
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
|
||||||
</Card>
|
</Card>
|
||||||
{/* 聊天设置 */}
|
|
||||||
<Card style={{ marginTop: '10px' }}>
|
|
||||||
<SettingsChats options={inputs} refresh={onRefresh} />
|
|
||||||
</Card>
|
|
||||||
</Spin>
|
</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 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 { API, showError } from '../../helpers/index.js';
|
||||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import RequestRateLimit from '../../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
|
import RequestRateLimit from '../../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
|
||||||
|
|
||||||
@@ -24,14 +23,14 @@ const RateLimitSetting = () => {
|
|||||||
if (success) {
|
if (success) {
|
||||||
let newInputs = {};
|
let newInputs = {};
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
if (item.key === 'ModelRequestRateLimitGroup') {
|
if (item.key === 'ModelRequestRateLimitGroup') {
|
||||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.key.endsWith('Enabled')) {
|
if (item.key.endsWith('Enabled')) {
|
||||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||||
} else {
|
} else {
|
||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const RatioSetting = () => {
|
|||||||
<Spin spinning={loading} size='large'>
|
<Spin spinning={loading} size='large'>
|
||||||
{/* 模型倍率设置以及可视化编辑器 */}
|
{/* 模型倍率设置以及可视化编辑器 */}
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<Tabs type='line'>
|
<Tabs type='card'>
|
||||||
<Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
|
<Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
|
||||||
<ModelRatioSettings options={inputs} refresh={onRefresh} />
|
<ModelRatioSettings options={inputs} refresh={onRefresh} />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
removeTrailingSlash,
|
removeTrailingSlash,
|
||||||
showError,
|
showError,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
verifyJSON,
|
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -42,17 +41,9 @@ const SystemSetting = () => {
|
|||||||
SMTPAccount: '',
|
SMTPAccount: '',
|
||||||
SMTPFrom: '',
|
SMTPFrom: '',
|
||||||
SMTPToken: '',
|
SMTPToken: '',
|
||||||
ServerAddress: '',
|
|
||||||
WorkerUrl: '',
|
WorkerUrl: '',
|
||||||
WorkerValidKey: '',
|
WorkerValidKey: '',
|
||||||
WorkerAllowHttpImageRequestEnabled: '',
|
WorkerAllowHttpImageRequestEnabled: '',
|
||||||
EpayId: '',
|
|
||||||
EpayKey: '',
|
|
||||||
Price: 7.3,
|
|
||||||
MinTopUp: 1,
|
|
||||||
TopupGroupRatio: '',
|
|
||||||
PayAddress: '',
|
|
||||||
CustomCallbackAddress: '',
|
|
||||||
Footer: '',
|
Footer: '',
|
||||||
WeChatAuthEnabled: '',
|
WeChatAuthEnabled: '',
|
||||||
WeChatServerAddress: '',
|
WeChatServerAddress: '',
|
||||||
@@ -73,7 +64,6 @@ const SystemSetting = () => {
|
|||||||
LinuxDOOAuthEnabled: '',
|
LinuxDOOAuthEnabled: '',
|
||||||
LinuxDOClientId: '',
|
LinuxDOClientId: '',
|
||||||
LinuxDOClientSecret: '',
|
LinuxDOClientSecret: '',
|
||||||
PayMethods: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
@@ -200,11 +190,6 @@ const SystemSetting = () => {
|
|||||||
setInputs(values);
|
setInputs(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitServerAddress = async () => {
|
|
||||||
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
|
|
||||||
await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitWorker = async () => {
|
const submitWorker = async () => {
|
||||||
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
||||||
const options = [
|
const options = [
|
||||||
@@ -220,56 +205,6 @@ const SystemSetting = () => {
|
|||||||
await updateOptions(options);
|
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 submitSMTP = async () => {
|
||||||
const options = [];
|
const options = [];
|
||||||
|
|
||||||
@@ -551,17 +486,6 @@ const SystemSetting = () => {
|
|||||||
marginTop: '10px',
|
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>
|
<Card>
|
||||||
<Form.Section text='代理设置'>
|
<Form.Section text='代理设置'>
|
||||||
<Text>
|
<Text>
|
||||||
@@ -604,80 +528,6 @@ const SystemSetting = () => {
|
|||||||
</Form.Section>
|
</Form.Section>
|
||||||
</Card>
|
</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>
|
<Card>
|
||||||
<Form.Section text='配置登录注册'>
|
<Form.Section text='配置登录注册'>
|
||||||
<Row
|
<Row
|
||||||
|
|||||||
@@ -1206,7 +1206,7 @@
|
|||||||
"默认折叠侧边栏": "Default collapse sidebar",
|
"默认折叠侧边栏": "Default collapse sidebar",
|
||||||
"聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below",
|
"聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below",
|
||||||
"你似乎并没有修改什么": "You seem to have not modified anything",
|
"你似乎并没有修改什么": "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",
|
"必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "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",
|
"链接中的{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",
|
"聊天配置": "Chat configuration",
|
||||||
@@ -1672,7 +1672,7 @@
|
|||||||
"获取倍率失败:": "Failed to get ratios: ",
|
"获取倍率失败:": "Failed to get ratios: ",
|
||||||
"后端请求失败": "Backend request failed",
|
"后端请求失败": "Backend request failed",
|
||||||
"部分渠道测试失败:": "Some channels failed to test: ",
|
"部分渠道测试失败:": "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: ",
|
"请求后端接口失败:": "Failed to request the backend interface: ",
|
||||||
"同步成功": "Synchronization successful",
|
"同步成功": "Synchronization successful",
|
||||||
"部分保存失败": "Some settings failed to save",
|
"部分保存失败": "Some settings failed to save",
|
||||||
@@ -1688,5 +1688,18 @@
|
|||||||
"暂无差异化倍率显示": "No differential ratio display",
|
"暂无差异化倍率显示": "No differential ratio display",
|
||||||
"请先选择同步渠道": "Please select the synchronization channel first",
|
"请先选择同步渠道": "Please select the synchronization channel first",
|
||||||
"与本地相同": "Same as local",
|
"与本地相同": "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 {
|
.components-transfer-selected-item .semi-icon-close:hover {
|
||||||
color: var(--semi-color-text-0);
|
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 {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
Button,
|
Button,
|
||||||
Col,
|
|
||||||
Form,
|
Form,
|
||||||
Popconfirm,
|
|
||||||
Row,
|
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
@@ -16,7 +13,6 @@ import {
|
|||||||
showSuccess,
|
showSuccess,
|
||||||
showWarning,
|
showWarning,
|
||||||
verifyJSON,
|
verifyJSON,
|
||||||
verifyJSONPromise,
|
|
||||||
} from '../../../helpers';
|
} from '../../../helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
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(() => {
|
useEffect(() => {
|
||||||
const currentInputs = {};
|
const currentInputs = {};
|
||||||
for (let key in props.options) {
|
for (let key in props.options) {
|
||||||
@@ -119,13 +100,7 @@ export default function SettingsChats(props) {
|
|||||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
style={{ marginBottom: 15 }}
|
style={{ marginBottom: 15 }}
|
||||||
>
|
>
|
||||||
<Form.Section text={t('令牌聊天设置')}>
|
<Form.Section text={t('聊天设置')}>
|
||||||
<Banner
|
|
||||||
type='warning'
|
|
||||||
description={t(
|
|
||||||
'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Banner
|
<Banner
|
||||||
type='info'
|
type='info'
|
||||||
description={t(
|
description={t(
|
||||||
@@ -388,11 +388,17 @@ const SettingsAnnouncements = ({ options, refresh }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 计算当前页显示的数据
|
// 计算当前页显示的数据(按发布时间倒序排序,最新优先显示)
|
||||||
const getCurrentPageData = () => {
|
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 startIndex = (currentPage - 1) * pageSize;
|
||||||
const endIndex = startIndex + pageSize;
|
const endIndex = startIndex + pageSize;
|
||||||
return announcementsList.slice(startIndex, endIndex);
|
return sortedList.slice(startIndex, endIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowSelection = {
|
const rowSelection = {
|
||||||
|
|||||||
@@ -209,8 +209,8 @@ export default function SettingGeminiModel(props) {
|
|||||||
label={t('思考预算占比')}
|
label={t('思考预算占比')}
|
||||||
field={'gemini.thinking_adapter_budget_tokens_percentage'}
|
field={'gemini.thinking_adapter_budget_tokens_percentage'}
|
||||||
initValue={''}
|
initValue={''}
|
||||||
extraText={t('0.1-1之间的小数')}
|
extraText={t('0.002-1之间的小数')}
|
||||||
min={0.1}
|
min={0.002}
|
||||||
max={1}
|
max={1}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setInputs({
|
setInputs({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Row,
|
Row,
|
||||||
Spin,
|
Spin,
|
||||||
Collapse,
|
|
||||||
Modal,
|
Modal,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
@@ -92,10 +91,6 @@ export default function GeneralSettings(props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Banner
|
|
||||||
type='warning'
|
|
||||||
description={t('聊天链接功能已经弃用,请使用下方聊天设置功能')}
|
|
||||||
/>
|
|
||||||
<Form
|
<Form
|
||||||
values={inputs}
|
values={inputs}
|
||||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Space vertical align='start' style={{ width: '100%' }}>
|
<Space vertical align='start' style={{ width: '100%' }}>
|
||||||
<Space>
|
<Space className='mt-2'>
|
||||||
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
|
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
|
||||||
{t('添加模型')}
|
{t('添加模型')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ export default function ModelSettingsVisualEditor(props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Space vertical align='start' style={{ width: '100%' }}>
|
<Space vertical align='start' style={{ width: '100%' }}>
|
||||||
<Space>
|
<Space className='mt-2'>
|
||||||
<Button
|
<Button
|
||||||
icon={<IconPlus />}
|
icon={<IconPlus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default function UpstreamRatioSync(props) {
|
|||||||
setHasSynced(true);
|
setHasSynced(true);
|
||||||
|
|
||||||
if (Object.keys(differences).length === 0) {
|
if (Object.keys(differences).length === 0) {
|
||||||
showSuccess(t('已与上游倍率完全一致,无需同步'));
|
showSuccess(t('未找到差异化倍率,无需同步'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(t('请求后端接口失败:') + e.message);
|
showError(t('请求后端接口失败:') + e.message);
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
|
import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 SystemSetting from '../../components/settings/SystemSetting.js';
|
||||||
import { isRoot } from '../../helpers';
|
import { isRoot } from '../../helpers';
|
||||||
@@ -11,6 +23,9 @@ import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
|
|||||||
import ModelSetting from '../../components/settings/ModelSetting.js';
|
import ModelSetting from '../../components/settings/ModelSetting.js';
|
||||||
import DashboardSetting from '../../components/settings/DashboardSetting.js';
|
import DashboardSetting from '../../components/settings/DashboardSetting.js';
|
||||||
import RatioSetting from '../../components/settings/RatioSetting.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 Setting = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -21,40 +36,105 @@ const Setting = () => {
|
|||||||
|
|
||||||
if (isRoot()) {
|
if (isRoot()) {
|
||||||
panes.push({
|
panes.push({
|
||||||
tab: t('运营设置'),
|
tab: (
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<Settings size={18} />
|
||||||
|
{t('运营设置')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
content: <OperationSetting />,
|
content: <OperationSetting />,
|
||||||
itemKey: 'operation',
|
itemKey: 'operation',
|
||||||
});
|
});
|
||||||
panes.push({
|
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 />,
|
content: <RatioSetting />,
|
||||||
itemKey: 'ratio',
|
itemKey: 'ratio',
|
||||||
});
|
});
|
||||||
panes.push({
|
panes.push({
|
||||||
tab: t('速率限制设置'),
|
tab: (
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<Gauge size={18} />
|
||||||
|
{t('速率限制设置')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
content: <RateLimitSetting />,
|
content: <RateLimitSetting />,
|
||||||
itemKey: 'ratelimit',
|
itemKey: 'ratelimit',
|
||||||
});
|
});
|
||||||
panes.push({
|
panes.push({
|
||||||
tab: t('模型相关设置'),
|
tab: (
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<Shapes size={18} />
|
||||||
|
{t('模型相关设置')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
content: <ModelSetting />,
|
content: <ModelSetting />,
|
||||||
itemKey: 'models',
|
itemKey: 'models',
|
||||||
});
|
});
|
||||||
panes.push({
|
panes.push({
|
||||||
tab: t('系统设置'),
|
tab: (
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<Cog size={18} />
|
||||||
|
{t('系统设置')}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
content: <SystemSetting />,
|
content: <SystemSetting />,
|
||||||
itemKey: 'system',
|
itemKey: 'system',
|
||||||
});
|
});
|
||||||
panes.push({
|
panes.push({
|
||||||
tab: t('其他设置'),
|
tab: (
|
||||||
content: <OtherSetting />,
|
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
itemKey: 'other',
|
<LayoutDashboard size={18} />
|
||||||
});
|
{t('仪表盘设置')}
|
||||||
panes.push({
|
</span>
|
||||||
tab: t('仪表盘设置'),
|
),
|
||||||
content: <DashboardSetting />,
|
content: <DashboardSetting />,
|
||||||
itemKey: 'dashboard',
|
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) => {
|
const onChangeTab = (key) => {
|
||||||
setTabActiveKey(key);
|
setTabActiveKey(key);
|
||||||
@@ -74,7 +154,8 @@ const Setting = () => {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Content>
|
<Layout.Content>
|
||||||
<Tabs
|
<Tabs
|
||||||
type='line'
|
type='card'
|
||||||
|
collapsible
|
||||||
activeKey={tabActiveKey}
|
activeKey={tabActiveKey}
|
||||||
onChange={(key) => onChangeTab(key)}
|
onChange={(key) => onChangeTab(key)}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user