From f6b292bebf4e38d632fbc870090896887ae7a5ba Mon Sep 17 00:00:00 2001 From: zhangxiaohua <827885272@qq.com> Date: Fri, 23 Jan 2026 14:46:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A2=84=E8=AE=BE=E8=AF=84?= =?UTF-8?q?=E8=AF=AD=E5=8A=9F=E8=83=BD=E5=8F=8A=E8=AF=84=E5=AE=A1=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增预设评语表(PresetComment)及相关API - 新增预设评语管理页面 - 优化评审作品弹框,支持预设评语选择 - 优化赛果发布列表页面 - 更新路由和菜单配置 Co-Authored-By: Claude Opus 4.5 --- backend/prisma/schema.prisma | 25 + backend/src/contests/contests.module.ts | 3 + .../dto/create-preset-comment.dto.ts | 20 + .../dto/sync-preset-comments.dto.ts | 11 + .../dto/update-preset-comment.dto.ts | 18 + .../preset-comments.controller.ts | 85 ++ .../preset-comments/preset-comments.module.ts | 12 + .../preset-comments.service.ts | 234 ++++ .../contests/reviews/dto/create-score.dto.ts | 10 +- .../src/contests/reviews/reviews.service.ts | 5 +- frontend/src/api/preset-comments.ts | 139 ++ frontend/src/router/index.ts | 21 +- frontend/src/utils/menu.ts | 1 + .../src/views/activities/PresetComments.vue | 479 +++++++ .../src/views/activities/ReviewDetail.vue | 26 + .../activities/components/ReviewWorkModal.vue | 1121 +++++++++++++---- frontend/src/views/contests/Activities.vue | 10 +- frontend/src/views/contests/results/Index.vue | 8 +- 18 files changed, 1981 insertions(+), 247 deletions(-) create mode 100644 backend/src/contests/preset-comments/dto/create-preset-comment.dto.ts create mode 100644 backend/src/contests/preset-comments/dto/sync-preset-comments.dto.ts create mode 100644 backend/src/contests/preset-comments/dto/update-preset-comment.dto.ts create mode 100644 backend/src/contests/preset-comments/preset-comments.controller.ts create mode 100644 backend/src/contests/preset-comments/preset-comments.module.ts create mode 100644 backend/src/contests/preset-comments/preset-comments.service.ts create mode 100644 frontend/src/api/preset-comments.ts create mode 100644 frontend/src/views/activities/PresetComments.vue diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 2a8a2d3..27673c4 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -158,6 +158,8 @@ model User { modifiedHomeworkScores HomeworkScore[] @relation("HomeworkScoreModifier") // AI 3D 生成关联 ai3dTasks AI3DTask[] @relation("AI3DTaskUser") /// 用户的 AI 3D 生成任务 + // 预设评语关联 + presetComments PresetComment[] @relation("PresetCommentJudge") /// 评委的预设评语 @@unique([tenantId, username]) @@unique([tenantId, email]) @@ -592,6 +594,7 @@ model Contest { workAssignments ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentContest") /// 作品分配 workScores ContestWorkScore[] @relation("ContestWorkScoreContest") /// 作品评分 notices ContestNotice[] /// 赛事公告 + presetComments PresetComment[] /// 预设评语 creatorUser User? @relation("ContestCreator", fields: [creator], references: [id], onDelete: SetNull) modifierUser User? @relation("ContestModifier", fields: [modifier], references: [id], onDelete: SetNull) @@ -1098,3 +1101,25 @@ model AI3DTask { @@index([createTime]) @@map("t_ai_3d_task") } + +/// 预设评语表 +model PresetComment { + id Int @id @default(autoincrement()) + contestId Int @map("contest_id") /// 赛事ID + judgeId Int @map("judge_id") /// 评委用户ID + content String @db.Text /// 评语内容 + score Decimal? @db.Decimal(10, 2) /// 关联评审分数 + sortOrder Int @default(0) @map("sort_order") /// 排序顺序 + useCount Int @default(0) @map("use_count") /// 使用次数 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 + + contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) + judge User @relation("PresetCommentJudge", fields: [judgeId], references: [id], onDelete: Cascade) + + @@index([contestId, judgeId]) + @@map("t_preset_comment") +} diff --git a/backend/src/contests/contests.module.ts b/backend/src/contests/contests.module.ts index af46e07..cc79c55 100644 --- a/backend/src/contests/contests.module.ts +++ b/backend/src/contests/contests.module.ts @@ -9,6 +9,7 @@ import { ReviewsModule } from './reviews/reviews.module'; import { NoticesModule } from './notices/notices.module'; import { JudgesModule } from './judges/judges.module'; import { ResultsModule } from './results/results.module'; +import { PresetCommentsModule } from './preset-comments/preset-comments.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { ResultsModule } from './results/results.module'; NoticesModule, JudgesModule, ResultsModule, + PresetCommentsModule, // ContestsCoreModule 放在最后,因为它有通配符路由 /contests/:id ContestsCoreModule, ], @@ -37,6 +39,7 @@ import { ResultsModule } from './results/results.module'; NoticesModule, JudgesModule, ResultsModule, + PresetCommentsModule, ], }) export class ContestsModule {} diff --git a/backend/src/contests/preset-comments/dto/create-preset-comment.dto.ts b/backend/src/contests/preset-comments/dto/create-preset-comment.dto.ts new file mode 100644 index 0000000..910a61f --- /dev/null +++ b/backend/src/contests/preset-comments/dto/create-preset-comment.dto.ts @@ -0,0 +1,20 @@ +import { IsInt, IsString, IsOptional, IsNumber, Min } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreatePresetCommentDto { + @IsInt() + contestId: number; + + @IsString() + content: string; + + @IsOptional() + @IsNumber() + @Type(() => Number) + score?: number; + + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; +} diff --git a/backend/src/contests/preset-comments/dto/sync-preset-comments.dto.ts b/backend/src/contests/preset-comments/dto/sync-preset-comments.dto.ts new file mode 100644 index 0000000..c49b00d --- /dev/null +++ b/backend/src/contests/preset-comments/dto/sync-preset-comments.dto.ts @@ -0,0 +1,11 @@ +import { IsInt, IsArray, ArrayMinSize } from 'class-validator'; + +export class SyncPresetCommentsDto { + @IsInt() + sourceContestId: number; + + @IsArray() + @ArrayMinSize(1) + @IsInt({ each: true }) + targetContestIds: number[]; +} diff --git a/backend/src/contests/preset-comments/dto/update-preset-comment.dto.ts b/backend/src/contests/preset-comments/dto/update-preset-comment.dto.ts new file mode 100644 index 0000000..e138d4d --- /dev/null +++ b/backend/src/contests/preset-comments/dto/update-preset-comment.dto.ts @@ -0,0 +1,18 @@ +import { IsString, IsOptional, IsNumber, IsInt, Min } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class UpdatePresetCommentDto { + @IsOptional() + @IsString() + content?: string; + + @IsOptional() + @IsNumber() + @Type(() => Number) + score?: number; + + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; +} diff --git a/backend/src/contests/preset-comments/preset-comments.controller.ts b/backend/src/contests/preset-comments/preset-comments.controller.ts new file mode 100644 index 0000000..fb3a7a2 --- /dev/null +++ b/backend/src/contests/preset-comments/preset-comments.controller.ts @@ -0,0 +1,85 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, + Request, + ParseIntPipe, + Query, +} from '@nestjs/common'; +import { PresetCommentsService } from './preset-comments.service'; +import { CreatePresetCommentDto } from './dto/create-preset-comment.dto'; +import { UpdatePresetCommentDto } from './dto/update-preset-comment.dto'; +import { SyncPresetCommentsDto } from './dto/sync-preset-comments.dto'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; + +@Controller('contests/preset-comments') +@UseGuards(JwtAuthGuard) +export class PresetCommentsController { + constructor(private readonly presetCommentsService: PresetCommentsService) {} + + @Post() + create(@Body() createDto: CreatePresetCommentDto, @Request() req) { + const judgeId = req.user?.userId; + return this.presetCommentsService.create(createDto, judgeId, judgeId); + } + + @Get() + findAll( + @Query('contestId', ParseIntPipe) contestId: number, + @Request() req, + ) { + const judgeId = req.user?.userId; + return this.presetCommentsService.findAll(contestId, judgeId); + } + + @Get('judge/contests') + getJudgeContests(@Request() req) { + const judgeId = req.user?.userId; + return this.presetCommentsService.getJudgeContests(judgeId); + } + + @Get(':id') + findOne(@Param('id', ParseIntPipe) id: number, @Request() req) { + const judgeId = req.user?.userId; + return this.presetCommentsService.findOne(id, judgeId); + } + + @Patch(':id') + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateDto: UpdatePresetCommentDto, + @Request() req, + ) { + const judgeId = req.user?.userId; + return this.presetCommentsService.update(id, updateDto, judgeId, judgeId); + } + + @Delete(':id') + remove(@Param('id', ParseIntPipe) id: number, @Request() req) { + const judgeId = req.user?.userId; + return this.presetCommentsService.remove(id, judgeId); + } + + @Post('batch-delete') + batchDelete(@Body() body: { ids: number[] }, @Request() req) { + const judgeId = req.user?.userId; + return this.presetCommentsService.batchDelete(body.ids, judgeId); + } + + @Post('sync') + sync(@Body() syncDto: SyncPresetCommentsDto, @Request() req) { + const judgeId = req.user?.userId; + return this.presetCommentsService.sync(syncDto, judgeId, judgeId); + } + + @Post(':id/use') + incrementUseCount(@Param('id', ParseIntPipe) id: number, @Request() req) { + const judgeId = req.user?.userId; + return this.presetCommentsService.incrementUseCount(id, judgeId); + } +} diff --git a/backend/src/contests/preset-comments/preset-comments.module.ts b/backend/src/contests/preset-comments/preset-comments.module.ts new file mode 100644 index 0000000..c9bfa34 --- /dev/null +++ b/backend/src/contests/preset-comments/preset-comments.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PresetCommentsService } from './preset-comments.service'; +import { PresetCommentsController } from './preset-comments.controller'; +import { PrismaModule } from '../../prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [PresetCommentsController], + providers: [PresetCommentsService], + exports: [PresetCommentsService], +}) +export class PresetCommentsModule {} diff --git a/backend/src/contests/preset-comments/preset-comments.service.ts b/backend/src/contests/preset-comments/preset-comments.service.ts new file mode 100644 index 0000000..56469ad --- /dev/null +++ b/backend/src/contests/preset-comments/preset-comments.service.ts @@ -0,0 +1,234 @@ +import { + Injectable, + NotFoundException, + ForbiddenException, +} from '@nestjs/common'; +import { PrismaService } from '../../prisma/prisma.service'; +import { CreatePresetCommentDto } from './dto/create-preset-comment.dto'; +import { UpdatePresetCommentDto } from './dto/update-preset-comment.dto'; +import { SyncPresetCommentsDto } from './dto/sync-preset-comments.dto'; + +@Injectable() +export class PresetCommentsService { + constructor(private prisma: PrismaService) {} + + async create( + createDto: CreatePresetCommentDto, + judgeId: number, + creatorId?: number, + ) { + // 验证赛事是否存在 + const contest = await this.prisma.contest.findUnique({ + where: { id: createDto.contestId }, + }); + + if (!contest) { + throw new NotFoundException('赛事不存在'); + } + + // 验证用户是否是该赛事的评委 + const isJudge = await this.prisma.contestJudge.findFirst({ + where: { + contestId: createDto.contestId, + judgeId, + validState: 1, + }, + }); + + if (!isJudge) { + throw new ForbiddenException('您不是该赛事的评委'); + } + + return this.prisma.presetComment.create({ + data: { + contestId: createDto.contestId, + judgeId, + content: createDto.content, + score: createDto.score, + sortOrder: createDto.sortOrder ?? 0, + creator: creatorId, + }, + }); + } + + async findAll(contestId: number, judgeId: number) { + return this.prisma.presetComment.findMany({ + where: { + contestId, + judgeId, + validState: 1, + }, + orderBy: [{ sortOrder: 'asc' }, { createTime: 'desc' }], + }); + } + + async findOne(id: number, judgeId: number) { + const comment = await this.prisma.presetComment.findFirst({ + where: { + id, + judgeId, + validState: 1, + }, + }); + + if (!comment) { + throw new NotFoundException('预设评语不存在'); + } + + return comment; + } + + async update( + id: number, + updateDto: UpdatePresetCommentDto, + judgeId: number, + modifierId?: number, + ) { + await this.findOne(id, judgeId); + + return this.prisma.presetComment.update({ + where: { id }, + data: { + ...updateDto, + modifier: modifierId, + }, + }); + } + + async remove(id: number, judgeId: number) { + await this.findOne(id, judgeId); + + return this.prisma.presetComment.update({ + where: { id }, + data: { + validState: 2, + }, + }); + } + + async batchDelete(ids: number[], judgeId: number) { + // 验证所有评语都属于该评委 + const comments = await this.prisma.presetComment.findMany({ + where: { + id: { in: ids }, + judgeId, + validState: 1, + }, + }); + + if (comments.length !== ids.length) { + throw new ForbiddenException('部分评语不存在或无权操作'); + } + + return this.prisma.presetComment.updateMany({ + where: { + id: { in: ids }, + judgeId, + }, + data: { + validState: 2, + }, + }); + } + + async sync(syncDto: SyncPresetCommentsDto, judgeId: number, creatorId?: number) { + // 获取源赛事的评语 + const sourceComments = await this.prisma.presetComment.findMany({ + where: { + contestId: syncDto.sourceContestId, + judgeId, + validState: 1, + }, + }); + + if (sourceComments.length === 0) { + throw new NotFoundException('源赛事没有预设评语'); + } + + // 验证目标赛事存在且当前用户是评委 + for (const targetContestId of syncDto.targetContestIds) { + const contest = await this.prisma.contest.findUnique({ + where: { id: targetContestId }, + }); + + if (!contest) { + throw new NotFoundException(`赛事 ${targetContestId} 不存在`); + } + + const isJudge = await this.prisma.contestJudge.findFirst({ + where: { + contestId: targetContestId, + judgeId, + validState: 1, + }, + }); + + if (!isJudge) { + throw new ForbiddenException(`您不是赛事 ${contest.contestName} 的评委`); + } + } + + // 为每个目标赛事创建评语副本 + const results = []; + for (const targetContestId of syncDto.targetContestIds) { + for (const comment of sourceComments) { + const created = await this.prisma.presetComment.create({ + data: { + contestId: targetContestId, + judgeId, + content: comment.content, + score: comment.score, + sortOrder: comment.sortOrder, + creator: creatorId, + }, + }); + results.push(created); + } + } + + return { + message: '同步成功', + count: results.length, + }; + } + + async getJudgeContests(judgeId: number) { + const contestJudges = await this.prisma.contestJudge.findMany({ + where: { + judgeId, + validState: 1, + contest: { + validState: 1, + }, + }, + include: { + contest: { + select: { + id: true, + contestName: true, + contestState: true, + status: true, + }, + }, + }, + orderBy: { + createTime: 'desc', + }, + }); + + return contestJudges.map((cj) => cj.contest); + } + + async incrementUseCount(id: number, judgeId: number) { + await this.findOne(id, judgeId); + + return this.prisma.presetComment.update({ + where: { id }, + data: { + useCount: { + increment: 1, + }, + }, + }); + } +} diff --git a/backend/src/contests/reviews/dto/create-score.dto.ts b/backend/src/contests/reviews/dto/create-score.dto.ts index ba6ed75..ef6e5d4 100644 --- a/backend/src/contests/reviews/dto/create-score.dto.ts +++ b/backend/src/contests/reviews/dto/create-score.dto.ts @@ -1,4 +1,4 @@ -import { IsInt, IsObject, IsString, IsOptional, IsNumber, Min, Max } from 'class-validator'; +import { IsInt, IsArray, IsString, IsOptional, IsNumber, Min, Max } from 'class-validator'; export class CreateScoreDto { @IsInt() @@ -7,9 +7,13 @@ export class CreateScoreDto { @IsInt() assignmentId: number; - @IsObject() + @IsArray() @IsOptional() - dimensionScores?: any; // JSON object + dimensionScores?: Array<{ + name: string; + score: number; + maxScore: number; + }>; // 维度评分数组 @IsNumber() @Min(0) diff --git a/backend/src/contests/reviews/reviews.service.ts b/backend/src/contests/reviews/reviews.service.ts index 0ddbae4..d568c92 100644 --- a/backend/src/contests/reviews/reviews.service.ts +++ b/backend/src/contests/reviews/reviews.service.ts @@ -157,7 +157,7 @@ export class ReviewsService { assignmentId: createScoreDto.assignmentId, judgeId, judgeName: judge?.nickname || judge?.username || '', - dimensionScores: createScoreDto.dimensionScores || {}, + dimensionScores: createScoreDto.dimensionScores || [], totalScore: createScoreDto.totalScore, comments: createScoreDto.comments || '', scoreTime: new Date(), @@ -543,7 +543,8 @@ export class ReviewsService { workId: assignment.workId, judgeId: assignment.judgeId, judge: assignment.judge, - score: latestScore?.totalScore ?? null, + totalScore: latestScore?.totalScore ?? null, + dimensionScores: latestScore?.dimensionScores ?? null, scoreTime: latestScore?.scoreTime ?? null, comments: latestScore?.comments ?? null, status: assignment.status, diff --git a/frontend/src/api/preset-comments.ts b/frontend/src/api/preset-comments.ts new file mode 100644 index 0000000..d05ae12 --- /dev/null +++ b/frontend/src/api/preset-comments.ts @@ -0,0 +1,139 @@ +import request from "@/utils/request"; + +export interface PresetComment { + id: number; + contestId: number; + judgeId: number; + content: string; + score?: number; + sortOrder: number; + useCount: number; + validState: number; + creator?: number; + modifier?: number; + createTime: string; + modifyTime: string; +} + +export interface CreatePresetCommentParams { + contestId: number; + content: string; + score?: number; + sortOrder?: number; +} + +export interface UpdatePresetCommentParams { + content?: string; + score?: number; + sortOrder?: number; +} + +export interface SyncPresetCommentsParams { + sourceContestId: number; + targetContestIds: number[]; +} + +export interface JudgeContest { + id: number; + contestName: string; + contestState: string; + status: string; +} + +// 获取预设评语列表 +export async function getPresetCommentsList( + contestId: number +): Promise { + const response = await request.get( + "/contests/preset-comments", + { + params: { contestId }, + } + ); + return response; +} + +// 获取单个预设评语详情 +export async function getPresetCommentDetail( + id: number +): Promise { + const response = await request.get( + `/contests/preset-comments/${id}` + ); + return response; +} + +// 创建预设评语 +export async function createPresetComment( + data: CreatePresetCommentParams +): Promise { + const response = await request.post( + "/contests/preset-comments", + data + ); + return response; +} + +// 更新预设评语 +export async function updatePresetComment( + id: number, + data: UpdatePresetCommentParams +): Promise { + const response = await request.patch( + `/contests/preset-comments/${id}`, + data + ); + return response; +} + +// 删除预设评语 +export async function deletePresetComment(id: number): Promise { + return await request.delete(`/contests/preset-comments/${id}`); +} + +// 批量删除预设评语 +export async function batchDeletePresetComments(ids: number[]): Promise { + return await request.post("/contests/preset-comments/batch-delete", { + ids, + }); +} + +// 同步预设评语到其他赛事 +export async function syncPresetComments( + data: SyncPresetCommentsParams +): Promise<{ message: string; count: number }> { + const response = await request.post( + "/contests/preset-comments/sync", + data + ); + return response; +} + +// 获取评委的赛事列表 +export async function getJudgeContests(): Promise { + const response = await request.get( + "/contests/preset-comments/judge/contests" + ); + return response; +} + +// 增加使用次数 +export async function incrementUseCount(id: number): Promise { + const response = await request.post( + `/contests/preset-comments/${id}/use` + ); + return response; +} + +// 兼容性导出:保留 presetCommentsApi 对象 +export const presetCommentsApi = { + getList: getPresetCommentsList, + getDetail: getPresetCommentDetail, + create: createPresetComment, + update: updatePresetComment, + delete: deletePresetComment, + batchDelete: batchDeletePresetComments, + sync: syncPresetComments, + getJudgeContests: getJudgeContests, + incrementUseCount: incrementUseCount, +}; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 0e025a0..5b3022f 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -120,7 +120,6 @@ const baseRoutes: RouteRecordRaw[] = [ meta: { title: "评审进度详情", requiresAuth: true, - permissions: ["review:read"], }, }, // 赛果发布详情路由 @@ -131,7 +130,6 @@ const baseRoutes: RouteRecordRaw[] = [ meta: { title: "赛果发布详情", requiresAuth: true, - permissions: ["result:read"], }, }, // 参赛作品详情列表路由 @@ -188,6 +186,16 @@ const baseRoutes: RouteRecordRaw[] = [ requiresAuth: true, }, }, + // 预设评语页面 + { + path: "activities/preset-comments", + name: "PresetComments", + component: () => import("@/views/activities/PresetComments.vue"), + meta: { + title: "预设评语", + requiresAuth: true, + }, + }, // 3D建模实验室路由(工作台模块下) { path: "workbench/3d-lab", @@ -539,6 +547,15 @@ router.beforeEach(async (to, _from, next) => { // 如果访问的是主路由,重定向到第一个菜单 const isMainRoute = to.name === "Main" + console.log('Route guard debug:', { + targetPath, + resolvedName: resolved.name, + resolvedPath: resolved.path, + isMainRoute, + toName: to.name, + toPath: to.path, + }) + // 如果解析后的路由不是404,说明路由存在,重新导航 if (resolved.name !== "NotFound" && !isMainRoute) { next({ path: targetPath, replace: true }) diff --git a/frontend/src/utils/menu.ts b/frontend/src/utils/menu.ts index e24962c..89f0bdb 100644 --- a/frontend/src/utils/menu.ts +++ b/frontend/src/utils/menu.ts @@ -53,6 +53,7 @@ const componentMap: Record Promise> = { "activities/Review": () => import("@/views/activities/Review.vue"), "activities/ReviewDetail": () => import("@/views/activities/ReviewDetail.vue"), "activities/Comments": () => import("@/views/activities/Comments.vue"), + "activities/PresetComments": () => import("@/views/activities/PresetComments.vue"), // 系统管理模块 "system/users/Index": () => import("@/views/system/users/Index.vue"), "system/roles/Index": () => import("@/views/system/roles/Index.vue"), diff --git a/frontend/src/views/activities/PresetComments.vue b/frontend/src/views/activities/PresetComments.vue new file mode 100644 index 0000000..3c13efc --- /dev/null +++ b/frontend/src/views/activities/PresetComments.vue @@ -0,0 +1,479 @@ + + + + + diff --git a/frontend/src/views/activities/ReviewDetail.vue b/frontend/src/views/activities/ReviewDetail.vue index 0da7feb..cf0cb3b 100644 --- a/frontend/src/views/activities/ReviewDetail.vue +++ b/frontend/src/views/activities/ReviewDetail.vue @@ -97,7 +97,11 @@ v-model:open="reviewModalVisible" :assignment-id="currentAssignmentId" :work-id="currentWorkId" + :contest-id="contestId" + :work-list="workListForNav" + :current-index="currentWorkIndex" @success="handleReviewSuccess" + @navigate="handleNavigate" /> @@ -190,6 +194,15 @@ const columns = [ const reviewModalVisible = ref(false) const currentAssignmentId = ref(null) const currentWorkId = ref(null) +const currentWorkIndex = ref(0) + +// 用于作品切换导航的列表 +const workListForNav = computed(() => { + return dataSource.value.map((item: any) => ({ + workId: item.workId, + assignmentId: item.id, + })) +}) // 作品详情弹框 const workDetailModalVisible = ref(false) @@ -257,9 +270,22 @@ const handleViewWork = (record: any) => { const handleReview = (record: any) => { currentAssignmentId.value = record.id currentWorkId.value = record.workId + // 查找当前作品在列表中的索引 + const index = dataSource.value.findIndex((item: any) => item.id === record.id) + currentWorkIndex.value = index >= 0 ? index : 0 reviewModalVisible.value = true } +// 导航到其他作品 +const handleNavigate = (index: number) => { + const item = dataSource.value[index] + if (item) { + currentAssignmentId.value = item.id + currentWorkId.value = item.workId + currentWorkIndex.value = index + } +} + // 评审成功回调 const handleReviewSuccess = () => { fetchList() diff --git a/frontend/src/views/activities/components/ReviewWorkModal.vue b/frontend/src/views/activities/components/ReviewWorkModal.vue index f7a873b..686d32a 100644 --- a/frontend/src/views/activities/components/ReviewWorkModal.vue +++ b/frontend/src/views/activities/components/ReviewWorkModal.vue @@ -1,331 +1,982 @@ diff --git a/frontend/src/views/contests/Activities.vue b/frontend/src/views/contests/Activities.vue index 07182fb..298a610 100644 --- a/frontend/src/views/contests/Activities.vue +++ b/frontend/src/views/contests/Activities.vue @@ -227,10 +227,10 @@ >