2
0
Эх сурвалжийг харах

✨ feat(submission): 添加提交记录对比功能

- 新增SubmissionComparisonDialog组件,支持多教室提交记录对比展示
- 实现按教室分组、用户横向对比的表格数据生成逻辑
- 添加记录选择功能,支持复选框批量选择记录
- 在提交记录页面添加"对比"按钮,可打开对比对话框
- 提取SubmissionRecordsItem等类型到单独文件,优化类型管理

✨ feat(ui): 增强提交记录页面交互体验

- 添加表格行复选框,支持单选和全选功能
- 实现选中记录数量实时显示
- 当选中记录不足2条时禁用对比功能
- 添加对比对话框空状态提示,引导用户选择合适的记录进行对比
yourname 6 сар өмнө
parent
commit
4fc952bbe7

+ 130 - 0
src/client/admin/components/SubmissionComparisonDialog.tsx

@@ -0,0 +1,130 @@
+import React from 'react';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Button } from '@/client/components/ui/button';
+import type { SubmissionRecordsItem } from '@/client/admin/types/submission';
+
+interface SubmissionComparisonDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  selectedRecords: SubmissionRecordsItem[];
+}
+
+export const SubmissionComparisonDialog: React.FC<SubmissionComparisonDialogProps> = ({
+  open,
+  onOpenChange,
+  selectedRecords
+}) => {
+  // 生成对比数据
+  const comparisonData = React.useMemo(() => {
+    if (selectedRecords.length < 2) return { data: [], classrooms: [] };
+
+    // 按教室号分组
+    const groupedByClassroom: Record<string, SubmissionRecordsItem[]> = {};
+    selectedRecords.forEach(record => {
+      const classroom = record.classroomNo || '未知教室';
+      if (!groupedByClassroom[classroom]) {
+        groupedByClassroom[classroom] = [];
+      }
+      groupedByClassroom[classroom].push(record);
+    });
+
+    // 获取所有教室
+    const classrooms = Object.keys(groupedByClassroom);
+    if (classrooms.length < 2) return { data: [], classrooms };
+
+    // 获取所有用户(按昵称或用户名)
+    const allUsers = new Set<string>();
+    selectedRecords.forEach(record => {
+      const userName = record.user?.nickname || record.user?.username || `用户${record.userId}`;
+      allUsers.add(userName);
+    });
+
+    // 生成对比表格数据
+    const result: Array<{
+      user: string;
+      [classroom: string]: string | number | undefined;
+    }> = [];
+
+    // 为每个用户在每个教室中查找对应的记录
+    allUsers.forEach(userName => {
+      const row: { user: string; [classroom: string]: string | number | undefined } = { user: userName };
+      
+      classrooms.forEach(classroom => {
+        const record = groupedByClassroom[classroom].find(
+          r => (r.user?.nickname || r.user?.username || `用户${r.userId}`) === userName
+        );
+        
+        if (record) {
+          row[classroom] = record.totalProfitPercent !== null && record.totalProfitPercent !== undefined 
+            ? `${record.totalProfitPercent}%`
+            : '-';
+        } else {
+          row[classroom] = '-';
+        }
+      });
+      
+      result.push(row);
+    });
+
+    return { data: result, classrooms };
+  }, [selectedRecords]);
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
+        <DialogHeader>
+          <DialogTitle>提交记录对比</DialogTitle>
+          <DialogDescription>
+            对比选中的提交记录数据
+          </DialogDescription>
+        </DialogHeader>
+
+        <div className="max-h-[60vh] overflow-auto">
+          {comparisonData.data.length > 0 ? (
+            <div className="rounded-md border">
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>用户</TableHead>
+                    {comparisonData.classrooms.map(classroom => (
+                      <TableHead key={classroom} className="text-center">
+                        教室 {classroom}
+                      </TableHead>
+                    ))}
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {comparisonData.data.map((row, index) => (
+                    <TableRow key={index}>
+                      <TableCell className="font-medium">{row.user}</TableCell>
+                      {comparisonData.classrooms.map(classroom => (
+                        <TableCell key={classroom} className="text-center">
+                          {row[classroom] || '-'}
+                        </TableCell>
+                      ))}
+                    </TableRow>
+                  ))}
+                </TableBody>
+              </Table>
+            </div>
+          ) : (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">
+                {selectedRecords.length < 2 
+                  ? '请至少选择2条记录进行对比' 
+                  : '无法生成对比数据,请选择不同教室的相同用户记录'}
+              </p>
+            </div>
+          )}
+        </div>
+
+        <DialogFooter>
+          <Button onClick={() => onOpenChange(false)}>
+            关闭
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+};

+ 61 - 11
src/client/admin/pages/SubmissionRecordsPage.tsx

@@ -1,17 +1,22 @@
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import { useQuery } from '@tanstack/react-query';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 import { zhCN } from 'date-fns/locale';
 import { zhCN } from 'date-fns/locale';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { useForm } from 'react-hook-form';
 import { useForm } from 'react-hook-form';
 import { toast } from 'sonner';
 import { toast } from 'sonner';
-import { Plus, Search, Edit, Trash2 } from 'lucide-react';
+import { Plus, Search, Edit, Trash2, GitCompare } from 'lucide-react';
 
 
 import { submissionRecordsClient } from '@/client/api';
 import { submissionRecordsClient } from '@/client/api';
-import type { InferResponseType, InferRequestType } from 'hono/client';
+import type {
+  SubmissionRecordsItem,
+  CreateSubmissionRecordsRequest,
+  UpdateSubmissionRecordsRequest
+} from '@/client/admin/types/submission';
 import { CreateSubmissionRecordsDto, UpdateSubmissionRecordsDto } from '@/server/modules/submission/submission-records.schema';
 import { CreateSubmissionRecordsDto, UpdateSubmissionRecordsDto } from '@/server/modules/submission/submission-records.schema';
 
 
 import { Button } from '@/client/components/ui/button';
 import { Button } from '@/client/components/ui/button';
+import { Checkbox } from '@/client/components/ui/checkbox';
 import { Input } from '@/client/components/ui/input';
 import { Input } from '@/client/components/ui/input';
 import { Card, CardContent, CardHeader, CardTitle } from '@/client/components/ui/card';
 import { Card, CardContent, CardHeader, CardTitle } from '@/client/components/ui/card';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
@@ -21,11 +26,8 @@ import { Skeleton } from '@/client/components/ui/skeleton';
 import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/client/components/ui/alert-dialog';
 import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/client/components/ui/alert-dialog';
 
 
 import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
 import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
+import { SubmissionComparisonDialog } from '@/client/admin/components/SubmissionComparisonDialog';
 
 
-type SubmissionRecordsListResponse = InferResponseType<typeof submissionRecordsClient.$get, 200>;
-type SubmissionRecordsItem = SubmissionRecordsListResponse['data'][0];
-type CreateSubmissionRecordsRequest = InferRequestType<typeof submissionRecordsClient.$post>['json'];
-type UpdateSubmissionRecordsRequest = InferRequestType<typeof submissionRecordsClient[':id']['$put']>['json'];
 
 
 // 表单schema
 // 表单schema
 const createFormSchema = CreateSubmissionRecordsDto;
 const createFormSchema = CreateSubmissionRecordsDto;
@@ -43,6 +45,8 @@ export const SubmissionRecordsPage = () => {
   const [editingRecord, setEditingRecord] = useState<any>(null);
   const [editingRecord, setEditingRecord] = useState<any>(null);
   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
   const [recordToDelete, setRecordToDelete] = useState<number | null>(null);
   const [recordToDelete, setRecordToDelete] = useState<number | null>(null);
+  const [selectedRecords, setSelectedRecords] = useState<number[]>([]);
+  const [compareDialogOpen, setCompareDialogOpen] = useState(false);
 
 
   // 创建表单
   // 创建表单
   const createForm = useForm<CreateSubmissionRecordsRequest>({
   const createForm = useForm<CreateSubmissionRecordsRequest>({
@@ -209,6 +213,11 @@ export const SubmissionRecordsPage = () => {
     }
     }
   };
   };
 
 
+  // 获取选中的记录数据
+  const selectedRecordsData = useMemo(() => {
+    return records.filter(record => selectedRecords.includes(record.id));
+  }, [records, selectedRecords]);
+
   // 加载状态
   // 加载状态
   if (isLoading) {
   if (isLoading) {
     return (
     return (
@@ -244,10 +253,20 @@ export const SubmissionRecordsPage = () => {
       {/* 页面标题 */}
       {/* 页面标题 */}
       <div className="flex justify-between items-center">
       <div className="flex justify-between items-center">
         <h1 className="text-2xl font-bold">提交记录管理</h1>
         <h1 className="text-2xl font-bold">提交记录管理</h1>
-        <Button onClick={showCreateModal}>
-          <Plus className="mr-2 h-4 w-4" />
-          添加记录
-        </Button>
+        <div className="flex gap-2">
+          <Button
+            variant="outline"
+            onClick={() => setCompareDialogOpen(true)}
+            disabled={selectedRecords.length < 2}
+          >
+            <GitCompare className="mr-2 h-4 w-4" />
+            对比({selectedRecords.length})
+          </Button>
+          <Button onClick={showCreateModal}>
+            <Plus className="mr-2 h-4 w-4" />
+            添加记录
+          </Button>
+        </div>
       </div>
       </div>
 
 
       {/* 搜索区域 */}
       {/* 搜索区域 */}
@@ -276,6 +295,18 @@ export const SubmissionRecordsPage = () => {
             <Table>
             <Table>
               <TableHeader>
               <TableHeader>
                 <TableRow>
                 <TableRow>
+                  <TableHead className="w-12">
+                    <Checkbox
+                      checked={selectedRecords.length === records.length && records.length > 0}
+                      onCheckedChange={(checked) => {
+                        if (checked) {
+                          setSelectedRecords(records.map(record => record.id));
+                        } else {
+                          setSelectedRecords([]);
+                        }
+                      }}
+                    />
+                  </TableHead>
                   <TableHead>ID</TableHead>
                   <TableHead>ID</TableHead>
                   <TableHead>教室号</TableHead>
                   <TableHead>教室号</TableHead>
                   <TableHead>用户</TableHead>
                   <TableHead>用户</TableHead>
@@ -291,6 +322,18 @@ export const SubmissionRecordsPage = () => {
               <TableBody>
               <TableBody>
                 {records.map((record) => (
                 {records.map((record) => (
                   <TableRow key={record.id}>
                   <TableRow key={record.id}>
+                    <TableCell>
+                      <Checkbox
+                        checked={selectedRecords.includes(record.id)}
+                        onCheckedChange={(checked) => {
+                          if (checked) {
+                            setSelectedRecords(prev => [...prev, record.id]);
+                          } else {
+                            setSelectedRecords(prev => prev.filter(id => id !== record.id));
+                          }
+                        }}
+                      />
+                    </TableCell>
                     <TableCell className="font-medium">{record.id}</TableCell>
                     <TableCell className="font-medium">{record.id}</TableCell>
                     <TableCell>{record.classroomNo || '-'}</TableCell>
                     <TableCell>{record.classroomNo || '-'}</TableCell>
                     <TableCell>
                     <TableCell>
@@ -909,6 +952,13 @@ export const SubmissionRecordsPage = () => {
           </AlertDialogFooter>
           </AlertDialogFooter>
         </AlertDialogContent>
         </AlertDialogContent>
       </AlertDialog>
       </AlertDialog>
+
+      {/* 对比对话框 */}
+      <SubmissionComparisonDialog
+        open={compareDialogOpen}
+        onOpenChange={setCompareDialogOpen}
+        selectedRecords={selectedRecordsData}
+      />
     </div>
     </div>
   );
   );
 };
 };

+ 7 - 0
src/client/admin/types/submission.ts

@@ -0,0 +1,7 @@
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { submissionRecordsClient } from '@/client/api';
+
+export type SubmissionRecordsListResponse = InferResponseType<typeof submissionRecordsClient.$get, 200>;
+export type SubmissionRecordsItem = SubmissionRecordsListResponse['data'][0];
+export type CreateSubmissionRecordsRequest = InferRequestType<typeof submissionRecordsClient.$post>['json'];
+export type UpdateSubmissionRecordsRequest = InferRequestType<typeof submissionRecordsClient[':id']['$put']>['json'];