Sfoglia il codice sorgente

Merge pull request #828 from Calcium-Ion/ui

feat: Add column visibility settings for Channels and Logs tables
Calcium-Ion 10 mesi fa
parent
commit
3352bacd35

+ 4 - 1
controller/channel-test.go

@@ -83,6 +83,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
 	c.Request.Header.Set("Content-Type", "application/json")
 	c.Set("channel", channel.Type)
 	c.Set("base_url", channel.GetBaseURL())
+	group, _ := model.GetUserGroup(1, false)
+	c.Set("group", group)
 
 	middleware.SetupContextForSelectedChannel(c, channel, testModel)
 
@@ -158,7 +160,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
 	tok := time.Now()
 	milliseconds := tok.Sub(tik).Milliseconds()
 	consumedTime := float64(milliseconds) / 1000.0
-	other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio, 0, 0.0, priceData.ModelPrice)
+	other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio,
+		usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice)
 	model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, info.OriginModelName, "模型测试",
 		quota, "模型测试", 0, quota, int(consumedTime), false, info.Group, other)
 	common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))

+ 175 - 13
web/src/components/ChannelsTable.js

@@ -29,10 +29,12 @@ import {
   Table,
   Tag,
   Tooltip,
-  Typography
+  Typography,
+  Checkbox,
+  Layout
 } from '@douyinfe/semi-ui';
 import EditChannel from '../pages/Channel/EditChannel';
-import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
+import { IconList, IconTreeTriangleDown, IconClose, IconFilter, IconPlus, IconRefresh, IconSetting } from '@douyinfe/semi-icons';
 import { loadChannelModels } from './utils.js';
 import EditTagModal from '../pages/Channel/EditTagModal.js';
 import TextNumberInput from './custom/TextNumberInput.js';
@@ -141,21 +143,105 @@ const ChannelsTable = () => {
     }
   };
 
-  const columns = [
-    // {
-    //     title: '',
-    //     dataIndex: 'checkbox',
-    //     className: 'checkbox',
-    // },
+  // Define column keys for selection
+  const COLUMN_KEYS = {
+    ID: 'id',
+    NAME: 'name',
+    GROUP: 'group',
+    TYPE: 'type',
+    STATUS: 'status',
+    RESPONSE_TIME: 'response_time',
+    BALANCE: 'balance',
+    PRIORITY: 'priority',
+    WEIGHT: 'weight',
+    OPERATE: 'operate'
+  };
+
+  // State for column visibility
+  const [visibleColumns, setVisibleColumns] = useState({});
+  const [showColumnSelector, setShowColumnSelector] = useState(false);
+
+  // Load saved column preferences from localStorage
+  useEffect(() => {
+    const savedColumns = localStorage.getItem('channels-table-columns');
+    if (savedColumns) {
+      try {
+        const parsed = JSON.parse(savedColumns);
+        // Make sure all columns are accounted for
+        const defaults = getDefaultColumnVisibility();
+        const merged = { ...defaults, ...parsed };
+        setVisibleColumns(merged);
+      } catch (e) {
+        console.error('Failed to parse saved column preferences', e);
+        initDefaultColumns();
+      }
+    } else {
+      initDefaultColumns();
+    }
+  }, []);
+
+  // Update table when column visibility changes
+  useEffect(() => {
+    if (Object.keys(visibleColumns).length > 0) {
+      // Save to localStorage
+      localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns));
+    }
+  }, [visibleColumns]);
+
+  // Get default column visibility
+  const getDefaultColumnVisibility = () => {
+    return {
+      [COLUMN_KEYS.ID]: true,
+      [COLUMN_KEYS.NAME]: true,
+      [COLUMN_KEYS.GROUP]: true,
+      [COLUMN_KEYS.TYPE]: true,
+      [COLUMN_KEYS.STATUS]: true,
+      [COLUMN_KEYS.RESPONSE_TIME]: true,
+      [COLUMN_KEYS.BALANCE]: true,
+      [COLUMN_KEYS.PRIORITY]: true,
+      [COLUMN_KEYS.WEIGHT]: true,
+      [COLUMN_KEYS.OPERATE]: true
+    };
+  };
+
+  // Initialize default column visibility
+  const initDefaultColumns = () => {
+    const defaults = getDefaultColumnVisibility();
+    setVisibleColumns(defaults);
+  };
+
+  // Handle column visibility change
+  const handleColumnVisibilityChange = (columnKey, checked) => {
+    const updatedColumns = { ...visibleColumns, [columnKey]: checked };
+    setVisibleColumns(updatedColumns);
+  };
+
+  // Handle "Select All" checkbox
+  const handleSelectAll = (checked) => {
+    const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]);
+    const updatedColumns = {};
+    
+    allKeys.forEach(key => {
+      updatedColumns[key] = checked;
+    });
+    
+    setVisibleColumns(updatedColumns);
+  };
+
+  // Define all columns with keys
+  const allColumns = [
     {
+      key: COLUMN_KEYS.ID,
       title: t('ID'),
       dataIndex: 'id'
     },
     {
+      key: COLUMN_KEYS.NAME,
       title: t('名称'),
       dataIndex: 'name'
     },
     {
+      key: COLUMN_KEYS.GROUP,
       title: t('分组'),
       dataIndex: 'group',
       render: (text, record, index) => {
@@ -177,6 +263,7 @@ const ChannelsTable = () => {
       }
     },
     {
+      key: COLUMN_KEYS.TYPE,
       title: t('类型'),
       dataIndex: 'type',
       render: (text, record, index) => {
@@ -188,6 +275,7 @@ const ChannelsTable = () => {
       }
     },
     {
+      key: COLUMN_KEYS.STATUS,
       title: t('状态'),
       dataIndex: 'status',
       render: (text, record, index) => {
@@ -211,6 +299,7 @@ const ChannelsTable = () => {
       }
     },
     {
+      key: COLUMN_KEYS.RESPONSE_TIME,
       title: t('响应时间'),
       dataIndex: 'response_time',
       render: (text, record, index) => {
@@ -218,6 +307,7 @@ const ChannelsTable = () => {
       }
     },
     {
+      key: COLUMN_KEYS.BALANCE,
       title: t('已用/剩余'),
       dataIndex: 'expired_time',
       render: (text, record, index) => {
@@ -255,6 +345,7 @@ const ChannelsTable = () => {
       }
     },
     {
+      key: COLUMN_KEYS.PRIORITY,
       title: t('优先级'),
       dataIndex: 'priority',
       render: (text, record, index) => {
@@ -304,6 +395,7 @@ const ChannelsTable = () => {
       }
     },
     {
+      key: COLUMN_KEYS.WEIGHT,
       title: t('权重'),
       dataIndex: 'weight',
       render: (text, record, index) => {
@@ -353,6 +445,7 @@ const ChannelsTable = () => {
       }
     },
     {
+      key: COLUMN_KEYS.OPERATE,
       title: '',
       dataIndex: 'operate',
       render: (text, record, index) => {
@@ -493,6 +586,68 @@ const ChannelsTable = () => {
     }
   ];
 
+  // Filter columns based on visibility settings
+  const getVisibleColumns = () => {
+    return allColumns.filter(column => visibleColumns[column.key]);
+  };
+
+  // Column selector modal
+  const renderColumnSelector = () => {
+    return (
+      <Modal
+        title={t('列设置')}
+        visible={showColumnSelector}
+        onCancel={() => setShowColumnSelector(false)}
+        footer={
+          <>
+            <Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
+            <Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button>
+            <Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
+          </>
+        }
+        style={{ width: 500 }}
+        bodyStyle={{ padding: '24px' }}
+      >
+        <div style={{ marginBottom: 20 }}>
+          <Checkbox
+            checked={Object.values(visibleColumns).every(v => v === true)}
+            indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)}
+            onChange={e => handleSelectAll(e.target.checked)}
+          >
+            {t('全选')}
+          </Checkbox>
+        </div>
+        <div style={{ 
+          display: 'flex', 
+          flexWrap: 'wrap', 
+          maxHeight: '400px', 
+          overflowY: 'auto',
+          border: '1px solid var(--semi-color-border)',
+          borderRadius: '6px',
+          padding: '16px'
+        }}>
+          {allColumns.map(column => {
+            // Skip columns without title
+            if (!column.title) {
+              return null;
+            }
+            
+            return (
+              <div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
+                <Checkbox
+                  checked={!!visibleColumns[column.key]}
+                  onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
+                >
+                  {column.title}
+                </Checkbox>
+              </div>
+            );
+          })}
+        </div>
+      </Modal>
+    );
+  };
+
   const [channels, setChannels] = useState([]);
   const [loading, setLoading] = useState(true);
   const [activePage, setActivePage] = useState(1);
@@ -1032,6 +1187,7 @@ const ChannelsTable = () => {
 
   return (
     <>
+      {renderColumnSelector()}
       <EditTagModal
         visible={showEditTag}
         tag={editingTag}
@@ -1238,15 +1394,22 @@ const ChannelsTable = () => {
           >
             {t('批量设置标签')}
           </Button>
+          <Button
+            theme="light"
+            type="tertiary"
+            icon={<IconSetting />}
+            onClick={() => setShowColumnSelector(true)}
+            style={{ marginRight: 8 }}
+          >
+            {t('列设置')}
+          </Button>
         </Space>
-
       </div>
 
 
       <Table
-        className={'channel-table'}
-        style={{ marginTop: 15 }}
-        columns={columns}
+        loading={loading}
+        columns={getVisibleColumns()}
         dataSource={pageData}
         pagination={{
           currentPage: activePage,
@@ -1260,7 +1423,6 @@ const ChannelsTable = () => {
           },
           onPageChange: handlePageChange
         }}
-        loading={loading}
         onRow={handleRow}
         rowSelection={
           enableBatchDelete

+ 189 - 5
web/src/components/LogsTable.js

@@ -21,7 +21,8 @@ import {
   Spin,
   Table,
   Tag,
-  Tooltip
+  Tooltip,
+  Checkbox
 } from '@douyinfe/semi-ui';
 import { ITEMS_PER_PAGE } from '../constants';
 import {
@@ -34,7 +35,7 @@ import {
 import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
 import { getLogOther } from '../helpers/other.js';
 import { StyleContext } from '../context/Style/index.js';
-import { IconInherit, IconRefresh } from '@douyinfe/semi-icons';
+import { IconInherit, IconRefresh, IconSetting } from '@douyinfe/semi-icons';
 
 const { Header } = Layout;
 
@@ -215,12 +216,104 @@ const LogsTable = () => {
 
   }
 
-  const columns = [
+  // Define column keys for selection
+  const COLUMN_KEYS = {
+    TIME: 'time',
+    CHANNEL: 'channel',
+    USERNAME: 'username',
+    TOKEN: 'token',
+    GROUP: 'group',
+    TYPE: 'type',
+    MODEL: 'model',
+    USE_TIME: 'use_time',
+    PROMPT: 'prompt',
+    COMPLETION: 'completion',
+    COST: 'cost',
+    RETRY: 'retry',
+    DETAILS: 'details'
+  };
+
+  // State for column visibility
+  const [visibleColumns, setVisibleColumns] = useState({});
+  const [showColumnSelector, setShowColumnSelector] = useState(false);
+
+  // Load saved column preferences from localStorage
+  useEffect(() => {
+    const savedColumns = localStorage.getItem('logs-table-columns');
+    if (savedColumns) {
+      try {
+        const parsed = JSON.parse(savedColumns);
+        // Make sure all columns are accounted for
+        const defaults = getDefaultColumnVisibility();
+        const merged = { ...defaults, ...parsed };
+        setVisibleColumns(merged);
+      } catch (e) {
+        console.error('Failed to parse saved column preferences', e);
+        initDefaultColumns();
+      }
+    } else {
+      initDefaultColumns();
+    }
+  }, []);
+
+  // Get default column visibility based on user role
+  const getDefaultColumnVisibility = () => {
+    return {
+      [COLUMN_KEYS.TIME]: true,
+      [COLUMN_KEYS.CHANNEL]: isAdminUser,
+      [COLUMN_KEYS.USERNAME]: isAdminUser,
+      [COLUMN_KEYS.TOKEN]: true,
+      [COLUMN_KEYS.GROUP]: true,
+      [COLUMN_KEYS.TYPE]: true,
+      [COLUMN_KEYS.MODEL]: true,
+      [COLUMN_KEYS.USE_TIME]: true,
+      [COLUMN_KEYS.PROMPT]: true,
+      [COLUMN_KEYS.COMPLETION]: true,
+      [COLUMN_KEYS.COST]: true,
+      [COLUMN_KEYS.RETRY]: isAdminUser,
+      [COLUMN_KEYS.DETAILS]: true
+    };
+  };
+
+  // Initialize default column visibility
+  const initDefaultColumns = () => {
+    const defaults = getDefaultColumnVisibility();
+    setVisibleColumns(defaults);
+    localStorage.setItem('logs-table-columns', JSON.stringify(defaults));
+  };
+
+  // Handle column visibility change
+  const handleColumnVisibilityChange = (columnKey, checked) => {
+    const updatedColumns = { ...visibleColumns, [columnKey]: checked };
+    setVisibleColumns(updatedColumns);
+  };
+
+  // Handle "Select All" checkbox
+  const handleSelectAll = (checked) => {
+    const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]);
+    const updatedColumns = {};
+    
+    allKeys.forEach(key => {
+      // For admin-only columns, only enable them if user is admin
+      if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.RETRY) && !isAdminUser) {
+        updatedColumns[key] = false;
+      } else {
+        updatedColumns[key] = checked;
+      }
+    });
+    
+    setVisibleColumns(updatedColumns);
+  };
+
+  // Define all columns
+  const allColumns = [
     {
+      key: COLUMN_KEYS.TIME,
       title: t('时间'),
       dataIndex: 'timestamp2string',
     },
     {
+      key: COLUMN_KEYS.CHANNEL,
       title: t('渠道'),
       dataIndex: 'channel',
       className: isAdmin() ? 'tableShow' : 'tableHiddle',
@@ -249,6 +342,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.USERNAME,
       title: t('用户'),
       dataIndex: 'username',
       className: isAdmin() ? 'tableShow' : 'tableHiddle',
@@ -274,6 +368,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.TOKEN,
       title: t('令牌'),
       dataIndex: 'token_name',
       render: (text, record, index) => {
@@ -297,6 +392,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.GROUP,
       title: t('分组'),
       dataIndex: 'group',
       render: (text, record, index) => {
@@ -333,6 +429,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.TYPE,
       title: t('类型'),
       dataIndex: 'type',
       render: (text, record, index) => {
@@ -340,6 +437,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.MODEL,
       title: t('模型'),
       dataIndex: 'model_name',
       render: (text, record, index) => {
@@ -351,6 +449,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.USE_TIME,
       title: t('用时/首字'),
       dataIndex: 'use_time',
       render: (text, record, index) => {
@@ -378,6 +477,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.PROMPT,
       title: t('提示'),
       dataIndex: 'prompt_tokens',
       render: (text, record, index) => {
@@ -389,6 +489,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.COMPLETION,
       title: t('补全'),
       dataIndex: 'completion_tokens',
       render: (text, record, index) => {
@@ -401,6 +502,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.COST,
       title: t('花费'),
       dataIndex: 'quota',
       render: (text, record, index) => {
@@ -412,6 +514,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.RETRY,
       title: t('重试'),
       dataIndex: 'retry',
       className: isAdmin() ? 'tableShow' : 'tableHiddle',
@@ -439,6 +542,7 @@ const LogsTable = () => {
       },
     },
     {
+      key: COLUMN_KEYS.DETAILS,
       title: t('详情'),
       dataIndex: 'content',
       render: (text, record, index) => {
@@ -481,6 +585,76 @@ const LogsTable = () => {
     },
   ];
 
+  // Update table when column visibility changes
+  useEffect(() => {
+    if (Object.keys(visibleColumns).length > 0) {
+      // Save to localStorage
+      localStorage.setItem('logs-table-columns', JSON.stringify(visibleColumns));
+    }
+  }, [visibleColumns]);
+
+  // Filter columns based on visibility settings
+  const getVisibleColumns = () => {
+    return allColumns.filter(column => visibleColumns[column.key]);
+  };
+
+  // Column selector modal
+  const renderColumnSelector = () => {
+    return (
+      <Modal
+        title={t('列设置')}
+        visible={showColumnSelector}
+        onCancel={() => setShowColumnSelector(false)}
+        footer={
+          <>
+            <Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
+            <Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button>
+            <Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
+          </>
+        }
+      >
+        <div style={{ marginBottom: 20 }}>
+          <Checkbox
+            checked={Object.values(visibleColumns).every(v => v === true)}
+            indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)}
+            onChange={e => handleSelectAll(e.target.checked)}
+          >
+            {t('全选')}
+          </Checkbox>
+        </div>
+        <div style={{ 
+          display: 'flex', 
+          flexWrap: 'wrap',
+          maxHeight: '400px',
+          overflowY: 'auto',
+          border: '1px solid var(--semi-color-border)',
+          borderRadius: '6px',
+          padding: '16px'
+        }}>
+          {allColumns.map(column => {
+            // Skip admin-only columns for non-admin users
+            if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL || 
+                                column.key === COLUMN_KEYS.USERNAME || 
+                                column.key === COLUMN_KEYS.RETRY)) {
+              return null;
+            }
+            
+            return (
+              <div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
+                <Checkbox
+                  checked={!!visibleColumns[column.key]}
+                  onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
+                >
+                  {column.title}
+                </Checkbox>
+              </div>
+            );
+          })}
+        </div>
+      </Modal>
+    );
+  };
+
   const [styleState, styleDispatch] = useContext(StyleContext);
   const [logs, setLogs] = useState([]);
   const [expandData, setExpandData] = useState({});
@@ -782,8 +956,9 @@ const LogsTable = () => {
 
   return (
     <>
+      {renderColumnSelector()}
       <Layout>
-        <Header>
+        <Header style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
           <Spin spinning={loadingStat}>
             <Space>
               <Tag color='green' size='large' style={{ padding: 15 }}>
@@ -917,10 +1092,19 @@ const LogsTable = () => {
             <Select.Option value='3'>{t('管理')}</Select.Option>
             <Select.Option value='4'>{t('系统')}</Select.Option>
           </Select>
+          <Button
+            theme='light'
+            type='tertiary'
+            icon={<IconSetting />}
+            onClick={() => setShowColumnSelector(true)}
+            style={{ marginLeft: 8 }}
+          >
+            {t('列设置')}
+          </Button>
         </div>
         <Table
           style={{ marginTop: 5 }}
-          columns={columns}
+          columns={getVisibleColumns()}
           expandedRowRender={expandRowRender}
           expandRowByClick={true}
           dataSource={logs}