Преглед изворни кода

Merge remote-tracking branch 'origin/main'

CaIon пре 5 месеци
родитељ
комит
7794788b1e

+ 22 - 1
relay/channel/aws/constants.go

@@ -21,6 +21,10 @@ var awsModelIDMap = map[string]string{
 	"nova-lite-v1:0":    "amazon.nova-lite-v1:0",
 	"nova-lite-v1:0":    "amazon.nova-lite-v1:0",
 	"nova-pro-v1:0":     "amazon.nova-pro-v1:0",
 	"nova-pro-v1:0":     "amazon.nova-pro-v1:0",
 	"nova-premier-v1:0": "amazon.nova-premier-v1:0",
 	"nova-premier-v1:0": "amazon.nova-premier-v1:0",
+	"nova-canvas-v1:0":  "amazon.nova-canvas-v1:0",
+	"nova-reel-v1:0":    "amazon.nova-reel-v1:0",
+	"nova-reel-v1:1":    "amazon.nova-reel-v1:1",
+	"nova-sonic-v1:0":   "amazon.nova-sonic-v1:0",
 }
 }
 
 
 var awsModelCanCrossRegionMap = map[string]map[string]bool{
 var awsModelCanCrossRegionMap = map[string]map[string]bool{
@@ -82,10 +86,27 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{
 		"apac": true,
 		"apac": true,
 	},
 	},
 	"amazon.nova-premier-v1:0": {
 	"amazon.nova-premier-v1:0": {
+		"us": true,
+	},
+	"amazon.nova-canvas-v1:0": {
+		"us":   true,
+		"eu":   true,
+		"apac": true,
+	},
+	"amazon.nova-reel-v1:0": {
+		"us":   true,
+		"eu":   true,
+		"apac": true,
+	},
+	"amazon.nova-reel-v1:1": {
+		"us": true,
+	},
+	"amazon.nova-sonic-v1:0": {
 		"us":   true,
 		"us":   true,
 		"eu":   true,
 		"eu":   true,
 		"apac": true,
 		"apac": true,
-	}}
+	},
+}
 
 
 var awsRegionCrossModelPrefixMap = map[string]string{
 var awsRegionCrossModelPrefixMap = map[string]string{
 	"us": "us",
 	"us": "us",

+ 26 - 0
relay/claude_handler.go

@@ -6,6 +6,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 	"one-api/common"
 	"one-api/common"
+	"one-api/constant"
 	"one-api/dto"
 	"one-api/dto"
 	relaycommon "one-api/relay/common"
 	relaycommon "one-api/relay/common"
 	"one-api/relay/helper"
 	"one-api/relay/helper"
@@ -69,6 +70,31 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
 		info.UpstreamModelName = request.Model
 		info.UpstreamModelName = request.Model
 	}
 	}
 
 
+	if info.ChannelSetting.SystemPrompt != "" {
+		if request.System == nil {
+			request.SetStringSystem(info.ChannelSetting.SystemPrompt)
+		} else if info.ChannelSetting.SystemPromptOverride {
+			common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
+			if request.IsStringSystem() {
+				existing := strings.TrimSpace(request.GetStringSystem())
+				if existing == "" {
+					request.SetStringSystem(info.ChannelSetting.SystemPrompt)
+				} else {
+					request.SetStringSystem(info.ChannelSetting.SystemPrompt + "\n" + existing)
+				}
+			} else {
+				systemContents := request.ParseSystem()
+				newSystem := dto.ClaudeMediaMessage{Type: dto.ContentTypeText}
+				newSystem.SetText(info.ChannelSetting.SystemPrompt)
+				if len(systemContents) == 0 {
+					request.System = []dto.ClaudeMediaMessage{newSystem}
+				} else {
+					request.System = append([]dto.ClaudeMediaMessage{newSystem}, systemContents...)
+				}
+			}
+		}
+	}
+
 	var requestBody io.Reader
 	var requestBody io.Reader
 	if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled {
 	if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled {
 		body, err := common.GetRequestBody(c)
 		body, err := common.GetRequestBody(c)

+ 27 - 0
relay/gemini_handler.go

@@ -6,6 +6,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 	"one-api/common"
 	"one-api/common"
+	"one-api/constant"
 	"one-api/dto"
 	"one-api/dto"
 	"one-api/logger"
 	"one-api/logger"
 	"one-api/relay/channel/gemini"
 	"one-api/relay/channel/gemini"
@@ -94,6 +95,32 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
 
 
 	adaptor.Init(info)
 	adaptor.Init(info)
 
 
+	if info.ChannelSetting.SystemPrompt != "" {
+		if request.SystemInstructions == nil {
+			request.SystemInstructions = &dto.GeminiChatContent{
+				Parts: []dto.GeminiPart{
+					{Text: info.ChannelSetting.SystemPrompt},
+				},
+			}
+		} else if len(request.SystemInstructions.Parts) == 0 {
+			request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}
+		} else if info.ChannelSetting.SystemPromptOverride {
+			common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
+			merged := false
+			for i := range request.SystemInstructions.Parts {
+				if request.SystemInstructions.Parts[i].Text == "" {
+					continue
+				}
+				request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
+				merged = true
+				break
+			}
+			if !merged {
+				request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...)
+			}
+		}
+	}
+
 	// Clean up empty system instruction
 	// Clean up empty system instruction
 	if request.SystemInstructions != nil {
 	if request.SystemInstructions != nil {
 		hasContent := false
 		hasContent := false

+ 9 - 0
web/jsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["src/**/*"]
+}

+ 87 - 83
web/src/components/layout/headerbar/UserArea.jsx

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 For commercial licensing, please contact support@quantumnous.com
 */
 */
 
 
-import React from 'react';
+import React, { useRef } from 'react';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
 import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
 import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
 import { ChevronDown } from 'lucide-react';
 import { ChevronDown } from 'lucide-react';
@@ -39,6 +39,7 @@ const UserArea = ({
   navigate,
   navigate,
   t,
   t,
 }) => {
 }) => {
+  const dropdownRef = useRef(null);
   if (isLoading) {
   if (isLoading) {
     return (
     return (
       <SkeletonWrapper
       <SkeletonWrapper
@@ -52,90 +53,93 @@ const UserArea = ({
 
 
   if (userState.user) {
   if (userState.user) {
     return (
     return (
-      <Dropdown
-        position='bottomRight'
-        render={
-          <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
-            <Dropdown.Item
-              onClick={() => {
-                navigate('/console/personal');
-              }}
-              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
-            >
-              <div className='flex items-center gap-2'>
-                <IconUserSetting
-                  size='small'
-                  className='text-gray-500 dark:text-gray-400'
-                />
-                <span>{t('个人设置')}</span>
-              </div>
-            </Dropdown.Item>
-            <Dropdown.Item
-              onClick={() => {
-                navigate('/console/token');
-              }}
-              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
-            >
-              <div className='flex items-center gap-2'>
-                <IconKey
-                  size='small'
-                  className='text-gray-500 dark:text-gray-400'
-                />
-                <span>{t('令牌管理')}</span>
-              </div>
-            </Dropdown.Item>
-            <Dropdown.Item
-              onClick={() => {
-                navigate('/console/topup');
-              }}
-              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
-            >
-              <div className='flex items-center gap-2'>
-                <IconCreditCard
-                  size='small'
-                  className='text-gray-500 dark:text-gray-400'
-                />
-                <span>{t('钱包管理')}</span>
-              </div>
-            </Dropdown.Item>
-            <Dropdown.Item
-              onClick={logout}
-              className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white'
-            >
-              <div className='flex items-center gap-2'>
-                <IconExit
-                  size='small'
-                  className='text-gray-500 dark:text-gray-400'
-                />
-                <span>{t('退出')}</span>
-              </div>
-            </Dropdown.Item>
-          </Dropdown.Menu>
-        }
-      >
-        <Button
-          theme='borderless'
-          type='tertiary'
-          className='flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
+      <div className='relative' ref={dropdownRef}>
+        <Dropdown
+          position='bottomRight'
+          getPopupContainer={() => dropdownRef.current}
+          render={
+            <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
+              <Dropdown.Item
+                onClick={() => {
+                  navigate('/console/personal');
+                }}
+                className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
+              >
+                <div className='flex items-center gap-2'>
+                  <IconUserSetting
+                    size='small'
+                    className='text-gray-500 dark:text-gray-400'
+                  />
+                  <span>{t('个人设置')}</span>
+                </div>
+              </Dropdown.Item>
+              <Dropdown.Item
+                onClick={() => {
+                  navigate('/console/token');
+                }}
+                className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
+              >
+                <div className='flex items-center gap-2'>
+                  <IconKey
+                    size='small'
+                    className='text-gray-500 dark:text-gray-400'
+                  />
+                  <span>{t('令牌管理')}</span>
+                </div>
+              </Dropdown.Item>
+              <Dropdown.Item
+                onClick={() => {
+                  navigate('/console/topup');
+                }}
+                className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
+              >
+                <div className='flex items-center gap-2'>
+                  <IconCreditCard
+                    size='small'
+                    className='text-gray-500 dark:text-gray-400'
+                  />
+                  <span>{t('钱包管理')}</span>
+                </div>
+              </Dropdown.Item>
+              <Dropdown.Item
+                onClick={logout}
+                className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white'
+              >
+                <div className='flex items-center gap-2'>
+                  <IconExit
+                    size='small'
+                    className='text-gray-500 dark:text-gray-400'
+                  />
+                  <span>{t('退出')}</span>
+                </div>
+              </Dropdown.Item>
+            </Dropdown.Menu>
+          }
         >
         >
-          <Avatar
-            size='extra-small'
-            color={stringToColor(userState.user.username)}
-            className='mr-1'
+          <Button
+            theme='borderless'
+            type='tertiary'
+            className='flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
           >
           >
-            {userState.user.username[0].toUpperCase()}
-          </Avatar>
-          <span className='hidden md:inline'>
-            <Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
-              {userState.user.username}
-            </Typography.Text>
-          </span>
-          <ChevronDown
-            size={14}
-            className='text-xs text-semi-color-text-2 dark:text-gray-400'
-          />
-        </Button>
-      </Dropdown>
+            <Avatar
+              size='extra-small'
+              color={stringToColor(userState.user.username)}
+              className='mr-1'
+            >
+              {userState.user.username[0].toUpperCase()}
+            </Avatar>
+            <span className='hidden md:inline'>
+              <Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
+                {userState.user.username}
+              </Typography.Text>
+            </span>
+            <ChevronDown
+              size={14}
+              className='text-xs text-semi-color-text-2 dark:text-gray-400'
+            />
+          </Button>
+        </Dropdown>
+      </div>
     );
     );
   } else {
   } else {
     const showRegisterButton = !isSelfUseMode;
     const showRegisterButton = !isSelfUseMode;

+ 8 - 1
web/src/components/settings/personal/cards/NotificationSettings.jsx

@@ -44,6 +44,7 @@ import CodeViewer from '../../../playground/CodeViewer';
 import { StatusContext } from '../../../../context/Status';
 import { StatusContext } from '../../../../context/Status';
 import { UserContext } from '../../../../context/User';
 import { UserContext } from '../../../../context/User';
 import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
 import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
+import { useSidebar } from '../../../../hooks/common/useSidebar';
 
 
 const NotificationSettings = ({
 const NotificationSettings = ({
   t,
   t,
@@ -97,6 +98,9 @@ const NotificationSettings = ({
     isSidebarModuleAllowed,
     isSidebarModuleAllowed,
   } = useUserPermissions();
   } = useUserPermissions();
 
 
+  // 使用useSidebar钩子获取刷新方法
+  const { refreshUserConfig } = useSidebar();
+
   // 左侧边栏设置处理函数
   // 左侧边栏设置处理函数
   const handleSectionChange = (sectionKey) => {
   const handleSectionChange = (sectionKey) => {
     return (checked) => {
     return (checked) => {
@@ -132,6 +136,9 @@ const NotificationSettings = ({
       });
       });
       if (res.data.success) {
       if (res.data.success) {
         showSuccess(t('侧边栏设置保存成功'));
         showSuccess(t('侧边栏设置保存成功'));
+
+        // 刷新useSidebar钩子中的用户配置,实现实时更新
+        await refreshUserConfig();
       } else {
       } else {
         showError(res.data.message);
         showError(res.data.message);
       }
       }
@@ -334,7 +341,7 @@ const NotificationSettings = ({
                 loading={sidebarLoading}
                 loading={sidebarLoading}
                 className='!rounded-lg'
                 className='!rounded-lg'
               >
               >
-                {t('保存边栏设置')}
+                {t('保存设置')}
               </Button>
               </Button>
             </>
             </>
           ) : (
           ) : (

+ 7 - 0
web/src/components/table/mj-logs/MjLogsFilters.jsx

@@ -21,6 +21,8 @@ import React from 'react';
 import { Button, Form } from '@douyinfe/semi-ui';
 import { Button, Form } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
 import { IconSearch } from '@douyinfe/semi-icons';
 
 
+import { DATE_RANGE_PRESETS } from '../../../constants/console.constants';
+
 const MjLogsFilters = ({
 const MjLogsFilters = ({
   formInitValues,
   formInitValues,
   setFormApi,
   setFormApi,
@@ -54,6 +56,11 @@ const MjLogsFilters = ({
               showClear
               showClear
               pure
               pure
               size='small'
               size='small'
+              presets={DATE_RANGE_PRESETS.map(preset => ({
+                text: t(preset.text),
+                start: preset.start(),
+                end: preset.end()
+              }))}
             />
             />
           </div>
           </div>
 
 

+ 7 - 0
web/src/components/table/task-logs/TaskLogsFilters.jsx

@@ -21,6 +21,8 @@ import React from 'react';
 import { Button, Form } from '@douyinfe/semi-ui';
 import { Button, Form } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
 import { IconSearch } from '@douyinfe/semi-icons';
 
 
+import { DATE_RANGE_PRESETS } from '../../../constants/console.constants';
+
 const TaskLogsFilters = ({
 const TaskLogsFilters = ({
   formInitValues,
   formInitValues,
   setFormApi,
   setFormApi,
@@ -54,6 +56,11 @@ const TaskLogsFilters = ({
               showClear
               showClear
               pure
               pure
               size='small'
               size='small'
+              presets={DATE_RANGE_PRESETS.map(preset => ({
+                text: t(preset.text),
+                start: preset.start(),
+                end: preset.end()
+              }))}
             />
             />
           </div>
           </div>
 
 

+ 7 - 0
web/src/components/table/usage-logs/UsageLogsFilters.jsx

@@ -21,6 +21,8 @@ import React from 'react';
 import { Button, Form } from '@douyinfe/semi-ui';
 import { Button, Form } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
 import { IconSearch } from '@douyinfe/semi-icons';
 
 
+import { DATE_RANGE_PRESETS } from '../../../constants/console.constants';
+
 const LogsFilters = ({
 const LogsFilters = ({
   formInitValues,
   formInitValues,
   setFormApi,
   setFormApi,
@@ -55,6 +57,11 @@ const LogsFilters = ({
               showClear
               showClear
               pure
               pure
               size='small'
               size='small'
+              presets={DATE_RANGE_PRESETS.map(preset => ({
+                text: t(preset.text),
+                start: preset.start(),
+                end: preset.end()
+              }))}
             />
             />
           </div>
           </div>
 
 

+ 49 - 0
web/src/constants/console.constants.js

@@ -0,0 +1,49 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import dayjs from 'dayjs';
+
+// ========== 日期预设常量 ==========
+export const DATE_RANGE_PRESETS = [
+  {
+    text: '今天',
+    start: () => dayjs().startOf('day').toDate(),
+    end: () => dayjs().endOf('day').toDate()
+  },
+  {
+    text: '近 7 天',
+    start: () => dayjs().subtract(6, 'day').startOf('day').toDate(),
+    end: () => dayjs().endOf('day').toDate()
+  },
+  {
+    text: '本周',
+    start: () => dayjs().startOf('week').toDate(),
+    end: () => dayjs().endOf('week').toDate()
+  },
+  {
+    text: '近 30 天',
+    start: () => dayjs().subtract(29, 'day').startOf('day').toDate(),
+    end: () => dayjs().endOf('day').toDate()
+  },
+  {
+    text: '本月',
+    start: () => dayjs().startOf('month').toDate(),
+    end: () => dayjs().endOf('month').toDate()
+  },
+];

+ 23 - 1
web/src/hooks/common/useSidebar.js

@@ -21,6 +21,10 @@ import { useState, useEffect, useMemo, useContext } from 'react';
 import { StatusContext } from '../../context/Status';
 import { StatusContext } from '../../context/Status';
 import { API } from '../../helpers';
 import { API } from '../../helpers';
 
 
+// 创建一个全局事件系统来同步所有useSidebar实例
+const sidebarEventTarget = new EventTarget();
+const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
+
 export const useSidebar = () => {
 export const useSidebar = () => {
   const [statusState] = useContext(StatusContext);
   const [statusState] = useContext(StatusContext);
   const [userConfig, setUserConfig] = useState(null);
   const [userConfig, setUserConfig] = useState(null);
@@ -124,9 +128,12 @@ export const useSidebar = () => {
 
 
   // 刷新用户配置的方法(供外部调用)
   // 刷新用户配置的方法(供外部调用)
   const refreshUserConfig = async () => {
   const refreshUserConfig = async () => {
-    if (Object.keys(adminConfig).length > 0) {
+     if (Object.keys(adminConfig).length > 0) {
       await loadUserConfig();
       await loadUserConfig();
     }
     }
+
+    // 触发全局刷新事件,通知所有useSidebar实例更新
+    sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
   };
   };
 
 
   // 加载用户配置
   // 加载用户配置
@@ -137,6 +144,21 @@ export const useSidebar = () => {
     }
     }
   }, [adminConfig]);
   }, [adminConfig]);
 
 
+  // 监听全局刷新事件
+  useEffect(() => {
+    const handleRefresh = () => {
+      if (Object.keys(adminConfig).length > 0) {
+        loadUserConfig();
+      }
+    };
+
+    sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
+
+    return () => {
+      sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
+    };
+  }, [adminConfig]);
+
   // 计算最终的显示配置
   // 计算最终的显示配置
   const finalConfig = useMemo(() => {
   const finalConfig = useMemo(() => {
     const result = {};
     const result = {};

+ 1 - 1
web/src/hooks/dashboard/useDashboardStats.jsx

@@ -102,7 +102,7 @@ export const useDashboardStats = (
           },
           },
           {
           {
             title: t('统计Tokens'),
             title: t('统计Tokens'),
-            value: isNaN(consumeTokens) ? 0 : consumeTokens,
+            value: isNaN(consumeTokens) ? 0 : consumeTokens.toLocaleString(),
             icon: <IconTextStroked />,
             icon: <IconTextStroked />,
             avatarColor: 'pink',
             avatarColor: 'pink',
             trendData: trendData.tokens,
             trendData: trendData.tokens,

+ 5 - 0
web/src/i18n/locales/en.json

@@ -2099,6 +2099,11 @@
   "优惠": "Discount",
   "优惠": "Discount",
   "折": "% off",
   "折": "% off",
   "节省": "Save",
   "节省": "Save",
+  "今天": "Today",
+  "近 7 天": "Last 7 Days",
+  "本周": "This Week",
+  "本月": "This Month",
+  "近 30 天": "Last 30 Days",
   "代理设置": "Proxy Settings",
   "代理设置": "Proxy Settings",
   "更新Worker设置": "Update Worker Settings",
   "更新Worker设置": "Update Worker Settings",
   "SSRF防护设置": "SSRF Protection Settings",
   "SSRF防护设置": "SSRF Protection Settings",

+ 6 - 0
web/vite.config.js

@@ -20,10 +20,16 @@ For commercial licensing, please contact support@quantumnous.com
 import react from '@vitejs/plugin-react';
 import react from '@vitejs/plugin-react';
 import { defineConfig, transformWithEsbuild } from 'vite';
 import { defineConfig, transformWithEsbuild } from 'vite';
 import pkg from '@douyinfe/vite-plugin-semi';
 import pkg from '@douyinfe/vite-plugin-semi';
+import path from 'path';
 const { vitePluginSemi } = pkg;
 const { vitePluginSemi } = pkg;
 
 
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, './src'),
+    },
+  },
   plugins: [
   plugins: [
     {
     {
       name: 'treat-js-files-as-jsx',
       name: 'treat-js-files-as-jsx',