Parcourir la source

标注平台功能优化

lingmin_package@163.com il y a 4 semaines
Parent
commit
5a7caec908
26 fichiers modifiés avec 1293 ajouts et 234 suppressions
  1. 1 1
      backend/middleware/auth_middleware.py
  2. 133 67
      backend/routers/project.py
  3. 131 52
      backend/routers/task.py
  4. 34 20
      backend/routers/user.py
  5. 12 0
      backend/schemas/project.py
  6. 22 3
      backend/schemas/task.py
  7. 5 0
      backend/schemas/user.py
  8. 13 0
      web/apps/lq_label/src/atoms/task-atoms.ts
  9. 6 6
      web/apps/lq_label/src/components/layout/sidebar.tsx
  10. 1 1
      web/apps/lq_label/src/components/layout/top-bar.tsx
  11. 2 2
      web/apps/lq_label/src/components/project-form/project-form.tsx
  12. 1 1
      web/apps/lq_label/src/index.html
  13. 60 11
      web/apps/lq_label/src/services/api.ts
  14. 1 1
      web/apps/lq_label/src/views/editor-test/editor-test.tsx
  15. 68 0
      web/apps/lq_label/src/views/external-projects-view/external-projects-view.module.scss
  16. 104 5
      web/apps/lq_label/src/views/external-projects-view/external-projects-view.tsx
  17. 7 7
      web/apps/lq_label/src/views/home-view.tsx
  18. 63 0
      web/apps/lq_label/src/views/my-tasks-view/my-tasks-view.module.scss
  19. 99 7
      web/apps/lq_label/src/views/my-tasks-view/my-tasks-view.tsx
  20. 1 1
      web/apps/lq_label/src/views/project-config-view/project-config-view.tsx
  21. 68 0
      web/apps/lq_label/src/views/project-list-view/project-list-view.module.scss
  22. 128 27
      web/apps/lq_label/src/views/project-list-view/project-list-view.tsx
  23. 73 0
      web/apps/lq_label/src/views/task-list-view/task-list-view.module.scss
  24. 103 10
      web/apps/lq_label/src/views/task-list-view/task-list-view.tsx
  25. 68 0
      web/apps/lq_label/src/views/user-management-view/user-management-view.module.scss
  26. 89 12
      web/apps/lq_label/src/views/user-management-view/user-management-view.tsx

+ 1 - 1
backend/middleware/auth_middleware.py

@@ -155,7 +155,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
                 }
             )
         except Exception as e:
-            logger.error(f"认证过程发生错误: {e}")
+            logger.error("认证过程发生错误:%s", str(e))
             return JSONResponse(
                 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                 content={

+ 133 - 67
backend/routers/project.py

@@ -12,14 +12,15 @@ from typing import List, Optional
 from fastapi import APIRouter, HTTPException, status, Request, Query
 from database import get_db_connection
 from schemas.project import (
-    ProjectCreate, 
-    ProjectUpdate, 
+    ProjectCreate,
+    ProjectUpdate,
     ProjectResponse,
     ProjectStatus,
     ProjectSource,
     ProjectStatusUpdate,
     ProjectConfigUpdate,
     ProjectResponseExtended,
+    ProjectListPaginationResponse,
 )
 from models import Project
 
@@ -39,112 +40,170 @@ VALID_STATUS_TRANSITIONS = {
 }
 
 
-@router.get("", response_model=List[ProjectResponseExtended])
+@router.get("", response_model=ProjectListPaginationResponse)
 async def list_projects(
     request: Request,
     status_filter: Optional[ProjectStatus] = Query(None, alias="status", description="按状态筛选"),
     source_filter: Optional[ProjectSource] = Query(None, alias="source", description="按来源筛选"),
+    page: int = Query(1, ge=1, description="页码"),
+    page_size: int = Query(20, ge=1, le=100, description="每页数量"),
 ):
     """
     List projects with extended information.
-    
+
     For admin users: Returns all projects with their total task counts.
     For annotator users: Returns only projects that have tasks assigned to them,
                          with task counts reflecting only their assigned tasks.
-    
+
     Query Parameters:
         status: Filter by project status (draft, configuring, ready, in_progress, completed)
         source: Filter by project source (internal, external)
-    
+        page: Page number (default: 1)
+        page_size: Number of items per page (default: 20, max: 100)
+
     Requires authentication.
     """
     user = request.state.user
     user_id = user["id"]
     user_role = user["role"]
-    
+
     with get_db_connection() as conn:
         cursor = conn.cursor()
-        
+
+        # Build base query
+        base_from = """
+            FROM projects p
+            LEFT JOIN tasks t ON p.id = t.project_id
+        """
+
         if user_role == "admin":
             # 管理员:返回所有项目及其全部任务统计
-            query = """
-                SELECT 
-                    p.id,
-                    p.name,
-                    p.description,
-                    p.config,
-                    p.task_type,
-                    p.status,
-                    p.source,
-                    p.external_id,
-                    p.created_at,
-                    p.updated_at,
-                    COUNT(t.id) as task_count,
-                    SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed_task_count,
-                    SUM(CASE WHEN t.assigned_to IS NOT NULL THEN 1 ELSE 0 END) as assigned_task_count
-                FROM projects p
-                LEFT JOIN tasks t ON p.id = t.project_id
+            select_cols = """
+                p.id,
+                p.name,
+                p.description,
+                p.config,
+                p.task_type,
+                p.status,
+                p.source,
+                p.external_id,
+                p.created_at,
+                p.updated_at,
+                COUNT(t.id) as task_count,
+                SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed_task_count,
+                SUM(CASE WHEN t.assigned_to IS NOT NULL THEN 1 ELSE 0 END) as assigned_task_count
             """
-            
+
             conditions = []
             params = []
-            
+
             if status_filter:
                 conditions.append("p.status = ?")
                 params.append(status_filter.value)
-            
+
             if source_filter:
                 conditions.append("p.source = ?")
                 params.append(source_filter.value)
-            
-            if conditions:
-                query += " WHERE " + " AND ".join(conditions)
-            
-            query += " GROUP BY p.id ORDER BY p.created_at DESC"
+
+            where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
+
+            # Count query for pagination
+            count_query = f"""
+                SELECT COUNT(DISTINCT p.id) as total
+                {base_from}
+                {where_clause}
+            """
+
+            cursor.execute(count_query, params)
+            total = cursor.fetchone()["total"]
+
+            # Calculate pagination
+            total_pages = (total + page_size - 1) // page_size
+            offset = (page - 1) * page_size
+
+            # Main query with pagination
+            query = f"""
+                SELECT {select_cols}
+                {base_from}
+                {where_clause}
+                GROUP BY p.id
+                ORDER BY p.created_at DESC
+                LIMIT ? OFFSET ?
+            """
+            params.extend([page_size, offset])
         else:
             # 标注员:只返回有分配给他们任务的项目,任务数量只统计分配给他们的任务
-            query = """
-                SELECT 
-                    p.id,
-                    p.name,
-                    p.description,
-                    p.config,
-                    p.task_type,
-                    p.status,
-                    p.source,
-                    p.external_id,
-                    p.created_at,
-                    p.updated_at,
-                    COUNT(t.id) as task_count,
-                    SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed_task_count,
-                    COUNT(t.id) as assigned_task_count
-                FROM projects p
-                INNER JOIN tasks t ON p.id = t.project_id AND t.assigned_to = ?
+            select_cols = """
+                p.id,
+                p.name,
+                p.description,
+                p.config,
+                p.task_type,
+                p.status,
+                p.source,
+                p.external_id,
+                p.created_at,
+                p.updated_at,
+                COUNT(t.id) as task_count,
+                SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed_task_count,
+                COUNT(t.id) as assigned_task_count
             """
-            logger.info(f"list_projects user_id: {user_id}")
+
+            conditions = ["t.assigned_to = ?"]
             params = [user_id]
-            conditions = []
-            
+
             if status_filter:
                 conditions.append("p.status = ?")
                 params.append(status_filter.value)
-            
+
             if source_filter:
                 conditions.append("p.source = ?")
                 params.append(source_filter.value)
-            
-            if conditions:
-                query += " WHERE " + " AND ".join(conditions)
-            
-            query += " GROUP BY p.id HAVING COUNT(t.id) > 0 ORDER BY p.created_at DESC"
-        
 
-        #logger.info(f"list_projects query: {query}")
-        logger.info(f"list_projects params: {params}")
-        
+            # 为标注员构建 where 子句(用于主查询)
+            # 从 conditions 中移除 t.assigned_to = ?,因为它已经在 JOIN 中了
+            main_query_conditions = [cond for cond in conditions if cond != "t.assigned_to = ?"]
+            where_clause = " WHERE " + " AND ".join(main_query_conditions) if main_query_conditions else ""
+
+            # 为 count 查询构建 where 子句(移除 t.assigned_to = ? 条件)
+            count_where_clause = where_clause
+
+            # Count query for pagination
+            count_query = f"""
+                SELECT COUNT(DISTINCT p.id) as total
+                FROM projects p
+                INNER JOIN tasks t ON p.id = t.project_id AND t.assigned_to = ?
+                {count_where_clause}
+            """
+            count_params = [user_id]
+            if status_filter:
+                count_params.append(status_filter.value)
+            if source_filter:
+                count_params.append(source_filter.value)
+
+            cursor.execute(count_query, count_params)
+            total = cursor.fetchone()["total"]
+
+            # Calculate pagination
+            total_pages = (total + page_size - 1) // page_size
+            offset = (page - 1) * page_size
+
+            # Main query with pagination
+            query = f"""
+                SELECT {select_cols}
+                FROM projects p
+                INNER JOIN tasks t ON p.id = t.project_id AND t.assigned_to = ?
+                {where_clause}
+                GROUP BY p.id
+                HAVING COUNT(t.id) > 0
+                ORDER BY p.created_at DESC
+                LIMIT ? OFFSET ?
+            """
+            params.extend([page_size, offset])
+
         cursor.execute(query, params)
         rows = cursor.fetchall()
-        
+
         projects = []
         for row in rows:
             projects.append(ProjectResponseExtended(
@@ -162,9 +221,16 @@ async def list_projects(
                 completed_task_count=row["completed_task_count"] or 0,
                 assigned_task_count=row["assigned_task_count"] or 0,
             ))
-        
-        #logger.info(msg=f"list_projects : {projects}")
-        return projects
+
+        return ProjectListPaginationResponse(
+            projects=projects,
+            total=total,
+            page=page,
+            page_size=page_size,
+            total_pages=total_pages,
+            has_next=page < total_pages,
+            has_prev=page > 1
+        )
 
 
 @router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)

+ 131 - 52
backend/routers/task.py

@@ -13,6 +13,7 @@ from schemas.task import (
     TaskAssignmentResponse, MyTasksResponse,
     AssignmentPreviewRequest, AssignmentPreviewResponse,
     DispatchRequest, DispatchResponse,
+    TaskListPaginationResponse,
 )
 from models import Task
 from datetime import datetime
@@ -36,33 +37,36 @@ def calculate_progress(data_str: str, annotation_count: int) -> float:
         return 0.0
 
 
-@router.get("", response_model=List[TaskResponse])
+@router.get("", response_model=TaskListPaginationResponse)
 async def list_tasks(
     request: Request,
     project_id: Optional[str] = Query(None, description="Filter by project ID"),
     status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
-    assigned_to: Optional[str] = Query(None, description="Filter by assigned user")
+    assigned_to: Optional[str] = Query(None, description="Filter by assigned user"),
+    page: int = Query(1, ge=1, description="Page number"),
+    page_size: int = Query(20, ge=1, le=100, description="Items per page")
 ):
     """
-    List tasks with optional filters.
-    
+    List tasks with optional filters and pagination.
+
     For admin users: Returns all tasks matching the filters.
     For annotator users: Returns only tasks assigned to them (ignores assigned_to filter).
-    
+
     Requires authentication.
     """
     user = request.state.user
     user_id = user["id"]
     user_role = user["role"]
-    
+
     with get_db_connection() as conn:
         cursor = conn.cursor()
-        
+
         # Build query with filters
-        query = """
-            SELECT 
+        base_query = """
+            SELECT
                 t.id,
                 t.project_id,
+                p.name as project_name,
                 t.name,
                 t.data,
                 t.status,
@@ -71,40 +75,75 @@ async def list_tasks(
                 COUNT(a.id) as annotation_count
             FROM tasks t
             LEFT JOIN annotations a ON t.id = a.task_id
+            LEFT JOIN projects p ON t.project_id = p.id
             WHERE 1=1
         """
         params = []
-        
+
         if project_id:
-            query += " AND t.project_id = ?"
+            base_query += " AND t.project_id = ?"
             params.append(project_id)
-        
+
         if status_filter:
-            query += " AND t.status = ?"
+            base_query += " AND t.status = ?"
             params.append(status_filter)
-        
+
         # 标注员只能看到分配给自己的任务
         if user_role != "admin":
-            query += " AND t.assigned_to = ?"
+            base_query += " AND t.assigned_to = ?"
             params.append(user_id)
         elif assigned_to:
             # 管理员可以按 assigned_to 过滤
-            query += " AND t.assigned_to = ?"
+            base_query += " AND t.assigned_to = ?"
             params.append(assigned_to)
-        
-        query += " GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at ORDER BY t.created_at DESC"
-        
-        cursor.execute(query, tuple(params))
+
+        # 计算总数
+        count_query = f"""
+            SELECT COUNT(DISTINCT t.id) as total
+            FROM tasks t
+            LEFT JOIN projects p ON t.project_id = p.id
+            WHERE 1=1
+        """
+        count_params = []
+
+        if project_id:
+            count_query += " AND t.project_id = ?"
+            count_params.append(project_id)
+
+        if status_filter:
+            count_query += " AND t.status = ?"
+            count_params.append(status_filter)
+
+        if user_role != "admin":
+            count_query += " AND t.assigned_to = ?"
+            count_params.append(user_id)
+        elif assigned_to:
+            count_query += " AND t.assigned_to = ?"
+            count_params.append(assigned_to)
+
+        cursor.execute(count_query, tuple(count_params))
+        total = cursor.fetchone()["total"]
+
+        # 计算分页
+        total_pages = (total + page_size - 1) // page_size
+        offset = (page - 1) * page_size
+
+        # 添加排序和分页
+        base_query += " GROUP BY t.id, t.project_id, p.name, t.name, t.data, t.status, t.assigned_to, t.created_at ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
+        params.extend([page_size, offset])
+
+        cursor.execute(base_query, tuple(params))
         rows = cursor.fetchall()
-        
+
         tasks = []
         for row in rows:
             data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
             progress = calculate_progress(row["data"], row["annotation_count"])
-            
+
             tasks.append(TaskResponse(
                 id=row["id"],
                 project_id=row["project_id"],
+                project_name=row["project_name"],
                 name=row["name"],
                 data=data,
                 status=row["status"],
@@ -112,8 +151,16 @@ async def list_tasks(
                 created_at=row["created_at"],
                 progress=progress
             ))
-        
-        return tasks
+
+        return TaskListPaginationResponse(
+            tasks=tasks,
+            total=total,
+            page=page,
+            page_size=page_size,
+            total_pages=total_pages,
+            has_next=page < total_pages,
+            has_prev=page > 1
+        )
 
 
 @router.post("", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
@@ -168,39 +215,72 @@ async def create_task(request: Request, task: TaskCreate):
 async def get_my_tasks(
     request: Request,
     project_id: Optional[str] = Query(None, description="Filter by project ID"),
-    status_filter: Optional[str] = Query(None, alias="status", description="Filter by status")
+    status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
+    page: int = Query(1, ge=1, description="Page number"),
+    page_size: int = Query(20, ge=1, le=100, description="Items per page")
 ):
     """
     Get tasks assigned to the current user.
     Requires authentication.
-    
+
     标注人员只能看到分配给自己的任务。
     """
     user = request.state.user
     user_id = user["id"]
-    
+
     with get_db_connection() as conn:
         cursor = conn.cursor()
-        
+
         # 构建查询条件
         where_clauses = ["t.assigned_to = ?"]
         params = [user_id]
-        
+
         if project_id:
             where_clauses.append("t.project_id = ?")
             params.append(project_id)
-        
+
         if status_filter:
             where_clauses.append("t.status = ?")
             params.append(status_filter)
-        
+
         where_sql = " AND ".join(where_clauses)
-        
-        # 查询任务列表
+
+        # 查询总数
+        count_query = f"""
+            SELECT COUNT(*) as total
+            FROM tasks t
+            WHERE {where_sql}
+        """
+        cursor.execute(count_query, tuple(params))
+        total = cursor.fetchone()["total"]
+
+        # 查询各状态的任务数(不受分页影响)
+        status_count_query = f"""
+            SELECT
+                SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
+                SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
+                SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending
+            FROM tasks t
+            WHERE {where_sql}
+        """
+        cursor.execute(status_count_query, tuple(params))
+        status_row = cursor.fetchone()
+        completed = int(status_row["completed"] or 0)
+        in_progress = int(status_row["in_progress"] or 0)
+        pending = int(status_row["pending"] or 0)
+
+        # 计算分页信息
+        total_pages = (total + page_size - 1) // page_size
+        has_next = page < total_pages
+        has_prev = page > 1
+        skip = (page - 1) * page_size
+
+        # 查询任务列表(带项目名称)
         query = f"""
-            SELECT 
+            SELECT
                 t.id,
                 t.project_id,
+                p.name as project_name,
                 t.name,
                 t.data,
                 t.status,
@@ -209,34 +289,28 @@ async def get_my_tasks(
                 COUNT(a.id) as annotation_count
             FROM tasks t
             LEFT JOIN annotations a ON t.id = a.task_id
+            LEFT JOIN projects p ON t.project_id = p.id
             WHERE {where_sql}
-            GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at
+            GROUP BY t.id, t.project_id, p.name, t.name, t.data, t.status, t.assigned_to, t.created_at
             ORDER BY t.created_at DESC
+            LIMIT ? OFFSET ?
         """
-        
+
+        params.extend([page_size, skip])
         cursor.execute(query, tuple(params))
         rows = cursor.fetchall()
-        
+
         tasks = []
-        completed = 0
-        in_progress = 0
-        pending = 0
-        
         for row in rows:
             data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
             progress = calculate_progress(row["data"], row["annotation_count"])
-            
+
             task_status = row["status"]
-            if task_status == "completed":
-                completed += 1
-            elif task_status == "in_progress":
-                in_progress += 1
-            else:
-                pending += 1
-            
+
             tasks.append(TaskResponse(
                 id=row["id"],
                 project_id=row["project_id"],
+                project_name=row["project_name"],
                 name=row["name"],
                 data=data,
                 status=task_status,
@@ -244,13 +318,18 @@ async def get_my_tasks(
                 created_at=row["created_at"],
                 progress=progress
             ))
-        
+
         return MyTasksResponse(
             tasks=tasks,
-            total=len(tasks),
+            total=total,
             completed=completed,
             in_progress=in_progress,
-            pending=pending
+            pending=pending,
+            page=page,
+            page_size=page_size,
+            total_pages=total_pages,
+            has_next=has_next,
+            has_prev=has_prev
         )
 
 

+ 34 - 20
backend/routers/user.py

@@ -106,51 +106,57 @@ async def list_users(
     request: Request,
     role: Optional[str] = Query(None, description="按角色筛选"),
     search: Optional[str] = Query(None, description="按用户名或邮箱搜索"),
-    skip: int = Query(0, ge=0, description="跳过记录数"),
-    limit: int = Query(50, ge=1, le=100, description="返回记录数")
+    page: int = Query(1, ge=1, description="当前页码"),
+    page_size: int = Query(20, ge=1, le=100, description="每页数量")
 ):
     """
     获取用户列表(管理员权限)。
-    
+
     支持按角色筛选和关键词搜索。
-    
+
     Args:
         request: FastAPI Request 对象
         role: 角色筛选(admin/annotator/viewer)
         search: 搜索关键词(用户名或邮箱)
-        skip: 分页偏移
-        limit: 每页数量
-        
+        page: 当前页码
+        page_size: 每页数量
+
     Returns:
-        用户列表和总数
+        用户列表和分页信息
     """
     require_admin(request)
-    
+
     with get_db_connection() as conn:
         cursor = conn.cursor()
-        
+
         # 构建查询条件
         where_clauses = []
         params = []
-        
+
         if role:
             where_clauses.append("role = ?")
             params.append(role)
-        
+
         if search:
             where_clauses.append("(username LIKE ? OR email LIKE ?)")
             search_pattern = f"%{search}%"
             params.extend([search_pattern, search_pattern])
-        
+
         where_sql = ""
         if where_clauses:
             where_sql = "WHERE " + " AND ".join(where_clauses)
-        
+
         # 查询总数
         count_sql = f"SELECT COUNT(*) as total FROM users {where_sql}"
         cursor.execute(count_sql, tuple(params))
         total = cursor.fetchone()["total"]
-        
+
+        # 计算分页信息
+        total_pages = (total + page_size - 1) // page_size
+        has_next = page < total_pages
+        has_prev = page > 1
+        skip = (page - 1) * page_size
+
         # 查询用户列表
         query_sql = f"""
             SELECT id, username, email, role, created_at
@@ -159,15 +165,15 @@ async def list_users(
             ORDER BY created_at DESC
             LIMIT ? OFFSET ?
         """
-        params.extend([limit, skip])
+        params.extend([page_size, skip])
         cursor.execute(query_sql, tuple(params))
         rows = cursor.fetchall()
-        
+
         users = []
         for row in rows:
             # 获取每个用户的任务统计
             task_stats = get_user_task_stats(cursor, row["id"])
-            
+
             users.append(UserWithStatsResponse(
                 id=row["id"],
                 username=row["username"],
@@ -176,8 +182,16 @@ async def list_users(
                 created_at=row["created_at"],
                 task_stats=task_stats
             ))
-        
-        return UserListResponse(users=users, total=total)
+
+        return UserListResponse(
+            users=users,
+            total=total,
+            page=page,
+            page_size=page_size,
+            total_pages=total_pages,
+            has_next=has_next,
+            has_prev=has_prev
+        )
 
 
 @router.get("/annotators", response_model=List[AssignableUserResponse])

+ 12 - 0
backend/schemas/project.py

@@ -168,3 +168,15 @@ class ProjectResponseExtended(BaseModel):
                 "assigned_task_count": 100
             }
         }
+
+
+class ProjectListPaginationResponse(BaseModel):
+    """Schema for paginated project list response."""
+
+    projects: List[ProjectResponseExtended] = Field(..., description="List of projects")
+    total: int = Field(..., description="Total number of projects")
+    page: int = Field(..., description="Current page number")
+    page_size: int = Field(..., description="Number of items per page")
+    total_pages: int = Field(..., description="Total number of pages")
+    has_next: bool = Field(..., description="Whether there is a next page")
+    has_prev: bool = Field(..., description="Whether there is a previous page")

+ 22 - 3
backend/schemas/task.py

@@ -49,21 +49,23 @@ class TaskUpdate(BaseModel):
 
 class TaskResponse(BaseModel):
     """Schema for task response."""
-    
+
     id: str = Field(..., description="Task unique identifier")
     project_id: str = Field(..., description="Project ID this task belongs to")
+    project_name: Optional[str] = Field(None, description="Project name")
     name: str = Field(..., description="Task name")
     data: dict = Field(..., description="Task data (JSON object)")
     status: str = Field(..., description="Task status (pending, in_progress, completed)")
     assigned_to: Optional[str] = Field(None, description="User ID assigned to this task")
     created_at: datetime = Field(..., description="Task creation timestamp")
     progress: float = Field(default=0.0, description="Task completion progress (0.0 to 1.0)")
-    
+
     class Config:
         json_schema_extra = {
             "example": {
                 "id": "task_456def",
                 "project_id": "proj_123abc",
+                "project_name": "Image Classification Project",
                 "name": "Annotate Image Batch 1",
                 "data": {
                     "image_url": "https://example.com/image1.jpg",
@@ -77,6 +79,18 @@ class TaskResponse(BaseModel):
         }
 
 
+class TaskListPaginationResponse(BaseModel):
+    """Schema for paginated task list response."""
+
+    tasks: List[TaskResponse] = Field(..., description="List of tasks")
+    total: int = Field(..., description="Total number of tasks")
+    page: int = Field(..., description="Current page number")
+    page_size: int = Field(..., description="Number of items per page")
+    total_pages: int = Field(..., description="Total number of pages")
+    has_next: bool = Field(..., description="Whether there is a next page")
+    has_prev: bool = Field(..., description="Whether there is a previous page")
+
+
 class TaskAssignRequest(BaseModel):
     """Schema for assigning a task to a user."""
     
@@ -136,12 +150,17 @@ class BatchAssignResponse(BaseModel):
 
 class MyTasksResponse(BaseModel):
     """Schema for current user's tasks response."""
-    
+
     tasks: List[TaskResponse] = Field(..., description="List of tasks assigned to current user")
     total: int = Field(..., description="Total number of tasks")
     completed: int = Field(..., description="Number of completed tasks")
     in_progress: int = Field(..., description="Number of in-progress tasks")
     pending: int = Field(..., description="Number of pending tasks")
+    page: int = Field(default=1, description="Current page number")
+    page_size: int = Field(default=20, description="Number of items per page")
+    total_pages: int = Field(default=0, description="Total number of pages")
+    has_next: bool = Field(default=False, description="Whether there is a next page")
+    has_prev: bool = Field(default=False, description="Whether there is a previous page")
 
 
 # ============== 任务分发相关Schema ==============

+ 5 - 0
backend/schemas/user.py

@@ -47,6 +47,11 @@ class UserListResponse(BaseModel):
     """用户列表响应"""
     users: List[UserWithStatsResponse] = Field(..., description="用户列表")
     total: int = Field(..., description="总数")
+    page: int = Field(..., description="当前页码")
+    page_size: int = Field(..., description="每页数量")
+    total_pages: int = Field(..., description="总页数")
+    has_next: bool = Field(..., description="是否有下一页")
+    has_prev: bool = Field(..., description="是否有上一页")
 
 
 class UserStatsResponse(BaseModel):

+ 13 - 0
web/apps/lq_label/src/atoms/task-atoms.ts

@@ -15,6 +15,7 @@ export type TaskStatus = 'pending' | 'in_progress' | 'completed';
 export interface Task {
   id: string;
   project_id: string;
+  project_name?: string;
   name: string;
   data: Record<string, any>;
   status: TaskStatus;
@@ -32,6 +33,18 @@ export interface TaskFilter {
   assignedTo: string | null;
 }
 
+/**
+ * Pagination state interface
+ */
+export interface PaginationState {
+  page: number;
+  page_size: number;
+  total: number;
+  total_pages: number;
+  has_next: boolean;
+  has_prev: boolean;
+}
+
 /**
  * Atom storing the list of all tasks
  */

+ 6 - 6
web/apps/lq_label/src/components/layout/sidebar.tsx

@@ -62,12 +62,12 @@ const menuItems: MenuItem[] = [
     path: '/my-tasks',
     icon: <ListChecks size={20} />,
   },
-  {
-    id: 'annotations',
-    label: '我的标注',
-    path: '/annotations',
-    icon: <FileCheck size={20} />,
-  },
+  // {
+  //   id: 'annotations',
+  //   label: '我的标注',
+  //   path: '/annotations',
+  //   icon: <FileCheck size={20} />,
+  // },
   {
     id: 'users',
     label: '用户管理',

+ 1 - 1
web/apps/lq_label/src/components/layout/top-bar.tsx

@@ -21,7 +21,7 @@ export const TopBar: React.FC = () => {
     if (path.startsWith('/projects/')) return '项目详情';
     if (path === '/tasks') return '任务管理';
     if (path.includes('/annotate')) return '标注任务';
-    if (path === '/annotations') return '我的标注';
+    // if (path === '/annotations') return '我的标注';
     if (path === '/editor-test') return '编辑器测试';
     
     return '标注平台';

+ 2 - 2
web/apps/lq_label/src/components/project-form/project-form.tsx

@@ -532,7 +532,7 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
               onChange={(e) => handleFieldChange('config', e.target.value)}
               onBlur={() => handleFieldBlur('config')}
               className={`${styles.textarea} ${styles.textareaCode} ${formErrors.config ? styles.error : ''}`}
-              placeholder='输入 Label Studio 配置 XML,例如:<View><Text name="text" value="$text"/></View>'
+              placeholder='输入多类标注配置 XML,例如:<View><Text name="text" value="$text"/></View>'
               rows={15}
               disabled={isSubmitting}
               aria-invalid={!!formErrors.config}
@@ -544,7 +544,7 @@ export const ProjectForm: React.FC<ProjectFormProps> = ({
               </span>
             )}
             <span className={styles.hint}>
-              请输入有效的 Label Studio XML 配置
+              请输入有效的多类标注 XML 配置
             </span>
           </div>
         </div>

+ 1 - 1
web/apps/lq_label/src/index.html

@@ -2,7 +2,7 @@
 <html lang="en">
   <head>
     <meta charset="utf-8" />
-    <title>LqLabel</title>
+    <title>四川路桥标注平台</title>
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link rel="icon" type="image/x-icon" href="favicon.ico" />
   </head>

+ 60 - 11
web/apps/lq_label/src/services/api.ts

@@ -328,18 +328,35 @@ export interface ProjectUpdateData {
   config?: string;
 }
 
+/**
+ * Project list response with pagination
+ */
+export interface ProjectListResponse {
+  projects: Project[];
+  total: number;
+  page: number;
+  page_size: number;
+  total_pages: number;
+  has_next: boolean;
+  has_prev: boolean;
+}
+
 /**
  * List all projects
  */
 export async function listProjects(filters?: {
   status?: string;
   source?: string;
-}): Promise<Project[]> {
+  page?: number;
+  page_size?: number;
+}): Promise<ProjectListResponse> {
   const params = new URLSearchParams();
   if (filters?.status) params.append('status', filters.status);
   if (filters?.source) params.append('source', filters.source);
-  
-  const response = await apiClient.get<Project[]>('/api/projects', { params });
+  if (filters?.page) params.append('page', String(filters.page));
+  if (filters?.page_size) params.append('page_size', String(filters.page_size));
+
+  const response = await apiClient.get<ProjectListResponse>('/api/projects', { params });
   return response.data;
 }
 
@@ -476,7 +493,8 @@ export async function dispatchTasks(
  * Get tasks for a specific project
  */
 export async function getProjectTasks(projectId: string): Promise<Task[]> {
-  return listTasks({ project_id: projectId });
+  const response = await listTasks({ project_id: projectId });
+  return response.tasks;
 }
 
 // ============================================================================
@@ -510,18 +528,35 @@ export interface TaskListFilters {
   project_id?: string;
   status?: string;
   assigned_to?: string;
+  page?: number;
+  page_size?: number;
+}
+
+/**
+ * Paginated task list response
+ */
+export interface TaskListResponse {
+  tasks: Task[];
+  total: number;
+  page: number;
+  page_size: number;
+  total_pages: number;
+  has_next: boolean;
+  has_prev: boolean;
 }
 
 /**
- * List all tasks with optional filters
+ * List all tasks with optional filters and pagination
  */
-export async function listTasks(filters?: TaskListFilters): Promise<Task[]> {
+export async function listTasks(filters?: TaskListFilters): Promise<TaskListResponse> {
   const params = new URLSearchParams();
   if (filters?.project_id) params.append('project_id', filters.project_id);
   if (filters?.status) params.append('status', filters.status);
   if (filters?.assigned_to) params.append('assigned_to', filters.assigned_to);
+  if (filters?.page) params.append('page', String(filters.page));
+  if (filters?.page_size) params.append('page_size', String(filters.page_size));
 
-  const response = await apiClient.get<Task[]>('/api/tasks', { params });
+  const response = await apiClient.get<TaskListResponse>('/api/tasks', { params });
   return response.data;
 }
 
@@ -687,6 +722,11 @@ export interface UserWithStats {
 export interface UserListResponse {
   users: UserWithStats[];
   total: number;
+  page: number;
+  page_size: number;
+  total_pages: number;
+  has_next: boolean;
+  has_prev: boolean;
 }
 
 /**
@@ -706,8 +746,8 @@ export interface AssignableUser {
 export interface UserListFilters {
   role?: UserRole;
   search?: string;
-  skip?: number;
-  limit?: number;
+  page?: number;
+  page_size?: number;
 }
 
 /**
@@ -717,8 +757,8 @@ export async function listUsers(filters?: UserListFilters): Promise<UserListResp
   const params = new URLSearchParams();
   if (filters?.role) params.append('role', filters.role);
   if (filters?.search) params.append('search', filters.search);
-  if (filters?.skip !== undefined) params.append('skip', String(filters.skip));
-  if (filters?.limit !== undefined) params.append('limit', String(filters.limit));
+  if (filters?.page !== undefined) params.append('page', String(filters.page));
+  if (filters?.page_size !== undefined) params.append('page_size', String(filters.page_size));
 
   const response = await apiClient.get<UserListResponse>('/api/users', { params });
   return response.data;
@@ -811,6 +851,11 @@ export interface MyTasksResponse {
   completed: number;
   in_progress: number;
   pending: number;
+  page: number;
+  page_size: number;
+  total_pages: number;
+  has_next: boolean;
+  has_prev: boolean;
 }
 
 /**
@@ -846,10 +891,14 @@ export async function batchAssignTasks(
 export async function getMyTasks(filters?: {
   project_id?: string;
   status?: string;
+  page?: number;
+  page_size?: number;
 }): Promise<MyTasksResponse> {
   const params = new URLSearchParams();
   if (filters?.project_id) params.append('project_id', filters.project_id);
   if (filters?.status) params.append('status', filters.status);
+  if (filters?.page !== undefined) params.append('page', String(filters.page));
+  if (filters?.page_size !== undefined) params.append('page_size', String(filters.page_size));
 
   const response = await apiClient.get<MyTasksResponse>('/api/tasks/my-tasks', { params });
   return response.data;

+ 1 - 1
web/apps/lq_label/src/views/editor-test/editor-test.tsx

@@ -182,7 +182,7 @@ export const EditorTest: React.FC = () => {
           </Button>
           <div className={styles.headerInfo}>
             <h1 className={styles.headerTitle}>
-              LabelStudio 编辑器测试
+              多类标注编辑器测试
             </h1>
             <p className={styles.headerSubtitle}>
               独立测试页面,用于验证编辑器样式和功能

+ 68 - 0
web/apps/lq_label/src/views/external-projects-view/external-projects-view.module.scss

@@ -428,3 +428,71 @@
     cursor: not-allowed;
   }
 }
+
+// Pagination styles
+.pagination {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background: var(--theme-card-background);
+  border: 1px solid var(--theme-border);
+  border-radius: 12px;
+  margin-top: 16px;
+}
+
+.paginationInfo {
+  font-size: 14px;
+  color: var(--theme-paragraph);
+}
+
+.paginationControls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.pageSizeSelect {
+  padding: 8px 12px;
+  background: var(--theme-background-secondary);
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover {
+    border-color: var(--theme-button);
+  }
+
+  &:focus {
+    outline: none;
+    border-color: var(--theme-button);
+  }
+}
+
+.paginationButton {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 8px 12px;
+  background: transparent;
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover:not(:disabled) {
+    background: var(--theme-background-secondary);
+    border-color: var(--theme-button);
+    color: var(--theme-button);
+  }
+
+  &:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+  }
+}

+ 104 - 5
web/apps/lq_label/src/views/external-projects-view/external-projects-view.tsx

@@ -7,7 +7,7 @@
  * 
  * Requirements: 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8
  */
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
 import { useNavigate } from 'react-router-dom';
 import {
   Search,
@@ -17,6 +17,8 @@ import {
   Send,
   Eye,
   ExternalLink,
+  ChevronLeft,
+  ChevronRight,
 } from 'lucide-react';
 import {
   PROJECT_STATUS_CONFIG,
@@ -28,16 +30,38 @@ import { ProjectDetailModal } from '../../components/project-detail-modal';
 import { TaskDispatchDialog } from '../../components/task-dispatch-dialog';
 import styles from './external-projects-view.module.scss';
 
+/**
+ * Pagination state interface
+ */
+interface PaginationState {
+  page: number;
+  page_size: number;
+  total: number;
+  total_pages: number;
+  has_next: boolean;
+  has_prev: boolean;
+}
+
 export const ExternalProjectsView: React.FC = () => {
   const navigate = useNavigate();
-  
+
   // State
   const [projects, setProjects] = useState<Project[]>([]);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
   const [statusFilter, setStatusFilter] = useState<ProjectStatus | ''>('');
   const [searchQuery, setSearchQuery] = useState('');
-  
+
+  // Pagination state
+  const [pagination, setPagination] = useState<PaginationState>({
+    page: 1,
+    page_size: 20,
+    total: 0,
+    total_pages: 0,
+    has_next: false,
+    has_prev: false,
+  });
+
   // Dialog state
   const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
   const [isDispatchDialogOpen, setIsDispatchDialogOpen] = useState(false);
@@ -47,7 +71,7 @@ export const ExternalProjectsView: React.FC = () => {
   // Load external projects on mount and when filter changes
   useEffect(() => {
     loadProjects();
-  }, [statusFilter]);
+  }, [statusFilter, pagination.page, pagination.page_size]);
 
   const loadProjects = async () => {
     try {
@@ -56,8 +80,18 @@ export const ExternalProjectsView: React.FC = () => {
       const data = await listProjects({
         source: 'external',
         status: statusFilter || undefined,
+        page: pagination.page,
+        page_size: pagination.page_size,
+      });
+      setProjects(data.projects);
+      setPagination({
+        page: data.page,
+        page_size: data.page_size,
+        total: data.total,
+        total_pages: data.total_pages,
+        has_next: data.has_next,
+        has_prev: data.has_prev,
       });
-      setProjects(data);
     } catch (err: any) {
       setError(err.message || '加载外部项目列表失败');
     } finally {
@@ -81,8 +115,18 @@ export const ExternalProjectsView: React.FC = () => {
 
   const handleStatusFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
     setStatusFilter(e.target.value as ProjectStatus | '');
+    setPagination(prev => ({ ...prev, page: 1 })); // Reset to first page when changing filter
   };
 
+  // Pagination handlers
+  const handlePageChange = useCallback((newPage: number) => {
+    setPagination(prev => ({ ...prev, page: newPage }));
+  }, []);
+
+  const handlePageSizeChange = useCallback((newPageSize: number) => {
+    setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
+  }, []);
+
   // Filter projects based on search query
   const filteredProjects = projects.filter((project) => {
     if (!searchQuery) return true;
@@ -326,6 +370,61 @@ export const ExternalProjectsView: React.FC = () => {
         )}
       </div>
 
+      {/* Pagination */}
+      {!loading && projects.length > 0 && (
+        <div className={styles.pagination}>
+          <div className={styles.paginationInfo}>
+            共 {pagination.total} 条记录,第 {pagination.page} / {pagination.total_pages} 页
+          </div>
+          <div className={styles.paginationControls}>
+            <select
+              className={styles.pageSizeSelect}
+              value={pagination.page_size}
+              onChange={(e) => handlePageSizeChange(Number(e.target.value))}
+            >
+              <option value={10}>10 条/页</option>
+              <option value={20}>20 条/页</option>
+              <option value={50}>50 条/页</option>
+              <option value={100}>100 条/页</option>
+            </select>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(1)}
+              disabled={!pagination.has_prev}
+              title="首页"
+            >
+              首页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page - 1)}
+              disabled={!pagination.has_prev}
+              title="上一页"
+            >
+              <ChevronLeft size={16} />
+              上一页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page + 1)}
+              disabled={!pagination.has_next}
+              title="下一页"
+            >
+              下一页
+              <ChevronRight size={16} />
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.total_pages)}
+              disabled={!pagination.has_next}
+              title="末页"
+            >
+              末页
+            </button>
+          </div>
+        </div>
+      )}
+
       {/* Project Detail Modal */}
       {selectedProjectId && (
         <ProjectDetailModal

+ 7 - 7
web/apps/lq_label/src/views/home-view.tsx

@@ -23,12 +23,12 @@ export const HomeView: React.FC = () => {
       description: '分配和跟踪标注任务,监控任务进度和状态',
       link: '/tasks',
     },
-    {
-      icon: <FileCheck size={32} />,
-      title: '我的标注',
-      description: '查看和管理您的标注记录,确保标注质量',
-      link: '/annotations',
-    },
+    // {
+    //   icon: <FileCheck size={32} />,
+    //   title: '我的标注',
+    //   description: '查看和管理您的标注记录,确保标注质量',
+    //   link: '/annotations',
+    // },
   ];
 
   const stats = [
@@ -47,7 +47,7 @@ export const HomeView: React.FC = () => {
         <p className={styles.heroDescription}>
           提供完整的标注工作流程,从项目创建、任务分配到人员标注,
           <br />
-          集成 LabelStudio 编辑器,支持文本、图片等多种数据类型的标注。
+          集成多类标注编辑器,支持文本、图片等多种数据类型的标注。
         </p>
         <div className={styles.heroActions}>
           <Link to="/projects" className={styles.primaryButton}>

+ 63 - 0
web/apps/lq_label/src/views/my-tasks-view/my-tasks-view.module.scss

@@ -445,3 +445,66 @@
     transform: translateY(-1px);
   }
 }
+
+.pagination {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background: var(--theme-card-background);
+  border: 1px solid var(--theme-border);
+  border-radius: 12px;
+  margin-top: 16px;
+}
+
+.paginationInfo {
+  font-size: 14px;
+  color: var(--theme-paragraph);
+}
+
+.paginationControls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.pageSizeSelect {
+  padding: 8px 12px;
+  background: var(--theme-background-secondary);
+  border: 1px solid var(--theme-border);
+  border-radius: 8px;
+  font-size: 13px;
+  color: var(--theme-headline);
+  cursor: pointer;
+
+  &:focus {
+    outline: none;
+    border-color: var(--theme-button);
+  }
+}
+
+.paginationButton {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 12px;
+  background: transparent;
+  border: 1px solid var(--theme-border);
+  border-radius: 8px;
+  font-size: 13px;
+  font-weight: 500;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover:not(:disabled) {
+    background: var(--theme-background-secondary);
+    border-color: var(--theme-button);
+    color: var(--theme-button);
+  }
+
+  &:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+  }
+}

+ 99 - 7
web/apps/lq_label/src/views/my-tasks-view/my-tasks-view.tsx

@@ -5,7 +5,7 @@
  * Shows task progress and allows starting annotation.
  * Requirements: 3.1, 3.2, 3.5
  */
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
 import { useAtom } from 'jotai';
 import { useNavigate } from 'react-router-dom';
 import {
@@ -16,6 +16,8 @@ import {
   Search,
   ListTodo,
   TrendingUp,
+  ChevronLeft,
+  ChevronRight,
 } from 'lucide-react';
 import { currentUserAtom } from '../../atoms/auth-atoms';
 import { getMyTasks, type MyTasksResponse } from '../../services/api';
@@ -33,10 +35,20 @@ export const MyTasksView: React.FC = () => {
   const [searchQuery, setSearchQuery] = useState('');
   const [statusFilter, setStatusFilter] = useState<TaskStatus | null>(null);
 
+  // Pagination state
+  const [pagination, setPagination] = useState({
+    page: 1,
+    page_size: 20,
+    total: 0,
+    total_pages: 0,
+    has_next: false,
+    has_prev: false,
+  });
+
   // Load tasks on mount
   useEffect(() => {
     loadMyTasks();
-  }, [statusFilter]);
+  }, [statusFilter, pagination.page, pagination.page_size]);
 
   const loadMyTasks = async () => {
     try {
@@ -44,8 +56,18 @@ export const MyTasksView: React.FC = () => {
       setError(null);
       const data = await getMyTasks({
         status: statusFilter || undefined,
+        page: pagination.page,
+        page_size: pagination.page_size,
       });
       setTasksData(data);
+      setPagination({
+        page: data.page,
+        page_size: data.page_size,
+        total: data.total,
+        total_pages: data.total_pages,
+        has_next: data.has_next,
+        has_prev: data.has_prev,
+      });
     } catch (err: any) {
       setError(err.message || '加载任务列表失败');
     } finally {
@@ -53,6 +75,21 @@ export const MyTasksView: React.FC = () => {
     }
   };
 
+  // Pagination handlers
+  const handlePageChange = useCallback((newPage: number) => {
+    setPagination(prev => ({ ...prev, page: newPage }));
+  }, []);
+
+  const handlePageSizeChange = useCallback((newPageSize: number) => {
+    setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
+  }, []);
+
+  // Status filter change handler - reset to first page
+  const handleStatusFilterChange = (newStatus: TaskStatus | null) => {
+    setStatusFilter(newStatus);
+    setPagination(prev => ({ ...prev, page: 1 }));
+  };
+
   const handleStartAnnotation = (task: Task) => {
     navigate(`/tasks/${task.id}/annotate`);
   };
@@ -160,7 +197,7 @@ export const MyTasksView: React.FC = () => {
             className={`${styles.filterButton} ${
               statusFilter === null ? styles.filterButtonActive : ''
             }`}
-            onClick={() => setStatusFilter(null)}
+            onClick={() => handleStatusFilterChange(null)}
           >
             全部
           </button>
@@ -168,7 +205,7 @@ export const MyTasksView: React.FC = () => {
             className={`${styles.filterButton} ${
               statusFilter === 'pending' ? styles.filterButtonActive : ''
             }`}
-            onClick={() => setStatusFilter('pending')}
+            onClick={() => handleStatusFilterChange('pending')}
           >
             待处理
           </button>
@@ -176,7 +213,7 @@ export const MyTasksView: React.FC = () => {
             className={`${styles.filterButton} ${
               statusFilter === 'in_progress' ? styles.filterButtonActive : ''
             }`}
-            onClick={() => setStatusFilter('in_progress')}
+            onClick={() => handleStatusFilterChange('in_progress')}
           >
             进行中
           </button>
@@ -184,7 +221,7 @@ export const MyTasksView: React.FC = () => {
             className={`${styles.filterButton} ${
               statusFilter === 'completed' ? styles.filterButtonActive : ''
             }`}
-            onClick={() => setStatusFilter('completed')}
+            onClick={() => handleStatusFilterChange('completed')}
           >
             已完成
           </button>
@@ -249,7 +286,7 @@ export const MyTasksView: React.FC = () => {
 
                   <div className={styles.taskMeta}>
                     <span className={styles.taskProject}>
-                      项目: {task.project_id}
+                      项目: {task.project_name || task.project_id}
                     </span>
                     <span className={styles.taskDate}>
                       创建于 {date.toLocaleDateString('zh-CN')}
@@ -282,6 +319,61 @@ export const MyTasksView: React.FC = () => {
             })}
           </div>
         )}
+
+        {/* Pagination */}
+        {!loading && tasksData && tasksData.tasks.length > 0 && (
+          <div className={styles.pagination}>
+            <div className={styles.paginationInfo}>
+              共 {pagination.total} 条记录,第 {pagination.page} / {pagination.total_pages} 页
+            </div>
+            <div className={styles.paginationControls}>
+              <select
+                className={styles.pageSizeSelect}
+                value={pagination.page_size}
+                onChange={(e) => handlePageSizeChange(Number(e.target.value))}
+              >
+                <option value={10}>10 条/页</option>
+                <option value={20}>20 条/页</option>
+                <option value={50}>50 条/页</option>
+                <option value={100}>100 条/页</option>
+              </select>
+              <button
+                className={styles.paginationButton}
+                onClick={() => handlePageChange(1)}
+                disabled={!pagination.has_prev}
+                title="首页"
+              >
+                首页
+              </button>
+              <button
+                className={styles.paginationButton}
+                onClick={() => handlePageChange(pagination.page - 1)}
+                disabled={!pagination.has_prev}
+                title="上一页"
+              >
+                <ChevronLeft size={16} />
+                上一页
+              </button>
+              <button
+                className={styles.paginationButton}
+                onClick={() => handlePageChange(pagination.page + 1)}
+                disabled={!pagination.has_next}
+                title="下一页"
+              >
+                下一页
+                <ChevronRight size={16} />
+              </button>
+              <button
+                className={styles.paginationButton}
+                onClick={() => handlePageChange(pagination.total_pages)}
+                disabled={!pagination.has_next}
+                title="末页"
+              >
+                末页
+              </button>
+            </div>
+          </div>
+        )}
       </div>
     </div>
   );

+ 1 - 1
web/apps/lq_label/src/views/project-config-view/project-config-view.tsx

@@ -405,7 +405,7 @@ export const ProjectConfigView: React.FC = () => {
             <div className={styles.editorSection}>
               <div className={styles.sectionHeader}>
                 <h2>XML 配置</h2>
-                <p>编辑 Label Studio XML 配置来定义标注界面</p>
+                <p>编辑多类标注 XML 配置来定义标注界面</p>
               </div>
               <ConfigEditor
                 initialValue={config}

+ 68 - 0
web/apps/lq_label/src/views/project-list-view/project-list-view.module.scss

@@ -652,3 +652,71 @@
   gap: 6px;
   flex-wrap: wrap;
 }
+
+// Pagination styles
+.pagination {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background: var(--theme-card-background);
+  border: 1px solid var(--theme-border);
+  border-radius: 12px;
+  margin-top: 16px;
+}
+
+.paginationInfo {
+  font-size: 14px;
+  color: var(--theme-paragraph);
+}
+
+.paginationControls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.pageSizeSelect {
+  padding: 8px 12px;
+  background: var(--theme-background-secondary);
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover {
+    border-color: var(--theme-button);
+  }
+
+  &:focus {
+    outline: none;
+    border-color: var(--theme-button);
+  }
+}
+
+.paginationButton {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 8px 12px;
+  background: transparent;
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover:not(:disabled) {
+    background: var(--theme-background-secondary);
+    border-color: var(--theme-button);
+    color: var(--theme-button);
+  }
+
+  &:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+  }
+}

+ 128 - 27
web/apps/lq_label/src/views/project-list-view/project-list-view.tsx

@@ -1,13 +1,13 @@
 /**
  * ProjectListView Component
- * 
+ *
  * Displays a list of projects with CRUD operations.
  * Modern design with theme support and clean table layout.
- * Only shows in_progress and completed projects.
+ * Supports filtering by status and source.
  * Admin can create projects, annotators can only view.
  * Requirements: 1.1, 1.2, 1.5, 3.1, 3.5, 3.6, 3.7, 3.8, 6.2, 6.3, 6.4, 6.5, 8.1
  */
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
 import { useAtom, useAtomValue } from 'jotai';
 import { useNavigate } from 'react-router-dom';
 import { isAdminAtom } from '../../atoms/auth-atoms';
@@ -20,6 +20,8 @@ import {
   FolderOpen,
   AlertCircle,
   Play,
+  ChevronLeft,
+  ChevronRight,
 } from 'lucide-react';
 import {
   projectsAtom,
@@ -62,28 +64,40 @@ export const ProjectListView: React.FC = () => {
   // Search state
   const [searchQuery, setSearchQuery] = useState('');
 
+  // Pagination state
+  const [pagination, setPagination] = useState({
+    page: 1,
+    page_size: 20,
+    total: 0,
+    total_pages: 0,
+    has_next: false,
+    has_prev: false,
+  });
+
   // Load projects on mount and when filters change
-  // Only load in_progress and completed projects (Requirement 3.1)
   useEffect(() => {
     loadProjects();
-  }, [filters]);
+  }, [filters, pagination.page, pagination.page_size]);
 
   const loadProjects = async () => {
     try {
       setLoading(true);
       setError(null);
-      // Fetch in_progress projects
-      const inProgressData = await listProjects({
-        status: 'in_progress',
+      const data = await listProjects({
+        status: filters.status,
         source: filters.source,
+        page: pagination.page,
+        page_size: pagination.page_size,
       });
-      // Fetch completed projects
-      const completedData = await listProjects({
-        status: 'completed',
-        source: filters.source,
+      setProjects(data.projects);
+      setPagination({
+        page: data.page,
+        page_size: data.page_size,
+        total: data.total,
+        total_pages: data.total_pages,
+        has_next: data.has_next,
+        has_prev: data.has_prev,
       });
-      // Combine both lists
-      setProjects([...inProgressData, ...completedData]);
     } catch (err: any) {
       setError(err.message || '加载项目列表失败');
     } finally {
@@ -91,6 +105,33 @@ export const ProjectListView: React.FC = () => {
     }
   };
 
+  const handleStatusFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+    const value = e.target.value;
+    setFilters(prev => ({
+      ...prev,
+      status: value ? value as ProjectStatus : undefined,
+    }));
+    setPagination(prev => ({ ...prev, page: 1 }));
+  };
+
+  const handleSourceFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+    const value = e.target.value;
+    setFilters(prev => ({
+      ...prev,
+      source: value ? value as ProjectSource : undefined,
+    }));
+    setPagination(prev => ({ ...prev, page: 1 }));
+  };
+
+  // Pagination handlers
+  const handlePageChange = useCallback((newPage: number) => {
+    setPagination(prev => ({ ...prev, page: newPage }));
+  }, []);
+
+  const handlePageSizeChange = useCallback((newPageSize: number) => {
+    setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
+  }, []);
+
   const handleCreateProject = async (formData: ProjectFormData) => {
     try {
       setIsSubmitting(true);
@@ -144,15 +185,6 @@ export const ProjectListView: React.FC = () => {
     navigate(`/projects/${project.id}/annotate`);
   };
 
-  // Source filter change handler (status filter removed per Requirement 3.5)
-  const handleSourceFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    const value = e.target.value;
-    setFilters(prev => ({
-      ...prev,
-      source: value ? value as ProjectSource : undefined,
-    }));
-  };
-
   // Filter projects based on search query
   const filteredProjects = projects.filter((project) => {
     if (!searchQuery) return true;
@@ -240,7 +272,21 @@ export const ProjectListView: React.FC = () => {
           />
         </div>
         <div className={styles.filterBar}>
-          {/* Only source filter, status filter removed (Requirement 3.5, 3.6) */}
+          {/* Status filter */}
+          <span className={styles.filterLabel}>状态:</span>
+          <select
+            className={styles.filterSelect}
+            value={filters.status || ''}
+            onChange={handleStatusFilterChange}
+          >
+            <option value="">全部</option>
+            <option value="draft">草稿</option>
+            <option value="configuring">配置中</option>
+            <option value="ready">待分发</option>
+            <option value="in_progress">进行中</option>
+            <option value="completed">已完成</option>
+          </select>
+          {/* Source filter */}
           <span className={styles.filterLabel}>来源:</span>
           <select
             className={styles.filterSelect}
@@ -272,10 +318,10 @@ export const ProjectListView: React.FC = () => {
         ) : filteredProjects.length === 0 ? (
           <div className={styles.emptyState}>
             <FolderOpen size={48} className={styles.emptyIcon} />
-            <h3>{searchQuery ? '未找到匹配的项目' : '暂无进行中或已完成的项目'}</h3>
+            <h3>{searchQuery || filters.status || filters.source ? '未找到匹配的项目' : '暂无项目'}</h3>
             <p>
-              {searchQuery
-                ? '尝试使用不同的搜索关键词'
+              {searchQuery || filters.status || filters.source
+                ? '尝试更改筛选条件或搜索关键词'
                 : isAdmin
                   ? '点击"创建项目"按钮开始创建您的第一个标注项目'
                   : '暂无分配给您的标注任务,请等待管理员分发任务'}
@@ -400,6 +446,61 @@ export const ProjectListView: React.FC = () => {
         )}
       </div>
 
+      {/* Pagination */}
+      {!loading && projects.length > 0 && (
+        <div className={styles.pagination}>
+          <div className={styles.paginationInfo}>
+            共 {pagination.total} 条记录,第 {pagination.page} / {pagination.total_pages} 页
+          </div>
+          <div className={styles.paginationControls}>
+            <select
+              className={styles.pageSizeSelect}
+              value={pagination.page_size}
+              onChange={(e) => handlePageSizeChange(Number(e.target.value))}
+            >
+              <option value={10}>10 条/页</option>
+              <option value={20}>20 条/页</option>
+              <option value={50}>50 条/页</option>
+              <option value={100}>100 条/页</option>
+            </select>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(1)}
+              disabled={!pagination.has_prev}
+              title="首页"
+            >
+              首页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page - 1)}
+              disabled={!pagination.has_prev}
+              title="上一页"
+            >
+              <ChevronLeft size={16} />
+              上一页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page + 1)}
+              disabled={!pagination.has_next}
+              title="下一页"
+            >
+              下一页
+              <ChevronRight size={16} />
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.total_pages)}
+              disabled={!pagination.has_next}
+              title="末页"
+            >
+              末页
+            </button>
+          </div>
+        </div>
+      )}
+
       {/* Create Project Dialog */}
       {isCreateDialogOpen && (
         <div className={styles.dialogOverlay} onClick={() => setIsCreateDialogOpen(false)}>

+ 73 - 0
web/apps/lq_label/src/views/task-list-view/task-list-view.module.scss

@@ -578,3 +578,76 @@
     cursor: not-allowed;
   }
 }
+
+// Pagination styles
+.pagination {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background: var(--theme-card-background);
+  border: 1px solid var(--theme-border);
+  border-radius: 12px;
+  margin-top: 16px;
+}
+
+.paginationInfo {
+  font-size: 14px;
+  color: var(--theme-paragraph);
+}
+
+.paginationControls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.pageSizeSelect {
+  padding: 8px 12px;
+  background: var(--theme-background-secondary);
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover {
+    border-color: var(--theme-button);
+  }
+
+  &:focus {
+    outline: none;
+    border-color: var(--theme-button);
+  }
+}
+
+.paginationButton {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 8px 12px;
+  background: transparent;
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover:not(:disabled) {
+    background: var(--theme-background-secondary);
+    border-color: var(--theme-button);
+    color: var(--theme-button);
+  }
+
+  &:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+  }
+}
+
+.projectName {
+  font-weight: 500;
+  color: var(--theme-headline);
+}

+ 103 - 10
web/apps/lq_label/src/views/task-list-view/task-list-view.tsx

@@ -1,7 +1,7 @@
 /**
  * TaskListView Component
- * 
- * Displays a list of tasks with filtering and operations.
+ *
+ * Displays a list of tasks with filtering, pagination and operations.
  * Modern design with theme support and clean table layout.
  * Supports task selection and batch assignment.
  * Requirements: 2.1, 2.3, 2.4, 2.7
@@ -20,6 +20,8 @@ import {
   CheckSquare,
   Square,
   MinusSquare,
+  ChevronLeft,
+  ChevronRight,
 } from 'lucide-react';
 import {
   tasksAtom,
@@ -29,6 +31,7 @@ import {
   filteredTasksAtom,
   type Task,
   type TaskStatus,
+  type PaginationState,
 } from '../../atoms/task-atoms';
 import { currentUserAtom } from '../../atoms/auth-atoms';
 import {
@@ -46,15 +49,25 @@ export const TaskListView: React.FC = () => {
   const [filter, setFilter] = useAtom(taskFilterAtom);
   const [filteredTasks] = useAtom(filteredTasksAtom);
   const [currentUser] = useAtom(currentUserAtom);
-  
+
   // Dialog state
   const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
   const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
   const [isSubmitting, setIsSubmitting] = useState(false);
-  
+
   // Search state
   const [searchQuery, setSearchQuery] = useState('');
 
+  // Pagination state
+  const [pagination, setPagination] = useState<PaginationState>({
+    page: 1,
+    page_size: 20,
+    total: 0,
+    total_pages: 0,
+    has_next: false,
+    has_prev: false,
+  });
+
   // Selection state
   const [selectedTaskIds, setSelectedTaskIds] = useState<Set<string>>(new Set());
   const [isAssignDialogOpen, setIsAssignDialogOpen] = useState(false);
@@ -65,14 +78,28 @@ export const TaskListView: React.FC = () => {
   // Load tasks on mount
   useEffect(() => {
     loadTasks();
-  }, []);
+  }, [filter, pagination.page, pagination.page_size]);
 
   const loadTasks = async () => {
     try {
       setLoading(true);
       setError(null);
-      const data = await listTasks();
-      setTasks(data);
+      const data = await listTasks({
+        project_id: filter.projectId || undefined,
+        status: filter.status || undefined,
+        assigned_to: filter.assignedTo || undefined,
+        page: pagination.page,
+        page_size: pagination.page_size,
+      });
+      setTasks(data.tasks);
+      setPagination({
+        page: data.page,
+        page_size: data.page_size,
+        total: data.total,
+        total_pages: data.total_pages,
+        has_next: data.has_next,
+        has_prev: data.has_prev,
+      });
     } catch (err: any) {
       setError(err.message || '加载任务列表失败');
     } finally {
@@ -82,6 +109,7 @@ export const TaskListView: React.FC = () => {
 
   const handleStatusFilter = (status: TaskStatus | null) => {
     setFilter({ ...filter, status });
+    setPagination(prev => ({ ...prev, page: 1 })); // Reset to first page when changing filter
   };
 
   const handleStartAnnotation = (task: Task) => {
@@ -125,16 +153,26 @@ export const TaskListView: React.FC = () => {
   };
 
   // Filter tasks by search query
-  const searchFilteredTasks = filteredTasks.filter((task) => {
+  const searchFilteredTasks = tasks.filter((task) => {
     if (!searchQuery) return true;
     const query = searchQuery.toLowerCase();
     return (
       task.name.toLowerCase().includes(query) ||
       task.project_id.toString().includes(query) ||
+      task.project_name?.toLowerCase().includes(query) ||
       task.assigned_to?.toLowerCase().includes(query)
     );
   });
 
+  // Pagination handlers
+  const handlePageChange = useCallback((newPage: number) => {
+    setPagination(prev => ({ ...prev, page: newPage }));
+  }, []);
+
+  const handlePageSizeChange = useCallback((newPageSize: number) => {
+    setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
+  }, []);
+
   // Selection handlers
   const toggleTaskSelection = useCallback((taskId: string) => {
     setSelectedTaskIds((prev) => {
@@ -339,8 +377,8 @@ export const TaskListView: React.FC = () => {
                         <span className={styles.taskName}>{task.name}</span>
                       </td>
                       <td>
-                        <span className={styles.projectId}>
-                          {task.project_id}
+                        <span className={styles.projectName}>
+                          {task.project_name || task.project_id}
                         </span>
                       </td>
                       <td>
@@ -420,6 +458,61 @@ export const TaskListView: React.FC = () => {
         )}
       </div>
 
+      {/* Pagination */}
+      {!loading && tasks.length > 0 && (
+        <div className={styles.pagination}>
+          <div className={styles.paginationInfo}>
+            共 {pagination.total} 条记录,第 {pagination.page} / {pagination.total_pages} 页
+          </div>
+          <div className={styles.paginationControls}>
+            <select
+              className={styles.pageSizeSelect}
+              value={pagination.page_size}
+              onChange={(e) => handlePageSizeChange(Number(e.target.value))}
+            >
+              <option value={10}>10 条/页</option>
+              <option value={20}>20 条/页</option>
+              <option value={50}>50 条/页</option>
+              <option value={100}>100 条/页</option>
+            </select>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(1)}
+              disabled={!pagination.has_prev}
+              title="首页"
+            >
+              首页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page - 1)}
+              disabled={!pagination.has_prev}
+              title="上一页"
+            >
+              <ChevronLeft size={16} />
+              上一页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page + 1)}
+              disabled={!pagination.has_next}
+              title="下一页"
+            >
+              下一页
+              <ChevronRight size={16} />
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.total_pages)}
+              disabled={!pagination.has_next}
+              title="末页"
+            >
+              末页
+            </button>
+          </div>
+        </div>
+      )}
+
       {/* Delete Confirmation Dialog */}
       {isDeleteDialogOpen && (
         <div className={styles.dialogOverlay} onClick={() => setIsDeleteDialogOpen(false)}>

+ 68 - 0
web/apps/lq_label/src/views/user-management-view/user-management-view.module.scss

@@ -606,3 +606,71 @@
     background: var(--theme-button-hover);
   }
 }
+
+// Pagination styles
+.pagination {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  background: var(--theme-card-background);
+  border: 1px solid var(--theme-border);
+  border-radius: 12px;
+  margin-top: 16px;
+}
+
+.paginationInfo {
+  font-size: 14px;
+  color: var(--theme-paragraph);
+}
+
+.paginationControls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.pageSizeSelect {
+  padding: 8px 12px;
+  background: var(--theme-background-secondary);
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover {
+    border-color: var(--theme-button);
+  }
+
+  &:focus {
+    outline: none;
+    border-color: var(--theme-button);
+  }
+}
+
+.paginationButton {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 8px 12px;
+  background: transparent;
+  border: 1px solid var(--theme-border);
+  border-radius: 6px;
+  font-size: 13px;
+  color: var(--theme-paragraph);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover:not(:disabled) {
+    background: var(--theme-background-secondary);
+    border-color: var(--theme-button);
+    color: var(--theme-button);
+  }
+
+  &:disabled {
+    opacity: 0.4;
+    cursor: not-allowed;
+  }
+}

+ 89 - 12
web/apps/lq_label/src/views/user-management-view/user-management-view.tsx

@@ -5,7 +5,7 @@
  * Admin-only view for managing platform users.
  * Requirements: 1.1, 1.2, 1.3, 1.4
  */
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
 import { useAtom } from 'jotai';
 import {
   Users,
@@ -16,6 +16,8 @@ import {
   Shield,
   Eye,
   ChevronDown,
+  ChevronLeft,
+  ChevronRight,
 } from 'lucide-react';
 import {
   usersAtom,
@@ -58,12 +60,21 @@ export const UserManagementView: React.FC = () => {
   // Local state
   const [selectedUser, setSelectedUser] = useState<UserWithStats | null>(null);
   const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
-  const [total, setTotal] = useState(0);
+
+  // Pagination state
+  const [pagination, setPagination] = useState({
+    page: 1,
+    page_size: 20,
+    total: 0,
+    total_pages: 0,
+    has_next: false,
+    has_prev: false,
+  });
 
   // Load users on mount
   useEffect(() => {
     loadUsers();
-  }, []);
+  }, [filter.role, filter.search, pagination.page, pagination.page_size]);
 
   const loadUsers = async () => {
     try {
@@ -72,9 +83,17 @@ export const UserManagementView: React.FC = () => {
       const response = await listUsers({
         role: filter.role || undefined,
         search: filter.search || undefined,
+        page: pagination.page,
+        page_size: pagination.page_size,
       });
       setUsers(response.users);
-      setTotal(response.total);
+      setPagination(prev => ({
+        ...prev,
+        total: response.total,
+        total_pages: response.total_pages,
+        has_next: response.has_next,
+        has_prev: response.has_prev,
+      }));
     } catch (err: any) {
       setError(err.message || '加载用户列表失败');
     } finally {
@@ -82,21 +101,24 @@ export const UserManagementView: React.FC = () => {
     }
   };
 
-  // Reload when filter changes
-  useEffect(() => {
-    const timer = setTimeout(() => {
-      loadUsers();
-    }, 300);
-    return () => clearTimeout(timer);
-  }, [filter.role, filter.search]);
+  // Pagination handlers
+  const handlePageChange = useCallback((newPage: number) => {
+    setPagination(prev => ({ ...prev, page: newPage }));
+  }, []);
+
+  const handlePageSizeChange = useCallback((newPageSize: number) => {
+    setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
+  }, []);
 
   const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     setFilter((prev) => ({ ...prev, search: e.target.value }));
+    setPagination(prev => ({ ...prev, page: 1 }));
   };
 
   const handleRoleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
     const value = e.target.value as UserRole | '';
     setFilter((prev) => ({ ...prev, role: value || null }));
+    setPagination(prev => ({ ...prev, page: 1 }));
   };
 
   const handleViewUser = (user: UserWithStats) => {
@@ -131,7 +153,7 @@ export const UserManagementView: React.FC = () => {
           </div>
           <div className={styles.headerStats}>
             <div className={styles.statItem}>
-              <span className={styles.statValue}>{total}</span>
+              <span className={styles.statValue}>{pagination.total}</span>
               <span className={styles.statLabel}>总用户数</span>
             </div>
           </div>
@@ -294,6 +316,61 @@ export const UserManagementView: React.FC = () => {
         )}
       </div>
 
+      {/* Pagination */}
+      {!loading && users.length > 0 && (
+        <div className={styles.pagination}>
+          <div className={styles.paginationInfo}>
+            共 {pagination.total} 条记录,第 {pagination.page} / {pagination.total_pages} 页
+          </div>
+          <div className={styles.paginationControls}>
+            <select
+              className={styles.pageSizeSelect}
+              value={pagination.page_size}
+              onChange={(e) => handlePageSizeChange(Number(e.target.value))}
+            >
+              <option value={10}>10 条/页</option>
+              <option value={20}>20 条/页</option>
+              <option value={50}>50 条/页</option>
+              <option value={100}>100 条/页</option>
+            </select>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(1)}
+              disabled={!pagination.has_prev}
+              title="首页"
+            >
+              首页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page - 1)}
+              disabled={!pagination.has_prev}
+              title="上一页"
+            >
+              <ChevronLeft size={16} />
+              上一页
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.page + 1)}
+              disabled={!pagination.has_next}
+              title="下一页"
+            >
+              下一页
+              <ChevronRight size={16} />
+            </button>
+            <button
+              className={styles.paginationButton}
+              onClick={() => handlePageChange(pagination.total_pages)}
+              disabled={!pagination.has_next}
+              title="末页"
+            >
+              末页
+            </button>
+          </div>
+        </div>
+      )}
+
       {/* User Detail Modal */}
       {isDetailModalOpen && selectedUser && (
         <div className={styles.dialogOverlay} onClick={closeDetailModal}>