✨ feat(ui): Implement unified compact/adaptive table mode + icon refinement
Summary
• Added per-table “Compact / Adaptive” view toggle to all major table components (Tokens, Channels, Logs, MjLogs, TaskLogs, Redemptions, Users).
• Persist user preference in a single localStorage entry (`table_compact_modes`) instead of scattered keys.
Details
1. utils.js
• Re-implemented `getTableCompactMode` / `setTableCompactMode` to read & write a shared JSON object.
• Imported storage-key constant from `constants`.
2. hooks/useTableCompactMode.js
• Hook now consumes the unified helpers and listens to `storage` events via the shared key constant.
3. constants
• Added `TABLE_COMPACT_MODES_KEY` to `common.constant.js` and re-exported via `constants/index.js`.
4. Table components
• Integrated `useTableCompactMode('<tableName>')`.
• Dynamically remove `fixed: 'right'` column and horizontal `scroll` when in compact mode.
• UI: toggle button placed at card title’s right; responsive layout on small screens.
5. UI polish
• Replaced all lucide-react `List`/`ListIcon` usages with Semi UI `IconDescend` for consistency.
• Restored correct icons where `Hash` was intended (TaskLogsTable).
Benefits
• Consistent UX for switching list density across the app.
• Cleaner localStorage footprint with easier future maintenance.
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Coins,
|
Coins,
|
||||||
Tags
|
Tags,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
|
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
|
||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
IconPlus,
|
IconPlus,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconSetting,
|
IconSetting,
|
||||||
|
IconDescend,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
@@ -64,6 +65,7 @@ import {
|
|||||||
import { loadChannelModels } from '../../helpers/index.js';
|
import { loadChannelModels } from '../../helpers/index.js';
|
||||||
import EditTagModal from '../../pages/Channel/EditTagModal.js';
|
import EditTagModal from '../../pages/Channel/EditTagModal.js';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||||
|
|
||||||
const ChannelsTable = () => {
|
const ChannelsTable = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -683,6 +685,7 @@ const ChannelsTable = () => {
|
|||||||
const [typeCounts, setTypeCounts] = useState({});
|
const [typeCounts, setTypeCounts] = useState({});
|
||||||
const requestCounter = useRef(0);
|
const requestCounter = useRef(0);
|
||||||
const [formApi, setFormApi] = useState(null);
|
const [formApi, setFormApi] = useState(null);
|
||||||
|
const [compactMode, setCompactMode] = useTableCompactMode('channels');
|
||||||
const formInitValues = {
|
const formInitValues = {
|
||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
searchGroup: '',
|
searchGroup: '',
|
||||||
@@ -1576,6 +1579,16 @@ const ChannelsTable = () => {
|
|||||||
{t('批量操作')}
|
{t('批量操作')}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='secondary'
|
||||||
|
icon={<IconDescend />}
|
||||||
|
className="!rounded-full w-full md:w-auto"
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
|
>
|
||||||
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-4 w-full md:w-auto order-1 md:order-2">
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-4 w-full md:w-auto order-1 md:order-2">
|
||||||
@@ -1766,9 +1779,9 @@ const ChannelsTable = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
|
||||||
dataSource={pageData}
|
dataSource={pageData}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage: activePage,
|
currentPage: activePage,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ import {
|
|||||||
} from '@douyinfe/semi-illustrations';
|
} from '@douyinfe/semi-illustrations';
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
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 { Route } from 'lucide-react';
|
||||||
|
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ const LogsTable = () => {
|
|||||||
if (!modelMapped) {
|
if (!modelMapped) {
|
||||||
return renderModelTag(record.model_name, {
|
return renderModelTag(record.model_name, {
|
||||||
onClick: (event) => {
|
onClick: (event) => {
|
||||||
copyText(event, record.model_name).then((r) => {});
|
copyText(event, record.model_name).then((r) => { });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -209,7 +210,7 @@ const LogsTable = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
{renderModelTag(record.model_name, {
|
{renderModelTag(record.model_name, {
|
||||||
onClick: (event) => {
|
onClick: (event) => {
|
||||||
copyText(event, record.model_name).then((r) => {});
|
copyText(event, record.model_name).then((r) => { });
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -220,7 +221,7 @@ const LogsTable = () => {
|
|||||||
{renderModelTag(other.upstream_model_name, {
|
{renderModelTag(other.upstream_model_name, {
|
||||||
onClick: (event) => {
|
onClick: (event) => {
|
||||||
copyText(event, other.upstream_model_name).then(
|
copyText(event, other.upstream_model_name).then(
|
||||||
(r) => {},
|
(r) => { },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
@@ -231,7 +232,7 @@ const LogsTable = () => {
|
|||||||
>
|
>
|
||||||
{renderModelTag(record.model_name, {
|
{renderModelTag(record.model_name, {
|
||||||
onClick: (event) => {
|
onClick: (event) => {
|
||||||
copyText(event, record.model_name).then((r) => {});
|
copyText(event, record.model_name).then((r) => { });
|
||||||
},
|
},
|
||||||
suffixIcon: (
|
suffixIcon: (
|
||||||
<Route
|
<Route
|
||||||
@@ -636,23 +637,23 @@ const LogsTable = () => {
|
|||||||
}
|
}
|
||||||
let content = other?.claude
|
let content = other?.claude
|
||||||
? renderClaudeModelPriceSimple(
|
? renderClaudeModelPriceSimple(
|
||||||
other.model_ratio,
|
other.model_ratio,
|
||||||
other.model_price,
|
other.model_price,
|
||||||
other.group_ratio,
|
other.group_ratio,
|
||||||
other?.user_group_ratio,
|
other?.user_group_ratio,
|
||||||
other.cache_tokens || 0,
|
other.cache_tokens || 0,
|
||||||
other.cache_ratio || 1.0,
|
other.cache_ratio || 1.0,
|
||||||
other.cache_creation_tokens || 0,
|
other.cache_creation_tokens || 0,
|
||||||
other.cache_creation_ratio || 1.0,
|
other.cache_creation_ratio || 1.0,
|
||||||
)
|
)
|
||||||
: renderModelPriceSimple(
|
: renderModelPriceSimple(
|
||||||
other.model_ratio,
|
other.model_ratio,
|
||||||
other.model_price,
|
other.model_price,
|
||||||
other.group_ratio,
|
other.group_ratio,
|
||||||
other?.user_group_ratio,
|
other?.user_group_ratio,
|
||||||
other.cache_tokens || 0,
|
other.cache_tokens || 0,
|
||||||
other.cache_ratio || 1.0,
|
other.cache_ratio || 1.0,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Paragraph
|
<Paragraph
|
||||||
ellipsis={{
|
ellipsis={{
|
||||||
@@ -985,27 +986,27 @@ const LogsTable = () => {
|
|||||||
key: t('日志详情'),
|
key: t('日志详情'),
|
||||||
value: other?.claude
|
value: other?.claude
|
||||||
? renderClaudeLogContent(
|
? renderClaudeLogContent(
|
||||||
other?.model_ratio,
|
other?.model_ratio,
|
||||||
other.completion_ratio,
|
other.completion_ratio,
|
||||||
other.model_price,
|
other.model_price,
|
||||||
other.group_ratio,
|
other.group_ratio,
|
||||||
other?.user_group_ratio,
|
other?.user_group_ratio,
|
||||||
other.cache_ratio || 1.0,
|
other.cache_ratio || 1.0,
|
||||||
other.cache_creation_ratio || 1.0,
|
other.cache_creation_ratio || 1.0,
|
||||||
)
|
)
|
||||||
: renderLogContent(
|
: renderLogContent(
|
||||||
other?.model_ratio,
|
other?.model_ratio,
|
||||||
other.completion_ratio,
|
other.completion_ratio,
|
||||||
other.model_price,
|
other.model_price,
|
||||||
other.group_ratio,
|
other.group_ratio,
|
||||||
other?.user_group_ratio,
|
other?.user_group_ratio,
|
||||||
false,
|
false,
|
||||||
1.0,
|
1.0,
|
||||||
other.web_search || false,
|
other.web_search || false,
|
||||||
other.web_search_call_count || 0,
|
other.web_search_call_count || 0,
|
||||||
other.file_search || false,
|
other.file_search || false,
|
||||||
other.file_search_call_count || 0,
|
other.file_search_call_count || 0,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (logs[i].type === 2) {
|
if (logs[i].type === 2) {
|
||||||
@@ -1145,7 +1146,7 @@ const LogsTable = () => {
|
|||||||
|
|
||||||
const handlePageChange = (page) => {
|
const handlePageChange = (page) => {
|
||||||
setActivePage(page);
|
setActivePage(page);
|
||||||
loadLogs(page, pageSize).then((r) => {}); // 不传入logType,让其从表单获取最新值
|
loadLogs(page, pageSize).then((r) => { }); // 不传入logType,让其从表单获取最新值
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageSizeChange = async (size) => {
|
const handlePageSizeChange = async (size) => {
|
||||||
@@ -1203,6 +1204,8 @@ const LogsTable = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [compactMode, setCompactMode] = useTableCompactMode('logs');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderColumnSelector()}
|
{renderColumnSelector()}
|
||||||
@@ -1211,45 +1214,57 @@ const LogsTable = () => {
|
|||||||
title={
|
title={
|
||||||
<div className='flex flex-col w-full'>
|
<div className='flex flex-col w-full'>
|
||||||
<Spin spinning={loadingStat}>
|
<Spin spinning={loadingStat}>
|
||||||
<Space>
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
||||||
<Tag
|
<Space>
|
||||||
color='blue'
|
<Tag
|
||||||
size='large'
|
color='blue'
|
||||||
style={{
|
size='large'
|
||||||
padding: 15,
|
style={{
|
||||||
borderRadius: '9999px',
|
padding: 15,
|
||||||
fontWeight: 500,
|
borderRadius: '9999px',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
fontWeight: 500,
|
||||||
}}
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('消耗额度')}: {renderQuota(stat.quota)}
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
color='pink'
|
||||||
|
size='large'
|
||||||
|
style={{
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: '9999px',
|
||||||
|
fontWeight: 500,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
RPM: {stat.rpm}
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
color='white'
|
||||||
|
size='large'
|
||||||
|
style={{
|
||||||
|
padding: 15,
|
||||||
|
border: 'none',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
TPM: {stat.tpm}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='secondary'
|
||||||
|
icon={<IconDescend />}
|
||||||
|
className="!rounded-full w-full md:w-auto"
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
>
|
>
|
||||||
{t('消耗额度')}: {renderQuota(stat.quota)}
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
</Tag>
|
</Button>
|
||||||
<Tag
|
</div>
|
||||||
color='pink'
|
|
||||||
size='large'
|
|
||||||
style={{
|
|
||||||
padding: 15,
|
|
||||||
borderRadius: '9999px',
|
|
||||||
fontWeight: 500,
|
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
RPM: {stat.rpm}
|
|
||||||
</Tag>
|
|
||||||
<Tag
|
|
||||||
color='white'
|
|
||||||
size='large'
|
|
||||||
style={{
|
|
||||||
padding: 15,
|
|
||||||
border: 'none',
|
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
||||||
borderRadius: '9999px',
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
TPM: {stat.tpm}
|
|
||||||
</Tag>
|
|
||||||
</Space>
|
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
||||||
<Divider margin='12px' />
|
<Divider margin='12px' />
|
||||||
@@ -1382,7 +1397,6 @@ const LogsTable = () => {
|
|||||||
if (formApi) {
|
if (formApi) {
|
||||||
formApi.reset();
|
formApi.reset();
|
||||||
setLogType(0);
|
setLogType(0);
|
||||||
// 重置后立即查询,使用setTimeout确保表单重置完成
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -1411,7 +1425,7 @@ const LogsTable = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
|
||||||
{...(hasExpandableRows() && {
|
{...(hasExpandableRows() && {
|
||||||
expandedRowRender: expandRowRender,
|
expandedRowRender: expandRowRender,
|
||||||
expandRowByClick: true,
|
expandRowByClick: true,
|
||||||
@@ -1421,7 +1435,7 @@ const LogsTable = () => {
|
|||||||
dataSource={logs}
|
dataSource={logs}
|
||||||
rowKey='key'
|
rowKey='key'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
className='rounded-xl overflow-hidden'
|
className='rounded-xl overflow-hidden'
|
||||||
size='middle'
|
size='middle'
|
||||||
empty={
|
empty={
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
Loader,
|
Loader,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Hash
|
Hash,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
@@ -59,8 +59,10 @@ import { ITEMS_PER_PAGE } from '../../constants';
|
|||||||
import {
|
import {
|
||||||
IconEyeOpened,
|
IconEyeOpened,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSetting
|
IconSetting,
|
||||||
|
IconDescend
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
|
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@ const LogsTable = () => {
|
|||||||
const [visibleColumns, setVisibleColumns] = useState({});
|
const [visibleColumns, setVisibleColumns] = useState({});
|
||||||
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
const [showColumnSelector, setShowColumnSelector] = useState(false);
|
||||||
const isAdminUser = isAdmin();
|
const isAdminUser = isAdmin();
|
||||||
|
const [compactMode, setCompactMode] = useTableCompactMode('mjLogs');
|
||||||
|
|
||||||
// 加载保存的列偏好设置
|
// 加载保存的列偏好设置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -802,7 +805,7 @@ const LogsTable = () => {
|
|||||||
className="!rounded-2xl mb-4"
|
className="!rounded-2xl mb-4"
|
||||||
title={
|
title={
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
||||||
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
|
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
|
||||||
<IconEyeOpened className="mr-2" />
|
<IconEyeOpened className="mr-2" />
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -821,6 +824,15 @@ const LogsTable = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='secondary'
|
||||||
|
icon={<IconDescend />}
|
||||||
|
className="!rounded-full w-full md:w-auto"
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
|
>
|
||||||
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider margin="12px" />
|
<Divider margin="12px" />
|
||||||
@@ -919,11 +931,11 @@ const LogsTable = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
|
||||||
dataSource={logs}
|
dataSource={logs}
|
||||||
rowKey='key'
|
rowKey='key'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
className="rounded-xl overflow-hidden"
|
className="rounded-xl overflow-hidden"
|
||||||
size="middle"
|
size="middle"
|
||||||
empty={
|
empty={
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ import {
|
|||||||
IconDelete,
|
IconDelete,
|
||||||
IconStop,
|
IconStop,
|
||||||
IconPlay,
|
IconPlay,
|
||||||
IconMore
|
IconMore,
|
||||||
|
IconDescend
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import EditRedemption from '../../pages/Redemption/EditRedemption';
|
import EditRedemption from '../../pages/Redemption/EditRedemption';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -266,6 +268,7 @@ const RedemptionsTable = () => {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
});
|
});
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
|
const [compactMode, setCompactMode] = useTableCompactMode('redemptions');
|
||||||
|
|
||||||
// Form 初始值
|
// Form 初始值
|
||||||
const formInitValues = {
|
const formInitValues = {
|
||||||
@@ -465,9 +468,20 @@ const RedemptionsTable = () => {
|
|||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex items-center text-orange-500">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
||||||
<Ticket size={16} className="mr-2" />
|
<div className="flex items-center text-orange-500">
|
||||||
<Text>{t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')}</Text>
|
<Ticket size={16} className="mr-2" />
|
||||||
|
<Text>{t('兑换码可以批量生成和分发,适合用于推广活动或批量充值。')}</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='secondary'
|
||||||
|
icon={<IconDescend />}
|
||||||
|
className="!rounded-full w-full md:w-auto"
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
|
>
|
||||||
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -610,9 +624,9 @@ const RedemptionsTable = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={compactMode ? columns.map(({ fixed, ...rest }) => rest) : columns}
|
||||||
dataSource={pageData}
|
dataSource={pageData}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage: activePage,
|
currentPage: activePage,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ import { ITEMS_PER_PAGE } from '../../constants';
|
|||||||
import {
|
import {
|
||||||
IconEyeOpened,
|
IconEyeOpened,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSetting
|
IconSetting,
|
||||||
|
IconDescend
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
|
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -471,6 +473,8 @@ const LogsTable = () => {
|
|||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const [compactMode, setCompactMode] = useTableCompactMode('taskLogs');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
|
const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE;
|
||||||
setPageSize(localPageSize);
|
setPageSize(localPageSize);
|
||||||
@@ -650,7 +654,7 @@ const LogsTable = () => {
|
|||||||
className="!rounded-2xl mb-4"
|
className="!rounded-2xl mb-4"
|
||||||
title={
|
title={
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
||||||
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
|
<div className="flex items-center text-orange-500 mb-2 md:mb-0">
|
||||||
<IconEyeOpened className="mr-2" />
|
<IconEyeOpened className="mr-2" />
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -665,6 +669,15 @@ const LogsTable = () => {
|
|||||||
<Text>{t('任务记录')}</Text>
|
<Text>{t('任务记录')}</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='secondary'
|
||||||
|
icon={<IconDescend />}
|
||||||
|
className="!rounded-full w-full md:w-auto"
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
|
>
|
||||||
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider margin="12px" />
|
<Divider margin="12px" />
|
||||||
@@ -763,11 +776,11 @@ const LogsTable = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={getVisibleColumns()}
|
columns={compactMode ? getVisibleColumns().map(({ fixed, ...rest }) => rest) : getVisibleColumns()}
|
||||||
dataSource={logs}
|
dataSource={logs}
|
||||||
rowKey='key'
|
rowKey='key'
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
className="rounded-xl overflow-hidden"
|
className="rounded-xl overflow-hidden"
|
||||||
size="middle"
|
size="middle"
|
||||||
empty={
|
empty={
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
copy,
|
copy,
|
||||||
@@ -52,10 +52,12 @@ import {
|
|||||||
IconDelete,
|
IconDelete,
|
||||||
IconStop,
|
IconStop,
|
||||||
IconPlay,
|
IconPlay,
|
||||||
IconMore
|
IconMore,
|
||||||
|
IconDescend
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import EditToken from '../../pages/Token/EditToken';
|
import EditToken from '../../pages/Token/EditToken';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -385,6 +387,7 @@ const TokensTable = () => {
|
|||||||
const [editingToken, setEditingToken] = useState({
|
const [editingToken, setEditingToken] = useState({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
});
|
});
|
||||||
|
const [compactMode, setCompactMode] = useTableCompactMode('tokens');
|
||||||
|
|
||||||
// Form 初始值
|
// Form 初始值
|
||||||
const formInitValues = {
|
const formInitValues = {
|
||||||
@@ -610,9 +613,20 @@ const TokensTable = () => {
|
|||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex items-center text-blue-500">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
||||||
<Key size={16} className="mr-2" />
|
<div className="flex items-center text-blue-500">
|
||||||
<Text>{t('令牌用于API访问认证,可以设置额度限制和模型权限。')}</Text>
|
<Key size={16} className="mr-2" />
|
||||||
|
<Text>{t('令牌用于API访问认证,可以设置额度限制和模型权限。')}</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
theme="light"
|
||||||
|
type="secondary"
|
||||||
|
icon={<IconDescend />}
|
||||||
|
className="!rounded-full w-full md:w-auto"
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
|
>
|
||||||
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -687,7 +701,6 @@ const TokensTable = () => {
|
|||||||
>
|
>
|
||||||
{t('复制所选令牌')}
|
{t('复制所选令牌')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="w-full md:hidden"></div>
|
|
||||||
<Button
|
<Button
|
||||||
theme="light"
|
theme="light"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -791,9 +804,15 @@ const TokensTable = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={compactMode ? columns.map(col => {
|
||||||
|
if (col.dataIndex === 'operate') {
|
||||||
|
const { fixed, ...rest } = col;
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}) : columns}
|
||||||
dataSource={tokens}
|
dataSource={tokens}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage: activePage,
|
currentPage: activePage,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Users,
|
Users,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
UserPlus
|
UserPlus,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -43,17 +43,20 @@ import {
|
|||||||
IconMore,
|
IconMore,
|
||||||
IconUserAdd,
|
IconUserAdd,
|
||||||
IconArrowUp,
|
IconArrowUp,
|
||||||
IconArrowDown
|
IconArrowDown,
|
||||||
|
IconDescend
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { ITEMS_PER_PAGE } from '../../constants';
|
import { ITEMS_PER_PAGE } from '../../constants';
|
||||||
import AddUser from '../../pages/User/AddUser';
|
import AddUser from '../../pages/User/AddUser';
|
||||||
import EditUser from '../../pages/User/EditUser';
|
import EditUser from '../../pages/User/EditUser';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const UsersTable = () => {
|
const UsersTable = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [compactMode, setCompactMode] = useTableCompactMode('users');
|
||||||
|
|
||||||
function renderRole(role) {
|
function renderRole(role) {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
@@ -527,9 +530,20 @@ const UsersTable = () => {
|
|||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex items-center text-blue-500">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
|
||||||
<IconUserAdd className="mr-2" />
|
<div className="flex items-center text-blue-500">
|
||||||
<Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
|
<IconUserAdd className="mr-2" />
|
||||||
|
<Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
theme='light'
|
||||||
|
type='secondary'
|
||||||
|
icon={<IconDescend />}
|
||||||
|
className="!rounded-full w-full md:w-auto"
|
||||||
|
onClick={() => setCompactMode(!compactMode)}
|
||||||
|
>
|
||||||
|
{compactMode ? t('自适应列表') : t('紧凑列表')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -645,9 +659,9 @@ const UsersTable = () => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={compactMode ? columns.map(({ fixed, ...rest }) => rest) : columns}
|
||||||
dataSource={users}
|
dataSource={users}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||||
pagination={{
|
pagination={{
|
||||||
formatPageText: (page) =>
|
formatPageText: (page) =>
|
||||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
|
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
|
||||||
|
|
||||||
export const DEFAULT_ENDPOINT = '/api/ratio_config';
|
export const DEFAULT_ENDPOINT = '/api/ratio_config';
|
||||||
|
|
||||||
|
export const TABLE_COMPACT_MODES_KEY = 'table_compact_modes';
|
||||||
@@ -3,6 +3,7 @@ import { toastConstants } from '../constants';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
|
import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
|
||||||
|
import { TABLE_COMPACT_MODES_KEY } from '../constants';
|
||||||
|
|
||||||
const HTMLToastContent = ({ htmlContent }) => {
|
const HTMLToastContent = ({ htmlContent }) => {
|
||||||
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||||
@@ -509,3 +510,31 @@ export const formatDateTimeString = (date) => {
|
|||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
34
web/src/hooks/useTableCompactMode.js
Normal file
34
web/src/hooks/useTableCompactMode.js
Normal file
@@ -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];
|
||||||
|
}
|
||||||
@@ -1724,5 +1724,7 @@
|
|||||||
"按倍率类型筛选": "Filter by ratio type",
|
"按倍率类型筛选": "Filter by ratio type",
|
||||||
"内容": "Content",
|
"内容": "Content",
|
||||||
"放大编辑": "Expand editor",
|
"放大编辑": "Expand editor",
|
||||||
"编辑公告内容": "Edit announcement content"
|
"编辑公告内容": "Edit announcement content",
|
||||||
|
"自适应列表": "Adaptive list",
|
||||||
|
"紧凑列表": "Compact list"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user