修复团队赛事相关功能
1. 修复团队成员无法在"我参与的赛事"中看到团队赛事的问题 2. 修复教师作为指导老师无法看到团队赛事的问题 3. 修复上传作品/参赛作品/我的队伍按钮500错误(userId获取方式错误) 4. 修复管理端成员弹框队长名称和成员数显示问题 5. 新增getMyRegistration接口支持团队成员查询报名状态 6. 优化赛事卡片按钮布局 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0cdc5d1ceb
commit
ac0c38c04a
@ -363,6 +363,7 @@ export class ContestsService {
|
|||||||
contestIds = judgeRecords.map((r) => r.contestId);
|
contestIds = judgeRecords.map((r) => r.contestId);
|
||||||
} else if (role === 'teacher') {
|
} else if (role === 'teacher') {
|
||||||
// 教师:查询作为指导老师参与的赛事
|
// 教师:查询作为指导老师参与的赛事
|
||||||
|
// 1. 从报名指导老师关联表查询(个人赛)
|
||||||
const teacherRecords = await this.prisma.contestRegistrationTeacher.findMany({
|
const teacherRecords = await this.prisma.contestRegistrationTeacher.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
@ -375,9 +376,29 @@ export class ContestsService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
contestIds = Array.from(new Set(teacherRecords.map((r) => r.registration.contestId as number)));
|
const contestIdsFromRegistration = teacherRecords.map((r) => r.registration.contestId as number);
|
||||||
|
|
||||||
|
// 2. 从团队成员表查询(团队赛,role='mentor')
|
||||||
|
const mentorRecords = await this.prisma.contestTeamMember.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
role: 'mentor',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
contestId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const contestIdsFromTeam = mentorRecords.map((r) => r.team.contestId);
|
||||||
|
|
||||||
|
// 合并去重
|
||||||
|
contestIds = Array.from(new Set([...contestIdsFromRegistration, ...contestIdsFromTeam]));
|
||||||
} else {
|
} else {
|
||||||
// 学生/默认:查询报名的赛事
|
// 学生/默认:查询报名的赛事
|
||||||
|
// 1. 从报名记录查询(个人赛报名或团队赛队长)
|
||||||
const registrationWhere: any = {
|
const registrationWhere: any = {
|
||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
@ -393,7 +414,32 @@ export class ContestsService {
|
|||||||
},
|
},
|
||||||
distinct: ['contestId'],
|
distinct: ['contestId'],
|
||||||
});
|
});
|
||||||
contestIds = registrations.map((r) => r.contestId);
|
const contestIdsFromRegistration = registrations.map((r) => r.contestId);
|
||||||
|
|
||||||
|
// 2. 从团队成员表查询(团队赛成员,role='leader' 或 'member')
|
||||||
|
const teamMemberWhere: any = {
|
||||||
|
userId,
|
||||||
|
role: { in: ['leader', 'member'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tenantId) {
|
||||||
|
teamMemberWhere.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamMembers = await this.prisma.contestTeamMember.findMany({
|
||||||
|
where: teamMemberWhere,
|
||||||
|
select: {
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
contestId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const contestIdsFromTeam = teamMembers.map((r) => r.team.contestId);
|
||||||
|
|
||||||
|
// 合并去重
|
||||||
|
contestIds = Array.from(new Set([...contestIdsFromRegistration, ...contestIdsFromTeam]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contestIds.length === 0) {
|
if (contestIds.length === 0) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { RegistrationsService } from './registrations.service';
|
import { RegistrationsService } from './registrations.service';
|
||||||
import { CreateRegistrationDto } from './dto/create-registration.dto';
|
import { CreateRegistrationDto } from './dto/create-registration.dto';
|
||||||
@ -28,9 +29,9 @@ export class RegistrationsController {
|
|||||||
create(@Body() createRegistrationDto: CreateRegistrationDto, @Request() req) {
|
create(@Body() createRegistrationDto: CreateRegistrationDto, @Request() req) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
const creatorId = req.user?.id;
|
const creatorId = req.user?.userId;
|
||||||
return this.registrationsService.create(
|
return this.registrationsService.create(
|
||||||
createRegistrationDto,
|
createRegistrationDto,
|
||||||
tenantId,
|
tenantId,
|
||||||
@ -45,6 +46,22 @@ export class RegistrationsController {
|
|||||||
return this.registrationsService.findAll(queryDto, tenantId);
|
return this.registrationsService.findAll(queryDto, tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户在某比赛中的报名记录(包括作为团队成员的情况)
|
||||||
|
*/
|
||||||
|
@Get('my/:contestId')
|
||||||
|
@RequirePermission('contest:read')
|
||||||
|
getMyRegistration(
|
||||||
|
@Param('contestId', ParseIntPipe) contestId: number,
|
||||||
|
@Request() req,
|
||||||
|
) {
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
throw new BadRequestException('用户未登录');
|
||||||
|
}
|
||||||
|
return this.registrationsService.getMyRegistration(contestId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePermission('contest:read')
|
@RequirePermission('contest:read')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
findOne(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
||||||
@ -60,7 +77,7 @@ export class RegistrationsController {
|
|||||||
@Request() req,
|
@Request() req,
|
||||||
) {
|
) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
const operatorId = req.user?.id;
|
const operatorId = req.user?.userId;
|
||||||
return this.registrationsService.review(id, reviewDto, operatorId, tenantId);
|
return this.registrationsService.review(id, reviewDto, operatorId, tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,9 +90,9 @@ export class RegistrationsController {
|
|||||||
) {
|
) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
const creatorId = req.user?.id;
|
const creatorId = req.user?.userId;
|
||||||
return this.registrationsService.addTeacher(
|
return this.registrationsService.addTeacher(
|
||||||
id,
|
id,
|
||||||
body.teacherUserId,
|
body.teacherUserId,
|
||||||
@ -93,7 +110,7 @@ export class RegistrationsController {
|
|||||||
) {
|
) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
return this.registrationsService.removeTeacher(id, teacherUserId, tenantId);
|
return this.registrationsService.removeTeacher(id, teacherUserId, tenantId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -626,5 +626,111 @@ export class RegistrationsService {
|
|||||||
where: { id: teacherRecord.id },
|
where: { id: teacherRecord.id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户在某比赛中的报名记录(包括作为团队成员的情况)
|
||||||
|
* @param contestId 比赛ID
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 报名记录或null
|
||||||
|
*/
|
||||||
|
async getMyRegistration(contestId: number, userId: number) {
|
||||||
|
try {
|
||||||
|
// 1. 先查询个人赛报名(直接按 userId 匹配)
|
||||||
|
const individualReg = await this.prisma.contestRegistration.findFirst({
|
||||||
|
where: {
|
||||||
|
contestId,
|
||||||
|
userId,
|
||||||
|
registrationType: 'individual',
|
||||||
|
registrationState: 'passed',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
contest: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contestName: true,
|
||||||
|
contestType: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
works: {
|
||||||
|
where: { validState: 1, isLatest: true },
|
||||||
|
orderBy: { submitTime: 'desc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (individualReg) {
|
||||||
|
return individualReg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询用户所属的团队(该比赛的)
|
||||||
|
const teamMemberships = await this.prisma.contestTeamMember.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
teamId: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contestId: true,
|
||||||
|
validState: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤出该比赛且有效的团队
|
||||||
|
const validTeamIds = teamMemberships
|
||||||
|
.filter((m) => m.team?.contestId === contestId && m.team?.validState === 1)
|
||||||
|
.map((m) => m.teamId);
|
||||||
|
|
||||||
|
if (validTeamIds.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 通过团队ID查询团队报名记录
|
||||||
|
const teamReg = await this.prisma.contestRegistration.findFirst({
|
||||||
|
where: {
|
||||||
|
contestId,
|
||||||
|
teamId: { in: validTeamIds },
|
||||||
|
registrationType: 'team',
|
||||||
|
registrationState: 'passed',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
contest: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contestName: true,
|
||||||
|
contestType: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
nickname: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
works: {
|
||||||
|
where: { validState: 1, isLatest: true },
|
||||||
|
orderBy: { submitTime: 'desc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return teamReg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getMyRegistration error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
Request,
|
Request,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { TeamsService } from './teams.service';
|
import { TeamsService } from './teams.service';
|
||||||
import { CreateTeamDto } from './dto/create-team.dto';
|
import { CreateTeamDto } from './dto/create-team.dto';
|
||||||
@ -27,9 +28,9 @@ export class TeamsController {
|
|||||||
create(@Body() createTeamDto: CreateTeamDto, @Request() req) {
|
create(@Body() createTeamDto: CreateTeamDto, @Request() req) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
const creatorId = req.user?.id;
|
const creatorId = req.user?.userId;
|
||||||
return this.teamsService.create(createTeamDto, tenantId, creatorId);
|
return this.teamsService.create(createTeamDto, tenantId, creatorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,9 +60,9 @@ export class TeamsController {
|
|||||||
) {
|
) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
const modifierId = req.user?.id;
|
const modifierId = req.user?.userId;
|
||||||
return this.teamsService.update(id, updateTeamDto, tenantId, modifierId);
|
return this.teamsService.update(id, updateTeamDto, tenantId, modifierId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,9 +75,9 @@ export class TeamsController {
|
|||||||
) {
|
) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
const creatorId = req.user?.id;
|
const creatorId = req.user?.userId;
|
||||||
return this.teamsService.inviteMember(
|
return this.teamsService.inviteMember(
|
||||||
teamId,
|
teamId,
|
||||||
inviteMemberDto,
|
inviteMemberDto,
|
||||||
@ -94,7 +95,7 @@ export class TeamsController {
|
|||||||
) {
|
) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
return this.teamsService.removeMember(teamId, userId, tenantId);
|
return this.teamsService.removeMember(teamId, userId, tenantId);
|
||||||
}
|
}
|
||||||
@ -104,7 +105,7 @@ export class TeamsController {
|
|||||||
remove(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
remove(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
||||||
const tenantId = req.tenantId || req.user?.tenantId;
|
const tenantId = req.tenantId || req.user?.tenantId;
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new Error('无法确定租户信息');
|
throw new BadRequestException('无法确定租户信息');
|
||||||
}
|
}
|
||||||
return this.teamsService.remove(id, tenantId);
|
return this.teamsService.remove(id, tenantId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -206,7 +206,7 @@ export class TeamsService {
|
|||||||
where.tenantId = tenantId;
|
where.tenantId = tenantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.contestTeam.findMany({
|
const teams = await this.prisma.contestTeam.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createTime: 'desc',
|
createTime: 'desc',
|
||||||
@ -230,6 +230,13 @@ export class TeamsService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
registrations: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
registrationState: true,
|
||||||
|
},
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
members: true,
|
members: true,
|
||||||
@ -238,15 +245,24 @@ export class TeamsService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 将报名状态扁平化到团队对象上
|
||||||
|
return teams.map((team) => ({
|
||||||
|
...team,
|
||||||
|
registrationState: team.registrations?.[0]?.registrationState || 'pending',
|
||||||
|
registrationId: team.registrations?.[0]?.id,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number, tenantId?: number) {
|
async findOne(id: number, tenantId?: number, strictTenantCheck = false) {
|
||||||
const where: any = {
|
const where: any = {
|
||||||
id,
|
id,
|
||||||
validState: 1,
|
validState: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tenantId) {
|
// 只有明确要求严格租户检查时才限制 tenantId
|
||||||
|
// 通过 ID 查询单个团队时,ID 已经是唯一的,不需要再限制 tenantId
|
||||||
|
if (tenantId && strictTenantCheck) {
|
||||||
where.tenantId = tenantId;
|
where.tenantId = tenantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -257,6 +257,8 @@ export interface ContestTeam {
|
|||||||
createTime?: string;
|
createTime?: string;
|
||||||
modifyTime?: string;
|
modifyTime?: string;
|
||||||
validState?: number;
|
validState?: number;
|
||||||
|
registrationState?: string; // 报名状态
|
||||||
|
registrationId?: number; // 报名记录ID
|
||||||
leader?: {
|
leader?: {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
@ -730,6 +732,16 @@ export const registrationsApi = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取当前用户在某比赛中的报名记录(包括作为团队成员的情况)
|
||||||
|
getMyRegistration: async (
|
||||||
|
contestId: number
|
||||||
|
): Promise<ContestRegistration | null> => {
|
||||||
|
const response = await request.get<any, ContestRegistration | null>(
|
||||||
|
`/contests/registrations/my/${contestId}`
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
// 创建报名
|
// 创建报名
|
||||||
create: async (
|
create: async (
|
||||||
data: CreateRegistrationForm
|
data: CreateRegistrationForm
|
||||||
|
|||||||
@ -105,17 +105,18 @@
|
|||||||
|
|
||||||
<!-- 底部区域 -->
|
<!-- 底部区域 -->
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
<div class="status-row">
|
||||||
<span
|
<span
|
||||||
class="status-dot"
|
class="status-dot"
|
||||||
:class="{ 'status-ongoing': contest.status === 'ongoing' }"
|
:class="{ 'status-ongoing': contest.status === 'ongoing' }"
|
||||||
></span>
|
></span>
|
||||||
<span class="status-text">{{ getStatusText(contest) }}</span>
|
<span class="status-text">{{ getStatusText(contest) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮区域 - 我的赛事tab显示 -->
|
<!-- 操作按钮区域 - 我的赛事tab显示 -->
|
||||||
<div v-if="activeTab === 'my'" class="card-actions" @click.stop>
|
<div v-if="activeTab === 'my'" class="card-actions" @click.stop>
|
||||||
<!-- 学生角色按钮 -->
|
<!-- 学生角色按钮 -->
|
||||||
<template v-if="userRole === 'student'">
|
<template v-if="userRole === 'student'">
|
||||||
<template v-if="contest.contestType === 'individual'">
|
|
||||||
<a-button
|
<a-button
|
||||||
v-if="isSubmitting(contest)"
|
v-if="isSubmitting(contest)"
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -127,16 +128,14 @@
|
|||||||
<a-button size="small" @click="handleViewWorks(contest.id)">
|
<a-button size="small" @click="handleViewWorks(contest.id)">
|
||||||
参赛作品
|
参赛作品
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
<a-button
|
||||||
<template v-else>
|
v-if="contest.contestType === 'team'"
|
||||||
<a-button size="small" @click="handleViewWorks(contest.id)">
|
size="small"
|
||||||
参赛作品
|
@click="handleViewTeam(contest)"
|
||||||
</a-button>
|
>
|
||||||
<a-button size="small" @click="handleViewTeam(contest.id)">
|
|
||||||
我的队伍
|
我的队伍
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 教师角色按钮 -->
|
<!-- 教师角色按钮 -->
|
||||||
<template v-if="userRole === 'teacher'">
|
<template v-if="userRole === 'teacher'">
|
||||||
@ -199,6 +198,52 @@
|
|||||||
v-model:open="viewWorkDrawerVisible"
|
v-model:open="viewWorkDrawerVisible"
|
||||||
:contest-id="currentContestIdForView"
|
:contest-id="currentContestIdForView"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 我的队伍弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="teamModalVisible"
|
||||||
|
:title="`我的队伍 - ${currentTeamContest?.contestName || ''}`"
|
||||||
|
:footer="null"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="teamLoading">
|
||||||
|
<div v-if="myTeamInfo" class="team-info">
|
||||||
|
<a-descriptions :column="2" bordered style="margin-bottom: 16px">
|
||||||
|
<a-descriptions-item label="团队名称">
|
||||||
|
{{ myTeamInfo.teamName }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="成员数">
|
||||||
|
{{ myTeamInfo.members?.length || 0 }}人
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
<div class="team-members">
|
||||||
|
<div class="members-title">团队成员</div>
|
||||||
|
<a-table
|
||||||
|
:columns="memberColumns"
|
||||||
|
:data-source="myTeamInfo.members || []"
|
||||||
|
:pagination="false"
|
||||||
|
row-key="id"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'nickname'">
|
||||||
|
{{ record.user?.nickname || '-' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'username'">
|
||||||
|
{{ record.user?.username || '-' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'role'">
|
||||||
|
<a-tag :color="getMemberRoleColor(record.role)">
|
||||||
|
{{ getMemberRoleText(record.role) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-empty v-else description="暂无团队信息" />
|
||||||
|
</a-spin>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -216,8 +261,10 @@ import {
|
|||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import {
|
import {
|
||||||
contestsApi,
|
contestsApi,
|
||||||
|
registrationsApi,
|
||||||
type Contest,
|
type Contest,
|
||||||
type QueryContestParams,
|
type QueryContestParams,
|
||||||
|
type ContestTeam,
|
||||||
} from "@/api/contests"
|
} from "@/api/contests"
|
||||||
import { useAuthStore } from "@/stores/auth"
|
import { useAuthStore } from "@/stores/auth"
|
||||||
import SubmitWorkDrawer from "./components/SubmitWorkDrawer.vue"
|
import SubmitWorkDrawer from "./components/SubmitWorkDrawer.vue"
|
||||||
@ -374,9 +421,59 @@ const handleViewWorks = (id: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查看我的队伍
|
// 查看我的队伍
|
||||||
const handleViewTeam = (id: number) => {
|
const teamModalVisible = ref(false)
|
||||||
// TODO: 跳转到我的队伍页面或打开抽屉
|
const teamLoading = ref(false)
|
||||||
message.info("查看我的队伍功能开发中")
|
const currentTeamContest = ref<Contest | null>(null)
|
||||||
|
const myTeamInfo = ref<ContestTeam | null>(null)
|
||||||
|
|
||||||
|
// 团队成员表格列
|
||||||
|
const memberColumns = [
|
||||||
|
{ title: "姓名", key: "nickname", width: 120 },
|
||||||
|
{ title: "账号", key: "username", width: 150 },
|
||||||
|
{ title: "角色", key: "role", width: 100 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 获取成员角色颜色
|
||||||
|
const getMemberRoleColor = (role?: string) => {
|
||||||
|
switch (role) {
|
||||||
|
case "leader":
|
||||||
|
return "gold"
|
||||||
|
case "mentor":
|
||||||
|
return "purple"
|
||||||
|
default:
|
||||||
|
return "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取成员角色文本
|
||||||
|
const getMemberRoleText = (role?: string) => {
|
||||||
|
switch (role) {
|
||||||
|
case "leader":
|
||||||
|
return "队长"
|
||||||
|
case "mentor":
|
||||||
|
return "指导老师"
|
||||||
|
default:
|
||||||
|
return "成员"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewTeam = async (contest: Contest) => {
|
||||||
|
currentTeamContest.value = contest
|
||||||
|
teamModalVisible.value = true
|
||||||
|
teamLoading.value = true
|
||||||
|
myTeamInfo.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取用户在该比赛的报名记录(包括团队信息)
|
||||||
|
const registration = await registrationsApi.getMyRegistration(contest.id)
|
||||||
|
if (registration?.team) {
|
||||||
|
myTeamInfo.value = registration.team
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error?.response?.data?.message || "获取团队信息失败")
|
||||||
|
} finally {
|
||||||
|
teamLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 教师相关操作 =====
|
// ===== 教师相关操作 =====
|
||||||
@ -797,11 +894,17 @@ $primary-light: #40a9ff;
|
|||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid #f5f5f5;
|
border-top: 1px solid #f5f5f5;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@ -823,16 +926,15 @@ $primary-light: #40a9ff;
|
|||||||
.card-actions {
|
.card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
// 渐变主要按钮 - 蓝色系
|
// 渐变主要按钮 - 蓝色系
|
||||||
:deep(.ant-btn-primary) {
|
:deep(.ant-btn-primary) {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 14px;
|
||||||
padding: 6px 16px;
|
padding: 4px 12px;
|
||||||
height: auto;
|
height: auto;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
$primary 0%,
|
$primary 0%,
|
||||||
@ -860,10 +962,10 @@ $primary-light: #40a9ff;
|
|||||||
// 渐变次要按钮
|
// 渐变次要按钮
|
||||||
:deep(.ant-btn-default) {
|
:deep(.ant-btn-default) {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 14px;
|
||||||
padding: 6px 16px;
|
padding: 4px 12px;
|
||||||
height: auto;
|
height: auto;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf0 100%);
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf0 100%);
|
||||||
color: rgba(0, 0, 0, 0.75);
|
color: rgba(0, 0, 0, 0.75);
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
||||||
@ -895,6 +997,18 @@ $primary-light: #40a9ff;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 团队信息弹窗样式
|
||||||
|
.team-info {
|
||||||
|
.members-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 响应式适配
|
// 响应式适配
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.contests-activities-page {
|
.contests-activities-page {
|
||||||
|
|||||||
@ -360,23 +360,13 @@ const fetchRegistrationId = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await registrationsApi.getList({
|
// 获取用户的报名记录(包括作为团队成员的情况)
|
||||||
contestId: props.contestId,
|
const registration = await registrationsApi.getMyRegistration(props.contestId)
|
||||||
userId: authStore.user.id,
|
|
||||||
page: 1,
|
|
||||||
pageSize: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.list && response.list.length > 0) {
|
if (registration) {
|
||||||
const registration = response.list[0]
|
|
||||||
if (registration.registrationState === "passed") {
|
|
||||||
registrationIdRef.value = registration.id
|
registrationIdRef.value = registration.id
|
||||||
} else {
|
} else {
|
||||||
message.warning("您的报名尚未通过审核,无法上传作品")
|
message.warning("您尚未报名该赛事或报名未通过,无法上传作品")
|
||||||
visible.value = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.warning("您尚未报名该赛事,无法上传作品")
|
|
||||||
visible.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@ -205,23 +205,16 @@ const fetchUserWork = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先获取用户的报名记录
|
// 获取用户的报名记录(包括作为团队成员的情况)
|
||||||
const registrationResponse = await registrationsApi.getList({
|
const registration = await registrationsApi.getMyRegistration(props.contestId)
|
||||||
contestId: props.contestId,
|
|
||||||
userId: userId,
|
|
||||||
registrationType: "individual",
|
|
||||||
registrationState: "passed",
|
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (registrationResponse.list.length === 0) {
|
if (!registration) {
|
||||||
message.warning("您尚未报名该赛事或报名未通过,无法查看作品")
|
message.warning("您尚未报名该赛事或报名未通过,无法查看作品")
|
||||||
visible.value = false
|
visible.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const registrationId = registrationResponse.list[0].id
|
const registrationId = registration.id
|
||||||
|
|
||||||
// 获取该报名的所有作品版本,取最新版本
|
// 获取该报名的所有作品版本,取最新版本
|
||||||
const works = await worksApi.getVersions(registrationId)
|
const works = await worksApi.getVersions(registrationId)
|
||||||
|
|||||||
@ -355,7 +355,7 @@
|
|||||||
<a-descriptions :column="3" bordered style="margin-bottom: 16px">
|
<a-descriptions :column="3" bordered style="margin-bottom: 16px">
|
||||||
<a-descriptions-item label="团队名称">{{ currentTeam.teamName }}</a-descriptions-item>
|
<a-descriptions-item label="团队名称">{{ currentTeam.teamName }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="队长">{{ currentTeam.leader?.nickname || "-" }}</a-descriptions-item>
|
<a-descriptions-item label="队长">{{ currentTeam.leader?.nickname || "-" }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="成员数">{{ currentTeam._count?.members || 0 }}人</a-descriptions-item>
|
<a-descriptions-item label="成员数">{{ teamMembers.length || currentTeam._count?.members || 0 }}人</a-descriptions-item>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
<a-table
|
<a-table
|
||||||
:columns="memberColumns"
|
:columns="memberColumns"
|
||||||
@ -778,15 +778,19 @@ const handleViewMembers = async (record: ContestRegistration) => {
|
|||||||
message.warning("暂无团队信息")
|
message.warning("暂无团队信息")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentTeam.value = record.team
|
|
||||||
membersModalVisible.value = true
|
membersModalVisible.value = true
|
||||||
membersLoading.value = true
|
membersLoading.value = true
|
||||||
|
currentTeam.value = null
|
||||||
|
teamMembers.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const teamDetail = await teamsApi.getDetail(record.team.id)
|
const teamDetail = await teamsApi.getDetail(record.team.id)
|
||||||
|
// 更新完整的团队信息(包含 leader 和成员数)
|
||||||
|
currentTeam.value = teamDetail
|
||||||
teamMembers.value = teamDetail.members || []
|
teamMembers.value = teamDetail.members || []
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error("获取团队成员失败")
|
message.error("获取团队成员失败")
|
||||||
|
currentTeam.value = record.team // 降级使用列表中的数据
|
||||||
teamMembers.value = []
|
teamMembers.value = []
|
||||||
} finally {
|
} finally {
|
||||||
membersLoading.value = false
|
membersLoading.value = false
|
||||||
|
|||||||
@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="loading-info">
|
<div class="loading-info">
|
||||||
<div class="loading-title">
|
<div class="loading-title">
|
||||||
{{ task?.status === 'pending' ? '排队中' : 'AI 生成中' }}
|
{{ task?.status === "pending" ? "排队中" : "AI 生成中" }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="task?.status === 'pending'" class="loading-text">
|
<div v-if="task?.status === 'pending'" class="loading-text">
|
||||||
<p>
|
<p>
|
||||||
@ -83,9 +83,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
预计时间:
|
预计时间:
|
||||||
<span class="highlight"
|
<span class="highlight">{{
|
||||||
>{{ formatEstimatedTime(queueInfo.estimatedTime) }}</span
|
formatEstimatedTime(queueInfo.estimatedTime)
|
||||||
>
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="loading-text">
|
<div v-else class="loading-text">
|
||||||
@ -290,10 +290,7 @@ const handleCardClick = (index: number) => {
|
|||||||
|
|
||||||
// 存储到 sessionStorage(避免URL过长)
|
// 存储到 sessionStorage(避免URL过长)
|
||||||
if (allResultUrls.length > 1) {
|
if (allResultUrls.length > 1) {
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem("model-viewer-urls", JSON.stringify(allResultUrls))
|
||||||
"model-viewer-urls",
|
|
||||||
JSON.stringify(allResultUrls)
|
|
||||||
)
|
|
||||||
sessionStorage.setItem("model-viewer-index", String(index))
|
sessionStorage.setItem("model-viewer-index", String(index))
|
||||||
// 清除单URL存储
|
// 清除单URL存储
|
||||||
sessionStorage.removeItem("model-viewer-url")
|
sessionStorage.removeItem("model-viewer-url")
|
||||||
@ -777,7 +774,7 @@ $gradient-card: linear-gradient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 120px;
|
width: 200px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: rgba($primary, 0.2);
|
background: rgba($primary, 0.2);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user