Просмотр исходного кода

Merge branch 'main' into g-main

# Conflicts:
#	web/src/App.js
CalciumIon 1 год назад
Родитель
Сommit
ed948c121a

+ 9 - 10
README.md

@@ -1,6 +1,11 @@
+<div align="center">
 
 # New API
 
+<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
+
+</div>
+
 > [!NOTE]
 > 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
 
@@ -115,24 +120,18 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
 ## Suno接口设置文档
 [对接文档](Suno.md)
 
-## 交流群
-<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="300">
-
 ## 界面截图
 ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/ad0e7aae-0203-471c-9716-2d83768927d4)
 
-![image](https://github.com/Calcium-Ion/new-api/assets/61247483/d1ac216e-0804-4105-9fdc-66b35022d861)
-
-![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605)  
-![image](https://github.com/Calcium-Ion/new-api/assets/61247483/f4f40ed4-8ccb-43d7-a580-90677827646d)  
+![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605)
 ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/90d7d763-6a77-4b36-9f76-2bb30f18583d)
-![image](https://github.com/Calcium-Ion/new-api/assets/61247483/e414228a-3c35-429a-b298-6451d76d9032)
 夜间模式  
 ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/1c66b593-bb9e-4757-9720-ff2759539242)
-
-![image](https://github.com/Calcium-Ion/new-api/assets/61247483/5b3228e8-2556-44f7-97d6-4f8d8ee6effa)  
 ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/af9a07ee-5101-4b3d-8bd9-ae21a4fd7e9e)
 
+## 交流群
+<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="200">
+
 ## 相关项目
 - [One API](https://github.com/songquanpeng/one-api):原版项目
 - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持

+ 5 - 0
common/utils.go

@@ -128,6 +128,11 @@ func IntMax(a int, b int) int {
 	}
 }
 
+func IsIP(s string) bool {
+	ip := net.ParseIP(s)
+	return ip != nil
+}
+
 func GetUUID() string {
 	code := uuid.New().String()
 	code = strings.Replace(code, "-", "", -1)

+ 40 - 13
controller/model.go

@@ -146,22 +146,49 @@ func ListModels(c *gin.Context) {
 		})
 		return
 	}
-	models := model.GetGroupModels(user.Group)
 	userOpenAiModels := make([]dto.OpenAIModels, 0)
 	permission := getPermission()
-	for _, s := range models {
-		if _, ok := openAIModelsMap[s]; ok {
-			userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
+
+	modelLimitEnable := c.GetBool("token_model_limit_enabled")
+	if modelLimitEnable {
+		s, ok := c.Get("token_model_limit")
+		var tokenModelLimit map[string]bool
+		if ok {
+			tokenModelLimit = s.(map[string]bool)
 		} else {
-			userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
-				Id:         s,
-				Object:     "model",
-				Created:    1626777600,
-				OwnedBy:    "custom",
-				Permission: permission,
-				Root:       s,
-				Parent:     nil,
-			})
+			tokenModelLimit = map[string]bool{}
+		}
+		for allowModel, _ := range tokenModelLimit {
+			if _, ok := openAIModelsMap[allowModel]; ok {
+				userOpenAiModels = append(userOpenAiModels, openAIModelsMap[allowModel])
+			} else {
+				userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
+					Id:         allowModel,
+					Object:     "model",
+					Created:    1626777600,
+					OwnedBy:    "custom",
+					Permission: permission,
+					Root:       allowModel,
+					Parent:     nil,
+				})
+			}
+		}
+	} else {
+		models := model.GetGroupModels(user.Group)
+		for _, s := range models {
+			if _, ok := openAIModelsMap[s]; ok {
+				userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
+			} else {
+				userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
+					Id:         s,
+					Object:     "model",
+					Created:    1626777600,
+					OwnedBy:    "custom",
+					Permission: permission,
+					Root:       s,
+					Parent:     nil,
+				})
+			}
 		}
 	}
 	c.JSON(200, gin.H{

+ 2 - 0
controller/token.go

@@ -134,6 +134,7 @@ func AddToken(c *gin.Context) {
 		UnlimitedQuota:     token.UnlimitedQuota,
 		ModelLimitsEnabled: token.ModelLimitsEnabled,
 		ModelLimits:        token.ModelLimits,
+		AllowIps:           token.AllowIps,
 	}
 	err = cleanToken.Insert()
 	if err != nil {
@@ -221,6 +222,7 @@ func UpdateToken(c *gin.Context) {
 		cleanToken.UnlimitedQuota = token.UnlimitedQuota
 		cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled
 		cleanToken.ModelLimits = token.ModelLimits
+		cleanToken.AllowIps = token.AllowIps
 	}
 	err = cleanToken.Update()
 	if err != nil {

+ 1 - 0
middleware/auth.go

@@ -175,6 +175,7 @@ func TokenAuth() func(c *gin.Context) {
 		} else {
 			c.Set("token_model_limit_enabled", false)
 		}
+		c.Set("allow_ips", token.GetIpLimitsMap())
 		if len(parts) > 1 {
 			if model.IsAdmin(token.UserId) {
 				c.Set("specific_channel_id", parts[1])

+ 8 - 0
middleware/distributor.go

@@ -22,6 +22,14 @@ type ModelRequest struct {
 
 func Distribute() func(c *gin.Context) {
 	return func(c *gin.Context) {
+		allowIpsMap := c.GetStringMap("allow_ips")
+		if len(allowIpsMap) != 0 {
+			clientIp := c.ClientIP()
+			if _, ok := allowIpsMap[clientIp]; !ok {
+				abortWithOpenAiMessage(c, http.StatusForbidden, "您的 IP 不在令牌允许访问的列表中")
+				return
+			}
+		}
 		userId := c.GetInt("id")
 		var channel *model.Channel
 		channelId, ok := c.Get("specific_channel_id")

+ 24 - 1
model/token.go

@@ -23,10 +23,33 @@ type Token struct {
 	UnlimitedQuota     bool           `json:"unlimited_quota" gorm:"default:false"`
 	ModelLimitsEnabled bool           `json:"model_limits_enabled" gorm:"default:false"`
 	ModelLimits        string         `json:"model_limits" gorm:"type:varchar(1024);default:''"`
+	AllowIps           *string        `json:"allow_ips" gorm:"default:''"`
 	UsedQuota          int            `json:"used_quota" gorm:"default:0"` // used quota
 	DeletedAt          gorm.DeletedAt `gorm:"index"`
 }
 
+func (token *Token) GetIpLimitsMap() map[string]any {
+	// delete empty spaces
+	//split with \n
+	ipLimitsMap := make(map[string]any)
+	if token.AllowIps == nil {
+		return ipLimitsMap
+	}
+	cleanIps := strings.ReplaceAll(*token.AllowIps, " ", "")
+	if cleanIps == "" {
+		return ipLimitsMap
+	}
+	ips := strings.Split(cleanIps, "\n")
+	for _, ip := range ips {
+		ip = strings.TrimSpace(ip)
+		ip = strings.ReplaceAll(ip, ",", "")
+		if common.IsIP(ip) {
+			ipLimitsMap[ip] = true
+		}
+	}
+	return ipLimitsMap
+}
+
 func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
 	var tokens []*Token
 	var err error
@@ -130,7 +153,7 @@ func (token *Token) Insert() error {
 // Update Make sure your token's fields is completed, because this will update non-zero values
 func (token *Token) Update() error {
 	var err error
-	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "model_limits_enabled", "model_limits").Updates(token).Error
+	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "model_limits_enabled", "model_limits", "allow_ips").Updates(token).Error
 	return err
 }
 

+ 189 - 192
web/src/App.js

@@ -20,12 +20,11 @@ import Redemption from './pages/Redemption';
 import TopUp from './pages/TopUp';
 import Log from './pages/Log';
 import Chat from './pages/Chat';
-import Chat2Link from './pages/Chat2Link'; 
+import Chat2Link from './pages/Chat2Link';
 import { Layout } from '@douyinfe/semi-ui';
 import Midjourney from './pages/Midjourney';
 import Pricing from './pages/Pricing/index.js';
 import Task from "./pages/Task/index.js";
-// import Detail from './pages/Detail';
 
 const Home = lazy(() => import('./pages/Home'));
 const Detail = lazy(() => import('./pages/Detail'));
@@ -59,204 +58,203 @@ function App() {
   }, []);
 
   return (
-    <Layout>
-      <Layout.Content>
-        <Routes>
-          <Route
-            path='/'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <Home />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/channel'
-            element={
-              <PrivateRoute>
-                <Channel />
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/channel/edit/:id'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <EditChannel />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/channel/add'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <EditChannel />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/token'
-            element={
-              <PrivateRoute>
-                <Token />
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/redemption'
-            element={
-              <PrivateRoute>
-                <Redemption />
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/user'
-            element={
-              <PrivateRoute>
-                <User />
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/user/edit/:id'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <EditUser />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/user/edit'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <EditUser />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/user/reset'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <PasswordResetConfirm />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/login'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <LoginForm />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/register'
-            element={
+    <>
+      <Routes>
+        <Route
+          path='/'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <Home />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/channel'
+          element={
+            <PrivateRoute>
+              <Channel />
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/channel/edit/:id'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <EditChannel />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/channel/add'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <EditChannel />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/token'
+          element={
+            <PrivateRoute>
+              <Token />
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/redemption'
+          element={
+            <PrivateRoute>
+              <Redemption />
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/user'
+          element={
+            <PrivateRoute>
+              <User />
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/user/edit/:id'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <EditUser />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/user/edit'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <EditUser />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/user/reset'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <PasswordResetConfirm />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/login'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <LoginForm />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/register'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <RegisterForm />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/reset'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <PasswordResetForm />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/oauth/github'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <GitHubOAuth />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/setting'
+          element={
+            <PrivateRoute>
               <Suspense fallback={<Loading></Loading>}>
-                <RegisterForm />
+                <Setting />
               </Suspense>
-            }
-          />
-          <Route
-            path='/reset'
-            element={
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/topup'
+          element={
+            <PrivateRoute>
               <Suspense fallback={<Loading></Loading>}>
-                <PasswordResetForm />
+                <TopUp />
               </Suspense>
-            }
-          />
-          <Route
-            path='/oauth/github'
-            element={
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/log'
+          element={
+            <PrivateRoute>
+              <Log />
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/detail'
+          element={
+            <PrivateRoute>
               <Suspense fallback={<Loading></Loading>}>
-                <GitHubOAuth />
+                <Detail />
               </Suspense>
-            }
-          />
-          <Route
-            path='/setting'
-            element={
-              <PrivateRoute>
-                <Suspense fallback={<Loading></Loading>}>
-                  <Setting />
-                </Suspense>
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/topup'
-            element={
-              <PrivateRoute>
-                <Suspense fallback={<Loading></Loading>}>
-                  <TopUp />
-                </Suspense>
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/log'
-            element={
-              <PrivateRoute>
-                <Log />
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/detail'
-            element={
-              <PrivateRoute>
-                <Suspense fallback={<Loading></Loading>}>
-                  <Detail />
-                </Suspense>
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/midjourney'
-            element={
-              <PrivateRoute>
-                <Suspense fallback={<Loading></Loading>}>
-                  <Midjourney />
-                </Suspense>
-              </PrivateRoute>
-            }
-          />
-          <Route
-            path='/task'
-            element={
-                <PrivateRoute>
-                    <Suspense fallback={<Loading></Loading>}>
-                        <Task />
-                    </Suspense>
-                </PrivateRoute>
-            }
-          />
-          <Route
-            path='/pricing'
-            element={
-              <Suspense fallback={<Loading></Loading>}>
-                <Pricing />
-              </Suspense>
-            }
-          />
-          <Route
-            path='/about'
-            element={
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/midjourney'
+          element={
+            <PrivateRoute>
               <Suspense fallback={<Loading></Loading>}>
-                <About />
+                <Midjourney />
               </Suspense>
-            }
-          />
-          <Route
-            path='/chat'
-            element={
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/task'
+          element={
+            <PrivateRoute>
               <Suspense fallback={<Loading></Loading>}>
-                <Chat />
+                <Task />
               </Suspense>
-            }
-          />
-          {/* 方便使用chat2link直接跳转聊天... */}
+            </PrivateRoute>
+          }
+        />
+        <Route
+          path='/pricing'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <Pricing />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/about'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <About />
+            </Suspense>
+          }
+        />
+        <Route
+          path='/chat'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <Chat />
+            </Suspense>
+          }
+        />
+        {/* 方便使用chat2link直接跳转聊天... */}
           <Route
             path='/chat2link'
             element={
@@ -269,8 +267,7 @@ function App() {
           />
           <Route path='*' element={<NotFound />} />
         </Routes>
-      </Layout.Content>
-    </Layout>
+      </>
   );
 }
 

+ 12 - 14
web/src/components/Footer.js

@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
 import { getFooterHTML, getSystemName } from '../helpers';
 import { Layout, Tooltip } from '@douyinfe/semi-ui';
 
-const Footer = () => {
+const FooterBar = () => {
   const systemName = getSystemName();
   const [footer, setFooter] = useState(getFooterHTML());
   let remainCheckTimes = 5;
@@ -56,19 +56,17 @@ const Footer = () => {
   }, []);
 
   return (
-    <Layout>
-      <Layout.Content style={{ textAlign: 'center' }}>
-        {footer ? (
-          <div
-            className='custom-footer'
-            dangerouslySetInnerHTML={{ __html: footer }}
-          ></div>
-        ) : (
-          defaultFooter
-        )}
-      </Layout.Content>
-    </Layout>
+    <div style={{ textAlign: 'center' }}>
+      {footer ? (
+        <div
+          className='custom-footer'
+          dangerouslySetInnerHTML={{ __html: footer }}
+        ></div>
+      ) : (
+        defaultFooter
+      )}
+    </div>
   );
 };
 
-export default Footer;
+export default FooterBar;

+ 53 - 12
web/src/components/HeaderBar.js

@@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom';
 import { UserContext } from '../context/User';
 import { useSetTheme, useTheme } from '../context/Theme';
 
-import { API, getLogo, getSystemName, showSuccess } from '../helpers';
+import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
 import '../index.css';
 
 import fireworks from 'react-fireworks';
 
-import { IconHelpCircle, IconKey, IconUser } from '@douyinfe/semi-icons';
+import {
+  IconHelpCircle,
+  IconHome,
+  IconHomeStroked,
+  IconKey,
+  IconNoteMoneyStroked,
+  IconPriceTag,
+  IconUser
+} from '@douyinfe/semi-icons';
 import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
 import { stringToColor } from '../helpers/render';
+import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 
 // HeaderBar Buttons
 let headerButtons = [
@@ -22,6 +31,21 @@ let headerButtons = [
   },
 ];
 
+let buttons = [
+  {
+    text: '首页',
+    itemKey: 'home',
+    to: '/',
+    icon: <IconHomeStroked />,
+  },
+  // {
+  //   text: '模型价格',
+  //   itemKey: 'pricing',
+  //   to: '/pricing',
+  //   icon: <IconNoteMoneyStroked />,
+  // },
+];
+
 if (localStorage.getItem('chat_link')) {
   headerButtons.splice(1, 0, {
     name: '聊天',
@@ -90,6 +114,7 @@ const HeaderBar = () => {
                 about: '/about',
                 login: '/login',
                 register: '/register',
+                home: '/',
               };
               return (
                 <Link
@@ -103,6 +128,18 @@ const HeaderBar = () => {
             selectedKeys={[]}
             // items={headerButtons}
             onSelect={(key) => {}}
+            header={isMobile()?{
+              logo: (
+                <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
+              ),
+            }:{
+              logo: (
+                <img src={logo} alt='logo' />
+              ),
+              text: systemName,
+
+            }}
+            items={buttons}
             footer={
               <>
                 {isNewYear && (
@@ -121,15 +158,19 @@ const HeaderBar = () => {
                   </Dropdown>
                 )}
                 <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
-                <Switch
-                  checkedText='🌞'
-                  size={'large'}
-                  checked={theme === 'dark'}
-                  uncheckedText='🌙'
-                  onChange={(checked) => {
-                    setTheme(checked);
-                  }}
-                />
+                <>
+                {!isMobile() && (
+                    <Switch
+                      checkedText='🌞'
+                      size={'large'}
+                      checked={theme === 'dark'}
+                      uncheckedText='🌙'
+                      onChange={(checked) => {
+                        setTheme(checked);
+                      }}
+                    />
+                  )}
+                </>
                 {userState.user ? (
                   <>
                     <Dropdown
@@ -155,7 +196,7 @@ const HeaderBar = () => {
                     <Nav.Item
                       itemKey={'login'}
                       text={'登录'}
-                      icon={<IconKey />}
+                      // icon={<IconKey />}
                     />
                     <Nav.Item
                       itemKey={'register'}

+ 69 - 55
web/src/components/SiderBar.js

@@ -17,7 +17,7 @@ import {
   IconCalendarClock, IconChecklistStroked,
   IconComment,
   IconCreditCard,
-  IconGift,
+  IconGift, IconHelpCircle,
   IconHistogram,
   IconHome,
   IconImage,
@@ -25,10 +25,12 @@ import {
   IconLayers,
   IconPriceTag,
   IconSetting,
-  IconUser,
+  IconUser
 } from '@douyinfe/semi-icons';
-import { Layout, Nav } from '@douyinfe/semi-ui';
+import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
 import { setStatusData } from '../helpers/data.js';
+import { stringToColor } from '../helpers/render.js';
+import { useSetTheme, useTheme } from '../context/Theme/index.js';
 
 // HeaderBar Buttons
 
@@ -43,6 +45,8 @@ const SiderBar = () => {
   const systemName = getSystemName();
   const logo = getLogo();
   const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
+  const theme = useTheme();
+  const setTheme = useSetTheme();
 
   const routerMap = {
     home: '/',
@@ -63,11 +67,17 @@ const SiderBar = () => {
 
   const headerButtons = useMemo(
     () => [
+      // {
+      //   text: '首页',
+      //   itemKey: 'home',
+      //   to: '/',
+      //   icon: <IconHome />,
+      // },
       {
-        text: '首页',
-        itemKey: 'home',
-        to: '/',
-        icon: <IconHome />,
+        text: '模型价格',
+        itemKey: 'pricing',
+        to: '/pricing',
+        icon: <IconPriceTag />,
       },
       {
         text: '渠道',
@@ -104,12 +114,6 @@ const SiderBar = () => {
         to: '/topup',
         icon: <IconCreditCard />,
       },
-      {
-        text: '模型价格',
-        itemKey: 'pricing',
-        to: '/pricing',
-        icon: <IconPriceTag />,
-      },
       {
         text: '用户管理',
         itemKey: 'user',
@@ -205,48 +209,58 @@ const SiderBar = () => {
 
   return (
     <>
-      <Layout>
-        <div style={{ height: '100%' }}>
-          <Nav
-            // bodyStyle={{ maxWidth: 200 }}
-            style={{ maxWidth: 200 }}
-            defaultIsCollapsed={
-              isMobile() ||
-              localStorage.getItem('default_collapse_sidebar') === 'true'
-            }
-            isCollapsed={isCollapsed}
-            onCollapseChange={(collapsed) => {
-              setIsCollapsed(collapsed);
-            }}
-            selectedKeys={selectedKeys}
-            renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
-              return (
-                <Link
-                  style={{ textDecoration: 'none' }}
-                  to={routerMap[props.itemKey]}
-                >
-                  {itemElement}
-                </Link>
-              );
-            }}
-            items={headerButtons}
-            onSelect={(key) => {
-              setSelectedKeys([key.itemKey]);
-            }}
-            header={{
-              logo: (
-                <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
-              ),
-              text: systemName,
-            }}
-            // footer={{
-            //   text: '© 2021 NekoAPI',
-            // }}
-          >
-            <Nav.Footer collapseButton={true}></Nav.Footer>
-          </Nav>
-        </div>
-      </Layout>
+      <Nav
+        style={{ maxWidth: 220, height: '100%' }}
+        defaultIsCollapsed={
+          isMobile() ||
+          localStorage.getItem('default_collapse_sidebar') === 'true'
+        }
+        isCollapsed={isCollapsed}
+        onCollapseChange={(collapsed) => {
+          setIsCollapsed(collapsed);
+        }}
+        selectedKeys={selectedKeys}
+        renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
+          return (
+            <Link
+              style={{ textDecoration: 'none' }}
+              to={routerMap[props.itemKey]}
+            >
+              {itemElement}
+            </Link>
+          );
+        }}
+        items={headerButtons}
+        onSelect={(key) => {
+          setSelectedKeys([key.itemKey]);
+        }}
+        // header={{
+        //   logo: (
+        //     <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
+        //   ),
+        //   text: systemName,
+        // }}
+        // footer={{
+        //   text: '© 2021 NekoAPI',
+        // }}
+        footer={
+          <>
+            {isMobile() && (
+              <Switch
+                checkedText='🌞'
+                size={'small'}
+                checked={theme === 'dark'}
+                uncheckedText='🌙'
+                onChange={(checked) => {
+                  setTheme(checked);
+                }}
+              />
+            )}
+          </>
+        }
+      >
+        <Nav.Footer collapseButton={true}></Nav.Footer>
+      </Nav>
     </>
   );
 };

+ 6 - 5
web/src/index.css

@@ -9,11 +9,12 @@ body {
   scrollbar-width: none;
   color: var(--semi-color-text-0) !important;
   background-color: var(--semi-color-bg-0) !important;
-  height: 100%;
+  height: 100vh;
 }
 
 #root {
-  height: 100%;
+  height: 100vh;
+  flex-direction: column;
 }
 
 @media only screen and (max-width: 767px) {
@@ -50,9 +51,9 @@ body {
   }
 }
 
-.semi-layout {
-  height: 100%;
-}
+/*.semi-layout {*/
+/*  height: 100%;*/
+/*}*/
 
 .tableShow {
   display: revert;

+ 20 - 20
web/src/index.js

@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
 import { BrowserRouter } from 'react-router-dom';
 import App from './App';
 import HeaderBar from './components/HeaderBar';
-import Footer from './components/Footer';
 import 'semantic-ui-offline/semantic.min.css';
 import './index.css';
 import { UserProvider } from './context/User';
@@ -13,35 +12,36 @@ import { StatusProvider } from './context/Status';
 import { Layout } from '@douyinfe/semi-ui';
 import SiderBar from './components/SiderBar';
 import { ThemeProvider } from './context/Theme';
+import FooterBar from './components/Footer';
 
 // initialization
 
 const root = ReactDOM.createRoot(document.getElementById('root'));
-const { Sider, Content, Header } = Layout;
+const { Sider, Content, Header, Footer } = Layout;
 root.render(
   <React.StrictMode>
     <StatusProvider>
       <UserProvider>
         <BrowserRouter>
           <ThemeProvider>
-            <Layout>
-              <Sider>
-                <SiderBar />
-              </Sider>
-              <Layout>
-                <Header>
-                  <HeaderBar />
-                </Header>
-                <Content
-                  style={{
-                    padding: '24px',
-                  }}
-                >
-                  <App />
-                </Content>
-                <Layout.Footer>
-                  <Footer></Footer>
-                </Layout.Footer>
+            <Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
+              <Header>
+                <HeaderBar />
+              </Header>
+              <Layout style={{ flex: 1, overflow: 'hidden' }}>
+                <Sider>
+                  <SiderBar />
+                </Sider>
+                <Layout>
+                  <Content
+                    style={{ overflowY: 'auto', padding: '24px' }}
+                  >
+                    <App />
+                  </Content>
+                  <Layout.Footer>
+                    <FooterBar></FooterBar>
+                  </Layout.Footer>
+                </Layout>
               </Layout>
               <ToastContainer />
             </Layout>

+ 17 - 2
web/src/pages/Token/EditToken.js

@@ -18,8 +18,8 @@ import {
   Select,
   SideSheet,
   Space,
-  Spin,
-  Typography,
+  Spin, TextArea,
+  Typography
 } from '@douyinfe/semi-ui';
 import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 import { Divider } from 'semantic-ui-react';
@@ -34,6 +34,7 @@ const EditToken = (props) => {
     unlimited_quota: false,
     model_limits_enabled: false,
     model_limits: [],
+    allow_ips: '',
   };
   const [inputs, setInputs] = useState(originInputs);
   const {
@@ -43,6 +44,7 @@ const EditToken = (props) => {
     unlimited_quota,
     model_limits_enabled,
     model_limits,
+    allow_ips
   } = inputs;
   // const [visible, setVisible] = useState(false);
   const [models, setModels] = useState({});
@@ -374,6 +376,19 @@ const EditToken = (props) => {
             </Button>
           </div>
           <Divider />
+          <div style={{ marginTop: 10 }}>
+            <Typography.Text>IP白名单(请勿过度信任此功能)</Typography.Text>
+          </div>
+          <TextArea
+            label='IP白名单'
+            name='allow_ips'
+            placeholder={'允许的IP,一行一个'}
+            onChange={(value) => {
+              handleInputChange('allow_ips', value);
+            }}
+            value={inputs.allow_ips}
+            style={{ fontFamily: 'JetBrains Mono, Consolas' }}
+          />
           <div style={{ marginTop: 10, display: 'flex' }}>
             <Space>
               <Checkbox