Explorar o código

feat: able to query logs now (close #144)

JustSong %!s(int64=2) %!d(string=hai) anos
pai
achega
cccf5e4a07
Modificáronse 8 ficheiros con 390 adicións e 144 borrados
  1. 47 2
      controller/log.go
  2. 7 4
      controller/relay-text.go
  3. 16 2
      i18n/en.json
  4. 102 7
      model/log.go
  5. 5 0
      model/user.go
  6. 2 0
      router/api-router.go
  7. 210 125
      web/src/components/LogsTable.js
  8. 1 4
      web/src/pages/Log/index.js

+ 47 - 2
controller/log.go

@@ -13,7 +13,11 @@ func GetAllLogs(c *gin.Context) {
 		p = 0
 	}
 	logType, _ := strconv.Atoi(c.Query("type"))
-	logs, err := model.GetAllLogs(logType, p*common.ItemsPerPage, common.ItemsPerPage)
+	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
+	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
+	username := c.Query("username")
+	modelName := c.Query("model_name")
+	logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, p*common.ItemsPerPage, common.ItemsPerPage)
 	if err != nil {
 		c.JSON(200, gin.H{
 			"success": false,
@@ -35,7 +39,11 @@ func GetUserLogs(c *gin.Context) {
 	}
 	userId := c.GetInt("id")
 	logType, _ := strconv.Atoi(c.Query("type"))
-	logs, err := model.GetUserLogs(userId, logType, p*common.ItemsPerPage, common.ItemsPerPage)
+	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
+	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
+	tokenName := c.Query("token_name")
+	modelName := c.Query("model_name")
+	logs, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*common.ItemsPerPage, common.ItemsPerPage)
 	if err != nil {
 		c.JSON(200, gin.H{
 			"success": false,
@@ -84,3 +92,40 @@ func SearchUserLogs(c *gin.Context) {
 		"data":    logs,
 	})
 }
+
+func GetLogsStat(c *gin.Context) {
+	logType, _ := strconv.Atoi(c.Query("type"))
+	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
+	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
+	username := c.Query("username")
+	modelName := c.Query("model_name")
+	quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, "")
+	//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "")
+	c.JSON(200, gin.H{
+		"success": true,
+		"message": "",
+		"data": gin.H{
+			"quota": quotaNum,
+			//"token": tokenNum,
+		},
+	})
+}
+
+func GetLogsSelfStat(c *gin.Context) {
+	username := c.GetString("username")
+	logType, _ := strconv.Atoi(c.Query("type"))
+	startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)
+	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
+	tokenName := c.Query("token_name")
+	modelName := c.Query("model_name")
+	quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
+	//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
+	c.JSON(200, gin.H{
+		"success": true,
+		"message": "",
+		"data": gin.H{
+			"quota": quotaNum,
+			//"token": tokenNum,
+		},
+	})
+}

+ 7 - 4
controller/relay-text.go

@@ -58,6 +58,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 		return err
 	}
 	var promptTokens int
+	var completionTokens int
 	switch relayMode {
 	case RelayModeChatCompletions:
 		promptTokens = countTokenMessages(textRequest.Messages, textRequest.Model)
@@ -128,11 +129,12 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 				completionRatio = 2
 			}
 			if isStream {
-				responseTokens := countTokenText(streamResponseText, textRequest.Model)
-				quota = promptTokens + int(float64(responseTokens)*completionRatio)
+				completionTokens = countTokenText(streamResponseText, textRequest.Model)
 			} else {
-				quota = textResponse.Usage.PromptTokens + int(float64(textResponse.Usage.CompletionTokens)*completionRatio)
+				promptTokens = textResponse.Usage.PromptTokens
+				completionTokens = textResponse.Usage.CompletionTokens
 			}
+			quota = promptTokens + int(float64(completionTokens)*completionRatio)
 			quota = int(float64(quota) * ratio)
 			if ratio != 0 && quota <= 0 {
 				quota = 1
@@ -143,7 +145,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 				common.SysError("error consuming token remain quota: " + err.Error())
 			}
 			tokenName := c.GetString("token_name")
-			model.RecordLog(userId, model.LogTypeConsume, fmt.Sprintf("通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)", tokenName, textRequest.Model, common.LogQuota(quota), modelRatio, groupRatio))
+			logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
+			model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent)
 			model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
 			channelId := c.GetInt("channel_id")
 			model.UpdateChannelUsedQuota(channelId, quota)

+ 16 - 2
i18n/en.json

@@ -441,5 +441,19 @@
   "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly",
   "已绑定的微信账户": "WeChat Account Bound",
   "已绑定的邮箱账户": "Email Account Bound",
-  "用户信息更新成功!": "User information updated successfully!"
-}
+  "用户信息更新成功!": "User information updated successfully!",
+  "模型倍率 %.2f,分组倍率 %.2f": "model rate %.2f, group rate %.2f",
+  "使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
+  "用户名称": "User Name",
+  "令牌名称": "Token Name",
+  "留空则查询全部用户": "Leave blank to query all users",
+  "留空则查询全部令牌": "Leave blank to query all tokens",
+  "模型名称": "Model Name",
+  "留空则查询全部模型": "Leave blank to query all models",
+  "起始时间": "Start Time",
+  "结束时间": "End Time",
+  "查询": "Query",
+  "提示令牌": "Prompt Token",
+  "补全令牌": "Completion Token",
+  "消耗额度": "Used Quota"
+}

+ 102 - 7
model/log.go

@@ -6,11 +6,17 @@ import (
 )
 
 type Log struct {
-	Id        int    `json:"id"`
-	UserId    int    `json:"user_id" gorm:"index"`
-	CreatedAt int64  `json:"created_at" gorm:"bigint"`
-	Type      int    `json:"type" gorm:"index"`
-	Content   string `json:"content"`
+	Id               int    `json:"id"`
+	UserId           int    `json:"user_id"`
+	CreatedAt        int64  `json:"created_at" gorm:"bigint;index"`
+	Type             int    `json:"type" gorm:"index"`
+	Content          string `json:"content"`
+	Username         string `json:"username" gorm:"index;default:''"`
+	TokenName        string `json:"token_name" gorm:"index;default:''"`
+	ModelName        string `json:"model_name" gorm:"index;default:''"`
+	Quota            int    `json:"quota" gorm:"default:0"`
+	PromptTokens     int    `json:"prompt_tokens" gorm:"default:0"`
+	CompletionTokens int    `json:"completion_tokens" gorm:"default:0"`
 }
 
 const (
@@ -27,6 +33,7 @@ func RecordLog(userId int, logType int, content string) {
 	}
 	log := &Log{
 		UserId:    userId,
+		Username:  GetUsernameById(userId),
 		CreatedAt: common.GetTimestamp(),
 		Type:      logType,
 		Content:   content,
@@ -37,24 +44,70 @@ func RecordLog(userId int, logType int, content string) {
 	}
 }
 
-func GetAllLogs(logType int, startIdx int, num int) (logs []*Log, err error) {
+func RecordConsumeLog(userId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string) {
+	if !common.LogConsumeEnabled {
+		return
+	}
+	log := &Log{
+		UserId:           userId,
+		Username:         GetUsernameById(userId),
+		CreatedAt:        common.GetTimestamp(),
+		Type:             LogTypeConsume,
+		Content:          content,
+		PromptTokens:     promptTokens,
+		CompletionTokens: completionTokens,
+		TokenName:        tokenName,
+		ModelName:        modelName,
+		Quota:            quota,
+	}
+	err := DB.Create(log).Error
+	if err != nil {
+		common.SysError("failed to record log: " + err.Error())
+	}
+}
+
+func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, startIdx int, num int) (logs []*Log, err error) {
 	var tx *gorm.DB
 	if logType == LogTypeUnknown {
 		tx = DB
 	} else {
 		tx = DB.Where("type = ?", logType)
 	}
+	if modelName != "" {
+		tx = tx.Where("model_name = ?", modelName)
+	}
+	if username != "" {
+		tx = tx.Where("username = ?", username)
+	}
+	if startTimestamp != 0 {
+		tx = tx.Where("created_at >= ?", startTimestamp)
+	}
+	if endTimestamp != 0 {
+		tx = tx.Where("created_at <= ?", endTimestamp)
+	}
 	err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
 	return logs, err
 }
 
-func GetUserLogs(userId int, logType int, startIdx int, num int) (logs []*Log, err error) {
+func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, err error) {
 	var tx *gorm.DB
 	if logType == LogTypeUnknown {
 		tx = DB.Where("user_id = ?", userId)
 	} else {
 		tx = DB.Where("user_id = ? and type = ?", userId, logType)
 	}
+	if modelName != "" {
+		tx = tx.Where("model_name = ?", modelName)
+	}
+	if tokenName != "" {
+		tx = tx.Where("token_name = ?", tokenName)
+	}
+	if startTimestamp != 0 {
+		tx = tx.Where("created_at >= ?", startTimestamp)
+	}
+	if endTimestamp != 0 {
+		tx = tx.Where("created_at <= ?", endTimestamp)
+	}
 	err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error
 	return logs, err
 }
@@ -68,3 +121,45 @@ func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) {
 	err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Omit("id").Find(&logs).Error
 	return logs, err
 }
+
+func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (quota int) {
+	tx := DB.Table("logs").Select("sum(quota)")
+	if username != "" {
+		tx = tx.Where("username = ?", username)
+	}
+	if tokenName != "" {
+		tx = tx.Where("token_name = ?", tokenName)
+	}
+	if startTimestamp != 0 {
+		tx = tx.Where("created_at >= ?", startTimestamp)
+	}
+	if endTimestamp != 0 {
+		tx = tx.Where("created_at <= ?", endTimestamp)
+	}
+	if modelName != "" {
+		tx = tx.Where("model_name = ?", modelName)
+	}
+	tx.Where("type = ?", LogTypeConsume).Scan(&quota)
+	return quota
+}
+
+func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (token int) {
+	tx := DB.Table("logs").Select("sum(prompt_tokens) + sum(completion_tokens)")
+	if username != "" {
+		tx = tx.Where("username = ?", username)
+	}
+	if tokenName != "" {
+		tx = tx.Where("token_name = ?", tokenName)
+	}
+	if startTimestamp != 0 {
+		tx = tx.Where("created_at >= ?", startTimestamp)
+	}
+	if endTimestamp != 0 {
+		tx = tx.Where("created_at <= ?", endTimestamp)
+	}
+	if modelName != "" {
+		tx = tx.Where("model_name = ?", modelName)
+	}
+	tx.Where("type = ?", LogTypeConsume).Scan(&token)
+	return token
+}

+ 5 - 0
model/user.go

@@ -303,3 +303,8 @@ func UpdateUserUsedQuotaAndRequestCount(id int, quota int) {
 		common.SysError("failed to update user used quota and request count: " + err.Error())
 	}
 }
+
+func GetUsernameById(id int) (username string) {
+	DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username)
+	return username
+}

+ 2 - 0
router/api-router.go

@@ -96,6 +96,8 @@ func SetApiRouter(router *gin.Engine) {
 		}
 		logRoute := apiRouter.Group("/log")
 		logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs)
+		logRoute.GET("/stat", middleware.AdminAuth(), controller.GetLogsStat)
+		logRoute.GET("/self/stat", middleware.UserAuth(), controller.GetLogsSelfStat)
 		logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs)
 		logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs)
 		logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs)

+ 210 - 125
web/src/components/LogsTable.js

@@ -1,8 +1,9 @@
 import React, { useEffect, useState } from 'react';
-import { Button, Label, Pagination, Select, Table } from 'semantic-ui-react';
+import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
 import { API, isAdmin, showError, timestamp2string } from '../helpers';
 
 import { ITEMS_PER_PAGE } from '../constants';
+import { renderQuota } from '../helpers/render';
 
 function renderTimestamp(timestamp) {
   return (
@@ -14,7 +15,7 @@ function renderTimestamp(timestamp) {
 
 const MODE_OPTIONS = [
   { key: 'all', text: '全部用户', value: 'all' },
-  { key: 'self', text: '当前用户', value: 'self' },
+  { key: 'self', text: '当前用户', value: 'self' }
 ];
 
 const LOG_OPTIONS = [
@@ -47,13 +48,57 @@ const LogsTable = () => {
   const [searchKeyword, setSearchKeyword] = useState('');
   const [searching, setSearching] = useState(false);
   const [logType, setLogType] = useState(0);
-  const [mode, setMode] = useState('self'); // all, self
-  const showModePanel = isAdmin();
+  const isAdminUser = isAdmin();
+  let now = new Date();
+  const [inputs, setInputs] = useState({
+    name: '',
+    model_name: '',
+    start_timestamp: timestamp2string(0),
+    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
+  });
+  const { name, model_name, start_timestamp, end_timestamp } = inputs;
+
+  const [stat, setStat] = useState({
+    quota: 0,
+    token: 0
+  });
+
+  const handleInputChange = (e, { name, value }) => {
+    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  };
+
+  const getLogSelfStat = async () => {
+    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
+    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
+    let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
+    const { success, message, data } = res.data;
+    if (success) {
+      setStat(data);
+    } else {
+      showError(message);
+    }
+  };
+
+  const getLogStat = async () => {
+    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
+    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
+    let res = await API.get(`/api/log/stat?type=${logType}&username=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
+    const { success, message, data } = res.data;
+    if (success) {
+      setStat(data);
+    } else {
+      showError(message);
+    }
+  };
 
   const loadLogs = async (startIdx) => {
-    let url = `/api/log/self/?p=${startIdx}&type=${logType}`;
-    if (mode === 'all') {
-      url = `/api/log/?p=${startIdx}&type=${logType}`;
+    let url = '';
+    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
+    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
+    if (isAdminUser) {
+      url = `/api/log/?p=${startIdx}&type=${logType}&username=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
+    } else {
+      url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
     }
     const res = await API.get(url);
     const { success, message, data } = res.data;
@@ -84,19 +129,16 @@ const LogsTable = () => {
   const refresh = async () => {
     setLoading(true);
     await loadLogs(0);
+    if (isAdminUser) {
+      getLogStat().then();
+    } else {
+      getLogSelfStat().then();
+    }
   };
 
-  useEffect(() => {
-    loadLogs(0)
-      .then()
-      .catch((reason) => {
-        showError(reason);
-      });
-  }, []);
-
   useEffect(() => {
     refresh().then();
-  }, [mode, logType]);
+  }, [logType]);
 
   const searchLogs = async () => {
     if (searchKeyword === '') {
@@ -137,118 +179,161 @@ const LogsTable = () => {
 
   return (
     <>
-      <Table basic>
-        <Table.Header>
-          <Table.Row>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortLog('created_time');
-              }}
-              width={3}
-            >
-              时间
-            </Table.HeaderCell>
-            {
-              showModePanel && (
-                <Table.HeaderCell
-                  style={{ cursor: 'pointer' }}
-                  onClick={() => {
-                    sortLog('user_id');
-                  }}
-                  width={1}
-                >
-                  用户
-                </Table.HeaderCell>
+      <Segment>
+        <Header as='h3'>使用明细(总消耗额度:{renderQuota(stat.quota)})</Header>
+        <Form>
+          <Form.Group>
+            <Form.Input fluid label={isAdminUser ? '用户名称' : '令牌名称'} width={3} value={name}
+                        placeholder={isAdminUser ? '留空则查询全部用户' : '留空则查询全部令牌'} name='name'
+                        onChange={handleInputChange} />
+            <Form.Input fluid label='模型名称' width={3} value={model_name} placeholder='留空则查询全部模型' name='model_name'
+                        onChange={handleInputChange} />
+            <Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
+                        name='start_timestamp'
+                        onChange={handleInputChange} />
+            <Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
+                        name='end_timestamp'
+                        onChange={handleInputChange} />
+            <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
+          </Form.Group>
+        </Form>
+        <Table basic compact size='small'>
+          <Table.Header>
+            <Table.Row>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  sortLog('created_time');
+                }}
+                width={3}
+              >
+                时间
+              </Table.HeaderCell>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                width={1}
+              >
+                {isAdminUser ? '用户' : '令牌'}
+              </Table.HeaderCell>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  sortLog('type');
+                }}
+                width={2}
+              >
+                类型
+              </Table.HeaderCell>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  sortLog('model_name');
+                }}
+                width={2}
+              >
+                模型
+              </Table.HeaderCell>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  sortLog('prompt_tokens');
+                }}
+                width={1}
+              >
+                提示令牌
+              </Table.HeaderCell>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  sortLog('completion_tokens');
+                }}
+                width={1}
+              >
+                补全令牌
+              </Table.HeaderCell>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  sortLog('quota');
+                }}
+                width={2}
+              >
+                消耗额度
+              </Table.HeaderCell>
+              <Table.HeaderCell
+                style={{ cursor: 'pointer' }}
+                onClick={() => {
+                  sortLog('content');
+                }}
+                width={4}
+              >
+                详情
+              </Table.HeaderCell>
+            </Table.Row>
+          </Table.Header>
+
+          <Table.Body>
+            {logs
+              .slice(
+                (activePage - 1) * ITEMS_PER_PAGE,
+                activePage * ITEMS_PER_PAGE
               )
-            }
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortLog('type');
-              }}
-              width={2}
-            >
-              类型
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortLog('content');
-              }}
-              width={showModePanel ? 10 : 11}
-            >
-              详情
-            </Table.HeaderCell>
-          </Table.Row>
-        </Table.Header>
+              .map((log, idx) => {
+                if (log.deleted) return <></>;
+                return (
+                  <Table.Row key={log.created_at}>
+                    <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
+                    {
+                      isAdminUser && (
+                        <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
+                      )
+                    }
+                    {
+                      !isAdminUser && (
+                        <Table.Cell>{log.token_name ? <Label>{log.token_name}</Label> : ''}</Table.Cell>
+                      )
+                    }
+                    <Table.Cell>{renderType(log.type)}</Table.Cell>
+                    <Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell>
+                    <Table.Cell>{log.prompt_tokens ? log.prompt_tokens: ''}</Table.Cell>
+                    <Table.Cell>{log.completion_tokens ? log.completion_tokens: ''}</Table.Cell>
+                    <Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
+                    <Table.Cell>{log.content}</Table.Cell>
+                  </Table.Row>
+                );
+              })}
+          </Table.Body>
 
-        <Table.Body>
-          {logs
-            .slice(
-              (activePage - 1) * ITEMS_PER_PAGE,
-              activePage * ITEMS_PER_PAGE
-            )
-            .map((log, idx) => {
-              if (log.deleted) return <></>;
-              return (
-                <Table.Row key={log.created_at}>
-                  <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
-                  {
-                    showModePanel && (
-                      <Table.Cell><Label>{log.user_id}</Label></Table.Cell>
-                    )
+          <Table.Footer>
+            <Table.Row>
+              <Table.HeaderCell colSpan={'8'}>
+                <Select
+                  placeholder='选择明细分类'
+                  options={LOG_OPTIONS}
+                  style={{ marginRight: '8px' }}
+                  name='logType'
+                  value={logType}
+                  onChange={(e, { name, value }) => {
+                    setLogType(value);
+                  }}
+                />
+                <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
+                <Pagination
+                  floated='right'
+                  activePage={activePage}
+                  onPageChange={onPaginationChange}
+                  size='small'
+                  siblingRange={1}
+                  totalPages={
+                    Math.ceil(logs.length / ITEMS_PER_PAGE) +
+                    (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
                   }
-                  <Table.Cell>{renderType(log.type)}</Table.Cell>
-                  <Table.Cell>{log.content}</Table.Cell>
-                </Table.Row>
-              );
-            })}
-        </Table.Body>
-
-        <Table.Footer>
-          <Table.Row>
-            <Table.HeaderCell colSpan={showModePanel ? '5' : '4'}>
-              {
-                showModePanel && (
-                  <Select
-                    placeholder='选择模式'
-                    options={MODE_OPTIONS}
-                    style={{ marginRight: '8px' }}
-                    name='mode'
-                    value={mode}
-                    onChange={(e, { name, value }) => {
-                      setMode(value);
-                    }}
-                  />
-                )
-              }
-              <Select
-                placeholder='选择明细分类'
-                options={LOG_OPTIONS}
-                style={{ marginRight: '8px' }}
-                name='logType'
-                value={logType}
-                onChange={(e, { name, value }) => {
-                  setLogType(value);
-                }}
-              />
-              <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
-              <Pagination
-                floated='right'
-                activePage={activePage}
-                onPageChange={onPaginationChange}
-                size='small'
-                siblingRange={1}
-                totalPages={
-                  Math.ceil(logs.length / ITEMS_PER_PAGE) +
-                  (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
-                }
-              />
-            </Table.HeaderCell>
-          </Table.Row>
-        </Table.Footer>
-      </Table>
+                />
+              </Table.HeaderCell>
+            </Table.Row>
+          </Table.Footer>
+        </Table>
+      </Segment>
     </>
   );
 };

+ 1 - 4
web/src/pages/Log/index.js

@@ -4,10 +4,7 @@ import LogsTable from '../../components/LogsTable';
 
 const Token = () => (
   <>
-    <Segment>
-      <Header as='h3'>额度明细</Header>
-      <LogsTable />
-    </Segment>
+    <LogsTable />
   </>
 );