360 lines
12 KiB
React
Raw Normal View History

/*
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 React, { useEffect, useRef, useState } from 'react';
import { Notification, Button, Space, Toast, Typography, Select } from '@douyinfe/semi-ui';
import { API, showError, getModelCategories, selectFilter } from '../../../helpers';
import CardPro from '../../common/ui/CardPro';
import TokensTable from './TokensTable.jsx';
import TokensActions from './TokensActions.jsx';
import TokensFilters from './TokensFilters.jsx';
import TokensDescription from './TokensDescription.jsx';
♻️ refactor(components): restructure RedemptionsTable to modular architecture Refactor the monolithic RedemptionsTable component (614 lines) into a clean, modular structure following the established tokens component pattern. ### Changes Made: **New Components:** - `RedemptionsColumnDefs.js` - Extract table column definitions and render logic - `RedemptionsActions.jsx` - Extract action buttons (add, batch copy, clear invalid) - `RedemptionsFilters.jsx` - Extract search and filter form components - `RedemptionsDescription.jsx` - Extract description area component - `redemptions/index.jsx` - Main container component managing state and composition **New Hook:** - `useRedemptionsData.js` - Extract all data management, CRUD operations, and business logic **New Constants:** - `redemption.constants.js` - Extract redemption status, actions, and form constants **Architecture Changes:** - Transform RedemptionsTable.jsx into pure table rendering component - Move state management and component composition to index.jsx - Implement consistent prop drilling pattern matching tokens module - Add memoization for performance optimization - Centralize translation function distribution ### Benefits: - **Maintainability**: Each component has single responsibility - **Reusability**: Components and hooks can be used elsewhere - **Testability**: Individual modules can be unit tested - **Team Collaboration**: Multiple developers can work on different modules - **Consistency**: Follows established architectural patterns ### File Structure: ``` redemptions/ ├── index.jsx # Main container (state + composition) ├── RedemptionsTable.jsx # Pure table component ├── RedemptionsActions.jsx # Action buttons ├── RedemptionsFilters.jsx # Search/filter form ├── RedemptionsDescription.jsx # Description area └── RedemptionsColumnDefs.js # Column definitions
2025-07-19 00:12:04 +08:00
import EditTokenModal from './modals/EditTokenModal';
import { useTokensData } from '../../../hooks/tokens/useTokensData';
import { useIsMobile } from '../../../hooks/common/useIsMobile';
✨ refactor: move table pagination to CardPro footer for consistent layout Implement unified pagination system by moving pagination from CardTable to CardPro footer area, ensuring consistent visual layout across all table pages. ## Changes Made ### Core Components - **CardPro**: Add `paginationArea` prop to display pagination in card footer - **CardTable**: Add `hidePagination` prop to control internal pagination visibility - **utils.js**: Add `createCardProPagination` helper with responsive design - Mobile: small size + showQuickJumper + showTotal - Desktop: default size + showTotal only ### Table Pages Updated - Users table (type1): Add external pagination control - Channels table (type3): Move pagination to CardPro footer - Tokens table (type1): Implement unified pagination layout - Redemptions table (type1): Apply consistent pagination pattern - Usage-logs table (type2): Migrate to external pagination - MJ-logs table (type2): Update pagination configuration - Task-logs table (type2): Standardize pagination approach ### Bug Fixes - Fix CardTable desktop pagination visibility when hidePagination=true - Standardize data access pattern across all table components - Remove redundant data destructuring in users table for consistency ## Benefits - ✅ Consistent pagination position across all tables - ✅ Better visual hierarchy with fixed footer pagination - ✅ Responsive design optimized for mobile and desktop - ✅ Unified codebase with reusable pagination utility - ✅ Backward compatible with existing table functionality ## Files Modified - `web/src/components/common/ui/CardPro.js` - `web/src/components/common/ui/CardTable.js` - `web/src/helpers/utils.js` - `web/src/components/table/*/index.jsx` (7 tables) - `web/src/components/table/*/*.jsx` (7 table components)
2025-07-20 02:27:33 +08:00
import { createCardProPagination } from '../../../helpers/utils';
function TokensPage() {
// Define the function first, then pass it into the hook to avoid TDZ errors
const openFluentNotificationRef = useRef(null);
const tokensData = useTokensData((key) => openFluentNotificationRef.current?.(key));
const isMobile = useIsMobile();
const latestRef = useRef({ tokens: [], selectedKeys: [], t: (k) => k, selectedModel: '', prefillKey: '' });
const [modelOptions, setModelOptions] = useState([]);
const [selectedModel, setSelectedModel] = useState('');
const [fluentNoticeOpen, setFluentNoticeOpen] = useState(false);
const [prefillKey, setPrefillKey] = useState('');
// Keep latest data for handlers inside notifications
useEffect(() => {
latestRef.current = {
tokens: tokensData.tokens,
selectedKeys: tokensData.selectedKeys,
t: tokensData.t,
selectedModel,
prefillKey,
};
}, [tokensData.tokens, tokensData.selectedKeys, tokensData.t, selectedModel, prefillKey]);
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: (
<span className="flex items-center gap-1">
{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';
if (localStorage.getItem(SUPPRESS_KEY) === '1') return;
const container = document.getElementById('fluent-new-api-container');
if (!container) {
Toast.warning(t('未检测到 FluentRead流畅阅读请确认扩展已启用'));
return;
}
setPrefillKey(key || '');
setFluentNoticeOpen(true);
if (modelOptions.length === 0) {
// fire-and-forget; a later effect will refresh the notice content
loadModels()
}
Notification.info({
id: 'fluent-detected',
title: t('检测到 FluentRead流畅阅读'),
content: (
<div>
<div style={{ marginBottom: 8 }}>
{prefillKey
? t('已检测到 FluentRead 扩展,已从操作中指定密钥,将使用该密钥进行填充。请选择模型后继续。')
: t('已检测到 FluentRead 扩展,请选择模型后可一键填充当前选中令牌(或本页第一个令牌)。')}
</div>
<div style={{ marginBottom: 8 }}>
<Select
placeholder={t('请选择模型')}
optionList={modelOptions}
onChange={setSelectedModel}
filter={selectFilter}
style={{ width: 320 }}
showClear
searchable
emptyContent={t('暂无数据')}
/>
</div>
<Space>
<Button theme="solid" type="primary" onClick={handlePrefillToFluent}>
{t('一键填充到 FluentRead')}
</Button>
<Button type="warning" onClick={() => {
localStorage.setItem(SUPPRESS_KEY, '1');
Notification.close('fluent-detected');
Toast.info(t('已关闭后续提醒'));
}}>
{t('不再提醒')}
</Button>
<Button type="tertiary" onClick={() => Notification.close('fluent-detected')}>
{t('关闭')}
</Button>
</Space>
</div>
),
duration: 0,
});
}
// assign after definition so hook callback can call it safely
openFluentNotificationRef.current = openFluentNotification;
// Prefill to Fluent handler
const handlePrefillToFluent = () => {
const { tokens, selectedKeys, t, selectedModel: chosenModel, prefillKey: overrideKey } = latestRef.current;
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 || '';
} catch (_) { }
}
if (!serverAddress) serverAddress = window.location.origin;
let apiKeyToUse = '';
if (overrideKey) {
apiKeyToUse = 'sk-' + overrideKey;
} else {
const token = (selectedKeys && selectedKeys.length === 1)
? selectedKeys[0]
: (tokens && tokens.length > 0 ? tokens[0] : null);
if (!token) {
Toast.warning(t('没有可用令牌用于填充'));
return;
}
apiKeyToUse = 'sk-' + token.key;
}
const payload = {
id: 'new-api',
baseUrl: serverAddress,
apiKey: apiKeyToUse,
model: chosenModel,
};
container.dispatchEvent(new CustomEvent('fluent:prefill', { detail: payload }));
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]);
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);
window.dispatchEvent(new CustomEvent('fluent-container:appeared', { detail: existing }));
}
const isOrContainsTarget = (node) => {
if (!(node && node.nodeType === 1)) return false;
if (node.id === 'fluent-new-api-container') return true;
return typeof node.querySelector === 'function' && !!node.querySelector(selector);
};
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);
window.dispatchEvent(new CustomEvent('fluent-container:appeared', { detail: el }));
}
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();
}, []);
const {
// Edit state
showEdit,
editingToken,
closeEdit,
refresh,
// Actions state
selectedKeys,
setEditingToken,
setShowEdit,
♻️ refactor(components): restructure RedemptionsTable to modular architecture Refactor the monolithic RedemptionsTable component (614 lines) into a clean, modular structure following the established tokens component pattern. ### Changes Made: **New Components:** - `RedemptionsColumnDefs.js` - Extract table column definitions and render logic - `RedemptionsActions.jsx` - Extract action buttons (add, batch copy, clear invalid) - `RedemptionsFilters.jsx` - Extract search and filter form components - `RedemptionsDescription.jsx` - Extract description area component - `redemptions/index.jsx` - Main container component managing state and composition **New Hook:** - `useRedemptionsData.js` - Extract all data management, CRUD operations, and business logic **New Constants:** - `redemption.constants.js` - Extract redemption status, actions, and form constants **Architecture Changes:** - Transform RedemptionsTable.jsx into pure table rendering component - Move state management and component composition to index.jsx - Implement consistent prop drilling pattern matching tokens module - Add memoization for performance optimization - Centralize translation function distribution ### Benefits: - **Maintainability**: Each component has single responsibility - **Reusability**: Components and hooks can be used elsewhere - **Testability**: Individual modules can be unit tested - **Team Collaboration**: Multiple developers can work on different modules - **Consistency**: Follows established architectural patterns ### File Structure: ``` redemptions/ ├── index.jsx # Main container (state + composition) ├── RedemptionsTable.jsx # Pure table component ├── RedemptionsActions.jsx # Action buttons ├── RedemptionsFilters.jsx # Search/filter form ├── RedemptionsDescription.jsx # Description area └── RedemptionsColumnDefs.js # Column definitions
2025-07-19 00:12:04 +08:00
batchCopyTokens,
batchDeleteTokens,
copyText,
// Filters state
formInitValues,
setFormApi,
searchTokens,
loading,
searching,
// Description state
compactMode,
setCompactMode,
// Translation
t,
} = tokensData;
return (
<>
♻️ refactor(components): restructure RedemptionsTable to modular architecture Refactor the monolithic RedemptionsTable component (614 lines) into a clean, modular structure following the established tokens component pattern. ### Changes Made: **New Components:** - `RedemptionsColumnDefs.js` - Extract table column definitions and render logic - `RedemptionsActions.jsx` - Extract action buttons (add, batch copy, clear invalid) - `RedemptionsFilters.jsx` - Extract search and filter form components - `RedemptionsDescription.jsx` - Extract description area component - `redemptions/index.jsx` - Main container component managing state and composition **New Hook:** - `useRedemptionsData.js` - Extract all data management, CRUD operations, and business logic **New Constants:** - `redemption.constants.js` - Extract redemption status, actions, and form constants **Architecture Changes:** - Transform RedemptionsTable.jsx into pure table rendering component - Move state management and component composition to index.jsx - Implement consistent prop drilling pattern matching tokens module - Add memoization for performance optimization - Centralize translation function distribution ### Benefits: - **Maintainability**: Each component has single responsibility - **Reusability**: Components and hooks can be used elsewhere - **Testability**: Individual modules can be unit tested - **Team Collaboration**: Multiple developers can work on different modules - **Consistency**: Follows established architectural patterns ### File Structure: ``` redemptions/ ├── index.jsx # Main container (state + composition) ├── RedemptionsTable.jsx # Pure table component ├── RedemptionsActions.jsx # Action buttons ├── RedemptionsFilters.jsx # Search/filter form ├── RedemptionsDescription.jsx # Description area └── RedemptionsColumnDefs.js # Column definitions
2025-07-19 00:12:04 +08:00
<EditTokenModal
refresh={refresh}
editingToken={editingToken}
visiable={showEdit}
handleClose={closeEdit}
/>
<CardPro
type="type1"
descriptionArea={
<TokensDescription
compactMode={compactMode}
setCompactMode={setCompactMode}
t={t}
/>
}
actionsArea={
<div className="flex flex-col md:flex-row justify-between items-center gap-2 w-full">
<TokensActions
selectedKeys={selectedKeys}
setEditingToken={setEditingToken}
setShowEdit={setShowEdit}
♻️ refactor(components): restructure RedemptionsTable to modular architecture Refactor the monolithic RedemptionsTable component (614 lines) into a clean, modular structure following the established tokens component pattern. ### Changes Made: **New Components:** - `RedemptionsColumnDefs.js` - Extract table column definitions and render logic - `RedemptionsActions.jsx` - Extract action buttons (add, batch copy, clear invalid) - `RedemptionsFilters.jsx` - Extract search and filter form components - `RedemptionsDescription.jsx` - Extract description area component - `redemptions/index.jsx` - Main container component managing state and composition **New Hook:** - `useRedemptionsData.js` - Extract all data management, CRUD operations, and business logic **New Constants:** - `redemption.constants.js` - Extract redemption status, actions, and form constants **Architecture Changes:** - Transform RedemptionsTable.jsx into pure table rendering component - Move state management and component composition to index.jsx - Implement consistent prop drilling pattern matching tokens module - Add memoization for performance optimization - Centralize translation function distribution ### Benefits: - **Maintainability**: Each component has single responsibility - **Reusability**: Components and hooks can be used elsewhere - **Testability**: Individual modules can be unit tested - **Team Collaboration**: Multiple developers can work on different modules - **Consistency**: Follows established architectural patterns ### File Structure: ``` redemptions/ ├── index.jsx # Main container (state + composition) ├── RedemptionsTable.jsx # Pure table component ├── RedemptionsActions.jsx # Action buttons ├── RedemptionsFilters.jsx # Search/filter form ├── RedemptionsDescription.jsx # Description area └── RedemptionsColumnDefs.js # Column definitions
2025-07-19 00:12:04 +08:00
batchCopyTokens={batchCopyTokens}
batchDeleteTokens={batchDeleteTokens}
copyText={copyText}
t={t}
/>
<div className="w-full md:w-full lg:w-auto order-1 md:order-2">
<TokensFilters
formInitValues={formInitValues}
setFormApi={setFormApi}
searchTokens={searchTokens}
loading={loading}
searching={searching}
t={t}
/>
</div>
</div>
}
✨ refactor: move table pagination to CardPro footer for consistent layout Implement unified pagination system by moving pagination from CardTable to CardPro footer area, ensuring consistent visual layout across all table pages. ## Changes Made ### Core Components - **CardPro**: Add `paginationArea` prop to display pagination in card footer - **CardTable**: Add `hidePagination` prop to control internal pagination visibility - **utils.js**: Add `createCardProPagination` helper with responsive design - Mobile: small size + showQuickJumper + showTotal - Desktop: default size + showTotal only ### Table Pages Updated - Users table (type1): Add external pagination control - Channels table (type3): Move pagination to CardPro footer - Tokens table (type1): Implement unified pagination layout - Redemptions table (type1): Apply consistent pagination pattern - Usage-logs table (type2): Migrate to external pagination - MJ-logs table (type2): Update pagination configuration - Task-logs table (type2): Standardize pagination approach ### Bug Fixes - Fix CardTable desktop pagination visibility when hidePagination=true - Standardize data access pattern across all table components - Remove redundant data destructuring in users table for consistency ## Benefits - ✅ Consistent pagination position across all tables - ✅ Better visual hierarchy with fixed footer pagination - ✅ Responsive design optimized for mobile and desktop - ✅ Unified codebase with reusable pagination utility - ✅ Backward compatible with existing table functionality ## Files Modified - `web/src/components/common/ui/CardPro.js` - `web/src/components/common/ui/CardTable.js` - `web/src/helpers/utils.js` - `web/src/components/table/*/index.jsx` (7 tables) - `web/src/components/table/*/*.jsx` (7 table components)
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,
isMobile: isMobile,
t: tokensData.t,
✨ refactor: move table pagination to CardPro footer for consistent layout Implement unified pagination system by moving pagination from CardTable to CardPro footer area, ensuring consistent visual layout across all table pages. ## Changes Made ### Core Components - **CardPro**: Add `paginationArea` prop to display pagination in card footer - **CardTable**: Add `hidePagination` prop to control internal pagination visibility - **utils.js**: Add `createCardProPagination` helper with responsive design - Mobile: small size + showQuickJumper + showTotal - Desktop: default size + showTotal only ### Table Pages Updated - Users table (type1): Add external pagination control - Channels table (type3): Move pagination to CardPro footer - Tokens table (type1): Implement unified pagination layout - Redemptions table (type1): Apply consistent pagination pattern - Usage-logs table (type2): Migrate to external pagination - MJ-logs table (type2): Update pagination configuration - Task-logs table (type2): Standardize pagination approach ### Bug Fixes - Fix CardTable desktop pagination visibility when hidePagination=true - Standardize data access pattern across all table components - Remove redundant data destructuring in users table for consistency ## Benefits - ✅ Consistent pagination position across all tables - ✅ Better visual hierarchy with fixed footer pagination - ✅ Responsive design optimized for mobile and desktop - ✅ Unified codebase with reusable pagination utility - ✅ Backward compatible with existing table functionality ## Files Modified - `web/src/components/common/ui/CardPro.js` - `web/src/components/common/ui/CardTable.js` - `web/src/helpers/utils.js` - `web/src/components/table/*/index.jsx` (7 tables) - `web/src/components/table/*/*.jsx` (7 table components)
2025-07-20 02:27:33 +08:00
})}
t={tokensData.t}
>
<TokensTable {...tokensData} />
</CardPro>
</>
);
}
export default TokensPage;