feat: Add column visibility settings for Channels and Logs tables
- Implemented dynamic column visibility for ChannelsTable and LogsTable - Added localStorage persistence for column preferences - Introduced column selector modal with select all/reset functionality - Supported role-based default column visibility - Added column settings button to table interfaces
This commit is contained in:
@@ -83,6 +83,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|||||||
c.Request.Header.Set("Content-Type", "application/json")
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
c.Set("channel", channel.Type)
|
c.Set("channel", channel.Type)
|
||||||
c.Set("base_url", channel.GetBaseURL())
|
c.Set("base_url", channel.GetBaseURL())
|
||||||
|
group, _ := model.GetUserGroup(1, false)
|
||||||
|
c.Set("group", group)
|
||||||
|
|
||||||
middleware.SetupContextForSelectedChannel(c, channel, testModel)
|
middleware.SetupContextForSelectedChannel(c, channel, testModel)
|
||||||
|
|
||||||
@@ -158,7 +160,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|||||||
tok := time.Now()
|
tok := time.Now()
|
||||||
milliseconds := tok.Sub(tik).Milliseconds()
|
milliseconds := tok.Sub(tik).Milliseconds()
|
||||||
consumedTime := float64(milliseconds) / 1000.0
|
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, "模型测试",
|
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, info.OriginModelName, "模型测试",
|
||||||
quota, "模型测试", 0, quota, int(consumedTime), false, info.Group, other)
|
quota, "模型测试", 0, quota, int(consumedTime), false, info.Group, other)
|
||||||
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
|
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography,
|
||||||
|
Checkbox,
|
||||||
|
Layout
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import EditChannel from '../pages/Channel/EditChannel';
|
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 { loadChannelModels } from './utils.js';
|
||||||
import EditTagModal from '../pages/Channel/EditTagModal.js';
|
import EditTagModal from '../pages/Channel/EditTagModal.js';
|
||||||
import TextNumberInput from './custom/TextNumberInput.js';
|
import TextNumberInput from './custom/TextNumberInput.js';
|
||||||
@@ -141,21 +143,105 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
// Define column keys for selection
|
||||||
// {
|
const COLUMN_KEYS = {
|
||||||
// title: '',
|
ID: 'id',
|
||||||
// dataIndex: 'checkbox',
|
NAME: 'name',
|
||||||
// className: 'checkbox',
|
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'),
|
title: t('ID'),
|
||||||
dataIndex: 'id'
|
dataIndex: 'id'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.NAME,
|
||||||
title: t('名称'),
|
title: t('名称'),
|
||||||
dataIndex: 'name'
|
dataIndex: 'name'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.GROUP,
|
||||||
title: t('分组'),
|
title: t('分组'),
|
||||||
dataIndex: 'group',
|
dataIndex: 'group',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -177,6 +263,7 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.TYPE,
|
||||||
title: t('类型'),
|
title: t('类型'),
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -188,6 +275,7 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.STATUS,
|
||||||
title: t('状态'),
|
title: t('状态'),
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -211,6 +299,7 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.RESPONSE_TIME,
|
||||||
title: t('响应时间'),
|
title: t('响应时间'),
|
||||||
dataIndex: 'response_time',
|
dataIndex: 'response_time',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -218,6 +307,7 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.BALANCE,
|
||||||
title: t('已用/剩余'),
|
title: t('已用/剩余'),
|
||||||
dataIndex: 'expired_time',
|
dataIndex: 'expired_time',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -255,6 +345,7 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.PRIORITY,
|
||||||
title: t('优先级'),
|
title: t('优先级'),
|
||||||
dataIndex: 'priority',
|
dataIndex: 'priority',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -304,6 +395,7 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.WEIGHT,
|
||||||
title: t('权重'),
|
title: t('权重'),
|
||||||
dataIndex: 'weight',
|
dataIndex: 'weight',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -353,6 +445,7 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.OPERATE,
|
||||||
title: '',
|
title: '',
|
||||||
dataIndex: 'operate',
|
dataIndex: 'operate',
|
||||||
render: (text, record, index) => {
|
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 [channels, setChannels] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activePage, setActivePage] = useState(1);
|
const [activePage, setActivePage] = useState(1);
|
||||||
@@ -1032,6 +1187,7 @@ const ChannelsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{renderColumnSelector()}
|
||||||
<EditTagModal
|
<EditTagModal
|
||||||
visible={showEditTag}
|
visible={showEditTag}
|
||||||
tag={editingTag}
|
tag={editingTag}
|
||||||
@@ -1238,15 +1394,22 @@ const ChannelsTable = () => {
|
|||||||
>
|
>
|
||||||
{t('批量设置标签')}
|
{t('批量设置标签')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme="light"
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconSetting />}
|
||||||
|
onClick={() => setShowColumnSelector(true)}
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
>
|
||||||
|
{t('列设置')}
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
className={'channel-table'}
|
loading={loading}
|
||||||
style={{ marginTop: 15 }}
|
columns={getVisibleColumns()}
|
||||||
columns={columns}
|
|
||||||
dataSource={pageData}
|
dataSource={pageData}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage: activePage,
|
currentPage: activePage,
|
||||||
@@ -1260,7 +1423,6 @@ const ChannelsTable = () => {
|
|||||||
},
|
},
|
||||||
onPageChange: handlePageChange
|
onPageChange: handlePageChange
|
||||||
}}
|
}}
|
||||||
loading={loading}
|
|
||||||
onRow={handleRow}
|
onRow={handleRow}
|
||||||
rowSelection={
|
rowSelection={
|
||||||
enableBatchDelete
|
enableBatchDelete
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Checkbox
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import { ITEMS_PER_PAGE } from '../constants';
|
||||||
import {
|
import {
|
||||||
@@ -34,7 +35,7 @@ import {
|
|||||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||||
import { getLogOther } from '../helpers/other.js';
|
import { getLogOther } from '../helpers/other.js';
|
||||||
import { StyleContext } from '../context/Style/index.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;
|
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('时间'),
|
title: t('时间'),
|
||||||
dataIndex: 'timestamp2string',
|
dataIndex: 'timestamp2string',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.CHANNEL,
|
||||||
title: t('渠道'),
|
title: t('渠道'),
|
||||||
dataIndex: 'channel',
|
dataIndex: 'channel',
|
||||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||||
@@ -249,6 +342,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.USERNAME,
|
||||||
title: t('用户'),
|
title: t('用户'),
|
||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||||
@@ -274,6 +368,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.TOKEN,
|
||||||
title: t('令牌'),
|
title: t('令牌'),
|
||||||
dataIndex: 'token_name',
|
dataIndex: 'token_name',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -297,6 +392,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.GROUP,
|
||||||
title: t('分组'),
|
title: t('分组'),
|
||||||
dataIndex: 'group',
|
dataIndex: 'group',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -333,6 +429,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.TYPE,
|
||||||
title: t('类型'),
|
title: t('类型'),
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -340,6 +437,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.MODEL,
|
||||||
title: t('模型'),
|
title: t('模型'),
|
||||||
dataIndex: 'model_name',
|
dataIndex: 'model_name',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -351,6 +449,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.USE_TIME,
|
||||||
title: t('用时/首字'),
|
title: t('用时/首字'),
|
||||||
dataIndex: 'use_time',
|
dataIndex: 'use_time',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -378,6 +477,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.PROMPT,
|
||||||
title: t('提示'),
|
title: t('提示'),
|
||||||
dataIndex: 'prompt_tokens',
|
dataIndex: 'prompt_tokens',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -389,6 +489,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.COMPLETION,
|
||||||
title: t('补全'),
|
title: t('补全'),
|
||||||
dataIndex: 'completion_tokens',
|
dataIndex: 'completion_tokens',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -401,6 +502,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.COST,
|
||||||
title: t('花费'),
|
title: t('花费'),
|
||||||
dataIndex: 'quota',
|
dataIndex: 'quota',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -412,6 +514,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.RETRY,
|
||||||
title: t('重试'),
|
title: t('重试'),
|
||||||
dataIndex: 'retry',
|
dataIndex: 'retry',
|
||||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||||
@@ -439,6 +542,7 @@ const LogsTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: COLUMN_KEYS.DETAILS,
|
||||||
title: t('详情'),
|
title: t('详情'),
|
||||||
dataIndex: 'content',
|
dataIndex: 'content',
|
||||||
render: (text, record, index) => {
|
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 [styleState, styleDispatch] = useContext(StyleContext);
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
const [expandData, setExpandData] = useState({});
|
const [expandData, setExpandData] = useState({});
|
||||||
@@ -782,8 +956,9 @@ const LogsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{renderColumnSelector()}
|
||||||
<Layout>
|
<Layout>
|
||||||
<Header>
|
<Header style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
|
||||||
<Spin spinning={loadingStat}>
|
<Spin spinning={loadingStat}>
|
||||||
<Space>
|
<Space>
|
||||||
<Tag color='green' size='large' style={{ padding: 15 }}>
|
<Tag color='green' size='large' style={{ padding: 15 }}>
|
||||||
@@ -917,10 +1092,19 @@ const LogsTable = () => {
|
|||||||
<Select.Option value='3'>{t('管理')}</Select.Option>
|
<Select.Option value='3'>{t('管理')}</Select.Option>
|
||||||
<Select.Option value='4'>{t('系统')}</Select.Option>
|
<Select.Option value='4'>{t('系统')}</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='tertiary'
|
||||||
|
icon={<IconSetting />}
|
||||||
|
onClick={() => setShowColumnSelector(true)}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
{t('列设置')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
style={{ marginTop: 5 }}
|
style={{ marginTop: 5 }}
|
||||||
columns={columns}
|
columns={getVisibleColumns()}
|
||||||
expandedRowRender={expandRowRender}
|
expandedRowRender={expandRowRender}
|
||||||
expandRowByClick={true}
|
expandRowByClick={true}
|
||||||
dataSource={logs}
|
dataSource={logs}
|
||||||
|
|||||||
Reference in New Issue
Block a user