2025-07-19 03:30:44 +08:00
|
|
|
|
/*
|
|
|
|
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
|
|
it under the terms of the GNU Affero General Public License as
|
|
|
|
|
|
published by the Free Software Foundation, either version 3 of the
|
|
|
|
|
|
License, or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-08-09 17:02:06 +08:00
|
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
2025-08-30 21:15:10 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Notification,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Toast,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
} from '@douyinfe/semi-ui';
|
|
|
|
|
|
import {
|
|
|
|
|
|
API,
|
|
|
|
|
|
showError,
|
|
|
|
|
|
getModelCategories,
|
|
|
|
|
|
selectFilter,
|
|
|
|
|
|
} from '../../../helpers';
|
2025-07-18 22:56:34 +08:00
|
|
|
|
import CardPro from '../../common/ui/CardPro';
|
2025-08-18 04:14:35 +08:00
|
|
|
|
import TokensTable from './TokensTable';
|
|
|
|
|
|
import TokensActions from './TokensActions';
|
|
|
|
|
|
import TokensFilters from './TokensFilters';
|
|
|
|
|
|
import TokensDescription from './TokensDescription';
|
2025-07-19 00:12:04 +08:00
|
|
|
|
import EditTokenModal from './modals/EditTokenModal';
|
2026-03-01 23:23:20 +08:00
|
|
|
|
import CCSwitchModal from './modals/CCSwitchModal';
|
2025-07-18 22:56:34 +08:00
|
|
|
|
import { useTokensData } from '../../../hooks/tokens/useTokensData';
|
2025-07-20 11:24:04 +08:00
|
|
|
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
2025-07-20 02:27:33 +08:00
|
|
|
|
import { createCardProPagination } from '../../../helpers/utils';
|
2025-07-18 22:56:34 +08:00
|
|
|
|
|
2025-08-09 17:02:06 +08:00
|
|
|
|
function TokensPage() {
|
|
|
|
|
|
// Define the function first, then pass it into the hook to avoid TDZ errors
|
|
|
|
|
|
const openFluentNotificationRef = useRef(null);
|
2026-03-01 23:23:20 +08:00
|
|
|
|
const openCCSwitchModalRef = useRef(null);
|
|
|
|
|
|
const tokensData = useTokensData(
|
|
|
|
|
|
(key) => openFluentNotificationRef.current?.(key),
|
|
|
|
|
|
(key) => openCCSwitchModalRef.current?.(key),
|
2025-08-30 21:15:10 +08:00
|
|
|
|
);
|
2025-07-20 11:24:04 +08:00
|
|
|
|
const isMobile = useIsMobile();
|
2025-08-30 21:15:10 +08:00
|
|
|
|
const latestRef = useRef({
|
|
|
|
|
|
tokens: [],
|
|
|
|
|
|
selectedKeys: [],
|
|
|
|
|
|
t: (k) => k,
|
|
|
|
|
|
selectedModel: '',
|
|
|
|
|
|
prefillKey: '',
|
|
|
|
|
|
});
|
2025-08-09 17:02:06 +08:00
|
|
|
|
const [modelOptions, setModelOptions] = useState([]);
|
|
|
|
|
|
const [selectedModel, setSelectedModel] = useState('');
|
|
|
|
|
|
const [fluentNoticeOpen, setFluentNoticeOpen] = useState(false);
|
|
|
|
|
|
const [prefillKey, setPrefillKey] = useState('');
|
2026-03-01 23:23:20 +08:00
|
|
|
|
const [ccSwitchVisible, setCCSwitchVisible] = useState(false);
|
|
|
|
|
|
const [ccSwitchKey, setCCSwitchKey] = useState('');
|
2025-08-09 17:02:06 +08:00
|
|
|
|
|
|
|
|
|
|
// Keep latest data for handlers inside notifications
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
latestRef.current = {
|
|
|
|
|
|
tokens: tokensData.tokens,
|
|
|
|
|
|
selectedKeys: tokensData.selectedKeys,
|
|
|
|
|
|
t: tokensData.t,
|
|
|
|
|
|
selectedModel,
|
|
|
|
|
|
prefillKey,
|
|
|
|
|
|
};
|
2025-08-30 21:15:10 +08:00
|
|
|
|
}, [
|
|
|
|
|
|
tokensData.tokens,
|
|
|
|
|
|
tokensData.selectedKeys,
|
|
|
|
|
|
tokensData.t,
|
|
|
|
|
|
selectedModel,
|
|
|
|
|
|
prefillKey,
|
|
|
|
|
|
]);
|
2025-08-09 17:02:06 +08:00
|
|
|
|
|
|
|
|
|
|
const loadModels = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await API.get('/api/user/models');
|
|
|
|
|
|
const { success, message, data } = res.data || {};
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
const categories = getModelCategories(tokensData.t);
|
|
|
|
|
|
const options = (data || []).map((model) => {
|
|
|
|
|
|
let icon = null;
|
|
|
|
|
|
for (const [key, category] of Object.entries(categories)) {
|
|
|
|
|
|
if (key !== 'all' && category.filter({ model_name: model })) {
|
|
|
|
|
|
icon = category.icon;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
label: (
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<span className='flex items-center gap-1'>
|
2025-08-09 17:02:06 +08:00
|
|
|
|
{icon}
|
|
|
|
|
|
{model}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
value: model,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
setModelOptions(options);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(tokensData.t(message));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showError(e.message || 'Failed to load models');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function openFluentNotification(key) {
|
|
|
|
|
|
const { t } = latestRef.current;
|
|
|
|
|
|
const SUPPRESS_KEY = 'fluent_notify_suppressed';
|
2025-08-09 18:37:08 +08:00
|
|
|
|
if (modelOptions.length === 0) {
|
|
|
|
|
|
// fire-and-forget; a later effect will refresh the notice content
|
2025-08-30 21:15:10 +08:00
|
|
|
|
loadModels();
|
2025-08-09 18:37:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!key && localStorage.getItem(SUPPRESS_KEY) === '1') return;
|
2025-08-09 17:02:06 +08:00
|
|
|
|
const container = document.getElementById('fluent-new-api-container');
|
|
|
|
|
|
if (!container) {
|
2025-08-09 18:26:45 +08:00
|
|
|
|
Toast.warning(t('未检测到 FluentRead(流畅阅读),请确认扩展已启用'));
|
2025-08-09 17:02:06 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setPrefillKey(key || '');
|
|
|
|
|
|
setFluentNoticeOpen(true);
|
|
|
|
|
|
Notification.info({
|
|
|
|
|
|
id: 'fluent-detected',
|
2025-08-09 18:26:45 +08:00
|
|
|
|
title: t('检测到 FluentRead(流畅阅读)'),
|
2025-08-09 17:02:06 +08:00
|
|
|
|
content: (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ marginBottom: 8 }}>
|
2025-08-09 18:37:08 +08:00
|
|
|
|
{key
|
|
|
|
|
|
? t('请选择模型。')
|
|
|
|
|
|
: t('选择模型后可一键填充当前选中令牌(或本页第一个令牌)。')}
|
2025-08-09 17:02:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ marginBottom: 8 }}>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
placeholder={t('请选择模型')}
|
|
|
|
|
|
optionList={modelOptions}
|
|
|
|
|
|
onChange={setSelectedModel}
|
|
|
|
|
|
filter={selectFilter}
|
|
|
|
|
|
style={{ width: 320 }}
|
|
|
|
|
|
showClear
|
|
|
|
|
|
searchable
|
|
|
|
|
|
emptyContent={t('暂无数据')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Space>
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
theme='solid'
|
|
|
|
|
|
type='primary'
|
|
|
|
|
|
onClick={handlePrefillToFluent}
|
|
|
|
|
|
>
|
2025-08-09 18:26:45 +08:00
|
|
|
|
{t('一键填充到 FluentRead')}
|
2025-08-09 17:02:06 +08:00
|
|
|
|
</Button>
|
2025-08-09 18:37:08 +08:00
|
|
|
|
{!key && (
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type='warning'
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
localStorage.setItem(SUPPRESS_KEY, '1');
|
|
|
|
|
|
Notification.close('fluent-detected');
|
|
|
|
|
|
Toast.info(t('已关闭后续提醒'));
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-08-09 18:37:08 +08:00
|
|
|
|
{t('不再提醒')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type='tertiary'
|
|
|
|
|
|
onClick={() => Notification.close('fluent-detected')}
|
|
|
|
|
|
>
|
2025-08-09 17:02:06 +08:00
|
|
|
|
{t('关闭')}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
duration: 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
// assign after definition so hook callback can call it safely
|
|
|
|
|
|
openFluentNotificationRef.current = openFluentNotification;
|
|
|
|
|
|
|
2026-03-01 23:23:20 +08:00
|
|
|
|
function openCCSwitchModal(key) {
|
|
|
|
|
|
if (modelOptions.length === 0) {
|
|
|
|
|
|
loadModels();
|
|
|
|
|
|
}
|
|
|
|
|
|
setCCSwitchKey(key || '');
|
|
|
|
|
|
setCCSwitchVisible(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
openCCSwitchModalRef.current = openCCSwitchModal;
|
|
|
|
|
|
|
2025-08-09 17:02:06 +08:00
|
|
|
|
// Prefill to Fluent handler
|
|
|
|
|
|
const handlePrefillToFluent = () => {
|
2025-08-30 21:15:10 +08:00
|
|
|
|
const {
|
|
|
|
|
|
tokens,
|
|
|
|
|
|
selectedKeys,
|
|
|
|
|
|
t,
|
|
|
|
|
|
selectedModel: chosenModel,
|
|
|
|
|
|
prefillKey: overrideKey,
|
|
|
|
|
|
} = latestRef.current;
|
2025-08-09 17:02:06 +08:00
|
|
|
|
const container = document.getElementById('fluent-new-api-container');
|
|
|
|
|
|
if (!container) {
|
|
|
|
|
|
Toast.error(t('未检测到 Fluent 容器'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!chosenModel) {
|
|
|
|
|
|
Toast.warning(t('请选择模型'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let status = localStorage.getItem('status');
|
|
|
|
|
|
let serverAddress = '';
|
|
|
|
|
|
if (status) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
status = JSON.parse(status);
|
|
|
|
|
|
serverAddress = status.server_address || '';
|
2025-08-30 21:15:10 +08:00
|
|
|
|
} catch (_) {}
|
2025-08-09 17:02:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!serverAddress) serverAddress = window.location.origin;
|
|
|
|
|
|
|
|
|
|
|
|
let apiKeyToUse = '';
|
|
|
|
|
|
if (overrideKey) {
|
|
|
|
|
|
apiKeyToUse = 'sk-' + overrideKey;
|
|
|
|
|
|
} else {
|
2025-08-30 21:15:10 +08:00
|
|
|
|
const token =
|
|
|
|
|
|
selectedKeys && selectedKeys.length === 1
|
|
|
|
|
|
? selectedKeys[0]
|
|
|
|
|
|
: tokens && tokens.length > 0
|
|
|
|
|
|
? tokens[0]
|
|
|
|
|
|
: null;
|
2025-08-09 17:02:06 +08:00
|
|
|
|
if (!token) {
|
|
|
|
|
|
Toast.warning(t('没有可用令牌用于填充'));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
apiKeyToUse = 'sk-' + token.key;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
id: 'new-api',
|
|
|
|
|
|
baseUrl: serverAddress,
|
|
|
|
|
|
apiKey: apiKeyToUse,
|
|
|
|
|
|
model: chosenModel,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-30 21:15:10 +08:00
|
|
|
|
container.dispatchEvent(
|
|
|
|
|
|
new CustomEvent('fluent:prefill', { detail: payload }),
|
|
|
|
|
|
);
|
2025-08-09 17:02:06 +08:00
|
|
|
|
Toast.success(t('已发送到 Fluent'));
|
|
|
|
|
|
Notification.close('fluent-detected');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Show notification when Fluent container is available
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const onAppeared = () => {
|
|
|
|
|
|
openFluentNotification();
|
|
|
|
|
|
};
|
|
|
|
|
|
const onRemoved = () => {
|
|
|
|
|
|
setFluentNoticeOpen(false);
|
|
|
|
|
|
Notification.close('fluent-detected');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('fluent-container:appeared', onAppeared);
|
|
|
|
|
|
window.addEventListener('fluent-container:removed', onRemoved);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener('fluent-container:appeared', onAppeared);
|
|
|
|
|
|
window.removeEventListener('fluent-container:removed', onRemoved);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// When modelOptions or language changes while the notice is open, refresh the content
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (fluentNoticeOpen) {
|
|
|
|
|
|
openFluentNotification();
|
|
|
|
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [modelOptions, selectedModel, tokensData.t, fluentNoticeOpen]);
|
2025-08-18 04:14:35 +08:00
|
|
|
|
|
2025-08-09 17:02:06 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const selector = '#fluent-new-api-container';
|
|
|
|
|
|
const root = document.body || document.documentElement;
|
|
|
|
|
|
|
|
|
|
|
|
const existing = document.querySelector(selector);
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
|
console.log('Fluent container detected (initial):', existing);
|
2025-08-30 21:15:10 +08:00
|
|
|
|
window.dispatchEvent(
|
|
|
|
|
|
new CustomEvent('fluent-container:appeared', { detail: existing }),
|
|
|
|
|
|
);
|
2025-08-09 17:02:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isOrContainsTarget = (node) => {
|
|
|
|
|
|
if (!(node && node.nodeType === 1)) return false;
|
|
|
|
|
|
if (node.id === 'fluent-new-api-container') return true;
|
2025-08-30 21:15:10 +08:00
|
|
|
|
return (
|
|
|
|
|
|
typeof node.querySelector === 'function' &&
|
|
|
|
|
|
!!node.querySelector(selector)
|
|
|
|
|
|
);
|
2025-08-09 17:02:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const observer = new MutationObserver((mutations) => {
|
|
|
|
|
|
for (const m of mutations) {
|
|
|
|
|
|
// appeared
|
|
|
|
|
|
for (const added of m.addedNodes) {
|
|
|
|
|
|
if (isOrContainsTarget(added)) {
|
|
|
|
|
|
const el = document.querySelector(selector);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
console.log('Fluent container appeared:', el);
|
2025-08-30 21:15:10 +08:00
|
|
|
|
window.dispatchEvent(
|
|
|
|
|
|
new CustomEvent('fluent-container:appeared', { detail: el }),
|
|
|
|
|
|
);
|
2025-08-09 17:02:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// removed
|
|
|
|
|
|
for (const removed of m.removedNodes) {
|
|
|
|
|
|
if (isOrContainsTarget(removed)) {
|
|
|
|
|
|
const elNow = document.querySelector(selector);
|
|
|
|
|
|
if (!elNow) {
|
|
|
|
|
|
console.log('Fluent container removed');
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('fluent-container:removed'));
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
observer.observe(root, { childList: true, subtree: true });
|
|
|
|
|
|
return () => observer.disconnect();
|
|
|
|
|
|
}, []);
|
2025-07-18 22:56:34 +08:00
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
// Edit state
|
|
|
|
|
|
showEdit,
|
|
|
|
|
|
editingToken,
|
|
|
|
|
|
closeEdit,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
|
|
|
|
|
|
// Actions state
|
|
|
|
|
|
selectedKeys,
|
|
|
|
|
|
setEditingToken,
|
|
|
|
|
|
setShowEdit,
|
2025-07-19 00:12:04 +08:00
|
|
|
|
batchCopyTokens,
|
2025-07-18 22:56:34 +08:00
|
|
|
|
batchDeleteTokens,
|
|
|
|
|
|
copyText,
|
|
|
|
|
|
|
|
|
|
|
|
// Filters state
|
|
|
|
|
|
formInitValues,
|
|
|
|
|
|
setFormApi,
|
|
|
|
|
|
searchTokens,
|
|
|
|
|
|
loading,
|
|
|
|
|
|
searching,
|
|
|
|
|
|
|
|
|
|
|
|
// Description state
|
|
|
|
|
|
compactMode,
|
|
|
|
|
|
setCompactMode,
|
|
|
|
|
|
|
|
|
|
|
|
// Translation
|
|
|
|
|
|
t,
|
|
|
|
|
|
} = tokensData;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
2025-07-19 00:12:04 +08:00
|
|
|
|
<EditTokenModal
|
2025-07-18 22:56:34 +08:00
|
|
|
|
refresh={refresh}
|
|
|
|
|
|
editingToken={editingToken}
|
|
|
|
|
|
visiable={showEdit}
|
|
|
|
|
|
handleClose={closeEdit}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-03-01 23:23:20 +08:00
|
|
|
|
<CCSwitchModal
|
|
|
|
|
|
visible={ccSwitchVisible}
|
|
|
|
|
|
onClose={() => setCCSwitchVisible(false)}
|
|
|
|
|
|
tokenKey={ccSwitchKey}
|
|
|
|
|
|
modelOptions={modelOptions}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-07-18 22:56:34 +08:00
|
|
|
|
<CardPro
|
2025-08-30 21:15:10 +08:00
|
|
|
|
type='type1'
|
2025-07-18 22:56:34 +08:00
|
|
|
|
descriptionArea={
|
|
|
|
|
|
<TokensDescription
|
|
|
|
|
|
compactMode={compactMode}
|
|
|
|
|
|
setCompactMode={setCompactMode}
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
actionsArea={
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='flex flex-col md:flex-row justify-between items-center gap-2 w-full'>
|
2025-07-18 22:56:34 +08:00
|
|
|
|
<TokensActions
|
|
|
|
|
|
selectedKeys={selectedKeys}
|
|
|
|
|
|
setEditingToken={setEditingToken}
|
|
|
|
|
|
setShowEdit={setShowEdit}
|
2025-07-19 00:12:04 +08:00
|
|
|
|
batchCopyTokens={batchCopyTokens}
|
2025-07-18 22:56:34 +08:00
|
|
|
|
batchDeleteTokens={batchDeleteTokens}
|
|
|
|
|
|
copyText={copyText}
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-08-30 21:15:10 +08:00
|
|
|
|
<div className='w-full md:w-full lg:w-auto order-1 md:order-2'>
|
2025-07-18 22:56:34 +08:00
|
|
|
|
<TokensFilters
|
|
|
|
|
|
formInitValues={formInitValues}
|
|
|
|
|
|
setFormApi={setFormApi}
|
|
|
|
|
|
searchTokens={searchTokens}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
searching={searching}
|
|
|
|
|
|
t={t}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-07-20 02:27:33 +08:00
|
|
|
|
paginationArea={createCardProPagination({
|
|
|
|
|
|
currentPage: tokensData.activePage,
|
|
|
|
|
|
pageSize: tokensData.pageSize,
|
|
|
|
|
|
total: tokensData.tokenCount,
|
|
|
|
|
|
onPageChange: tokensData.handlePageChange,
|
|
|
|
|
|
onPageSizeChange: tokensData.handlePageSizeChange,
|
2025-07-20 11:24:04 +08:00
|
|
|
|
isMobile: isMobile,
|
2025-07-22 16:11:21 +08:00
|
|
|
|
t: tokensData.t,
|
2025-07-20 02:27:33 +08:00
|
|
|
|
})}
|
2025-07-22 16:11:21 +08:00
|
|
|
|
t={tokensData.t}
|
2025-07-18 22:56:34 +08:00
|
|
|
|
>
|
|
|
|
|
|
<TokensTable {...tokensData} />
|
|
|
|
|
|
</CardPro>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
2025-08-09 17:02:06 +08:00
|
|
|
|
}
|
2025-07-18 22:56:34 +08:00
|
|
|
|
|
2025-08-30 21:15:10 +08:00
|
|
|
|
export default TokensPage;
|