The i18n middleware runs before UserAuth, so user settings weren't available when language was detected. Now GetLangFromContext checks user settings first (set by UserAuth) before falling back to the language set by middleware or Accept-Language header.
394 lines
9.9 KiB
JavaScript
394 lines
9.9 KiB
JavaScript
/*
|
|
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
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Modal } from '@douyinfe/semi-ui';
|
|
import {
|
|
API,
|
|
copy,
|
|
showError,
|
|
showSuccess,
|
|
encodeToBase64,
|
|
} from '../../helpers';
|
|
import { ITEMS_PER_PAGE } from '../../constants';
|
|
import { useTableCompactMode } from '../common/useTableCompactMode';
|
|
|
|
export const useTokensData = (openFluentNotification) => {
|
|
const { t } = useTranslation();
|
|
|
|
// Basic state
|
|
const [tokens, setTokens] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activePage, setActivePage] = useState(1);
|
|
const [tokenCount, setTokenCount] = useState(0);
|
|
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) => {
|
|
if (url && url.startsWith('fluent')) {
|
|
openFluentNotification(record.key);
|
|
return;
|
|
}
|
|
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(
|
|
encodeToBase64(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(t('操作成功完成!'));
|
|
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,
|
|
};
|
|
};
|