mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-18 22:07:26 +00:00
- Split monolithic 922-line TokensTable.js into modular components: * useTokensData.js: Custom hook for centralized state and logic management * TokensColumnDefs.js: Column definitions and rendering functions * TokensTable.jsx: Pure table component for rendering * TokensActions.jsx: Actions area (add, copy, delete tokens) * TokensFilters.jsx: Search form component with keyword and token filters * TokensDescription.jsx: Description area with compact mode toggle * index.jsx: Main orchestrator component - Features preserved: * Token status management with switch controls * Quota progress bars and visual indicators * Model limitations display with vendor avatars * IP restrictions handling and display * Chat integrations with dropdown menu * Batch operations (copy, delete) with confirmations * Key visibility toggle and copy functionality * Compact mode for responsive layouts * Search and filtering capabilities * Pagination and loading states - Improvements: * Better separation of concerns * Enhanced reusability and testability * Simplified maintenance and debugging * Consistent modular architecture pattern * Performance optimizations with useMemo * Backward compatibility maintained This refactoring follows the same successful pattern used for LogsTable, MjLogsTable, and TaskLogsTable, significantly improving code maintainability while preserving all existing functionality.
369 lines
9.0 KiB
JavaScript
369 lines
9.0 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Modal } from '@douyinfe/semi-ui';
|
|
import {
|
|
API,
|
|
copy,
|
|
showError,
|
|
showSuccess,
|
|
} from '../../helpers';
|
|
import { ITEMS_PER_PAGE } from '../../constants';
|
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
|
|
|
export const useTokensData = () => {
|
|
const { t } = useTranslation();
|
|
|
|
// Basic state
|
|
const [tokens, setTokens] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activePage, setActivePage] = useState(1);
|
|
const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
|
|
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
|
|
const [searching, setSearching] = useState(false);
|
|
|
|
// Selection state
|
|
const [selectedKeys, setSelectedKeys] = useState([]);
|
|
|
|
// Edit state
|
|
const [showEdit, setShowEdit] = useState(false);
|
|
const [editingToken, setEditingToken] = useState({
|
|
id: undefined,
|
|
});
|
|
|
|
// UI state
|
|
const [compactMode, setCompactMode] = useTableCompactMode('tokens');
|
|
const [showKeys, setShowKeys] = useState({});
|
|
|
|
// Form state
|
|
const [formApi, setFormApi] = useState(null);
|
|
const formInitValues = {
|
|
searchKeyword: '',
|
|
searchToken: '',
|
|
};
|
|
|
|
// Get form values helper function
|
|
const getFormValues = () => {
|
|
const formValues = formApi ? formApi.getValues() : {};
|
|
return {
|
|
searchKeyword: formValues.searchKeyword || '',
|
|
searchToken: formValues.searchToken || '',
|
|
};
|
|
};
|
|
|
|
// Close edit modal
|
|
const closeEdit = () => {
|
|
setShowEdit(false);
|
|
setTimeout(() => {
|
|
setEditingToken({
|
|
id: undefined,
|
|
});
|
|
}, 500);
|
|
};
|
|
|
|
// Sync page data from API response
|
|
const syncPageData = (payload) => {
|
|
setTokens(payload.items || []);
|
|
setTokenCount(payload.total || 0);
|
|
setActivePage(payload.page || 1);
|
|
setPageSize(payload.page_size || pageSize);
|
|
};
|
|
|
|
// Load tokens function
|
|
const loadTokens = async (page = 1, size = pageSize) => {
|
|
setLoading(true);
|
|
const res = await API.get(`/api/token/?p=${page}&size=${size}`);
|
|
const { success, message, data } = res.data;
|
|
if (success) {
|
|
syncPageData(data);
|
|
} else {
|
|
showError(message);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
// Refresh function
|
|
const refresh = async (page = activePage) => {
|
|
await loadTokens(page);
|
|
setSelectedKeys([]);
|
|
};
|
|
|
|
// Copy text function
|
|
const copyText = async (text) => {
|
|
if (await copy(text)) {
|
|
showSuccess(t('已复制到剪贴板!'));
|
|
} else {
|
|
Modal.error({
|
|
title: t('无法复制到剪贴板,请手动复制'),
|
|
content: text,
|
|
size: 'large',
|
|
});
|
|
}
|
|
};
|
|
|
|
// Open link function for chat integrations
|
|
const onOpenLink = async (type, url, record) => {
|
|
let status = localStorage.getItem('status');
|
|
let serverAddress = '';
|
|
if (status) {
|
|
status = JSON.parse(status);
|
|
serverAddress = status.server_address;
|
|
}
|
|
if (serverAddress === '') {
|
|
serverAddress = window.location.origin;
|
|
}
|
|
if (url.includes('{cherryConfig}') === true) {
|
|
let cherryConfig = {
|
|
id: 'new-api',
|
|
baseUrl: serverAddress,
|
|
apiKey: 'sk-' + record.key,
|
|
}
|
|
let encodedConfig = encodeURIComponent(
|
|
btoa(JSON.stringify(cherryConfig))
|
|
);
|
|
url = url.replaceAll('{cherryConfig}', encodedConfig);
|
|
} else {
|
|
let encodedServerAddress = encodeURIComponent(serverAddress);
|
|
url = url.replaceAll('{address}', encodedServerAddress);
|
|
url = url.replaceAll('{key}', 'sk-' + record.key);
|
|
}
|
|
|
|
window.open(url, '_blank');
|
|
};
|
|
|
|
// Manage token function (delete, enable, disable)
|
|
const manageToken = async (id, action, record) => {
|
|
setLoading(true);
|
|
let data = { id };
|
|
let res;
|
|
switch (action) {
|
|
case 'delete':
|
|
res = await API.delete(`/api/token/${id}/`);
|
|
break;
|
|
case 'enable':
|
|
data.status = 1;
|
|
res = await API.put('/api/token/?status_only=true', data);
|
|
break;
|
|
case 'disable':
|
|
data.status = 2;
|
|
res = await API.put('/api/token/?status_only=true', data);
|
|
break;
|
|
}
|
|
const { success, message } = res.data;
|
|
if (success) {
|
|
showSuccess('操作成功完成!');
|
|
let token = res.data.data;
|
|
let newTokens = [...tokens];
|
|
if (action !== 'delete') {
|
|
record.status = token.status;
|
|
}
|
|
setTokens(newTokens);
|
|
} else {
|
|
showError(message);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
// Search tokens function
|
|
const searchTokens = async () => {
|
|
const { searchKeyword, searchToken } = getFormValues();
|
|
if (searchKeyword === '' && searchToken === '') {
|
|
await loadTokens(1);
|
|
return;
|
|
}
|
|
setSearching(true);
|
|
const res = await API.get(
|
|
`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`,
|
|
);
|
|
const { success, message, data } = res.data;
|
|
if (success) {
|
|
setTokens(data);
|
|
setTokenCount(data.length);
|
|
setActivePage(1);
|
|
} else {
|
|
showError(message);
|
|
}
|
|
setSearching(false);
|
|
};
|
|
|
|
// Sort tokens function
|
|
const sortToken = (key) => {
|
|
if (tokens.length === 0) return;
|
|
setLoading(true);
|
|
let sortedTokens = [...tokens];
|
|
sortedTokens.sort((a, b) => {
|
|
return ('' + a[key]).localeCompare(b[key]);
|
|
});
|
|
if (sortedTokens[0].id === tokens[0].id) {
|
|
sortedTokens.reverse();
|
|
}
|
|
setTokens(sortedTokens);
|
|
setLoading(false);
|
|
};
|
|
|
|
// Page handlers
|
|
const handlePageChange = (page) => {
|
|
loadTokens(page, pageSize).then();
|
|
};
|
|
|
|
const handlePageSizeChange = async (size) => {
|
|
setPageSize(size);
|
|
await loadTokens(1, size);
|
|
};
|
|
|
|
// Row selection handlers
|
|
const rowSelection = {
|
|
onSelect: (record, selected) => { },
|
|
onSelectAll: (selected, selectedRows) => { },
|
|
onChange: (selectedRowKeys, selectedRows) => {
|
|
setSelectedKeys(selectedRows);
|
|
},
|
|
};
|
|
|
|
// Handle row styling
|
|
const handleRow = (record, index) => {
|
|
if (record.status !== 1) {
|
|
return {
|
|
style: {
|
|
background: 'var(--semi-color-disabled-border)',
|
|
},
|
|
};
|
|
} else {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
// Batch delete tokens
|
|
const batchDeleteTokens = async () => {
|
|
if (selectedKeys.length === 0) {
|
|
showError(t('请先选择要删除的令牌!'));
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const ids = selectedKeys.map((token) => token.id);
|
|
const res = await API.post('/api/token/batch', { ids });
|
|
if (res?.data?.success) {
|
|
const count = res.data.data || 0;
|
|
showSuccess(t('已删除 {{count}} 个令牌!', { count }));
|
|
await refresh();
|
|
setTimeout(() => {
|
|
if (tokens.length === 0 && activePage > 1) {
|
|
refresh(activePage - 1);
|
|
}
|
|
}, 100);
|
|
} else {
|
|
showError(res?.data?.message || t('删除失败'));
|
|
}
|
|
} catch (error) {
|
|
showError(error.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Batch copy tokens
|
|
const batchCopyTokens = (copyType) => {
|
|
if (selectedKeys.length === 0) {
|
|
showError(t('请至少选择一个令牌!'));
|
|
return;
|
|
}
|
|
|
|
Modal.info({
|
|
title: t('复制令牌'),
|
|
icon: null,
|
|
content: t('请选择你的复制方式'),
|
|
footer: (
|
|
<div className="flex gap-2">
|
|
<button
|
|
className="px-3 py-1 bg-gray-200 rounded"
|
|
onClick={async () => {
|
|
let content = '';
|
|
for (let i = 0; i < selectedKeys.length; i++) {
|
|
content +=
|
|
selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
|
|
}
|
|
await copyText(content);
|
|
Modal.destroyAll();
|
|
}}
|
|
>
|
|
{t('名称+密钥')}
|
|
</button>
|
|
<button
|
|
className="px-3 py-1 bg-blue-500 text-white rounded"
|
|
onClick={async () => {
|
|
let content = '';
|
|
for (let i = 0; i < selectedKeys.length; i++) {
|
|
content += 'sk-' + selectedKeys[i].key + '\n';
|
|
}
|
|
await copyText(content);
|
|
Modal.destroyAll();
|
|
}}
|
|
>
|
|
{t('仅密钥')}
|
|
</button>
|
|
</div>
|
|
),
|
|
});
|
|
};
|
|
|
|
// Initialize data
|
|
useEffect(() => {
|
|
loadTokens(1)
|
|
.then()
|
|
.catch((reason) => {
|
|
showError(reason);
|
|
});
|
|
}, [pageSize]);
|
|
|
|
return {
|
|
// Basic state
|
|
tokens,
|
|
loading,
|
|
activePage,
|
|
tokenCount,
|
|
pageSize,
|
|
searching,
|
|
|
|
// Selection state
|
|
selectedKeys,
|
|
setSelectedKeys,
|
|
|
|
// Edit state
|
|
showEdit,
|
|
setShowEdit,
|
|
editingToken,
|
|
setEditingToken,
|
|
closeEdit,
|
|
|
|
// UI state
|
|
compactMode,
|
|
setCompactMode,
|
|
showKeys,
|
|
setShowKeys,
|
|
|
|
// Form state
|
|
formApi,
|
|
setFormApi,
|
|
formInitValues,
|
|
getFormValues,
|
|
|
|
// Functions
|
|
loadTokens,
|
|
refresh,
|
|
copyText,
|
|
onOpenLink,
|
|
manageToken,
|
|
searchTokens,
|
|
sortToken,
|
|
handlePageChange,
|
|
handlePageSizeChange,
|
|
rowSelection,
|
|
handleRow,
|
|
batchDeleteTokens,
|
|
batchCopyTokens,
|
|
syncPageData,
|
|
|
|
// Translation
|
|
t,
|
|
};
|
|
};
|