Просмотр исходного кода

✨ 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)
t0ng7u 7 месяцев назад
Родитель
Сommit
818e34682c

+ 18 - 1
web/src/components/common/ui/CardPro.js

@@ -28,12 +28,13 @@ const { Text } = Typography;
 /**
  * CardPro 高级卡片组件
  * 
- * 布局分为5个区域:
+ * 布局分为6个区域:
  * 1. 统计信息区域 (statsArea)
  * 2. 描述信息区域 (descriptionArea) 
  * 3. 类型切换/标签区域 (tabsArea)
  * 4. 操作按钮区域 (actionsArea)
  * 5. 搜索表单区域 (searchArea)
+ * 6. 分页区域 (paginationArea) - 固定在卡片底部
  * 
  * 支持三种布局类型:
  * - type1: 操作型 (如TokensTable) - 描述信息 + 操作按钮 + 搜索表单
@@ -50,6 +51,7 @@ const CardPro = ({
   tabsArea,
   actionsArea,
   searchArea,
+  paginationArea, // 新增分页区域
   // 卡片属性
   shadows = 'always',
   bordered = false,
@@ -159,10 +161,24 @@ const CardPro = ({
 
   const headerContent = renderHeader();
 
+  // 渲染分页区域
+  const renderFooter = () => {
+    if (!paginationArea) return null;
+
+    return (
+      <div className="flex justify-center w-full pt-4 border-t" style={{ borderColor: 'var(--semi-color-border)' }}>
+        {paginationArea}
+      </div>
+    );
+  };
+
+  const footerContent = renderFooter();
+
   return (
     <Card
       className={`table-scroll-card !rounded-2xl ${className}`}
       title={headerContent}
+      footer={footerContent}
       shadows={shadows}
       bordered={bordered}
       style={style}
@@ -190,6 +206,7 @@ CardPro.propTypes = {
     PropTypes.arrayOf(PropTypes.node),
   ]),
   searchArea: PropTypes.node,
+  paginationArea: PropTypes.node, // 新增分页区域
   // 表格内容
   children: PropTypes.node,
   // 国际化函数

+ 17 - 4
web/src/components/common/ui/CardTable.js

@@ -30,7 +30,14 @@ import { useIsMobile } from '../../../hooks/common/useIsMobile';
  * 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。
  * 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。
  */
-const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'key', ...tableProps }) => {
+const CardTable = ({
+  columns = [],
+  dataSource = [],
+  loading = false,
+  rowKey = 'key',
+  hidePagination = false, // 新增参数,控制是否隐藏内部分页
+  ...tableProps
+}) => {
   const isMobile = useIsMobile();
   const { t } = useTranslation();
 
@@ -62,13 +69,18 @@ const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'k
 
   // 如果不是移动端,直接渲染原 Table
   if (!isMobile) {
+    // 如果要隐藏分页,则从tableProps中移除pagination
+    const finalTableProps = hidePagination
+      ? { ...tableProps, pagination: false }
+      : tableProps;
+
     return (
       <Table
         columns={columns}
         dataSource={dataSource}
         loading={loading}
         rowKey={rowKey}
-        {...tableProps}
+        {...finalTableProps}
       />
     );
   }
@@ -215,8 +227,8 @@ const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'k
       {dataSource.map((record, index) => (
         <MobileRowCard key={getRowKey(record, index)} record={record} index={index} />
       ))}
-      {/* 分页组件 */}
-      {tableProps.pagination && dataSource.length > 0 && (
+      {/* 分页组件 - 只在不隐藏分页且有pagination配置时显示 */}
+      {!hidePagination && tableProps.pagination && dataSource.length > 0 && (
         <div className="mt-2 flex justify-center">
           <Pagination {...tableProps.pagination} />
         </div>
@@ -230,6 +242,7 @@ CardTable.propTypes = {
   dataSource: PropTypes.array,
   loading: PropTypes.bool,
   rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
+  hidePagination: PropTypes.bool, // 控制是否隐藏内部分页
 };
 
 export default CardTable; 

+ 1 - 0
web/src/components/table/channels/ChannelsTable.jsx

@@ -129,6 +129,7 @@ const ChannelsTable = (channelsData) => {
         onPageSizeChange: handlePageSizeChange,
         onPageChange: handlePageChange,
       }}
+      hidePagination={true}
       expandAllRows={false}
       onRow={handleRow}
       rowSelection={

+ 8 - 0
web/src/components/table/channels/index.jsx

@@ -29,6 +29,7 @@ import ModelTestModal from './modals/ModelTestModal.jsx';
 import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
 import EditChannelModal from './modals/EditChannelModal.jsx';
 import EditTagModal from './modals/EditTagModal.jsx';
+import { createCardProPagination } from '../../../helpers/utils';
 
 const ChannelsPage = () => {
   const channelsData = useChannelsData();
@@ -58,6 +59,13 @@ const ChannelsPage = () => {
         tabsArea={<ChannelsTabs {...channelsData} />}
         actionsArea={<ChannelsActions {...channelsData} />}
         searchArea={<ChannelsFilters {...channelsData} />}
+        paginationArea={createCardProPagination({
+          currentPage: channelsData.activePage,
+          pageSize: channelsData.pageSize,
+          total: channelsData.channelCount,
+          onPageChange: channelsData.handlePageChange,
+          onPageSizeChange: channelsData.handlePageSizeChange,
+        })}
         t={channelsData.t}
       >
         <ChannelsTable {...channelsData} />

+ 1 - 0
web/src/components/table/mj-logs/MjLogsTable.jsx

@@ -109,6 +109,7 @@ const MjLogsTable = (mjLogsData) => {
         onPageSizeChange: handlePageSizeChange,
         onPageChange: handlePageChange,
       }}
+      hidePagination={true}
     />
   );
 };

+ 8 - 0
web/src/components/table/mj-logs/index.jsx

@@ -26,6 +26,7 @@ import MjLogsFilters from './MjLogsFilters.jsx';
 import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
 import ContentModal from './modals/ContentModal.jsx';
 import { useMjLogsData } from '../../../hooks/mj-logs/useMjLogsData.js';
+import { createCardProPagination } from '../../../helpers/utils';
 
 const MjLogsPage = () => {
   const mjLogsData = useMjLogsData();
@@ -41,6 +42,13 @@ const MjLogsPage = () => {
           type="type2"
           statsArea={<MjLogsActions {...mjLogsData} />}
           searchArea={<MjLogsFilters {...mjLogsData} />}
+          paginationArea={createCardProPagination({
+            currentPage: mjLogsData.activePage,
+            pageSize: mjLogsData.pageSize,
+            total: mjLogsData.logCount,
+            onPageChange: mjLogsData.handlePageChange,
+            onPageSizeChange: mjLogsData.handlePageSizeChange,
+          })}
           t={mjLogsData.t}
         >
           <MjLogsTable {...mjLogsData} />

+ 1 - 0
web/src/components/table/redemptions/RedemptionsTable.jsx

@@ -107,6 +107,7 @@ const RedemptionsTable = (redemptionsData) => {
           onPageSizeChange: redemptionsData.handlePageSizeChange,
           onPageChange: handlePageChange,
         }}
+        hidePagination={true}
         loading={loading}
         rowSelection={rowSelection}
         onRow={handleRow}

+ 8 - 0
web/src/components/table/redemptions/index.jsx

@@ -25,6 +25,7 @@ import RedemptionsFilters from './RedemptionsFilters.jsx';
 import RedemptionsDescription from './RedemptionsDescription.jsx';
 import EditRedemptionModal from './modals/EditRedemptionModal';
 import { useRedemptionsData } from '../../../hooks/redemptions/useRedemptionsData';
+import { createCardProPagination } from '../../../helpers/utils';
 
 const RedemptionsPage = () => {
   const redemptionsData = useRedemptionsData();
@@ -99,6 +100,13 @@ const RedemptionsPage = () => {
             </div>
           </div>
         }
+        paginationArea={createCardProPagination({
+          currentPage: redemptionsData.activePage,
+          pageSize: redemptionsData.pageSize,
+          total: redemptionsData.tokenCount,
+          onPageChange: redemptionsData.handlePageChange,
+          onPageSizeChange: redemptionsData.handlePageSizeChange,
+        })}
         t={t}
       >
         <RedemptionsTable {...redemptionsData} />

+ 1 - 0
web/src/components/table/task-logs/TaskLogsTable.jsx

@@ -106,6 +106,7 @@ const TaskLogsTable = (taskLogsData) => {
         onPageSizeChange: handlePageSizeChange,
         onPageChange: handlePageChange,
       }}
+      hidePagination={true}
     />
   );
 };

+ 8 - 0
web/src/components/table/task-logs/index.jsx

@@ -26,6 +26,7 @@ import TaskLogsFilters from './TaskLogsFilters.jsx';
 import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
 import ContentModal from './modals/ContentModal.jsx';
 import { useTaskLogsData } from '../../../hooks/task-logs/useTaskLogsData.js';
+import { createCardProPagination } from '../../../helpers/utils';
 
 const TaskLogsPage = () => {
   const taskLogsData = useTaskLogsData();
@@ -41,6 +42,13 @@ const TaskLogsPage = () => {
           type="type2"
           statsArea={<TaskLogsActions {...taskLogsData} />}
           searchArea={<TaskLogsFilters {...taskLogsData} />}
+          paginationArea={createCardProPagination({
+            currentPage: taskLogsData.activePage,
+            pageSize: taskLogsData.pageSize,
+            total: taskLogsData.logCount,
+            onPageChange: taskLogsData.handlePageChange,
+            onPageSizeChange: taskLogsData.handlePageSizeChange,
+          })}
           t={taskLogsData.t}
         >
           <TaskLogsTable {...taskLogsData} />

+ 1 - 0
web/src/components/table/tokens/TokensTable.jsx

@@ -99,6 +99,7 @@ const TokensTable = (tokensData) => {
         onPageSizeChange: handlePageSizeChange,
         onPageChange: handlePageChange,
       }}
+      hidePagination={true}
       loading={loading}
       rowSelection={rowSelection}
       onRow={handleRow}

+ 8 - 0
web/src/components/table/tokens/index.jsx

@@ -25,6 +25,7 @@ import TokensFilters from './TokensFilters.jsx';
 import TokensDescription from './TokensDescription.jsx';
 import EditTokenModal from './modals/EditTokenModal';
 import { useTokensData } from '../../../hooks/tokens/useTokensData';
+import { createCardProPagination } from '../../../helpers/utils';
 
 const TokensPage = () => {
   const tokensData = useTokensData();
@@ -101,6 +102,13 @@ const TokensPage = () => {
             </div>
           </div>
         }
+        paginationArea={createCardProPagination({
+          currentPage: tokensData.activePage,
+          pageSize: tokensData.pageSize,
+          total: tokensData.tokenCount,
+          onPageChange: tokensData.handlePageChange,
+          onPageSizeChange: tokensData.handlePageSizeChange,
+        })}
         t={t}
       >
         <TokensTable {...tokensData} />

+ 1 - 0
web/src/components/table/usage-logs/UsageLogsTable.jsx

@@ -120,6 +120,7 @@ const LogsTable = (logsData) => {
         },
         onPageChange: handlePageChange,
       }}
+      hidePagination={true}
     />
   );
 };

+ 8 - 0
web/src/components/table/usage-logs/index.jsx

@@ -25,6 +25,7 @@ import LogsFilters from './UsageLogsFilters.jsx';
 import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx';
 import UserInfoModal from './modals/UserInfoModal.jsx';
 import { useLogsData } from '../../../hooks/usage-logs/useUsageLogsData.js';
+import { createCardProPagination } from '../../../helpers/utils';
 
 const LogsPage = () => {
   const logsData = useLogsData();
@@ -40,6 +41,13 @@ const LogsPage = () => {
         type="type2"
         statsArea={<LogsActions {...logsData} />}
         searchArea={<LogsFilters {...logsData} />}
+        paginationArea={createCardProPagination({
+          currentPage: logsData.activePage,
+          pageSize: logsData.pageSize,
+          total: logsData.logCount,
+          onPageChange: logsData.handlePageChange,
+          onPageSizeChange: logsData.handlePageSizeChange,
+        })}
         t={logsData.t}
       >
         <LogsTable {...logsData} />

+ 1 - 0
web/src/components/table/users/UsersTable.jsx

@@ -137,6 +137,7 @@ const UsersTable = (usersData) => {
           onPageSizeChange: handlePageSizeChange,
           onPageChange: handlePageChange,
         }}
+        hidePagination={true}
         loading={loading}
         onRow={handleRow}
         empty={

+ 8 - 0
web/src/components/table/users/index.jsx

@@ -26,6 +26,7 @@ import UsersDescription from './UsersDescription.jsx';
 import AddUserModal from './modals/AddUserModal.jsx';
 import EditUserModal from './modals/EditUserModal.jsx';
 import { useUsersData } from '../../../hooks/users/useUsersData';
+import { createCardProPagination } from '../../../helpers/utils';
 
 const UsersPage = () => {
   const usersData = useUsersData();
@@ -104,6 +105,13 @@ const UsersPage = () => {
             />
           </div>
         }
+        paginationArea={createCardProPagination({
+          currentPage: usersData.activePage,
+          pageSize: usersData.pageSize,
+          total: usersData.userCount,
+          onPageChange: usersData.handlePageChange,
+          onPageSizeChange: usersData.handlePageSizeChange,
+        })}
         t={t}
       >
         <UsersTable {...usersData} />

+ 34 - 1
web/src/helpers/utils.js

@@ -17,13 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import { Toast } from '@douyinfe/semi-ui';
+import { Toast, Pagination } from '@douyinfe/semi-ui';
 import { toastConstants } from '../constants';
 import React from 'react';
 import { toast } from 'react-toastify';
 import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
 import { TABLE_COMPACT_MODES_KEY } from '../constants';
 import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile.js';
+import { useIsMobile } from '../hooks/common/useIsMobile.js';
 
 const HTMLToastContent = ({ htmlContent }) => {
   return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
@@ -567,3 +568,35 @@ export const modelSelectFilter = (input, option) => {
   const val = (option?.value || '').toString().toLowerCase();
   return val.includes(input.trim().toLowerCase());
 };
+
+// -------------------------------
+// CardPro 分页配置组件
+// 用于创建 CardPro 的 paginationArea 配置
+export const createCardProPagination = ({
+  currentPage,
+  pageSize,
+  total,
+  onPageChange,
+  onPageSizeChange,
+  pageSizeOpts = [10, 20, 50, 100],
+  showSizeChanger = true,
+}) => {
+  const isMobile = useIsMobile();
+
+  if (!total || total <= 0) return null;
+
+  return (
+    <Pagination
+      currentPage={currentPage}
+      pageSize={pageSize}
+      total={total}
+      pageSizeOpts={pageSizeOpts}
+      showSizeChanger={showSizeChanger}
+      onPageSizeChange={onPageSizeChange}
+      onPageChange={onPageChange}
+      size={isMobile ? "small" : "default"}
+      showQuickJumper={isMobile}
+      showTotal
+    />
+  );
+};