diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 7aef69ce..33e683a9 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -17,7 +17,7 @@ import { AlertCircle, HelpCircle, Coins, - Tags + Tags, } from 'lucide-react'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js'; @@ -52,6 +52,7 @@ import { IconPlus, IconRefresh, IconSetting, + IconDescend, IconSearch, IconEdit, IconDelete, @@ -64,6 +65,7 @@ import { import { loadChannelModels } from '../../helpers/index.js'; import EditTagModal from '../../pages/Channel/EditTagModal.js'; import { useTranslation } from 'react-i18next'; +import { useTableCompactMode } from '../../hooks/useTableCompactMode'; const ChannelsTable = () => { const { t } = useTranslation(); @@ -683,6 +685,7 @@ const ChannelsTable = () => { const [typeCounts, setTypeCounts] = useState({}); const requestCounter = useRef(0); const [formApi, setFormApi] = useState(null); + const [compactMode, setCompactMode] = useTableCompactMode('channels'); const formInitValues = { searchKeyword: '', searchGroup: '', @@ -1576,6 +1579,16 @@ const ChannelsTable = () => { {t('批量操作')} + +
@@ -1766,9 +1779,9 @@ const ChannelsTable = () => { bordered={false} > rest) : getVisibleColumns()} dataSource={pageData} - scroll={{ x: 'max-content' }} + scroll={compactMode ? undefined : { x: 'max-content' }} pagination={{ currentPage: activePage, pageSize: pageSize, diff --git a/web/src/components/table/LogsTable.js b/web/src/components/table/LogsTable.js index 90e4a809..a6199441 100644 --- a/web/src/components/table/LogsTable.js +++ b/web/src/components/table/LogsTable.js @@ -47,8 +47,9 @@ import { } from '@douyinfe/semi-illustrations'; import { ITEMS_PER_PAGE } from '../../constants'; import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; -import { IconSetting, IconSearch, IconHelpCircle } from '@douyinfe/semi-icons'; +import { IconSetting, IconSearch, IconHelpCircle, IconDescend } from '@douyinfe/semi-icons'; import { Route } from 'lucide-react'; +import { useTableCompactMode } from '../../hooks/useTableCompactMode'; const { Text } = Typography; @@ -192,7 +193,7 @@ const LogsTable = () => { if (!modelMapped) { return renderModelTag(record.model_name, { onClick: (event) => { - copyText(event, record.model_name).then((r) => {}); + copyText(event, record.model_name).then((r) => { }); }, }); } else { @@ -209,7 +210,7 @@ const LogsTable = () => { {renderModelTag(record.model_name, { onClick: (event) => { - copyText(event, record.model_name).then((r) => {}); + copyText(event, record.model_name).then((r) => { }); }, })} @@ -220,7 +221,7 @@ const LogsTable = () => { {renderModelTag(other.upstream_model_name, { onClick: (event) => { copyText(event, other.upstream_model_name).then( - (r) => {}, + (r) => { }, ); }, })} @@ -231,7 +232,7 @@ const LogsTable = () => { > {renderModelTag(record.model_name, { onClick: (event) => { - copyText(event, record.model_name).then((r) => {}); + copyText(event, record.model_name).then((r) => { }); }, suffixIcon: ( { } let content = other?.claude ? renderClaudeModelPriceSimple( - other.model_ratio, - other.model_price, - other.group_ratio, - other?.user_group_ratio, - other.cache_tokens || 0, - other.cache_ratio || 1.0, - other.cache_creation_tokens || 0, - other.cache_creation_ratio || 1.0, - ) + other.model_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + other.cache_tokens || 0, + other.cache_ratio || 1.0, + other.cache_creation_tokens || 0, + other.cache_creation_ratio || 1.0, + ) : renderModelPriceSimple( - other.model_ratio, - other.model_price, - other.group_ratio, - other?.user_group_ratio, - other.cache_tokens || 0, - other.cache_ratio || 1.0, - ); + other.model_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + other.cache_tokens || 0, + other.cache_ratio || 1.0, + ); return ( { key: t('日志详情'), value: other?.claude ? renderClaudeLogContent( - other?.model_ratio, - other.completion_ratio, - other.model_price, - other.group_ratio, - other?.user_group_ratio, - other.cache_ratio || 1.0, - other.cache_creation_ratio || 1.0, - ) + other?.model_ratio, + other.completion_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + other.cache_ratio || 1.0, + other.cache_creation_ratio || 1.0, + ) : renderLogContent( - other?.model_ratio, - other.completion_ratio, - other.model_price, - other.group_ratio, - other?.user_group_ratio, - false, - 1.0, - other.web_search || false, - other.web_search_call_count || 0, - other.file_search || false, - other.file_search_call_count || 0, - ), + other?.model_ratio, + other.completion_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + false, + 1.0, + other.web_search || false, + other.web_search_call_count || 0, + other.file_search || false, + other.file_search_call_count || 0, + ), }); } if (logs[i].type === 2) { @@ -1145,7 +1146,7 @@ const LogsTable = () => { const handlePageChange = (page) => { setActivePage(page); - loadLogs(page, pageSize).then((r) => {}); // 不传入logType,让其从表单获取最新值 + loadLogs(page, pageSize).then((r) => { }); // 不传入logType,让其从表单获取最新值 }; const handlePageSizeChange = async (size) => { @@ -1203,6 +1204,8 @@ const LogsTable = () => { ); }; + const [compactMode, setCompactMode] = useTableCompactMode('logs'); + return ( <> {renderColumnSelector()} @@ -1211,45 +1214,57 @@ const LogsTable = () => { title={
- - + + + {t('消耗额度')}: {renderQuota(stat.quota)} + + + RPM: {stat.rpm} + + + TPM: {stat.tpm} + + + + +
@@ -1382,7 +1397,6 @@ const LogsTable = () => { if (formApi) { formApi.reset(); setLogType(0); - // 重置后立即查询,使用setTimeout确保表单重置完成 setTimeout(() => { refresh(); }, 100); @@ -1411,7 +1425,7 @@ const LogsTable = () => { bordered={false} >
rest) : getVisibleColumns()} {...(hasExpandableRows() && { expandedRowRender: expandRowRender, expandRowByClick: true, @@ -1421,7 +1435,7 @@ const LogsTable = () => { dataSource={logs} rowKey='key' loading={loading} - scroll={{ x: 'max-content' }} + scroll={compactMode ? undefined : { x: 'max-content' }} className='rounded-xl overflow-hidden' size='middle' empty={ diff --git a/web/src/components/table/MjLogsTable.js b/web/src/components/table/MjLogsTable.js index 869db485..008a7785 100644 --- a/web/src/components/table/MjLogsTable.js +++ b/web/src/components/table/MjLogsTable.js @@ -24,7 +24,7 @@ import { XCircle, Loader, AlertCircle, - Hash + Hash, } from 'lucide-react'; import { API, @@ -59,8 +59,10 @@ import { ITEMS_PER_PAGE } from '../../constants'; import { IconEyeOpened, IconSearch, - IconSetting + IconSetting, + IconDescend } from '@douyinfe/semi-icons'; +import { useTableCompactMode } from '../../hooks/useTableCompactMode'; const { Text } = Typography; @@ -107,6 +109,7 @@ const LogsTable = () => { const [visibleColumns, setVisibleColumns] = useState({}); const [showColumnSelector, setShowColumnSelector] = useState(false); const isAdminUser = isAdmin(); + const [compactMode, setCompactMode] = useTableCompactMode('mjLogs'); // 加载保存的列偏好设置 useEffect(() => { @@ -802,7 +805,7 @@ const LogsTable = () => { className="!rounded-2xl mb-4" title={
-
+
{loading ? ( @@ -821,6 +824,15 @@ const LogsTable = () => { )}
+
@@ -919,11 +931,11 @@ const LogsTable = () => { bordered={false} >
rest) : getVisibleColumns()} dataSource={logs} rowKey='key' loading={loading} - scroll={{ x: 'max-content' }} + scroll={compactMode ? undefined : { x: 'max-content' }} className="rounded-xl overflow-hidden" size="middle" empty={ diff --git a/web/src/components/table/RedemptionsTable.js b/web/src/components/table/RedemptionsTable.js index e11a4657..f02d6166 100644 --- a/web/src/components/table/RedemptionsTable.js +++ b/web/src/components/table/RedemptionsTable.js @@ -45,10 +45,12 @@ import { IconDelete, IconStop, IconPlay, - IconMore + IconMore, + IconDescend } from '@douyinfe/semi-icons'; import EditRedemption from '../../pages/Redemption/EditRedemption'; import { useTranslation } from 'react-i18next'; +import { useTableCompactMode } from '../../hooks/useTableCompactMode'; const { Text } = Typography; @@ -266,6 +268,7 @@ const RedemptionsTable = () => { id: undefined, }); const [showEdit, setShowEdit] = useState(false); + const [compactMode, setCompactMode] = useTableCompactMode('redemptions'); // Form 初始值 const formInitValues = { @@ -465,9 +468,20 @@ const RedemptionsTable = () => { const renderHeader = () => (
-
- - {t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')} +
+
+ + {t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')} +
+
@@ -610,9 +624,9 @@ const RedemptionsTable = () => { bordered={false} >
rest) : columns} dataSource={pageData} - scroll={{ x: 'max-content' }} + scroll={compactMode ? undefined : { x: 'max-content' }} pagination={{ currentPage: activePage, pageSize: pageSize, diff --git a/web/src/components/table/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js index 37bdde57..8b309942 100644 --- a/web/src/components/table/TaskLogsTable.js +++ b/web/src/components/table/TaskLogsTable.js @@ -47,8 +47,10 @@ import { ITEMS_PER_PAGE } from '../../constants'; import { IconEyeOpened, IconSearch, - IconSetting + IconSetting, + IconDescend } from '@douyinfe/semi-icons'; +import { useTableCompactMode } from '../../hooks/useTableCompactMode'; const { Text } = Typography; @@ -471,6 +473,8 @@ const LogsTable = () => { const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(false); + const [compactMode, setCompactMode] = useTableCompactMode('taskLogs'); + useEffect(() => { const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE; setPageSize(localPageSize); @@ -650,7 +654,7 @@ const LogsTable = () => { className="!rounded-2xl mb-4" title={
-
+
{loading ? ( @@ -665,6 +669,15 @@ const LogsTable = () => { {t('任务记录')} )}
+
@@ -763,11 +776,11 @@ const LogsTable = () => { bordered={false} >
rest) : getVisibleColumns()} dataSource={logs} rowKey='key' loading={loading} - scroll={{ x: 'max-content' }} + scroll={compactMode ? undefined : { x: 'max-content' }} className="rounded-xl overflow-hidden" size="middle" empty={ diff --git a/web/src/components/table/TokensTable.js b/web/src/components/table/TokensTable.js index 0a049c39..5c3ba658 100644 --- a/web/src/components/table/TokensTable.js +++ b/web/src/components/table/TokensTable.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { API, copy, @@ -52,10 +52,12 @@ import { IconDelete, IconStop, IconPlay, - IconMore + IconMore, + IconDescend } from '@douyinfe/semi-icons'; import EditToken from '../../pages/Token/EditToken'; import { useTranslation } from 'react-i18next'; +import { useTableCompactMode } from '../../hooks/useTableCompactMode'; const { Text } = Typography; @@ -385,6 +387,7 @@ const TokensTable = () => { const [editingToken, setEditingToken] = useState({ id: undefined, }); + const [compactMode, setCompactMode] = useTableCompactMode('tokens'); // Form 初始值 const formInitValues = { @@ -610,9 +613,20 @@ const TokensTable = () => { const renderHeader = () => (
-
- - {t('令牌用于API访问认证,可以设置额度限制和模型权限。')} +
+
+ + {t('令牌用于API访问认证,可以设置额度限制和模型权限。')} +
+
@@ -687,7 +701,6 @@ const TokensTable = () => { > {t('复制所选令牌')} -
{ + if (col.dataIndex === 'operate') { + const { fixed, ...rest } = col; + return rest; + } + return col; + }) : columns} dataSource={tokens} - scroll={{ x: 'max-content' }} + scroll={compactMode ? undefined : { x: 'max-content' }} pagination={{ currentPage: activePage, pageSize: pageSize, diff --git a/web/src/components/table/UsersTable.js b/web/src/components/table/UsersTable.js index d245c56f..94b82912 100644 --- a/web/src/components/table/UsersTable.js +++ b/web/src/components/table/UsersTable.js @@ -13,7 +13,7 @@ import { Activity, Users, DollarSign, - UserPlus + UserPlus, } from 'lucide-react'; import { Button, @@ -43,17 +43,20 @@ import { IconMore, IconUserAdd, IconArrowUp, - IconArrowDown + IconArrowDown, + IconDescend } from '@douyinfe/semi-icons'; import { ITEMS_PER_PAGE } from '../../constants'; import AddUser from '../../pages/User/AddUser'; import EditUser from '../../pages/User/EditUser'; import { useTranslation } from 'react-i18next'; +import { useTableCompactMode } from '../../hooks/useTableCompactMode'; const { Text } = Typography; const UsersTable = () => { const { t } = useTranslation(); + const [compactMode, setCompactMode] = useTableCompactMode('users'); function renderRole(role) { switch (role) { @@ -527,9 +530,20 @@ const UsersTable = () => { const renderHeader = () => (
-
- - {t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')} +
+
+ + {t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')} +
+
@@ -645,9 +659,9 @@ const UsersTable = () => { bordered={false} >
rest) : columns} dataSource={users} - scroll={{ x: 'max-content' }} + scroll={compactMode ? undefined : { x: 'max-content' }} pagination={{ formatPageText: (page) => t('第 {{start}} - {{end}} 条,共 {{total}} 条', { diff --git a/web/src/constants/common.constant.js b/web/src/constants/common.constant.js index 9ce83432..3ec08f55 100644 --- a/web/src/constants/common.constant.js +++ b/web/src/constants/common.constant.js @@ -1,3 +1,5 @@ export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend! -export const DEFAULT_ENDPOINT = '/api/ratio_config'; \ No newline at end of file +export const DEFAULT_ENDPOINT = '/api/ratio_config'; + +export const TABLE_COMPACT_MODES_KEY = 'table_compact_modes'; \ No newline at end of file diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index 56e1104d..68a05846 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -3,6 +3,7 @@ import { toastConstants } from '../constants'; import React from 'react'; import { toast } from 'react-toastify'; import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants'; +import { TABLE_COMPACT_MODES_KEY } from '../constants'; const HTMLToastContent = ({ htmlContent }) => { return
; @@ -509,3 +510,31 @@ export const formatDateTimeString = (date) => { const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; }; + +function readTableCompactModes() { + try { + const json = localStorage.getItem(TABLE_COMPACT_MODES_KEY); + return json ? JSON.parse(json) : {}; + } catch { + return {}; + } +} + +function writeTableCompactModes(modes) { + try { + localStorage.setItem(TABLE_COMPACT_MODES_KEY, JSON.stringify(modes)); + } catch { + // ignore + } +} + +export function getTableCompactMode(tableKey = 'global') { + const modes = readTableCompactModes(); + return !!modes[tableKey]; +} + +export function setTableCompactMode(compact, tableKey = 'global') { + const modes = readTableCompactModes(); + modes[tableKey] = compact; + writeTableCompactModes(modes); +} diff --git a/web/src/hooks/useTableCompactMode.js b/web/src/hooks/useTableCompactMode.js new file mode 100644 index 00000000..f943bda7 --- /dev/null +++ b/web/src/hooks/useTableCompactMode.js @@ -0,0 +1,34 @@ +import { useState, useEffect, useCallback } from 'react'; +import { getTableCompactMode, setTableCompactMode } from '../helpers'; +import { TABLE_COMPACT_MODES_KEY } from '../constants'; + +/** + * 自定义 Hook:管理表格紧凑/自适应模式 + * 返回 [compactMode, setCompactMode]。 + * 内部使用 localStorage 保存状态,并监听 storage 事件保持多标签页同步。 + */ +export function useTableCompactMode(tableKey = 'global') { + const [compactMode, setCompactModeState] = useState(() => getTableCompactMode(tableKey)); + + const setCompactMode = useCallback((value) => { + setCompactModeState(value); + setTableCompactMode(value, tableKey); + }, [tableKey]); + + useEffect(() => { + const handleStorage = (e) => { + if (e.key === TABLE_COMPACT_MODES_KEY) { + try { + const modes = JSON.parse(e.newValue || '{}'); + setCompactModeState(!!modes[tableKey]); + } catch { + // ignore parse error + } + } + }; + window.addEventListener('storage', handleStorage); + return () => window.removeEventListener('storage', handleStorage); + }, [tableKey]); + + return [compactMode, setCompactMode]; +} \ No newline at end of file diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index a5151cbc..295c464d 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1724,5 +1724,7 @@ "按倍率类型筛选": "Filter by ratio type", "内容": "Content", "放大编辑": "Expand editor", - "编辑公告内容": "Edit announcement content" + "编辑公告内容": "Edit announcement content", + "自适应列表": "Adaptive list", + "紧凑列表": "Compact list" } \ No newline at end of file