Browse Source

Add: Work records pagination

iaun 3 months ago
parent
commit
a85cf85886

+ 212 - 0
.kiro/specs/work-records-pagination/design.md

@@ -0,0 +1,212 @@
+# Design Document: Work Records Backend Pagination
+
+## Overview
+
+本设计实现工作记录列表的后端分页功能,将当前的一次性加载全部数据改为按需加载当前页数据。主要修改涉及后端服务层、API路由层和前端组件。
+
+### 影响范围分析
+
+经代码分析,`WorkRecordService.get_all()` 方法仅在 `/work-records` GET 端点中被调用。其他功能模块(导出、汇总统计等)使用独立的查询方法,不受影响:
+
+| 模块 | 方法 | 影响 |
+|------|------|------|
+| 工作记录列表 | `get_all()` | ✅ 需要修改 |
+| 每日汇总 | `get_daily_summary()` | ❌ 不受影响 |
+| 月度汇总 | `get_monthly_summary()` | ❌ 不受影响 |
+| 年度汇总 | `get_yearly_summary()` | ❌ 不受影响 |
+| 批量结算 | `batch_update_settlement()` | ❌ 不受影响 |
+| 导出功能 | `ExportService` | ❌ 不受影响(独立查询) |
+
+### 向后兼容性
+
+- 分页参数 `page` 和 `page_size` 均为可选参数,有默认值
+- 不传分页参数时,行为与原来一致(返回第1页,默认20条)
+- API 响应结构新增 `pagination` 字段,原有 `data` 字段保持不变
+
+## Architecture
+
+```
+┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
+│   Frontend      │     │   Backend API   │     │   Database      │
+│  WorkRecordList │────▶│  /work-records  │────▶│   WorkRecord    │
+│                 │     │                 │     │                 │
+│  - page         │     │  - get_all()    │     │  - LIMIT        │
+│  - page_size    │     │  - pagination   │     │  - OFFSET       │
+│  - total        │     │  - filters      │     │  - COUNT        │
+└─────────────────┘     └─────────────────┘     └─────────────────┘
+```
+
+### 分页流程
+
+1. 前端发送请求,携带 `page`、`page_size` 和筛选参数
+2. 后端服务层构建查询,应用筛选条件
+3. 执行 COUNT 查询获取总数
+4. 执行分页查询(LIMIT/OFFSET)获取当前页数据
+5. 返回数据和分页元数据
+
+## Components and Interfaces
+
+### Backend Service Layer
+
+修改 `WorkRecordService.get_all()` 方法:
+
+```python
+@staticmethod
+def get_all(person_id=None, start_date=None, end_date=None, 
+            year=None, month=None, is_settled=None,
+            page=1, page_size=20):
+    """Get work records with pagination and filters.
+    
+    Args:
+        person_id: Filter by person ID (optional)
+        start_date: Filter by start date (optional)
+        end_date: Filter by end date (optional)
+        year: Filter by year (optional)
+        month: Filter by month 1-12 (optional)
+        is_settled: Filter by settlement status (optional)
+        page: Page number, starting from 1 (default: 1)
+        page_size: Number of records per page (default: 20, max: 100)
+        
+    Returns:
+        Dictionary with:
+        - data: List of work record dictionaries
+        - total: Total number of matching records
+        - page: Current page number
+        - page_size: Records per page
+        - total_pages: Total number of pages
+    """
+```
+
+### Backend API Layer
+
+修改 `/work-records` GET 端点:
+
+```python
+@work_record_ns.param('page', '页码,从1开始', type=int, default=1)
+@work_record_ns.param('page_size', '每页数量,默认20,最大100', type=int, default=20)
+```
+
+### API Response Format
+
+```json
+{
+  "success": true,
+  "data": [...],
+  "pagination": {
+    "total": 150,
+    "page": 1,
+    "page_size": 20,
+    "total_pages": 8
+  },
+  "message": "Work records retrieved successfully"
+}
+```
+
+### Frontend API Service
+
+修改 `workRecordApi.getAll()` 调用:
+
+```javascript
+getAll: (params) => api.get('/work-records', { 
+  params: {
+    ...params,
+    page: params.page || 1,
+    page_size: params.page_size || 20
+  }
+})
+```
+
+### Frontend Component
+
+修改 `WorkRecordList` 组件:
+
+- 添加 `currentPage` 和 `pageSize` 状态
+- 添加 `total` 状态存储后端返回的总数
+- 修改 Table 的 `pagination` 配置使用后端返回的 `total`
+- 添加 `onChange` 处理器响应分页变化
+
+## Data Models
+
+### Pagination Parameters
+
+| 参数 | 类型 | 默认值 | 范围 | 说明 |
+|------|------|--------|------|------|
+| page | int | 1 | >= 1 | 页码,小于1时使用1 |
+| page_size | int | 20 | 1-100 | 每页数量,超出范围使用20 |
+
+### Pagination Response
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| total | int | 匹配筛选条件的总记录数 |
+| page | int | 当前页码 |
+| page_size | int | 每页数量 |
+| total_pages | int | 总页数,计算公式: ceil(total / page_size) |
+
+## Correctness Properties
+
+*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
+
+### Property 1: Pagination Data Count Correctness
+
+*For any* valid page and page_size parameters, the number of records returned should equal `min(page_size, total - (page - 1) * page_size)` when page is within valid range, or 0 when page exceeds total_pages.
+
+**Validates: Requirements 1.4, 4.3**
+
+### Property 2: Pagination Metadata Consistency
+
+*For any* pagination request, the returned metadata should satisfy:
+- `total_pages == ceil(total / page_size)`
+- `page == requested_page` (after normalization)
+- `page_size == requested_page_size` (after normalization)
+- `len(data) <= page_size`
+
+**Validates: Requirements 1.5**
+
+### Property 3: Filter and Pagination Composition
+
+*For any* combination of filter parameters and pagination parameters, the returned `total` should equal the count of all records matching the filter criteria, and the returned data should be a correct subset of filtered records for the requested page.
+
+**Validates: Requirements 2.1, 2.2**
+
+### Property 4: Sort Order Preservation
+
+*For any* pagination request, the returned records should be sorted by `work_date` descending, then by `id` ascending. This order should be consistent across all pages.
+
+**Validates: Requirements 2.3**
+
+### Property 5: Boundary Parameter Normalization
+
+*For any* page < 1, the service should return page 1 data.
+*For any* page_size < 1 or page_size > 100, the service should use page_size = 20.
+
+**Validates: Requirements 4.1, 4.2**
+
+## Error Handling
+
+| 场景 | 处理方式 |
+|------|----------|
+| page < 1 | 使用 page = 1 |
+| page_size < 1 或 > 100 | 使用 page_size = 20 |
+| page > total_pages | 返回空数据列表,分页元数据正确 |
+| 无匹配数据 | 返回空列表,total = 0,total_pages = 0 |
+
+## Testing Strategy
+
+### Unit Tests
+
+- 测试默认分页参数(不传 page/page_size)
+- 测试边界值处理(page=0, page=-1, page_size=0, page_size=101)
+- 测试空结果集的分页元数据
+
+### Property-Based Tests
+
+使用 `hypothesis` 库进行属性测试:
+
+- **Property 1**: 生成随机 page/page_size,验证返回数据量正确
+- **Property 2**: 生成随机请求,验证分页元数据一致性
+- **Property 3**: 生成随机筛选条件和分页参数,验证筛选与分页组合正确
+- **Property 4**: 生成随机分页请求,验证排序顺序
+- **Property 5**: 生成边界参数,验证参数规范化
+
+每个属性测试运行至少 100 次迭代。

+ 57 - 0
.kiro/specs/work-records-pagination/requirements.md

@@ -0,0 +1,57 @@
+# Requirements Document
+
+## Introduction
+
+实现工作记录(Work Records)的后端分页功能。当前系统一次性加载所有工作记录数据,在数据量大时会导致性能问题和内存占用过高。需要改为真正的后端分页,每次只从数据库获取当前页的数据。
+
+## Glossary
+
+- **Work_Record_Service**: 后端工作记录服务,负责处理工作记录的CRUD操作和查询
+- **Work_Record_API**: 前端调用的工作记录API接口
+- **Pagination**: 分页机制,包含页码(page)、每页数量(page_size)、总数(total)等信息
+- **Backend_Pagination**: 后端分页,数据库层面的分页查询,只返回当前页数据
+- **Frontend_Pagination**: 前端分页,所有数据加载到前端后在客户端进行分页显示
+
+## Requirements
+
+### Requirement 1: 后端分页查询
+
+**User Story:** 作为系统用户,我希望工作记录列表支持后端分页,以便在数据量大时也能快速加载和浏览数据。
+
+#### Acceptance Criteria
+
+1. WHEN 前端请求工作记录列表时,THE Work_Record_API SHALL 接受 page 和 page_size 参数
+2. WHEN page 参数未提供时,THE Work_Record_Service SHALL 默认使用第1页
+3. WHEN page_size 参数未提供时,THE Work_Record_Service SHALL 默认使用每页20条
+4. WHEN 执行分页查询时,THE Work_Record_Service SHALL 只从数据库获取当前页的数据
+5. THE Work_Record_API SHALL 返回分页元数据,包含 total(总记录数)、page(当前页)、page_size(每页数量)、total_pages(总页数)
+
+### Requirement 2: 分页与筛选条件组合
+
+**User Story:** 作为系统用户,我希望在使用筛选条件时分页功能仍然正常工作,以便精确查找特定记录。
+
+#### Acceptance Criteria
+
+1. WHEN 同时提供分页参数和筛选条件(person_id、date、year/month、is_settled)时,THE Work_Record_Service SHALL 先应用筛选条件再进行分页
+2. WHEN 筛选条件改变时,THE Work_Record_Service SHALL 返回筛选后数据的正确总数
+3. THE Work_Record_Service SHALL 保持现有的排序规则(按日期降序、ID升序)
+
+### Requirement 3: 前端分页组件适配
+
+**User Story:** 作为系统用户,我希望分页控件能正确显示总数和页码,以便了解数据规模并快速跳转。
+
+#### Acceptance Criteria
+
+1. WHEN 后端返回分页数据时,THE Work_Record_List SHALL 使用返回的 total 值更新分页组件
+2. WHEN 用户切换页码或每页数量时,THE Work_Record_List SHALL 向后端发送新的分页参数请求数据
+3. WHEN 筛选条件改变时,THE Work_Record_List SHALL 重置到第1页并重新请求数据
+
+### Requirement 4: 分页参数边界处理
+
+**User Story:** 作为系统开发者,我希望系统能正确处理异常的分页参数,以保证系统稳定性。
+
+#### Acceptance Criteria
+
+1. IF page 参数小于1,THEN THE Work_Record_Service SHALL 使用第1页
+2. IF page_size 参数小于1或大于100,THEN THE Work_Record_Service SHALL 使用默认值20
+3. IF 请求的页码超出总页数,THE Work_Record_Service SHALL 返回空数据列表和正确的分页元数据

+ 60 - 0
.kiro/specs/work-records-pagination/tasks.md

@@ -0,0 +1,60 @@
+# Implementation Plan: Work Records Backend Pagination
+
+## Overview
+
+实现工作记录列表的后端分页功能,修改后端服务层、API路由层和前端组件。
+
+## Tasks
+
+- [-] 1. 修改后端服务层实现分页查询
+  - [x] 1.1 修改 `WorkRecordService.get_all()` 方法添加分页参数
+    - 添加 `page` 和 `page_size` 参数,设置默认值
+    - 实现参数边界处理(page < 1 使用 1,page_size 超范围使用 20)
+    - 使用 SQLAlchemy 的 `count()` 获取总数
+    - 使用 `limit()` 和 `offset()` 实现分页查询
+    - 返回包含数据和分页元数据的字典
+    - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 4.1, 4.2, 4.3_
+
+  - [ ]* 1.2 编写属性测试:分页数据量正确性
+    - **Property 1: Pagination Data Count Correctness**
+    - **Validates: Requirements 1.4, 4.3**
+
+  - [ ]* 1.3 编写属性测试:分页元数据一致性
+    - **Property 2: Pagination Metadata Consistency**
+    - **Validates: Requirements 1.5**
+
+- [x] 2. 修改后端 API 路由层
+  - [x] 2.1 更新 `/work-records` GET 端点
+    - 添加 `page` 和 `page_size` 查询参数
+    - 更新 Swagger 文档
+    - 修改响应格式,添加 `pagination` 字段
+    - _Requirements: 1.1, 1.5_
+
+  - [ ]* 2.2 编写属性测试:筛选与分页组合
+    - **Property 3: Filter and Pagination Composition**
+    - **Validates: Requirements 2.1, 2.2**
+
+  - [ ]* 2.3 编写属性测试:排序顺序保持
+    - **Property 4: Sort Order Preservation**
+    - **Validates: Requirements 2.3**
+
+- [x] 3. Checkpoint - 后端测试验证
+  - 确保所有后端测试通过,如有问题请询问用户
+
+- [x] 4. 修改前端组件适配后端分页
+  - [x] 4.1 更新 `WorkRecordList` 组件
+    - 添加 `currentPage`、`pageSize`、`total` 状态
+    - 修改 `fetchWorkRecords` 函数发送分页参数
+    - 更新 Table 的 `pagination` 配置使用后端返回的 `total`
+    - 添加 `onChange` 处理器响应分页变化
+    - 筛选条件改变时重置到第1页
+    - _Requirements: 3.1, 3.2, 3.3_
+
+- [x] 5. Checkpoint - 完整功能验证
+  - 确保前后端联调正常,所有测试通过,如有问题请询问用户
+
+## Notes
+
+- 任务标记 `*` 为可选测试任务,可跳过以加快 MVP 开发
+- 每个任务引用具体需求以便追溯
+- 属性测试验证通用正确性属性

+ 32 - 5
backend/app/routes/work_record.py

@@ -57,6 +57,14 @@ batch_settlement_response = work_record_ns.model('BatchSettlementResponse', {
     'total_matched': fields.Integer(description='匹配的总记录数')
 })
 
+# Pagination model
+pagination_model = work_record_ns.model('Pagination', {
+    'total': fields.Integer(description='总记录数'),
+    'page': fields.Integer(description='当前页码'),
+    'page_size': fields.Integer(description='每页数量'),
+    'total_pages': fields.Integer(description='总页数')
+})
+
 # Response models
 success_response = work_record_ns.model('SuccessResponse', {
     'success': fields.Boolean(description='操作是否成功'),
@@ -64,6 +72,13 @@ success_response = work_record_ns.model('SuccessResponse', {
     'message': fields.String(description='消息')
 })
 
+paginated_response = work_record_ns.model('PaginatedResponse', {
+    'success': fields.Boolean(description='操作是否成功'),
+    'data': fields.List(fields.Nested(work_record_model), description='工作记录列表'),
+    'pagination': fields.Nested(pagination_model, description='分页信息'),
+    'message': fields.String(description='消息')
+})
+
 error_response = work_record_ns.model('ErrorResponse', {
     'success': fields.Boolean(description='操作是否成功'),
     'error': fields.String(description='错误信息'),
@@ -158,16 +173,20 @@ class WorkRecordList(Resource):
     @work_record_ns.param('start_date', '开始日期 (YYYY-MM-DD)', type=str)
     @work_record_ns.param('end_date', '结束日期 (YYYY-MM-DD)', type=str)
     @work_record_ns.param('is_settled', '按结算状态筛选 (true/false)', type=str)
-    @work_record_ns.response(200, 'Success', success_response)
+    @work_record_ns.param('page', '页码,从1开始', type=int, default=1)
+    @work_record_ns.param('page_size', '每页数量,默认20,最大100', type=int, default=20)
+    @work_record_ns.response(200, 'Success', paginated_response)
     @require_auth
     def get(self):
-        """获取工作记录列表(支持筛选)"""
+        """获取工作记录列表(支持筛选和分页)"""
         person_id = request.args.get('person_id', type=int)
         date = request.args.get('date')
         year = request.args.get('year', type=int)
         month = request.args.get('month', type=int)
         start_date = request.args.get('start_date')
         end_date = request.args.get('end_date')
+        page = request.args.get('page', 1, type=int)
+        page_size = request.args.get('page_size', 20, type=int)
         
         # Parse is_settled parameter
         is_settled_param = request.args.get('is_settled')
@@ -180,17 +199,25 @@ class WorkRecordList(Resource):
             start_date = date
             end_date = date
         
-        work_records = WorkRecordService.get_all(
+        result = WorkRecordService.get_all(
             person_id=person_id,
             start_date=start_date,
             end_date=end_date,
             year=year,
             month=month,
-            is_settled=is_settled
+            is_settled=is_settled,
+            page=page,
+            page_size=page_size
         )
         return {
             'success': True,
-            'data': work_records,
+            'data': result['data'],
+            'pagination': {
+                'total': result['total'],
+                'page': result['page'],
+                'page_size': result['page_size'],
+                'total_pages': result['total_pages']
+            },
             'message': 'Work records retrieved successfully'
         }, 200
 

+ 34 - 5
backend/app/services/work_record_service.py

@@ -58,8 +58,9 @@ class WorkRecordService:
         return work_record.to_dict(), None
     
     @staticmethod
-    def get_all(person_id=None, start_date=None, end_date=None, year=None, month=None, is_settled=None):
-        """Get all work records with optional filters.
+    def get_all(person_id=None, start_date=None, end_date=None, year=None, month=None, is_settled=None,
+                page=1, page_size=20):
+        """Get work records with pagination and filters.
         
         All filters are applied as intersection (AND logic).
         
@@ -70,10 +71,23 @@ class WorkRecordService:
             year: Filter by year (optional, used with month)
             month: Filter by month 1-12 (optional, used with year)
             is_settled: Filter by settlement status (optional, True/False)
+            page: Page number, starting from 1 (default: 1)
+            page_size: Number of records per page (default: 20, max: 100)
             
         Returns:
-            List of work record dictionaries
+            Dictionary with:
+            - data: List of work record dictionaries
+            - total: Total number of matching records
+            - page: Current page number
+            - page_size: Records per page
+            - total_pages: Total number of pages
         """
+        # Normalize pagination parameters
+        if page < 1:
+            page = 1
+        if page_size < 1 or page_size > 100:
+            page_size = 20
+        
         query = WorkRecord.query
         
         if person_id is not None:
@@ -104,8 +118,23 @@ class WorkRecordService:
                 end_date = datetime.fromisoformat(end_date).date()
             query = query.filter(WorkRecord.work_date <= end_date)
         
-        work_records = query.order_by(WorkRecord.work_date.desc(), WorkRecord.id).all()
-        return [wr.to_dict() for wr in work_records]
+        # Get total count before pagination
+        total = query.count()
+        
+        # Calculate total pages
+        total_pages = (total + page_size - 1) // page_size if total > 0 else 0
+        
+        # Apply sorting and pagination
+        offset = (page - 1) * page_size
+        work_records = query.order_by(WorkRecord.work_date.desc(), WorkRecord.id).limit(page_size).offset(offset).all()
+        
+        return {
+            'data': [wr.to_dict() for wr in work_records],
+            'total': total,
+            'page': page,
+            'page_size': page_size,
+            'total_pages': total_pages
+        }
     
     @staticmethod
     def get_by_id(work_record_id):

+ 8 - 7
backend/tests/test_export.py

@@ -125,16 +125,17 @@ class TestExportAPI:
         headers = [cell.value for cell in summary_sheet[1]]
         assert headers == ['人员', '供应商', '总金额', '结算状态']
         
-        # Check that summary has person+supplier+settlement rows plus total row
+        # Check that summary has person+supplier+settlement rows plus total rows (合计, 已结算, 未结算)
         data_rows = list(summary_sheet.iter_rows(min_row=2, values_only=True))
-        assert len(data_rows) >= 2  # At least 2 persons
+        assert len(data_rows) >= 5  # At least 2 persons + 3 total rows (合计, 已结算, 未结算)
         
-        # Last row should be total
-        last_row = data_rows[-1]
-        assert last_row[0] == '合计'
+        # Last 3 rows should be total rows: 合计, 已结算, 未结算
+        assert data_rows[-3][0] == '合计'
+        assert data_rows[-2][0] == '已结算'
+        assert data_rows[-1][0] == '未结算'
         
-        # Check settlement status column values (except total row)
-        for row in data_rows[:-1]:
+        # Check settlement status column values (except total rows)
+        for row in data_rows[:-3]:
             assert row[3] in ['已结算', '未结算']
     
     def test_monthly_export_missing_year(self, client, sample_data, auth_headers):

+ 44 - 1
backend/tests/test_work_record.py

@@ -30,7 +30,7 @@ class TestWorkRecordAPI:
         assert data['data']['total_price'] == 52.50
     
     def test_get_all_work_records(self, client, db_session, auth_headers):
-        """Test getting all work records."""
+        """Test getting all work records with pagination."""
         person_resp = client.post('/api/persons/create', json={'name': 'Test Worker'}, headers=auth_headers)
         person_id = person_resp.get_json()['data']['id']
         
@@ -50,6 +50,14 @@ class TestWorkRecordAPI:
         data = response.get_json()
         assert data['success'] is True
         assert len(data['data']) >= 1
+        # Verify pagination metadata is present
+        assert 'pagination' in data
+        assert 'total' in data['pagination']
+        assert 'page' in data['pagination']
+        assert 'page_size' in data['pagination']
+        assert 'total_pages' in data['pagination']
+        assert data['pagination']['page'] == 1
+        assert data['pagination']['page_size'] == 20
     
     def test_get_work_record_not_found(self, client, db_session, auth_headers):
         """Test getting a non-existent work record."""
@@ -66,3 +74,38 @@ class TestWorkRecordAPI:
         data = response.get_json()
         assert data['success'] is False
         assert data['code'] == 'UNAUTHORIZED'
+
+    def test_get_work_records_with_pagination_params(self, client, db_session, auth_headers):
+        """Test getting work records with custom pagination parameters."""
+        person_resp = client.post('/api/persons/create', json={'name': 'Test Worker'}, headers=auth_headers)
+        person_id = person_resp.get_json()['data']['id']
+        
+        item_resp = client.post('/api/items/create', json={'name': 'Test Item', 'unit_price': 10.0}, headers=auth_headers)
+        item_id = item_resp.get_json()['data']['id']
+        
+        # Create multiple work records
+        for i in range(5):
+            client.post('/api/work-records/create', json={
+                'person_id': person_id,
+                'item_id': item_id,
+                'work_date': f'2024-01-{15+i:02d}',
+                'quantity': i + 1
+            }, headers=auth_headers)
+        
+        # Test with custom page_size
+        response = client.get('/api/work-records?page=1&page_size=2', headers=auth_headers)
+        
+        assert response.status_code == 200
+        data = response.get_json()
+        assert data['success'] is True
+        assert len(data['data']) == 2
+        assert data['pagination']['page'] == 1
+        assert data['pagination']['page_size'] == 2
+        assert data['pagination']['total'] >= 5
+        assert data['pagination']['total_pages'] >= 3
+        
+        # Test page 2
+        response = client.get('/api/work-records?page=2&page_size=2', headers=auth_headers)
+        data = response.get_json()
+        assert data['pagination']['page'] == 2
+        assert len(data['data']) == 2

+ 42 - 7
frontend/src/components/WorkRecordList.jsx

@@ -23,6 +23,11 @@ function WorkRecordList() {
   const [selectedMonth, setSelectedMonth] = useState(null)
   const [selectedSettlement, setSelectedSettlement] = useState(null)
   
+  // Pagination states
+  const [currentPage, setCurrentPage] = useState(1)
+  const [pageSize, setPageSize] = useState(20)
+  const [total, setTotal] = useState(0)
+  
   // Batch settlement modal states
   const [batchModalVisible, setBatchModalVisible] = useState(false)
   const [batchPersonId, setBatchPersonId] = useState(null)
@@ -61,7 +66,10 @@ function WorkRecordList() {
     setLoading(true)
     setError(null)
     try {
-      const params = {}
+      const params = {
+        page: currentPage,
+        page_size: pageSize
+      }
       if (selectedPersonId) {
         params.person_id = selectedPersonId
       }
@@ -77,6 +85,10 @@ function WorkRecordList() {
       }
       const response = await workRecordApi.getAll(params)
       setWorkRecords(response.data || [])
+      // Update pagination from backend response
+      if (response.pagination) {
+        setTotal(response.pagination.total)
+      }
     } catch (error) {
       const errorMsg = error.message || '获取工作记录失败'
       setError(errorMsg)
@@ -93,7 +105,24 @@ function WorkRecordList() {
 
   useEffect(() => {
     fetchWorkRecords()
-  }, [selectedPersonId, selectedDate, selectedMonth, selectedSettlement])
+  }, [selectedPersonId, selectedDate, selectedMonth, selectedSettlement, currentPage, pageSize])
+
+  // Reset to page 1 when filter conditions change
+  const handleFilterChange = (setter) => (value) => {
+    setCurrentPage(1)
+    setter(value)
+  }
+
+  // Handle pagination change
+  const handleTableChange = (pagination) => {
+    if (pagination.current !== currentPage) {
+      setCurrentPage(pagination.current)
+    }
+    if (pagination.pageSize !== pageSize) {
+      setPageSize(pagination.pageSize)
+      setCurrentPage(1) // Reset to page 1 when page size changes
+    }
+  }
 
   const handleAdd = () => {
     setEditingRecord(null)
@@ -131,6 +160,7 @@ function WorkRecordList() {
     setSelectedDate(null)
     setSelectedMonth(null)
     setSelectedSettlement(null)
+    setCurrentPage(1)
   }
 
   const handleToggleSettlement = async (record) => {
@@ -311,7 +341,7 @@ function WorkRecordList() {
             allowClear
             style={{ width: '100%' }}
             value={selectedPersonId}
-            onChange={setSelectedPersonId}
+            onChange={handleFilterChange(setSelectedPersonId)}
             options={persons.map(p => ({ label: p.name, value: p.id }))}
           />
         </Col>
@@ -319,7 +349,7 @@ function WorkRecordList() {
           <DatePicker.MonthPicker
             placeholder="选择月份"
             value={selectedMonth}
-            onChange={setSelectedMonth}
+            onChange={handleFilterChange(setSelectedMonth)}
             format="YYYY-MM"
             style={{ width: '100%' }}
           />
@@ -328,7 +358,7 @@ function WorkRecordList() {
           <DatePicker
             placeholder="选择日期"
             value={selectedDate}
-            onChange={setSelectedDate}
+            onChange={handleFilterChange(setSelectedDate)}
             style={{ width: '100%' }}
           />
         </Col>
@@ -338,7 +368,7 @@ function WorkRecordList() {
             allowClear
             style={{ width: '100%' }}
             value={selectedSettlement}
-            onChange={setSelectedSettlement}
+            onChange={handleFilterChange(setSelectedSettlement)}
             options={[
               { label: '已结算', value: true },
               { label: '未结算', value: false }
@@ -359,11 +389,16 @@ function WorkRecordList() {
         tableLayout="auto"
         scroll={isMobile ? undefined : { x: 800 }}
         size={isMobile ? 'small' : 'middle'}
+        onChange={handleTableChange}
         pagination={{
+          current: currentPage,
+          pageSize: pageSize,
+          total: total,
           showSizeChanger: !isMobile,
           showQuickJumper: !isMobile,
           showTotal: (total) => `共 ${total} 条`,
-          size: isMobile ? 'small' : 'default'
+          size: isMobile ? 'small' : 'default',
+          pageSizeOptions: ['10', '20', '50', '100']
         }}
       />
       <WorkRecordForm