修改代码
This commit is contained in:
parent
bbdeaac93a
commit
aecd72f9ee
@ -6,7 +6,7 @@
|
||||
"component": "workbench/Index",
|
||||
"parentId": null,
|
||||
"sort": 1,
|
||||
"permission": null
|
||||
"permission": "workbench:read"
|
||||
},
|
||||
{
|
||||
"name": "学校管理",
|
||||
@ -14,8 +14,8 @@
|
||||
"icon": "BankOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 5,
|
||||
"permission": null,
|
||||
"sort": 2,
|
||||
"permission": "school:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "学校信息",
|
||||
@ -67,70 +67,113 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "赛事活动",
|
||||
"path": "/activities",
|
||||
"icon": "FlagOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 3,
|
||||
"permission": "activity:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "活动列表",
|
||||
"path": "/activities",
|
||||
"icon": "UnorderedListOutlined",
|
||||
"component": "contests/Activities",
|
||||
"sort": 1,
|
||||
"permission": "activity:read"
|
||||
},
|
||||
{
|
||||
"name": "我的报名",
|
||||
"path": "/activities/registrations",
|
||||
"icon": "UserAddOutlined",
|
||||
"component": "contests/registrations/Index",
|
||||
"sort": 2,
|
||||
"permission": "registration:read"
|
||||
},
|
||||
{
|
||||
"name": "我的作品",
|
||||
"path": "/activities/works",
|
||||
"icon": "FileTextOutlined",
|
||||
"component": "contests/works/Index",
|
||||
"sort": 3,
|
||||
"permission": "work:read"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "赛事管理",
|
||||
"path": "/contests",
|
||||
"icon": "TrophyOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 6,
|
||||
"permission": null,
|
||||
"sort": 4,
|
||||
"permission": "contest:create",
|
||||
"children": [
|
||||
{
|
||||
"name": "赛事活动",
|
||||
"path": "/contests/activities",
|
||||
"icon": "AppstoreOutlined",
|
||||
"component": "contests/Activities",
|
||||
"sort": 0,
|
||||
"permission": "contest:read"
|
||||
},
|
||||
{
|
||||
"name": "赛事列表",
|
||||
"path": "/contests",
|
||||
"icon": "UnorderedListOutlined",
|
||||
"component": "contests/Index",
|
||||
"sort": 1,
|
||||
"permission": "contest:read"
|
||||
"permission": "contest:create"
|
||||
},
|
||||
{
|
||||
"name": "评委管理",
|
||||
"path": "/contests/judges",
|
||||
"icon": "SolutionOutlined",
|
||||
"component": "contests/judges/Index",
|
||||
"sort": 2,
|
||||
"permission": "judge:read"
|
||||
},
|
||||
{
|
||||
"name": "报名管理",
|
||||
"path": "/contests/registrations",
|
||||
"icon": "UserAddOutlined",
|
||||
"component": "contests/registrations/Index",
|
||||
"sort": 2,
|
||||
"permission": "contest:registration:read"
|
||||
"sort": 3,
|
||||
"permission": "registration:approve"
|
||||
},
|
||||
{
|
||||
"name": "作品管理",
|
||||
"path": "/contests/works",
|
||||
"icon": "FileTextOutlined",
|
||||
"component": "contests/works/Index",
|
||||
"sort": 3,
|
||||
"permission": "contest:work:read"
|
||||
"sort": 4,
|
||||
"permission": "work:read"
|
||||
},
|
||||
{
|
||||
"name": "评审任务",
|
||||
"path": "/contests/review-tasks",
|
||||
"icon": "AuditOutlined",
|
||||
"component": "contests/reviews/Tasks",
|
||||
"sort": 5,
|
||||
"permission": "review:read"
|
||||
},
|
||||
{
|
||||
"name": "评审规则",
|
||||
"path": "/contests/reviews",
|
||||
"icon": "CheckCircleOutlined",
|
||||
"component": "contests/reviews/Index",
|
||||
"sort": 4,
|
||||
"permission": "contest:review:read"
|
||||
"sort": 6,
|
||||
"permission": "review-rule:read"
|
||||
},
|
||||
{
|
||||
"name": "赛果发布",
|
||||
"path": "/contests/results",
|
||||
"icon": "TrophyOutlined",
|
||||
"component": "contests/results/Index",
|
||||
"sort": 5,
|
||||
"permission": "contest:result:read"
|
||||
"sort": 7,
|
||||
"permission": "contest:create"
|
||||
},
|
||||
{
|
||||
"name": "通知管理",
|
||||
"path": "/contests/notices",
|
||||
"icon": "BellOutlined",
|
||||
"component": "contests/notices/Index",
|
||||
"sort": 7,
|
||||
"permission": "contest:notice:read"
|
||||
"sort": 8,
|
||||
"permission": "notice:create"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -140,11 +183,11 @@
|
||||
"icon": "FormOutlined",
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 7,
|
||||
"permission": null,
|
||||
"sort": 5,
|
||||
"permission": "homework:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "作业管理",
|
||||
"name": "作业列表",
|
||||
"path": "/homework",
|
||||
"icon": "FileTextOutlined",
|
||||
"component": "homework/Index",
|
||||
@ -157,7 +200,7 @@
|
||||
"icon": "EditOutlined",
|
||||
"component": "homework/Submissions",
|
||||
"sort": 2,
|
||||
"permission": "homework:read"
|
||||
"permission": "homework-submission:read"
|
||||
},
|
||||
{
|
||||
"name": "评审规则",
|
||||
@ -165,7 +208,7 @@
|
||||
"icon": "CheckCircleOutlined",
|
||||
"component": "homework/ReviewRules",
|
||||
"sort": 3,
|
||||
"permission": "homework:read"
|
||||
"permission": "homework-review-rule:read"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -176,7 +219,7 @@
|
||||
"component": null,
|
||||
"parentId": null,
|
||||
"sort": 10,
|
||||
"permission": null,
|
||||
"permission": "user:read",
|
||||
"children": [
|
||||
{
|
||||
"name": "用户管理",
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
[
|
||||
{
|
||||
"code": "workbench:read",
|
||||
"resource": "workbench",
|
||||
"action": "read",
|
||||
"name": "查看工作台",
|
||||
"description": "允许查看工作台"
|
||||
},
|
||||
{
|
||||
"code": "user:create",
|
||||
"resource": "user",
|
||||
@ -125,6 +132,517 @@
|
||||
"name": "删除菜单",
|
||||
"description": "允许删除菜单"
|
||||
},
|
||||
{
|
||||
"code": "tenant:create",
|
||||
"resource": "tenant",
|
||||
"action": "create",
|
||||
"name": "创建租户",
|
||||
"description": "允许创建租户"
|
||||
},
|
||||
{
|
||||
"code": "tenant:read",
|
||||
"resource": "tenant",
|
||||
"action": "read",
|
||||
"name": "查看租户",
|
||||
"description": "允许查看租户列表"
|
||||
},
|
||||
{
|
||||
"code": "tenant:update",
|
||||
"resource": "tenant",
|
||||
"action": "update",
|
||||
"name": "更新租户",
|
||||
"description": "允许更新租户信息"
|
||||
},
|
||||
{
|
||||
"code": "tenant:delete",
|
||||
"resource": "tenant",
|
||||
"action": "delete",
|
||||
"name": "删除租户",
|
||||
"description": "允许删除租户"
|
||||
},
|
||||
{
|
||||
"code": "school:create",
|
||||
"resource": "school",
|
||||
"action": "create",
|
||||
"name": "创建学校",
|
||||
"description": "允许创建学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:read",
|
||||
"resource": "school",
|
||||
"action": "read",
|
||||
"name": "查看学校",
|
||||
"description": "允许查看学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:update",
|
||||
"resource": "school",
|
||||
"action": "update",
|
||||
"name": "更新学校",
|
||||
"description": "允许更新学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:delete",
|
||||
"resource": "school",
|
||||
"action": "delete",
|
||||
"name": "删除学校",
|
||||
"description": "允许删除学校信息"
|
||||
},
|
||||
{
|
||||
"code": "department:create",
|
||||
"resource": "department",
|
||||
"action": "create",
|
||||
"name": "创建部门",
|
||||
"description": "允许创建部门"
|
||||
},
|
||||
{
|
||||
"code": "department:read",
|
||||
"resource": "department",
|
||||
"action": "read",
|
||||
"name": "查看部门",
|
||||
"description": "允许查看部门列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "department:update",
|
||||
"resource": "department",
|
||||
"action": "update",
|
||||
"name": "更新部门",
|
||||
"description": "允许更新部门信息"
|
||||
},
|
||||
{
|
||||
"code": "department:delete",
|
||||
"resource": "department",
|
||||
"action": "delete",
|
||||
"name": "删除部门",
|
||||
"description": "允许删除部门"
|
||||
},
|
||||
{
|
||||
"code": "grade:create",
|
||||
"resource": "grade",
|
||||
"action": "create",
|
||||
"name": "创建年级",
|
||||
"description": "允许创建年级"
|
||||
},
|
||||
{
|
||||
"code": "grade:read",
|
||||
"resource": "grade",
|
||||
"action": "read",
|
||||
"name": "查看年级",
|
||||
"description": "允许查看年级列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "grade:update",
|
||||
"resource": "grade",
|
||||
"action": "update",
|
||||
"name": "更新年级",
|
||||
"description": "允许更新年级信息"
|
||||
},
|
||||
{
|
||||
"code": "grade:delete",
|
||||
"resource": "grade",
|
||||
"action": "delete",
|
||||
"name": "删除年级",
|
||||
"description": "允许删除年级"
|
||||
},
|
||||
{
|
||||
"code": "class:create",
|
||||
"resource": "class",
|
||||
"action": "create",
|
||||
"name": "创建班级",
|
||||
"description": "允许创建班级"
|
||||
},
|
||||
{
|
||||
"code": "class:read",
|
||||
"resource": "class",
|
||||
"action": "read",
|
||||
"name": "查看班级",
|
||||
"description": "允许查看班级列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "class:update",
|
||||
"resource": "class",
|
||||
"action": "update",
|
||||
"name": "更新班级",
|
||||
"description": "允许更新班级信息"
|
||||
},
|
||||
{
|
||||
"code": "class:delete",
|
||||
"resource": "class",
|
||||
"action": "delete",
|
||||
"name": "删除班级",
|
||||
"description": "允许删除班级"
|
||||
},
|
||||
{
|
||||
"code": "teacher:create",
|
||||
"resource": "teacher",
|
||||
"action": "create",
|
||||
"name": "创建教师",
|
||||
"description": "允许创建教师"
|
||||
},
|
||||
{
|
||||
"code": "teacher:read",
|
||||
"resource": "teacher",
|
||||
"action": "read",
|
||||
"name": "查看教师",
|
||||
"description": "允许查看教师列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "teacher:update",
|
||||
"resource": "teacher",
|
||||
"action": "update",
|
||||
"name": "更新教师",
|
||||
"description": "允许更新教师信息"
|
||||
},
|
||||
{
|
||||
"code": "teacher:delete",
|
||||
"resource": "teacher",
|
||||
"action": "delete",
|
||||
"name": "删除教师",
|
||||
"description": "允许删除教师"
|
||||
},
|
||||
{
|
||||
"code": "student:create",
|
||||
"resource": "student",
|
||||
"action": "create",
|
||||
"name": "创建学生",
|
||||
"description": "允许创建学生"
|
||||
},
|
||||
{
|
||||
"code": "student:read",
|
||||
"resource": "student",
|
||||
"action": "read",
|
||||
"name": "查看学生",
|
||||
"description": "允许查看学生列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "student:update",
|
||||
"resource": "student",
|
||||
"action": "update",
|
||||
"name": "更新学生",
|
||||
"description": "允许更新学生信息"
|
||||
},
|
||||
{
|
||||
"code": "student:delete",
|
||||
"resource": "student",
|
||||
"action": "delete",
|
||||
"name": "删除学生",
|
||||
"description": "允许删除学生"
|
||||
},
|
||||
{
|
||||
"code": "contest:create",
|
||||
"resource": "contest",
|
||||
"action": "create",
|
||||
"name": "创建赛事",
|
||||
"description": "允许创建赛事"
|
||||
},
|
||||
{
|
||||
"code": "contest:read",
|
||||
"resource": "contest",
|
||||
"action": "read",
|
||||
"name": "查看赛事",
|
||||
"description": "允许查看赛事列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "contest:update",
|
||||
"resource": "contest",
|
||||
"action": "update",
|
||||
"name": "更新赛事",
|
||||
"description": "允许更新赛事信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:delete",
|
||||
"resource": "contest",
|
||||
"action": "delete",
|
||||
"name": "删除赛事",
|
||||
"description": "允许删除赛事"
|
||||
},
|
||||
{
|
||||
"code": "contest:publish",
|
||||
"resource": "contest",
|
||||
"action": "publish",
|
||||
"name": "发布赛事",
|
||||
"description": "允许发布/取消发布赛事"
|
||||
},
|
||||
{
|
||||
"code": "contest:finish",
|
||||
"resource": "contest",
|
||||
"action": "finish",
|
||||
"name": "结束赛事",
|
||||
"description": "允许结束赛事"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:create",
|
||||
"resource": "review-rule",
|
||||
"action": "create",
|
||||
"name": "创建评审规则",
|
||||
"description": "允许创建评审规则"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:read",
|
||||
"resource": "review-rule",
|
||||
"action": "read",
|
||||
"name": "查看评审规则",
|
||||
"description": "允许查看评审规则"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:update",
|
||||
"resource": "review-rule",
|
||||
"action": "update",
|
||||
"name": "更新评审规则",
|
||||
"description": "允许更新评审规则"
|
||||
},
|
||||
{
|
||||
"code": "review-rule:delete",
|
||||
"resource": "review-rule",
|
||||
"action": "delete",
|
||||
"name": "删除评审规则",
|
||||
"description": "允许删除评审规则"
|
||||
},
|
||||
{
|
||||
"code": "judge:create",
|
||||
"resource": "judge",
|
||||
"action": "create",
|
||||
"name": "添加评委",
|
||||
"description": "允许添加评委"
|
||||
},
|
||||
{
|
||||
"code": "judge:read",
|
||||
"resource": "judge",
|
||||
"action": "read",
|
||||
"name": "查看评委",
|
||||
"description": "允许查看评委列表"
|
||||
},
|
||||
{
|
||||
"code": "judge:update",
|
||||
"resource": "judge",
|
||||
"action": "update",
|
||||
"name": "更新评委",
|
||||
"description": "允许更新评委信息"
|
||||
},
|
||||
{
|
||||
"code": "judge:delete",
|
||||
"resource": "judge",
|
||||
"action": "delete",
|
||||
"name": "删除评委",
|
||||
"description": "允许删除评委"
|
||||
},
|
||||
{
|
||||
"code": "judge:assign",
|
||||
"resource": "judge",
|
||||
"action": "assign",
|
||||
"name": "分配评委",
|
||||
"description": "允许为赛事分配评委"
|
||||
},
|
||||
{
|
||||
"code": "registration:create",
|
||||
"resource": "registration",
|
||||
"action": "create",
|
||||
"name": "创建报名",
|
||||
"description": "允许报名赛事"
|
||||
},
|
||||
{
|
||||
"code": "registration:read",
|
||||
"resource": "registration",
|
||||
"action": "read",
|
||||
"name": "查看报名",
|
||||
"description": "允许查看报名记录"
|
||||
},
|
||||
{
|
||||
"code": "registration:update",
|
||||
"resource": "registration",
|
||||
"action": "update",
|
||||
"name": "更新报名",
|
||||
"description": "允许更新报名信息"
|
||||
},
|
||||
{
|
||||
"code": "registration:delete",
|
||||
"resource": "registration",
|
||||
"action": "delete",
|
||||
"name": "取消报名",
|
||||
"description": "允许取消报名"
|
||||
},
|
||||
{
|
||||
"code": "registration:approve",
|
||||
"resource": "registration",
|
||||
"action": "approve",
|
||||
"name": "审核报名",
|
||||
"description": "允许审核报名"
|
||||
},
|
||||
{
|
||||
"code": "work:create",
|
||||
"resource": "work",
|
||||
"action": "create",
|
||||
"name": "上传作品",
|
||||
"description": "允许上传参赛作品"
|
||||
},
|
||||
{
|
||||
"code": "work:read",
|
||||
"resource": "work",
|
||||
"action": "read",
|
||||
"name": "查看作品",
|
||||
"description": "允许查看参赛作品"
|
||||
},
|
||||
{
|
||||
"code": "work:update",
|
||||
"resource": "work",
|
||||
"action": "update",
|
||||
"name": "更新作品",
|
||||
"description": "允许更新作品信息"
|
||||
},
|
||||
{
|
||||
"code": "work:delete",
|
||||
"resource": "work",
|
||||
"action": "delete",
|
||||
"name": "删除作品",
|
||||
"description": "允许删除作品"
|
||||
},
|
||||
{
|
||||
"code": "work:submit",
|
||||
"resource": "work",
|
||||
"action": "submit",
|
||||
"name": "提交作品",
|
||||
"description": "允许提交作品"
|
||||
},
|
||||
{
|
||||
"code": "review:read",
|
||||
"resource": "review",
|
||||
"action": "read",
|
||||
"name": "查看评审任务",
|
||||
"description": "允许查看待评审作品"
|
||||
},
|
||||
{
|
||||
"code": "review:score",
|
||||
"resource": "review",
|
||||
"action": "score",
|
||||
"name": "评审打分",
|
||||
"description": "允许对作品打分"
|
||||
},
|
||||
{
|
||||
"code": "notice:create",
|
||||
"resource": "notice",
|
||||
"action": "create",
|
||||
"name": "创建公告",
|
||||
"description": "允许创建赛事公告"
|
||||
},
|
||||
{
|
||||
"code": "notice:read",
|
||||
"resource": "notice",
|
||||
"action": "read",
|
||||
"name": "查看公告",
|
||||
"description": "允许查看赛事公告"
|
||||
},
|
||||
{
|
||||
"code": "notice:update",
|
||||
"resource": "notice",
|
||||
"action": "update",
|
||||
"name": "更新公告",
|
||||
"description": "允许更新公告信息"
|
||||
},
|
||||
{
|
||||
"code": "notice:delete",
|
||||
"resource": "notice",
|
||||
"action": "delete",
|
||||
"name": "删除公告",
|
||||
"description": "允许删除公告"
|
||||
},
|
||||
{
|
||||
"code": "homework:create",
|
||||
"resource": "homework",
|
||||
"action": "create",
|
||||
"name": "创建作业",
|
||||
"description": "允许创建作业"
|
||||
},
|
||||
{
|
||||
"code": "homework:read",
|
||||
"resource": "homework",
|
||||
"action": "read",
|
||||
"name": "查看作业",
|
||||
"description": "允许查看作业列表"
|
||||
},
|
||||
{
|
||||
"code": "homework:update",
|
||||
"resource": "homework",
|
||||
"action": "update",
|
||||
"name": "更新作业",
|
||||
"description": "允许更新作业信息"
|
||||
},
|
||||
{
|
||||
"code": "homework:delete",
|
||||
"resource": "homework",
|
||||
"action": "delete",
|
||||
"name": "删除作业",
|
||||
"description": "允许删除作业"
|
||||
},
|
||||
{
|
||||
"code": "homework:publish",
|
||||
"resource": "homework",
|
||||
"action": "publish",
|
||||
"name": "发布作业",
|
||||
"description": "允许发布作业"
|
||||
},
|
||||
{
|
||||
"code": "homework-submission:create",
|
||||
"resource": "homework-submission",
|
||||
"action": "create",
|
||||
"name": "提交作业",
|
||||
"description": "允许提交作业"
|
||||
},
|
||||
{
|
||||
"code": "homework-submission:read",
|
||||
"resource": "homework-submission",
|
||||
"action": "read",
|
||||
"name": "查看作业提交",
|
||||
"description": "允许查看作业提交记录"
|
||||
},
|
||||
{
|
||||
"code": "homework-submission:update",
|
||||
"resource": "homework-submission",
|
||||
"action": "update",
|
||||
"name": "更新作业提交",
|
||||
"description": "允许更新提交的作业"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:create",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "create",
|
||||
"name": "创建作业评审规则",
|
||||
"description": "允许创建作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:read",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "read",
|
||||
"name": "查看作业评审规则",
|
||||
"description": "允许查看作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:update",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "update",
|
||||
"name": "更新作业评审规则",
|
||||
"description": "允许更新作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-review-rule:delete",
|
||||
"resource": "homework-review-rule",
|
||||
"action": "delete",
|
||||
"name": "删除作业评审规则",
|
||||
"description": "允许删除作业评审规则"
|
||||
},
|
||||
{
|
||||
"code": "homework-score:create",
|
||||
"resource": "homework-score",
|
||||
"action": "create",
|
||||
"name": "作业评分",
|
||||
"description": "允许对作业评分"
|
||||
},
|
||||
{
|
||||
"code": "homework-score:read",
|
||||
"resource": "homework-score",
|
||||
"action": "read",
|
||||
"name": "查看作业评分",
|
||||
"description": "允许查看作业评分"
|
||||
},
|
||||
{
|
||||
"code": "dict:create",
|
||||
"resource": "dict",
|
||||
@ -196,423 +714,17 @@
|
||||
"description": "允许删除系统日志"
|
||||
},
|
||||
{
|
||||
"code": "school:create",
|
||||
"resource": "school",
|
||||
"action": "create",
|
||||
"name": "创建学校",
|
||||
"description": "允许创建学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:read",
|
||||
"resource": "school",
|
||||
"code": "activity:read",
|
||||
"resource": "activity",
|
||||
"action": "read",
|
||||
"name": "查看学校",
|
||||
"description": "允许查看学校信息"
|
||||
"name": "查看赛事活动",
|
||||
"description": "允许查看已发布的赛事活动"
|
||||
},
|
||||
{
|
||||
"code": "school:update",
|
||||
"resource": "school",
|
||||
"action": "update",
|
||||
"name": "更新学校",
|
||||
"description": "允许更新学校信息"
|
||||
},
|
||||
{
|
||||
"code": "school:delete",
|
||||
"resource": "school",
|
||||
"action": "delete",
|
||||
"name": "删除学校",
|
||||
"description": "允许删除学校信息"
|
||||
},
|
||||
{
|
||||
"code": "grade:create",
|
||||
"resource": "grade",
|
||||
"action": "create",
|
||||
"name": "创建年级",
|
||||
"description": "允许创建年级"
|
||||
},
|
||||
{
|
||||
"code": "grade:read",
|
||||
"resource": "grade",
|
||||
"action": "read",
|
||||
"name": "查看年级",
|
||||
"description": "允许查看年级列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "grade:update",
|
||||
"resource": "grade",
|
||||
"action": "update",
|
||||
"name": "更新年级",
|
||||
"description": "允许更新年级信息"
|
||||
},
|
||||
{
|
||||
"code": "grade:delete",
|
||||
"resource": "grade",
|
||||
"action": "delete",
|
||||
"name": "删除年级",
|
||||
"description": "允许删除年级"
|
||||
},
|
||||
{
|
||||
"code": "class:create",
|
||||
"resource": "class",
|
||||
"action": "create",
|
||||
"name": "创建班级",
|
||||
"description": "允许创建班级"
|
||||
},
|
||||
{
|
||||
"code": "class:read",
|
||||
"resource": "class",
|
||||
"action": "read",
|
||||
"name": "查看班级",
|
||||
"description": "允许查看班级列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "class:update",
|
||||
"resource": "class",
|
||||
"action": "update",
|
||||
"name": "更新班级",
|
||||
"description": "允许更新班级信息"
|
||||
},
|
||||
{
|
||||
"code": "class:delete",
|
||||
"resource": "class",
|
||||
"action": "delete",
|
||||
"name": "删除班级",
|
||||
"description": "允许删除班级"
|
||||
},
|
||||
{
|
||||
"code": "department:create",
|
||||
"resource": "department",
|
||||
"action": "create",
|
||||
"name": "创建部门",
|
||||
"description": "允许创建部门"
|
||||
},
|
||||
{
|
||||
"code": "department:read",
|
||||
"resource": "department",
|
||||
"action": "read",
|
||||
"name": "查看部门",
|
||||
"description": "允许查看部门列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "department:update",
|
||||
"resource": "department",
|
||||
"action": "update",
|
||||
"name": "更新部门",
|
||||
"description": "允许更新部门信息"
|
||||
},
|
||||
{
|
||||
"code": "department:delete",
|
||||
"resource": "department",
|
||||
"action": "delete",
|
||||
"name": "删除部门",
|
||||
"description": "允许删除部门"
|
||||
},
|
||||
{
|
||||
"code": "teacher:create",
|
||||
"resource": "teacher",
|
||||
"action": "create",
|
||||
"name": "创建教师",
|
||||
"description": "允许创建教师"
|
||||
},
|
||||
{
|
||||
"code": "teacher:read",
|
||||
"resource": "teacher",
|
||||
"action": "read",
|
||||
"name": "查看教师",
|
||||
"description": "允许查看教师列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "teacher:update",
|
||||
"resource": "teacher",
|
||||
"action": "update",
|
||||
"name": "更新教师",
|
||||
"description": "允许更新教师信息"
|
||||
},
|
||||
{
|
||||
"code": "teacher:delete",
|
||||
"resource": "teacher",
|
||||
"action": "delete",
|
||||
"name": "删除教师",
|
||||
"description": "允许删除教师"
|
||||
},
|
||||
{
|
||||
"code": "student:create",
|
||||
"resource": "student",
|
||||
"action": "create",
|
||||
"name": "创建学生",
|
||||
"description": "允许创建学生"
|
||||
},
|
||||
{
|
||||
"code": "student:read",
|
||||
"resource": "student",
|
||||
"action": "read",
|
||||
"name": "查看学生",
|
||||
"description": "允许查看学生列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "student:update",
|
||||
"resource": "student",
|
||||
"action": "update",
|
||||
"name": "更新学生",
|
||||
"description": "允许更新学生信息"
|
||||
},
|
||||
{
|
||||
"code": "student:delete",
|
||||
"resource": "student",
|
||||
"action": "delete",
|
||||
"name": "删除学生",
|
||||
"description": "允许删除学生"
|
||||
},
|
||||
{
|
||||
"code": "contest:create",
|
||||
"resource": "contest",
|
||||
"action": "create",
|
||||
"name": "创建比赛",
|
||||
"description": "允许创建比赛"
|
||||
},
|
||||
{
|
||||
"code": "contest:read",
|
||||
"resource": "contest",
|
||||
"action": "read",
|
||||
"name": "查看比赛",
|
||||
"description": "允许查看比赛列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "contest:update",
|
||||
"resource": "contest",
|
||||
"action": "update",
|
||||
"name": "更新比赛",
|
||||
"description": "允许更新比赛信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:delete",
|
||||
"resource": "contest",
|
||||
"action": "delete",
|
||||
"name": "删除比赛",
|
||||
"description": "允许删除比赛"
|
||||
},
|
||||
{
|
||||
"code": "contest:publish",
|
||||
"resource": "contest",
|
||||
"action": "publish",
|
||||
"name": "发布比赛",
|
||||
"description": "允许发布比赛"
|
||||
},
|
||||
{
|
||||
"code": "contest:team:create",
|
||||
"resource": "contest:team",
|
||||
"action": "create",
|
||||
"name": "创建团队",
|
||||
"description": "允许创建比赛团队"
|
||||
},
|
||||
{
|
||||
"code": "contest:team:read",
|
||||
"resource": "contest:team",
|
||||
"action": "read",
|
||||
"name": "查看团队",
|
||||
"description": "允许查看团队列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "contest:team:update",
|
||||
"resource": "contest:team",
|
||||
"action": "update",
|
||||
"name": "更新团队",
|
||||
"description": "允许更新团队信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:team:delete",
|
||||
"resource": "contest:team",
|
||||
"action": "delete",
|
||||
"name": "删除团队",
|
||||
"description": "允许删除团队"
|
||||
},
|
||||
{
|
||||
"code": "contest:team:manage",
|
||||
"resource": "contest:team",
|
||||
"action": "manage",
|
||||
"name": "管理团队成员",
|
||||
"description": "允许管理团队成员"
|
||||
},
|
||||
{
|
||||
"code": "contest:review:create",
|
||||
"resource": "contest:review",
|
||||
"action": "create",
|
||||
"name": "创建评审规则",
|
||||
"description": "允许创建评审规则"
|
||||
},
|
||||
{
|
||||
"code": "contest:review:read",
|
||||
"resource": "contest:review",
|
||||
"action": "read",
|
||||
"name": "查看评审",
|
||||
"description": "允许查看评审规则和评审记录"
|
||||
},
|
||||
{
|
||||
"code": "contest:review:update",
|
||||
"resource": "contest:review",
|
||||
"action": "update",
|
||||
"name": "更新评审规则",
|
||||
"description": "允许更新评审规则"
|
||||
},
|
||||
{
|
||||
"code": "contest:review:delete",
|
||||
"resource": "contest:review",
|
||||
"action": "delete",
|
||||
"name": "删除评审规则",
|
||||
"description": "允许删除评审规则"
|
||||
},
|
||||
{
|
||||
"code": "contest:review:assign",
|
||||
"resource": "contest:review",
|
||||
"action": "assign",
|
||||
"name": "分配评审任务",
|
||||
"description": "允许分配评审任务给评委"
|
||||
},
|
||||
{
|
||||
"code": "contest:review:score",
|
||||
"resource": "contest:review",
|
||||
"action": "score",
|
||||
"name": "评审打分",
|
||||
"description": "允许对作品进行评审打分"
|
||||
},
|
||||
{
|
||||
"code": "contest:judge:create",
|
||||
"resource": "contest:judge",
|
||||
"action": "create",
|
||||
"name": "添加评委",
|
||||
"description": "允许添加比赛评委"
|
||||
},
|
||||
{
|
||||
"code": "contest:judge:read",
|
||||
"resource": "contest:judge",
|
||||
"action": "read",
|
||||
"name": "查看评委",
|
||||
"description": "允许查看评委列表"
|
||||
},
|
||||
{
|
||||
"code": "contest:judge:update",
|
||||
"resource": "contest:judge",
|
||||
"action": "update",
|
||||
"name": "更新评委",
|
||||
"description": "允许更新评委信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:judge:delete",
|
||||
"resource": "contest:judge",
|
||||
"action": "delete",
|
||||
"name": "删除评委",
|
||||
"description": "允许删除评委"
|
||||
},
|
||||
{
|
||||
"code": "contest:work:create",
|
||||
"resource": "contest:work",
|
||||
"action": "create",
|
||||
"name": "创建作品",
|
||||
"description": "允许创建参赛作品"
|
||||
},
|
||||
{
|
||||
"code": "contest:work:read",
|
||||
"resource": "contest:work",
|
||||
"action": "read",
|
||||
"name": "查看作品",
|
||||
"description": "允许查看作品列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "contest:work:update",
|
||||
"resource": "contest:work",
|
||||
"action": "update",
|
||||
"name": "更新作品",
|
||||
"description": "允许更新作品信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:work:delete",
|
||||
"resource": "contest:work",
|
||||
"action": "delete",
|
||||
"name": "删除作品",
|
||||
"description": "允许删除作品"
|
||||
},
|
||||
{
|
||||
"code": "contest:work:submit",
|
||||
"resource": "contest:work",
|
||||
"action": "submit",
|
||||
"name": "提交作品",
|
||||
"description": "允许提交作品"
|
||||
},
|
||||
{
|
||||
"code": "contest:work:review",
|
||||
"resource": "contest:work",
|
||||
"action": "review",
|
||||
"name": "审核作品",
|
||||
"description": "允许审核作品状态"
|
||||
},
|
||||
{
|
||||
"code": "contest:registration:create",
|
||||
"resource": "contest:registration",
|
||||
"action": "create",
|
||||
"name": "创建报名",
|
||||
"description": "允许创建报名记录"
|
||||
},
|
||||
{
|
||||
"code": "contest:registration:read",
|
||||
"resource": "contest:registration",
|
||||
"action": "read",
|
||||
"name": "查看报名",
|
||||
"description": "允许查看报名列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "contest:registration:update",
|
||||
"resource": "contest:registration",
|
||||
"action": "update",
|
||||
"name": "更新报名",
|
||||
"description": "允许更新报名信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:registration:delete",
|
||||
"resource": "contest:registration",
|
||||
"action": "delete",
|
||||
"name": "删除报名",
|
||||
"description": "允许删除报名记录"
|
||||
},
|
||||
{
|
||||
"code": "contest:registration:approve",
|
||||
"resource": "contest:registration",
|
||||
"action": "approve",
|
||||
"name": "审核报名",
|
||||
"description": "允许审核报名(通过/拒绝)"
|
||||
},
|
||||
{
|
||||
"code": "contest:notice:create",
|
||||
"resource": "contest:notice",
|
||||
"action": "create",
|
||||
"name": "创建公告",
|
||||
"description": "允许创建比赛公告"
|
||||
},
|
||||
{
|
||||
"code": "contest:notice:read",
|
||||
"resource": "contest:notice",
|
||||
"action": "read",
|
||||
"name": "查看公告",
|
||||
"description": "允许查看公告列表和详情"
|
||||
},
|
||||
{
|
||||
"code": "contest:notice:update",
|
||||
"resource": "contest:notice",
|
||||
"action": "update",
|
||||
"name": "更新公告",
|
||||
"description": "允许更新公告信息"
|
||||
},
|
||||
{
|
||||
"code": "contest:notice:delete",
|
||||
"resource": "contest:notice",
|
||||
"action": "delete",
|
||||
"name": "删除公告",
|
||||
"description": "允许删除公告"
|
||||
},
|
||||
{
|
||||
"code": "contest:notice:publish",
|
||||
"resource": "contest:notice",
|
||||
"action": "publish",
|
||||
"name": "发布公告",
|
||||
"description": "允许发布公告"
|
||||
"code": "activity:guidance",
|
||||
"resource": "activity",
|
||||
"action": "guidance",
|
||||
"name": "指导学生",
|
||||
"description": "允许指导学生参赛"
|
||||
}
|
||||
]
|
||||
|
||||
@ -31,12 +31,17 @@
|
||||
"init:admin:permissions": "ts-node scripts/init-admin-permissions.ts",
|
||||
"init:menus": "ts-node scripts/init-menus.ts",
|
||||
"init:super-tenant": "ts-node scripts/init-super-tenant.ts",
|
||||
"init:linksea-tenant": "ts-node scripts/init-linksea-tenant.ts",
|
||||
"init:tenant-admin": "ts-node scripts/init-tenant-admin.ts",
|
||||
"init:tenant-admin:permissions": "ts-node scripts/init-tenant-admin.ts --permissions-only",
|
||||
"init:tenant-permissions": "ts-node scripts/init-tenant-permissions.ts",
|
||||
"init:tenant-menu-permissions": "ts-node scripts/init-tenant-menu-permissions.ts",
|
||||
"update:password": "ts-node scripts/update-password.ts",
|
||||
"fix:invalid-datetime": "ts-node scripts/fix-invalid-datetime.ts"
|
||||
"fix:invalid-datetime": "ts-node scripts/fix-invalid-datetime.ts",
|
||||
"cleanup:tenant-permissions": "ts-node scripts/cleanup-tenant-permissions.ts",
|
||||
"init:roles:super": "ts-node scripts/init-roles-permissions.ts --super",
|
||||
"init:roles": "ts-node scripts/init-roles-permissions.ts",
|
||||
"init:roles:all": "ts-node scripts/init-roles-permissions.ts --all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.3.3",
|
||||
|
||||
@ -57,17 +57,18 @@ model Tenant {
|
||||
|
||||
/// 用户表
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId Int @map("tenant_id") /// 租户ID
|
||||
username String /// 用户名(在租户内唯一)
|
||||
password String /// 密码(加密存储)
|
||||
nickname String /// 昵称
|
||||
email String? /// 邮箱(在租户内唯一,可选)
|
||||
phone String? /// 联系方式/手机号
|
||||
gender String? /// 性别:male-男,female-女
|
||||
avatar String? /// 头像URL
|
||||
status String @default("enabled") /// 账号状态:enabled-启用,disabled-停用
|
||||
validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效
|
||||
id Int @id @default(autoincrement())
|
||||
tenantId Int @map("tenant_id") /// 租户ID
|
||||
username String /// 用户名(在租户内唯一)
|
||||
password String /// 密码(加密存储)
|
||||
nickname String /// 昵称
|
||||
email String? /// 邮箱(在租户内唯一,可选)
|
||||
phone String? /// 联系方式/手机号
|
||||
gender String? /// 性别:male-男,female-女
|
||||
avatar String? /// 头像URL
|
||||
organization String? /// 所属单位(用于评委等独立用户)
|
||||
status String @default("enabled") /// 账号状态:enabled-启用,disabled-停用
|
||||
validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效
|
||||
creator Int? @map("creator") /// 创建人ID
|
||||
modifier Int? @map("modifier") /// 修改人ID
|
||||
createTime DateTime @default(now()) @map("create_time") /// 创建时间
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 添加赛事活动下的赛事列表菜单...\n');
|
||||
|
||||
// 查找赛事活动父菜单
|
||||
const activityParent = await prisma.menu.findFirst({
|
||||
where: { path: '/student-activities' }
|
||||
});
|
||||
|
||||
if (!activityParent) {
|
||||
console.log('❌ 未找到赛事活动父菜单');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已存在赛事列表子菜单
|
||||
const existing = await prisma.menu.findFirst({
|
||||
where: { path: '/student-activities/list' }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log('⏭️ 赛事列表菜单已存在');
|
||||
} else {
|
||||
// 创建赛事列表子菜单
|
||||
const listMenu = await prisma.menu.create({
|
||||
data: {
|
||||
name: '赛事列表',
|
||||
path: '/student-activities/list',
|
||||
component: 'contests/Activities',
|
||||
permission: 'contest:activity:read',
|
||||
sort: 0, // 排在最前面
|
||||
parentId: activityParent.id,
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log('✅ 已创建: 赛事列表');
|
||||
|
||||
// 分配给所有租户
|
||||
const tenants = await prisma.tenant.findMany({ select: { id: true } });
|
||||
for (const tenant of tenants) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: { tenantId: tenant.id, menuId: listMenu.id }
|
||||
});
|
||||
}
|
||||
console.log('✅ 已分配给所有租户');
|
||||
}
|
||||
|
||||
// 更新其他子菜单的排序
|
||||
await prisma.menu.updateMany({
|
||||
where: { path: '/student-activities/guidance' },
|
||||
data: { sort: 1 }
|
||||
});
|
||||
await prisma.menu.updateMany({
|
||||
where: { path: '/student-activities/review' },
|
||||
data: { sort: 2 }
|
||||
});
|
||||
await prisma.menu.updateMany({
|
||||
where: { path: '/student-activities/comments' },
|
||||
data: { sort: 3 }
|
||||
});
|
||||
|
||||
// 显示最终结构
|
||||
console.log('\n📋 赛事活动最终菜单结构:');
|
||||
const children = await prisma.menu.findMany({
|
||||
where: { parentId: activityParent.id },
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
|
||||
console.log(`- ${activityParent.name} (${activityParent.path})`);
|
||||
children.forEach(c => {
|
||||
console.log(` - ${c.name} (${c.path})`);
|
||||
});
|
||||
|
||||
console.log('\n✅ 完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,97 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔍 检查现有 homework 权限...');
|
||||
|
||||
// 获取所有租户
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
console.log(`找到 ${tenants.length} 个租户`);
|
||||
|
||||
// 定义作业管理权限
|
||||
const homeworkPermissions = [
|
||||
{ code: 'homework:read', name: '查看作业', resource: 'homework', action: 'read' },
|
||||
{ code: 'homework:create', name: '创建作业', resource: 'homework', action: 'create' },
|
||||
{ code: 'homework:update', name: '编辑作业', resource: 'homework', action: 'update' },
|
||||
{ code: 'homework:delete', name: '删除作业', resource: 'homework', action: 'delete' },
|
||||
];
|
||||
|
||||
for (const tenant of tenants) {
|
||||
console.log(`\n📝 为租户 "${tenant.name}" (ID: ${tenant.id}) 添加权限...`);
|
||||
|
||||
for (const perm of homeworkPermissions) {
|
||||
// 检查权限是否已存在
|
||||
const existing = await prisma.permission.findFirst({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
code: perm.code,
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(` ⏭️ ${perm.name} (${perm.code}) 已存在`);
|
||||
} else {
|
||||
await prisma.permission.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
code: perm.code,
|
||||
name: perm.name,
|
||||
resource: perm.resource,
|
||||
action: perm.action,
|
||||
description: `作业管理 - ${perm.name}`,
|
||||
}
|
||||
});
|
||||
console.log(` ✅ ${perm.name} (${perm.code}) 已创建`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取管理员角色并分配权限
|
||||
const adminRole = await prisma.role.findFirst({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
code: 'admin',
|
||||
}
|
||||
});
|
||||
|
||||
if (adminRole) {
|
||||
console.log(`\n🔐 为管理员角色分配权限...`);
|
||||
|
||||
const allHomeworkPerms = await prisma.permission.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
resource: 'homework',
|
||||
}
|
||||
});
|
||||
|
||||
for (const perm of allHomeworkPerms) {
|
||||
const existing = await prisma.rolePermission.findFirst({
|
||||
where: {
|
||||
roleId: adminRole.id,
|
||||
permissionId: perm.id,
|
||||
}
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: adminRole.id,
|
||||
permissionId: perm.id,
|
||||
}
|
||||
});
|
||||
console.log(` ✅ 已分配 ${perm.code} 给管理员角色`);
|
||||
} else {
|
||||
console.log(` ⏭️ ${perm.code} 已分配`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ 权限配置完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,86 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 添加缺失的赛事管理菜单...\n');
|
||||
|
||||
// 查找赛事管理父菜单
|
||||
const contestMenu = await prisma.menu.findFirst({
|
||||
where: { name: '赛事管理', parentId: null }
|
||||
});
|
||||
|
||||
if (!contestMenu) {
|
||||
console.log('❌ 未找到赛事管理菜单');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 找到赛事管理菜单 ID:', contestMenu.id);
|
||||
|
||||
// 要添加的菜单
|
||||
const menusToAdd = [
|
||||
{
|
||||
name: '评委管理',
|
||||
path: '/contests/judges',
|
||||
component: 'contests/judges/Index',
|
||||
permission: 'contest:judge:read',
|
||||
sort: 35, // 在评审规则之后
|
||||
},
|
||||
{
|
||||
name: '评审进度',
|
||||
path: '/contests/reviews/progress',
|
||||
component: 'contests/reviews/Progress',
|
||||
permission: 'contest:review:read',
|
||||
sort: 36,
|
||||
},
|
||||
];
|
||||
|
||||
for (const menuData of menusToAdd) {
|
||||
// 检查是否已存在
|
||||
const existing = await prisma.menu.findFirst({
|
||||
where: { path: menuData.path }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(`⏭️ ${menuData.name} 已存在`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建菜单
|
||||
const newMenu = await prisma.menu.create({
|
||||
data: {
|
||||
...menuData,
|
||||
parentId: contestMenu.id,
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(`✅ 已创建: ${menuData.name} (ID: ${newMenu.id})`);
|
||||
|
||||
// 分配给所有租户
|
||||
const tenants = await prisma.tenant.findMany({ select: { id: true } });
|
||||
for (const tenant of tenants) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: { tenantId: tenant.id, menuId: newMenu.id }
|
||||
});
|
||||
}
|
||||
console.log(` 已分配给 ${tenants.length} 个租户`);
|
||||
}
|
||||
|
||||
// 显示最终结构
|
||||
console.log('\n📋 赛事管理最终菜单结构:');
|
||||
const children = await prisma.menu.findMany({
|
||||
where: { parentId: contestMenu.id },
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
|
||||
console.log(`- ${contestMenu.name}`);
|
||||
children.forEach(c => {
|
||||
console.log(` - ${c.name} | ${c.path}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ 完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,124 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 添加学生作业菜单...\n');
|
||||
|
||||
// 获取所有租户
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
|
||||
for (const tenant of tenants) {
|
||||
console.log(`\n📝 处理租户: ${tenant.name} (ID: ${tenant.id})`);
|
||||
|
||||
// 1. 创建学生作业权限(如果不存在)
|
||||
let studentHomeworkPerm = await prisma.permission.findFirst({
|
||||
where: { tenantId: tenant.id, code: 'homework:student:read' }
|
||||
});
|
||||
|
||||
if (!studentHomeworkPerm) {
|
||||
studentHomeworkPerm = await prisma.permission.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
code: 'homework:student:read',
|
||||
name: '学生查看作业',
|
||||
resource: 'homework:student',
|
||||
action: 'read',
|
||||
description: '学生查看作业列表',
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 创建权限: homework:student:read');
|
||||
}
|
||||
|
||||
// 2. 分配给学生角色
|
||||
const studentRole = await prisma.role.findFirst({
|
||||
where: { tenantId: tenant.id, code: 'student' }
|
||||
});
|
||||
|
||||
if (studentRole) {
|
||||
const existing = await prisma.rolePermission.findFirst({
|
||||
where: { roleId: studentRole.id, permissionId: studentHomeworkPerm.id }
|
||||
});
|
||||
if (!existing) {
|
||||
await prisma.rolePermission.create({
|
||||
data: { roleId: studentRole.id, permissionId: studentHomeworkPerm.id }
|
||||
});
|
||||
console.log(' ✅ 已分配给学生角色');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 创建学生作业菜单
|
||||
// 先检查是否已存在
|
||||
const existingMenu = await prisma.menu.findFirst({
|
||||
where: { path: '/student-homework' }
|
||||
});
|
||||
|
||||
if (!existingMenu) {
|
||||
const studentHomeworkMenu = await prisma.menu.create({
|
||||
data: {
|
||||
name: '我的作业',
|
||||
path: '/student-homework',
|
||||
component: 'homework/StudentList',
|
||||
icon: 'BookOutlined',
|
||||
permission: 'homework:student:read',
|
||||
sort: 15, // 放在作业管理之前
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 创建菜单: 我的作业');
|
||||
|
||||
// 4. 将菜单分配给租户
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: studentHomeworkMenu.id,
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 菜单已分配给租户');
|
||||
} else {
|
||||
console.log(' ⏭️ 菜单已存在');
|
||||
|
||||
// 确保菜单已分配给租户
|
||||
const tenantMenu = await prisma.tenantMenu.findFirst({
|
||||
where: { tenantId: tenant.id, menuId: existingMenu.id }
|
||||
});
|
||||
if (!tenantMenu) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: existingMenu.id,
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 菜单已分配给租户');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 移除学生角色的 homework:read 权限(这个权限是给管理员的)
|
||||
console.log('\n📝 更新权限配置...');
|
||||
const allStudentRoles = await prisma.role.findMany({
|
||||
where: { code: 'student' }
|
||||
});
|
||||
|
||||
for (const studentRole of allStudentRoles) {
|
||||
// 获取 homework:read 权限
|
||||
const homeworkReadPerm = await prisma.permission.findFirst({
|
||||
where: { tenantId: studentRole.tenantId, code: 'homework:read' }
|
||||
});
|
||||
|
||||
if (homeworkReadPerm) {
|
||||
await prisma.rolePermission.deleteMany({
|
||||
where: { roleId: studentRole.id, permissionId: homeworkReadPerm.id }
|
||||
});
|
||||
console.log(` ✅ 移除学生角色(ID:${studentRole.id})的 homework:read 权限`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ 学生作业菜单配置完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,54 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔍 查询角色和权限...');
|
||||
|
||||
// 获取所有角色
|
||||
const roles = await prisma.role.findMany({
|
||||
select: { id: true, name: true, code: true, tenantId: true }
|
||||
});
|
||||
console.log('角色:', roles);
|
||||
|
||||
// 获取 homework 权限
|
||||
const homeworkPerms = await prisma.permission.findMany({
|
||||
where: { resource: 'homework' },
|
||||
select: { id: true, code: true, tenantId: true }
|
||||
});
|
||||
console.log('Homework 权限:', homeworkPerms);
|
||||
|
||||
// 为每个角色分配所有 homework 权限
|
||||
for (const role of roles) {
|
||||
console.log(`\n🔐 为角色 "${role.name}" 分配权限...`);
|
||||
|
||||
const permsForTenant = homeworkPerms.filter(p => p.tenantId === role.tenantId);
|
||||
|
||||
for (const perm of permsForTenant) {
|
||||
const existing = await prisma.rolePermission.findFirst({
|
||||
where: {
|
||||
roleId: role.id,
|
||||
permissionId: perm.id,
|
||||
}
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: role.id,
|
||||
permissionId: perm.id,
|
||||
}
|
||||
});
|
||||
console.log(` ✅ 已分配 ${perm.code}`);
|
||||
} else {
|
||||
console.log(` ⏭️ ${perm.code} 已分配`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ 权限分配完成!请重新登录以刷新权限。');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,46 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// 查找赛事管理菜单
|
||||
const contestMenu = await prisma.menu.findFirst({
|
||||
where: { name: '赛事管理' }
|
||||
});
|
||||
|
||||
console.log('📋 赛事管理菜单:', contestMenu ? `ID ${contestMenu.id}` : '不存在');
|
||||
|
||||
if (contestMenu) {
|
||||
const children = await prisma.menu.findMany({
|
||||
where: { parentId: contestMenu.id },
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
console.log('\n子菜单:');
|
||||
if (children.length === 0) {
|
||||
console.log(' (无子菜单)');
|
||||
}
|
||||
children.forEach(c => console.log(` - ${c.name} | ${c.path} | ${c.permission}`));
|
||||
}
|
||||
|
||||
// 查找所有赛事相关菜单
|
||||
console.log('\n📋 所有赛事相关菜单:');
|
||||
const allContestMenus = await prisma.menu.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ path: { contains: 'contest' } },
|
||||
{ name: { contains: '赛事' } },
|
||||
{ name: { contains: '赛果' } },
|
||||
{ name: { contains: '报名' } },
|
||||
{ name: { contains: '作品' } },
|
||||
]
|
||||
},
|
||||
orderBy: [{ parentId: 'asc' }, { sort: 'asc' }]
|
||||
});
|
||||
|
||||
allContestMenus.forEach(m => {
|
||||
const type = m.parentId ? ' (子)' : '(父)';
|
||||
console.log(`${type} ${m.name} | ${m.path} | parentId: ${m.parentId}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@ -1,47 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const studentRole = await prisma.role.findFirst({ where: { code: 'student' } });
|
||||
|
||||
if (!studentRole) {
|
||||
console.log('未找到学生角色');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取学生权限
|
||||
const perms = await prisma.rolePermission.findMany({
|
||||
where: { roleId: studentRole.id },
|
||||
include: { permission: true }
|
||||
});
|
||||
|
||||
const permCodes = perms.map(p => p.permission.code);
|
||||
console.log('📋 学生权限:', permCodes.join(', '));
|
||||
|
||||
// 获取租户菜单
|
||||
const tenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: { tenantId: studentRole.tenantId },
|
||||
include: { menu: true }
|
||||
});
|
||||
|
||||
// 过滤出学生能看到的菜单
|
||||
const visibleMenus = tenantMenus
|
||||
.filter(tm => tm.menu.permission && permCodes.includes(tm.menu.permission))
|
||||
.map(tm => ({
|
||||
name: tm.menu.name,
|
||||
path: tm.menu.path,
|
||||
permission: tm.menu.permission,
|
||||
component: tm.menu.component,
|
||||
}));
|
||||
|
||||
console.log('\n📱 学生可见菜单:');
|
||||
visibleMenus.forEach(m => {
|
||||
console.log(` - ${m.name}`);
|
||||
console.log(` 路径: ${m.path}`);
|
||||
console.log(` 权限: ${m.permission}`);
|
||||
console.log(` 组件: ${m.component}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@ -1,49 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// 获取所有赛事活动相关菜单
|
||||
const activityMenus = await prisma.menu.findMany({
|
||||
where: { path: { startsWith: '/student-activities' } },
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
|
||||
console.log('📋 赛事活动相关菜单:');
|
||||
activityMenus.forEach(m => {
|
||||
console.log(` ID: ${m.id} | ${m.name} | ${m.path}`);
|
||||
});
|
||||
|
||||
// 获取租户1的菜单分配
|
||||
console.log('\n📋 租户1的菜单分配:');
|
||||
const tenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: { tenantId: 1 },
|
||||
include: { menu: true }
|
||||
});
|
||||
|
||||
const assignedMenuIds = tenantMenus.map(tm => tm.menuId);
|
||||
|
||||
// 检查哪些赛事活动菜单已分配
|
||||
console.log('\n赛事活动菜单分配状态:');
|
||||
activityMenus.forEach(m => {
|
||||
const assigned = assignedMenuIds.includes(m.id);
|
||||
console.log(` ${assigned ? '✅' : '❌'} ${m.name} (ID: ${m.id})`);
|
||||
});
|
||||
|
||||
// 补充分配缺失的菜单
|
||||
console.log('\n📝 补充分配缺失的菜单...');
|
||||
for (const menu of activityMenus) {
|
||||
if (!assignedMenuIds.includes(menu.id)) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: { tenantId: 1, menuId: menu.id }
|
||||
});
|
||||
console.log(` ✅ 已分配: ${menu.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ 完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
127
backend/scripts/cleanup-tenant-permissions.ts
Normal file
127
backend/scripts/cleanup-tenant-permissions.ts
Normal file
@ -0,0 +1,127 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('DATABASE_URL not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 超级管理员专属权限(普通租户不应该有这些权限)
|
||||
const superAdminOnlyPermissions = [
|
||||
'tenant:create',
|
||||
'tenant:update',
|
||||
'tenant:delete',
|
||||
];
|
||||
|
||||
async function cleanupTenantPermissions() {
|
||||
try {
|
||||
console.log('🚀 开始清理普通租户的超级管理员权限...\n');
|
||||
|
||||
// 1. 获取所有非超级租户
|
||||
const normalTenants = await prisma.tenant.findMany({
|
||||
where: {
|
||||
isSuper: { not: 1 },
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`找到 ${normalTenants.length} 个普通租户\n`);
|
||||
|
||||
for (const tenant of normalTenants) {
|
||||
console.log(`处理租户: ${tenant.name} (${tenant.code})`);
|
||||
|
||||
// 2. 找到该租户下的超级管理员专属权限
|
||||
const permissionsToRemove = await prisma.permission.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
code: { in: superAdminOnlyPermissions },
|
||||
},
|
||||
});
|
||||
|
||||
if (permissionsToRemove.length === 0) {
|
||||
console.log(` ✓ 没有需要清理的权限\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const permissionIds = permissionsToRemove.map((p) => p.id);
|
||||
console.log(` 找到 ${permissionsToRemove.length} 个需要清理的权限: ${permissionsToRemove.map((p) => p.code).join(', ')}`);
|
||||
|
||||
// 3. 删除角色-权限关联
|
||||
const deletedRolePermissions = await prisma.rolePermission.deleteMany({
|
||||
where: {
|
||||
permissionId: { in: permissionIds },
|
||||
},
|
||||
});
|
||||
console.log(` 删除了 ${deletedRolePermissions.count} 条角色-权限关联`);
|
||||
|
||||
// 4. 删除权限记录
|
||||
const deletedPermissions = await prisma.permission.deleteMany({
|
||||
where: {
|
||||
id: { in: permissionIds },
|
||||
},
|
||||
});
|
||||
console.log(` 删除了 ${deletedPermissions.count} 条权限记录\n`);
|
||||
}
|
||||
|
||||
// 5. 更新租户管理菜单权限
|
||||
console.log('更新租户管理菜单权限...');
|
||||
const tenantMenu = await prisma.menu.findFirst({
|
||||
where: {
|
||||
name: '租户管理',
|
||||
path: '/system/tenants',
|
||||
},
|
||||
});
|
||||
|
||||
if (tenantMenu) {
|
||||
if (tenantMenu.permission !== 'tenant:update') {
|
||||
await prisma.menu.update({
|
||||
where: { id: tenantMenu.id },
|
||||
data: { permission: 'tenant:update' },
|
||||
});
|
||||
console.log(`✅ 菜单权限已更新为 tenant:update (原: ${tenantMenu.permission})`);
|
||||
} else {
|
||||
console.log('✅ 菜单权限已经是 tenant:update');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 未找到租户管理菜单');
|
||||
}
|
||||
|
||||
console.log('\n✅ 清理完成!');
|
||||
console.log('\n说明:');
|
||||
console.log(' - 普通租户现在只有 tenant:read 权限(用于读取租户列表)');
|
||||
console.log(' - 租户管理菜单需要 tenant:update 权限才能看到');
|
||||
console.log(' - 只有超级租户才有 tenant:create/update/delete 权限');
|
||||
} catch (error) {
|
||||
console.error('❌ 清理失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
cleanupTenantPermissions()
|
||||
.then(() => {
|
||||
console.log('\n🎉 脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,21 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const menus = await prisma.menu.findMany({
|
||||
where: { path: { startsWith: '/student-activities' } },
|
||||
orderBy: [{ parentId: 'asc' }, { sort: 'asc' }]
|
||||
});
|
||||
|
||||
console.log('📋 赛事活动相关菜单:');
|
||||
menus.forEach(m => {
|
||||
console.log(`\n name: ${m.name}`);
|
||||
console.log(` path: ${m.path}`);
|
||||
console.log(` component: ${m.component || '(无)'}`);
|
||||
console.log(` parentId: ${m.parentId || '(无 - 顶级菜单)'}`);
|
||||
console.log(` permission: ${m.permission}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@ -1,130 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const tenantId = 1;
|
||||
|
||||
// 获取租户分配的菜单ID
|
||||
const tenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: { tenantId },
|
||||
});
|
||||
const menuIds = tenantMenus.map((tm) => tm.menuId);
|
||||
|
||||
console.log('📋 租户分配的菜单IDs:', menuIds.length, '个');
|
||||
|
||||
// 获取租户分配的所有菜单(包括父菜单)
|
||||
const allMenus = await prisma.menu.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: { in: menuIds } },
|
||||
{ children: { some: { id: { in: menuIds } } } },
|
||||
],
|
||||
validState: 1,
|
||||
},
|
||||
orderBy: {
|
||||
sort: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
// 过滤出赛事活动相关的
|
||||
console.log('\n📋 赛事活动相关菜单 (从数据库查询):');
|
||||
const activityMenus = allMenus.filter(m => m.path?.startsWith('/student-activities'));
|
||||
activityMenus.forEach(m => {
|
||||
console.log(` ID: ${m.id} | ${m.name}`);
|
||||
console.log(` path: ${m.path}`);
|
||||
console.log(` component: ${m.component || '(null)'}`);
|
||||
console.log(` parentId: ${m.parentId}`);
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
const buildTree = (menus: any[], parentId: number | null = null): any[] => {
|
||||
return menus
|
||||
.filter((menu) => menu.parentId === parentId)
|
||||
.map((menu) => ({
|
||||
...menu,
|
||||
children: buildTree(menus, menu.id),
|
||||
}));
|
||||
};
|
||||
|
||||
const menuTree = buildTree(allMenus);
|
||||
|
||||
// 找到赛事活动菜单树
|
||||
const activityTree = menuTree.find(m => m.path === '/student-activities');
|
||||
if (activityTree) {
|
||||
console.log('\n📋 赛事活动菜单树 (模拟API返回):');
|
||||
const printTree = (menu: any, indent = '') => {
|
||||
console.log(`${indent}${menu.name} (${menu.path})`);
|
||||
console.log(`${indent} component: ${menu.component || '(null)'}`);
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.forEach((child: any) => printTree(child, indent + ' '));
|
||||
}
|
||||
};
|
||||
printTree(activityTree);
|
||||
} else {
|
||||
console.log('\n❌ 未找到赛事活动菜单树');
|
||||
}
|
||||
|
||||
// 模拟权限过滤
|
||||
const userPermissions = ['workbench:read', 'contest:activity:read', 'homework:student:read'];
|
||||
|
||||
const filterMenus = (menus: any[]): any[] => {
|
||||
return menus
|
||||
.map((menu) => {
|
||||
let filteredChildren: any[] = [];
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
filteredChildren = filterMenus(menu.children);
|
||||
}
|
||||
|
||||
const hasPermissionField = menu.permission && menu.permission.trim() !== '';
|
||||
|
||||
if (hasPermissionField) {
|
||||
if (!userPermissions.includes(menu.permission)) {
|
||||
if (filteredChildren.length > 0) {
|
||||
return { ...menu, children: filteredChildren };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPermissionField) {
|
||||
if (filteredChildren.length === 0 && (!menu.path || menu.path.trim() === '')) {
|
||||
return null;
|
||||
}
|
||||
if (filteredChildren.length > 0) {
|
||||
return { ...menu, children: filteredChildren };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = { ...menu };
|
||||
if (filteredChildren.length > 0) {
|
||||
filtered.children = filteredChildren;
|
||||
}
|
||||
return filtered;
|
||||
})
|
||||
.filter((menu) => menu !== null);
|
||||
};
|
||||
|
||||
const filteredTree = filterMenus(menuTree);
|
||||
const filteredActivityTree = filteredTree.find(m => m.path === '/student-activities');
|
||||
|
||||
if (filteredActivityTree) {
|
||||
console.log('\n📋 权限过滤后的赛事活动菜单:');
|
||||
const printTree = (menu: any, indent = '') => {
|
||||
console.log(`${indent}${menu.name} (${menu.path})`);
|
||||
console.log(`${indent} component: ${menu.component || '(null)'}`);
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.forEach((child: any) => printTree(child, indent + ' '));
|
||||
}
|
||||
};
|
||||
printTree(filteredActivityTree);
|
||||
|
||||
console.log('\n📋 JSON格式 (检查component字段):');
|
||||
console.log(JSON.stringify(filteredActivityTree, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,96 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 最终配置学生权限...\n');
|
||||
|
||||
// 获取学生角色
|
||||
const studentRoles = await prisma.role.findMany({
|
||||
where: { code: 'student' }
|
||||
});
|
||||
|
||||
for (const studentRole of studentRoles) {
|
||||
console.log(`\n处理学生角色 ID: ${studentRole.id}`);
|
||||
|
||||
// 学生最终需要的权限
|
||||
const neededPerms = [
|
||||
'workbench:read', // 工作台
|
||||
'contest:activity:read', // 赛事活动
|
||||
'homework:student:read', // 我的作业菜单
|
||||
];
|
||||
|
||||
// 确保所有需要的权限都已分配
|
||||
for (const permCode of neededPerms) {
|
||||
const perm = await prisma.permission.findFirst({
|
||||
where: { tenantId: studentRole.tenantId, code: permCode }
|
||||
});
|
||||
|
||||
if (perm) {
|
||||
const existing = await prisma.rolePermission.findFirst({
|
||||
where: { roleId: studentRole.id, permissionId: perm.id }
|
||||
});
|
||||
if (!existing) {
|
||||
await prisma.rolePermission.create({
|
||||
data: { roleId: studentRole.id, permissionId: perm.id }
|
||||
});
|
||||
console.log(` ✅ 已添加: ${permCode}`);
|
||||
} else {
|
||||
console.log(` ⏭️ 已存在: ${permCode}`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ❌ 权限不存在: ${permCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除不需要的权限
|
||||
const permsToRemove = ['homework:read'];
|
||||
for (const permCode of permsToRemove) {
|
||||
const perm = await prisma.permission.findFirst({
|
||||
where: { tenantId: studentRole.tenantId, code: permCode }
|
||||
});
|
||||
if (perm) {
|
||||
const deleted = await prisma.rolePermission.deleteMany({
|
||||
where: { roleId: studentRole.id, permissionId: perm.id }
|
||||
});
|
||||
if (deleted.count > 0) {
|
||||
console.log(` 🗑️ 已移除: ${permCode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新学生作业菜单
|
||||
console.log('\n📝 更新学生作业菜单...');
|
||||
const studentMenu = await prisma.menu.findFirst({
|
||||
where: { path: '/student-homework' }
|
||||
});
|
||||
|
||||
if (studentMenu) {
|
||||
await prisma.menu.update({
|
||||
where: { id: studentMenu.id },
|
||||
data: {
|
||||
permission: 'homework:student:read',
|
||||
component: 'homework/StudentList',
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 菜单已更新');
|
||||
}
|
||||
|
||||
// 显示最终权限
|
||||
console.log('\n📋 学生角色最终权限:');
|
||||
for (const studentRole of studentRoles) {
|
||||
const perms = await prisma.rolePermission.findMany({
|
||||
where: { roleId: studentRole.id },
|
||||
include: { permission: true }
|
||||
});
|
||||
console.log(`\n 角色 ID ${studentRole.id}:`);
|
||||
perms.forEach(p => console.log(` - ${p.permission.code}: ${p.permission.name}`));
|
||||
}
|
||||
|
||||
console.log('\n✅ 配置完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,46 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 修复重复的赛事活动菜单...\n');
|
||||
|
||||
// 查找原来的赛事活动菜单(在赛事管理下的,路径是 /contests/activities)
|
||||
const oldActivityMenu = await prisma.menu.findFirst({
|
||||
where: { path: '/contests/activities' }
|
||||
});
|
||||
|
||||
if (oldActivityMenu) {
|
||||
// 把它改回 contest:read 权限,这样只有管理员能看到
|
||||
await prisma.menu.update({
|
||||
where: { id: oldActivityMenu.id },
|
||||
data: { permission: 'contest:read' }
|
||||
});
|
||||
console.log('✅ 原赛事活动菜单权限更新为 contest:read');
|
||||
}
|
||||
|
||||
// 验证最终结果
|
||||
console.log('\n📋 最终菜单配置:');
|
||||
const menus = await prisma.menu.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ path: '/contests/activities' },
|
||||
{ path: '/student-activities' },
|
||||
]
|
||||
},
|
||||
select: { name: true, path: true, permission: true, parentId: true }
|
||||
});
|
||||
|
||||
menus.forEach(m => {
|
||||
const type = m.parentId ? '(子菜单)' : '(一级菜单)';
|
||||
console.log(` - ${m.name} ${type}`);
|
||||
console.log(` 路径: ${m.path}`);
|
||||
console.log(` 权限: ${m.permission}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ 修复完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,138 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function fixInvalidDatetime() {
|
||||
try {
|
||||
console.log('🔧 开始修复无效的日期时间数据...\n');
|
||||
|
||||
// 获取当前时间作为默认值
|
||||
const now = new Date();
|
||||
|
||||
// 修复 users 表中的无效日期
|
||||
console.log('📋 修复 users 表中的 modify_time...');
|
||||
const nowStr = now.toISOString().slice(0, 19).replace('T', ' ');
|
||||
const userResult = await prisma.$executeRawUnsafe(`
|
||||
UPDATE \`users\`
|
||||
SET modify_time = '${nowStr}'
|
||||
WHERE modify_time = '0000-00-00 00:00:00'
|
||||
OR modify_time < '1970-01-01 00:00:00'
|
||||
OR YEAR(modify_time) = 0
|
||||
OR MONTH(modify_time) = 0
|
||||
OR DAY(modify_time) = 0
|
||||
`);
|
||||
console.log(` ✅ 修复了 ${userResult} 条用户记录\n`);
|
||||
|
||||
// 修复其他所有包含 modify_time 的表
|
||||
const tables = [
|
||||
'tenants',
|
||||
'roles',
|
||||
'menus',
|
||||
'permissions',
|
||||
'dicts',
|
||||
'dict_items',
|
||||
'configs',
|
||||
'schools',
|
||||
'grades',
|
||||
'departments',
|
||||
'classes',
|
||||
'teachers',
|
||||
'students',
|
||||
'contests',
|
||||
'contest_teams',
|
||||
'contest_team_members',
|
||||
'contest_registrations',
|
||||
'contest_works',
|
||||
'contest_work_attachments',
|
||||
'contest_work_scores',
|
||||
'contest_work_judge_assignments',
|
||||
'contest_judges',
|
||||
'contest_notices',
|
||||
'contest_results',
|
||||
];
|
||||
|
||||
let totalFixed = userResult;
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
// 使用 Prisma 的原始 SQL,直接插入日期值
|
||||
const result = await prisma.$executeRawUnsafe(`
|
||||
UPDATE \`${table}\`
|
||||
SET modify_time = '${nowStr}'
|
||||
WHERE modify_time = '0000-00-00 00:00:00'
|
||||
OR modify_time < '1970-01-01 00:00:00'
|
||||
OR YEAR(modify_time) = 0
|
||||
OR MONTH(modify_time) = 0
|
||||
OR DAY(modify_time) = 0
|
||||
`);
|
||||
if (result > 0) {
|
||||
console.log(` ✅ ${table}: 修复了 ${result} 条记录`);
|
||||
totalFixed += result;
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 如果表不存在或没有 modify_time 字段,跳过
|
||||
if (
|
||||
error.code !== 'P2025' &&
|
||||
!error.message.includes('Unknown column')
|
||||
) {
|
||||
console.log(` ⚠️ ${table}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ 总共修复了 ${totalFixed} 条记录`);
|
||||
console.log('\n💡 建议:检查 MySQL 的 sql_mode 设置,确保禁止无效日期');
|
||||
console.log(' 可以在 my.cnf 或 my.ini 中添加:');
|
||||
console.log(
|
||||
' sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('\n💥 修复失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行修复
|
||||
fixInvalidDatetime()
|
||||
.then(() => {
|
||||
console.log('\n🎉 修复脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 修复脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,76 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 修复学生赛事活动菜单...\n');
|
||||
|
||||
// 获取所有租户
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
|
||||
// 检查是否已存在独立的赛事活动菜单
|
||||
let studentActivityMenu = await prisma.menu.findFirst({
|
||||
where: { path: '/student-activities' }
|
||||
});
|
||||
|
||||
if (!studentActivityMenu) {
|
||||
// 创建独立的赛事活动菜单(一级菜单)
|
||||
studentActivityMenu = await prisma.menu.create({
|
||||
data: {
|
||||
name: '赛事活动',
|
||||
path: '/student-activities',
|
||||
component: 'contests/Activities',
|
||||
icon: 'TrophyOutlined',
|
||||
permission: 'contest:activity:read',
|
||||
sort: 10, // 放在工作台之后
|
||||
parentId: null, // 一级菜单
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log('✅ 创建菜单: 赛事活动 (独立路由)');
|
||||
} else {
|
||||
console.log('⏭️ 菜单已存在: 赛事活动');
|
||||
}
|
||||
|
||||
// 为每个租户分配菜单
|
||||
for (const tenant of tenants) {
|
||||
const existing = await prisma.tenantMenu.findFirst({
|
||||
where: { tenantId: tenant.id, menuId: studentActivityMenu.id }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: studentActivityMenu.id,
|
||||
}
|
||||
});
|
||||
console.log(`✅ 菜单已分配给租户: ${tenant.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新原有的赛事活动菜单(在赛事管理下的)权限,确保只有管理员能看到
|
||||
const oldActivityMenu = await prisma.menu.findFirst({
|
||||
where: {
|
||||
path: '/contests/activities',
|
||||
parentId: { not: null } // 有父菜单的
|
||||
}
|
||||
});
|
||||
|
||||
if (oldActivityMenu) {
|
||||
// 保持原有权限 contest:read,这样管理员可以看到
|
||||
console.log('⏭️ 原赛事活动菜单保持不变(管理员使用)');
|
||||
}
|
||||
|
||||
console.log('\n✅ 配置完成!');
|
||||
console.log('\n学生菜单结构:');
|
||||
console.log(' - 工作台');
|
||||
console.log(' - 赛事活动 (独立路由 /student-activities)');
|
||||
console.log(' - 我的作业 (独立路由 /student-homework)');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,46 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const studentRole = await prisma.role.findFirst({ where: { code: 'student' } });
|
||||
if (!studentRole) {
|
||||
console.log('未找到学生角色');
|
||||
return;
|
||||
}
|
||||
|
||||
// 需要的权限
|
||||
const neededPerms = ['workbench:read', 'homework:read', 'contest:activity:read'];
|
||||
|
||||
for (const permCode of neededPerms) {
|
||||
const perm = await prisma.permission.findFirst({
|
||||
where: { tenantId: studentRole.tenantId, code: permCode }
|
||||
});
|
||||
|
||||
if (perm) {
|
||||
const existing = await prisma.rolePermission.findFirst({
|
||||
where: { roleId: studentRole.id, permissionId: perm.id }
|
||||
});
|
||||
if (!existing) {
|
||||
await prisma.rolePermission.create({
|
||||
data: { roleId: studentRole.id, permissionId: perm.id }
|
||||
});
|
||||
console.log(`✅ 已添加: ${permCode}`);
|
||||
} else {
|
||||
console.log(`⏭️ 已存在: ${permCode}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`❌ 权限不存在: ${permCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示最终权限
|
||||
const perms = await prisma.rolePermission.findMany({
|
||||
where: { roleId: studentRole.id },
|
||||
include: { permission: true }
|
||||
});
|
||||
console.log('\n📋 学生最终权限:');
|
||||
perms.forEach(p => console.log(` - ${p.permission.code}: ${p.permission.name}`));
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@ -1,84 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 修复工作台权限配置...\n');
|
||||
|
||||
// 1. 获取所有租户
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
|
||||
for (const tenant of tenants) {
|
||||
console.log(`📝 处理租户: ${tenant.name} (ID: ${tenant.id})`);
|
||||
|
||||
// 2. 检查/创建 workbench:read 权限
|
||||
let perm = await prisma.permission.findFirst({
|
||||
where: { tenantId: tenant.id, code: 'workbench:read' }
|
||||
});
|
||||
|
||||
if (!perm) {
|
||||
perm = await prisma.permission.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
code: 'workbench:read',
|
||||
name: '查看工作台',
|
||||
resource: 'workbench',
|
||||
action: 'read',
|
||||
description: '访问工作台页面',
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 创建权限: workbench:read');
|
||||
} else {
|
||||
console.log(' ⏭️ 权限已存在: workbench:read');
|
||||
}
|
||||
|
||||
// 3. 为该租户的所有角色分配此权限
|
||||
const roles = await prisma.role.findMany({
|
||||
where: { tenantId: tenant.id },
|
||||
select: { id: true, name: true, code: true }
|
||||
});
|
||||
|
||||
for (const role of roles) {
|
||||
const existing = await prisma.rolePermission.findFirst({
|
||||
where: { roleId: role.id, permissionId: perm.id }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await prisma.rolePermission.create({
|
||||
data: { roleId: role.id, permissionId: perm.id }
|
||||
});
|
||||
console.log(` ✅ 分配给角色: ${role.name}`);
|
||||
} else {
|
||||
console.log(` ⏭️ 角色 ${role.name} 已有此权限`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更新工作台菜单的权限配置
|
||||
console.log('\n🔧 更新工作台菜单权限配置...');
|
||||
const workbenchMenu = await prisma.menu.findFirst({
|
||||
where: { name: '工作台' }
|
||||
});
|
||||
|
||||
if (workbenchMenu) {
|
||||
if (!workbenchMenu.permission || workbenchMenu.permission.trim() === '') {
|
||||
await prisma.menu.update({
|
||||
where: { id: workbenchMenu.id },
|
||||
data: { permission: 'workbench:read' }
|
||||
});
|
||||
console.log('✅ 工作台菜单已设置 permission: workbench:read');
|
||||
} else {
|
||||
console.log(`⏭️ 工作台菜单已有权限: ${workbenchMenu.permission}`);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 未找到工作台菜单');
|
||||
}
|
||||
|
||||
console.log('\n✅ 完成!请重新登录以查看更新后的菜单。');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
210
backend/scripts/init-linksea-tenant.ts
Normal file
210
backend/scripts/init-linksea-tenant.ts
Normal file
@ -0,0 +1,210 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 开始创建 LinkSea 普通租户...\n');
|
||||
|
||||
const tenantCode = 'linksea';
|
||||
const menuNames = ['赛事管理', '系统管理'];
|
||||
|
||||
// 1. 查找或创建租户
|
||||
console.log(`📋 步骤 1: 查找或创建租户 "${tenantCode}"...`);
|
||||
let tenant = await prisma.tenant.findUnique({
|
||||
where: { code: tenantCode },
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
// 创建普通租户
|
||||
tenant = await prisma.tenant.create({
|
||||
data: {
|
||||
name: 'LinkSea 租户',
|
||||
code: tenantCode,
|
||||
domain: tenantCode,
|
||||
description: 'LinkSea 普通租户',
|
||||
isSuper: 0,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(`✅ 租户创建成功: ${tenant.name} (${tenant.code})\n`);
|
||||
} else {
|
||||
if (tenant.validState !== 1) {
|
||||
console.error(`❌ 错误: 租户 "${tenantCode}" 状态无效!`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
}
|
||||
|
||||
// 2. 查找指定的菜单(顶级菜单)
|
||||
console.log(`📋 步骤 2: 查找菜单 "${menuNames.join('", "')}"...`);
|
||||
const menus = await prisma.menu.findMany({
|
||||
where: {
|
||||
name: { in: menuNames },
|
||||
parentId: null, // 只查找顶级菜单
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (menus.length === 0) {
|
||||
console.error(`❌ 错误: 未找到指定的菜单!`);
|
||||
console.error(` 请确保菜单 "${menuNames.join('", "')}" 已初始化`);
|
||||
console.error(` 运行: pnpm init:menus`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (menus.length !== menuNames.length) {
|
||||
const foundMenuNames = menus.map((m) => m.name);
|
||||
const missingMenus = menuNames.filter(
|
||||
(name) => !foundMenuNames.includes(name),
|
||||
);
|
||||
console.warn(`⚠️ 警告: 部分菜单未找到: ${missingMenus.join(', ')}`);
|
||||
console.log(` 找到的菜单: ${foundMenuNames.join(', ')}\n`);
|
||||
} else {
|
||||
console.log(`✅ 找到 ${menus.length} 个菜单:`);
|
||||
menus.forEach((menu) => {
|
||||
console.log(` ✓ ${menu.name}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 3. 递归获取菜单及其所有子菜单
|
||||
console.log(`📋 步骤 3: 获取菜单及其所有子菜单...`);
|
||||
const menuIds = new Set<number>();
|
||||
|
||||
// 递归函数:获取菜单及其所有子菜单的ID
|
||||
async function getMenuAndChildrenIds(menuId: number) {
|
||||
menuIds.add(menuId);
|
||||
|
||||
// 获取所有子菜单
|
||||
const children = await prisma.menu.findMany({
|
||||
where: {
|
||||
parentId: menuId,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 递归获取子菜单的子菜单
|
||||
for (const child of children) {
|
||||
await getMenuAndChildrenIds(child.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个顶级菜单获取所有子菜单
|
||||
for (const menu of menus) {
|
||||
await getMenuAndChildrenIds(menu.id);
|
||||
}
|
||||
|
||||
const menuIdArray = Array.from(menuIds);
|
||||
console.log(`✅ 共找到 ${menuIdArray.length} 个菜单(包括子菜单)\n`);
|
||||
|
||||
// 4. 获取租户已分配的菜单
|
||||
console.log(`📋 步骤 4: 检查租户已分配的菜单...`);
|
||||
const existingTenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
},
|
||||
select: {
|
||||
menuId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const existingMenuIds = new Set(existingTenantMenus.map((tm) => tm.menuId));
|
||||
|
||||
// 5. 为租户分配菜单(只分配新的菜单)
|
||||
console.log(`📋 步骤 5: 为租户分配菜单...`);
|
||||
const menusToAdd = menuIdArray.filter((id) => !existingMenuIds.has(id));
|
||||
|
||||
if (menusToAdd.length === 0) {
|
||||
console.log(`✅ 租户已拥有所有指定的菜单\n`);
|
||||
} else {
|
||||
let addedCount = 0;
|
||||
const menuNamesToAdd: string[] = [];
|
||||
|
||||
for (const menuId of menusToAdd) {
|
||||
const menu = await prisma.menu.findUnique({
|
||||
where: { id: menuId },
|
||||
select: { name: true },
|
||||
});
|
||||
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: menuId,
|
||||
},
|
||||
});
|
||||
addedCount++;
|
||||
if (menu) {
|
||||
menuNamesToAdd.push(menu.name);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ 为租户添加了 ${addedCount} 个菜单:`);
|
||||
menuNamesToAdd.forEach((name) => {
|
||||
console.log(` ✓ ${name}`);
|
||||
});
|
||||
console.log(
|
||||
`\n✅ 租户现在拥有 ${menuIdArray.length} 个菜单(包括子菜单)\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// 6. 验证结果
|
||||
console.log('📊 初始化结果:');
|
||||
console.log('========================================');
|
||||
console.log('租户信息:');
|
||||
console.log(` 租户编码: ${tenant.code}`);
|
||||
console.log(` 租户名称: ${tenant.name}`);
|
||||
console.log(` 租户类型: ${tenant.isSuper === 1 ? '超级租户' : '普通租户'}`);
|
||||
console.log(` 访问链接: http://your-domain.com/?tenant=${tenant.code}`);
|
||||
console.log('========================================');
|
||||
console.log('分配的菜单:');
|
||||
console.log(` 顶级菜单: ${menuNames.join(', ')}`);
|
||||
console.log(` 菜单总数: ${menuIdArray.length} 个(包括子菜单)`);
|
||||
console.log('========================================');
|
||||
console.log('\n💡 提示:');
|
||||
console.log(' 如需创建管理员账号,请运行: pnpm init:tenant-admin linksea');
|
||||
console.log('========================================');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('\n🎉 LinkSea 租户创建脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 LinkSea 租户创建脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@ -44,6 +44,9 @@ if (!fs.existsSync(menusFilePath)) {
|
||||
|
||||
const menus = JSON.parse(fs.readFileSync(menusFilePath, 'utf-8'));
|
||||
|
||||
// 超级租户可见的菜单名称
|
||||
const SUPER_TENANT_MENUS = ['工作台', '赛事活动', '赛事管理', '系统管理'];
|
||||
|
||||
async function initMenus() {
|
||||
try {
|
||||
console.log('🚀 开始初始化菜单数据...\n');
|
||||
@ -105,8 +108,10 @@ async function initMenus() {
|
||||
}
|
||||
|
||||
// 清空现有菜单(重新初始化)
|
||||
console.log('🗑️ 清空现有菜单...');
|
||||
// 先删除所有子菜单,再删除父菜单(避免外键约束问题)
|
||||
console.log('🗑️ 清空现有菜单和租户菜单关联...');
|
||||
// 先删除租户菜单关联
|
||||
await prisma.tenantMenu.deleteMany({});
|
||||
// 再删除所有子菜单,再删除父菜单(避免外键约束问题)
|
||||
await prisma.menu.deleteMany({
|
||||
where: {
|
||||
parentId: {
|
||||
@ -163,7 +168,7 @@ async function initMenus() {
|
||||
printMenuTree(menu);
|
||||
});
|
||||
|
||||
// 为所有现有租户分配新菜单
|
||||
// 为所有现有租户分配菜单(区分超级租户和普通租户)
|
||||
console.log(`\n📋 为所有租户分配菜单...`);
|
||||
const allTenants = await prisma.tenant.findMany({
|
||||
where: { validState: 1 },
|
||||
@ -174,40 +179,46 @@ async function initMenus() {
|
||||
} else {
|
||||
console.log(` 找到 ${allTenants.length} 个租户\n`);
|
||||
|
||||
for (const tenant of allTenants) {
|
||||
// 获取租户已分配的菜单
|
||||
const existingTenantMenus = await prisma.tenantMenu.findMany({
|
||||
where: { tenantId: tenant.id },
|
||||
select: { menuId: true },
|
||||
});
|
||||
|
||||
const existingMenuIds = new Set(
|
||||
existingTenantMenus.map((tm) => tm.menuId),
|
||||
);
|
||||
|
||||
// 为租户分配所有新菜单
|
||||
let addedMenuCount = 0;
|
||||
for (const menu of allMenus) {
|
||||
if (!existingMenuIds.has(menu.id)) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: menu.id,
|
||||
},
|
||||
});
|
||||
addedMenuCount++;
|
||||
// 获取超级租户专属菜单ID(工作台、赛事管理、系统管理及其子菜单)
|
||||
const superTenantMenuIds = new Set<number>();
|
||||
for (const menu of allMenus) {
|
||||
// 顶级菜单
|
||||
if (!menu.parentId && SUPER_TENANT_MENUS.includes(menu.name)) {
|
||||
superTenantMenuIds.add(menu.id);
|
||||
}
|
||||
// 子菜单(检查父菜单是否在超级租户菜单中)
|
||||
if (menu.parentId) {
|
||||
const parentMenu = allMenus.find(m => m.id === menu.parentId);
|
||||
if (parentMenu && SUPER_TENANT_MENUS.includes(parentMenu.name)) {
|
||||
superTenantMenuIds.add(menu.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addedMenuCount > 0) {
|
||||
console.log(
|
||||
` ✓ 租户 "${tenant.name}" (${tenant.code}): 添加了 ${addedMenuCount} 个菜单`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
` ✓ 租户 "${tenant.name}" (${tenant.code}): 已拥有所有菜单`,
|
||||
);
|
||||
for (const tenant of allTenants) {
|
||||
const isSuperTenant = tenant.isSuper === 1;
|
||||
|
||||
// 确定要分配的菜单
|
||||
const menusToAssign = isSuperTenant
|
||||
? allMenus.filter(m => superTenantMenuIds.has(m.id))
|
||||
: allMenus;
|
||||
|
||||
// 为租户分配菜单
|
||||
let addedMenuCount = 0;
|
||||
for (const menu of menusToAssign) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
menuId: menu.id,
|
||||
},
|
||||
});
|
||||
addedMenuCount++;
|
||||
}
|
||||
|
||||
const tenantType = isSuperTenant ? '(超级租户)' : '(普通租户)';
|
||||
console.log(
|
||||
` ✓ 租户 "${tenant.name}" ${tenantType}: 分配了 ${addedMenuCount} 个菜单`,
|
||||
);
|
||||
}
|
||||
console.log(`\n✅ 菜单分配完成!`);
|
||||
}
|
||||
|
||||
561
backend/scripts/init-roles-permissions.ts
Normal file
561
backend/scripts/init-roles-permissions.ts
Normal file
@ -0,0 +1,561 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('DATABASE_URL not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ============================================
|
||||
// 权限定义
|
||||
// ============================================
|
||||
|
||||
// 基础权限(所有角色共享的权限池)
|
||||
const allPermissions = [
|
||||
// 工作台
|
||||
{ code: 'workbench:read', resource: 'workbench', action: 'read', name: '查看工作台', description: '允许查看工作台' },
|
||||
|
||||
// 用户管理
|
||||
{ code: 'user:create', resource: 'user', action: 'create', name: '创建用户', description: '允许创建新用户' },
|
||||
{ code: 'user:read', resource: 'user', action: 'read', name: '查看用户', description: '允许查看用户列表和详情' },
|
||||
{ code: 'user:update', resource: 'user', action: 'update', name: '更新用户', description: '允许更新用户信息' },
|
||||
{ code: 'user:delete', resource: 'user', action: 'delete', name: '删除用户', description: '允许删除用户' },
|
||||
|
||||
// 角色管理
|
||||
{ code: 'role:create', resource: 'role', action: 'create', name: '创建角色', description: '允许创建新角色' },
|
||||
{ code: 'role:read', resource: 'role', action: 'read', name: '查看角色', description: '允许查看角色列表和详情' },
|
||||
{ code: 'role:update', resource: 'role', action: 'update', name: '更新角色', description: '允许更新角色信息' },
|
||||
{ code: 'role:delete', resource: 'role', action: 'delete', name: '删除角色', description: '允许删除角色' },
|
||||
{ code: 'role:assign', resource: 'role', action: 'assign', name: '分配角色', description: '允许给用户分配角色' },
|
||||
|
||||
// 权限管理
|
||||
{ code: 'permission:read', resource: 'permission', action: 'read', name: '查看权限', description: '允许查看权限列表' },
|
||||
|
||||
// 菜单管理
|
||||
{ code: 'menu:read', resource: 'menu', action: 'read', name: '查看菜单', description: '允许查看菜单列表' },
|
||||
|
||||
// 租户管理(超级租户专属)
|
||||
{ code: 'tenant:create', resource: 'tenant', action: 'create', name: '创建租户', description: '允许创建租户' },
|
||||
{ code: 'tenant:read', resource: 'tenant', action: 'read', name: '查看租户', description: '允许查看租户列表' },
|
||||
{ code: 'tenant:update', resource: 'tenant', action: 'update', name: '更新租户', description: '允许更新租户信息' },
|
||||
{ code: 'tenant:delete', resource: 'tenant', action: 'delete', name: '删除租户', description: '允许删除租户' },
|
||||
|
||||
// 学校管理
|
||||
{ code: 'school:create', resource: 'school', action: 'create', name: '创建学校', description: '允许创建学校信息' },
|
||||
{ code: 'school:read', resource: 'school', action: 'read', name: '查看学校', description: '允许查看学校信息' },
|
||||
{ code: 'school:update', resource: 'school', action: 'update', name: '更新学校', description: '允许更新学校信息' },
|
||||
{ code: 'school:delete', resource: 'school', action: 'delete', name: '删除学校', description: '允许删除学校信息' },
|
||||
|
||||
// 部门管理
|
||||
{ code: 'department:create', resource: 'department', action: 'create', name: '创建部门', description: '允许创建部门' },
|
||||
{ code: 'department:read', resource: 'department', action: 'read', name: '查看部门', description: '允许查看部门列表' },
|
||||
{ code: 'department:update', resource: 'department', action: 'update', name: '更新部门', description: '允许更新部门信息' },
|
||||
{ code: 'department:delete', resource: 'department', action: 'delete', name: '删除部门', description: '允许删除部门' },
|
||||
|
||||
// 年级管理
|
||||
{ code: 'grade:create', resource: 'grade', action: 'create', name: '创建年级', description: '允许创建年级' },
|
||||
{ code: 'grade:read', resource: 'grade', action: 'read', name: '查看年级', description: '允许查看年级列表' },
|
||||
{ code: 'grade:update', resource: 'grade', action: 'update', name: '更新年级', description: '允许更新年级信息' },
|
||||
{ code: 'grade:delete', resource: 'grade', action: 'delete', name: '删除年级', description: '允许删除年级' },
|
||||
|
||||
// 班级管理
|
||||
{ code: 'class:create', resource: 'class', action: 'create', name: '创建班级', description: '允许创建班级' },
|
||||
{ code: 'class:read', resource: 'class', action: 'read', name: '查看班级', description: '允许查看班级列表' },
|
||||
{ code: 'class:update', resource: 'class', action: 'update', name: '更新班级', description: '允许更新班级信息' },
|
||||
{ code: 'class:delete', resource: 'class', action: 'delete', name: '删除班级', description: '允许删除班级' },
|
||||
|
||||
// 教师管理
|
||||
{ code: 'teacher:create', resource: 'teacher', action: 'create', name: '创建教师', description: '允许创建教师' },
|
||||
{ code: 'teacher:read', resource: 'teacher', action: 'read', name: '查看教师', description: '允许查看教师列表' },
|
||||
{ code: 'teacher:update', resource: 'teacher', action: 'update', name: '更新教师', description: '允许更新教师信息' },
|
||||
{ code: 'teacher:delete', resource: 'teacher', action: 'delete', name: '删除教师', description: '允许删除教师' },
|
||||
|
||||
// 学生管理
|
||||
{ code: 'student:create', resource: 'student', action: 'create', name: '创建学生', description: '允许创建学生' },
|
||||
{ code: 'student:read', resource: 'student', action: 'read', name: '查看学生', description: '允许查看学生列表' },
|
||||
{ code: 'student:update', resource: 'student', action: 'update', name: '更新学生', description: '允许更新学生信息' },
|
||||
{ code: 'student:delete', resource: 'student', action: 'delete', name: '删除学生', description: '允许删除学生' },
|
||||
|
||||
// 赛事管理(超级租户)
|
||||
{ code: 'contest:create', resource: 'contest', action: 'create', name: '创建赛事', description: '允许创建赛事' },
|
||||
{ code: 'contest:read', resource: 'contest', action: 'read', name: '查看赛事', description: '允许查看赛事列表' },
|
||||
{ code: 'contest:update', resource: 'contest', action: 'update', name: '更新赛事', description: '允许更新赛事信息' },
|
||||
{ code: 'contest:delete', resource: 'contest', action: 'delete', name: '删除赛事', description: '允许删除赛事' },
|
||||
{ code: 'contest:publish', resource: 'contest', action: 'publish', name: '发布赛事', description: '允许发布/取消发布赛事' },
|
||||
{ code: 'contest:finish', resource: 'contest', action: 'finish', name: '结束赛事', description: '允许结束赛事' },
|
||||
|
||||
// 评审规则管理
|
||||
{ code: 'review-rule:create', resource: 'review-rule', action: 'create', name: '创建评审规则', description: '允许创建评审规则' },
|
||||
{ code: 'review-rule:read', resource: 'review-rule', action: 'read', name: '查看评审规则', description: '允许查看评审规则' },
|
||||
{ code: 'review-rule:update', resource: 'review-rule', action: 'update', name: '更新评审规则', description: '允许更新评审规则' },
|
||||
{ code: 'review-rule:delete', resource: 'review-rule', action: 'delete', name: '删除评审规则', description: '允许删除评审规则' },
|
||||
|
||||
// 评委管理
|
||||
{ code: 'judge:create', resource: 'judge', action: 'create', name: '添加评委', description: '允许添加评委' },
|
||||
{ code: 'judge:read', resource: 'judge', action: 'read', name: '查看评委', description: '允许查看评委列表' },
|
||||
{ code: 'judge:update', resource: 'judge', action: 'update', name: '更新评委', description: '允许更新评委信息' },
|
||||
{ code: 'judge:delete', resource: 'judge', action: 'delete', name: '删除评委', description: '允许删除评委' },
|
||||
{ code: 'judge:assign', resource: 'judge', action: 'assign', name: '分配评委', description: '允许为赛事分配评委' },
|
||||
|
||||
// 赛事报名(学校端)
|
||||
{ code: 'registration:create', resource: 'registration', action: 'create', name: '创建报名', description: '允许报名赛事' },
|
||||
{ code: 'registration:read', resource: 'registration', action: 'read', name: '查看报名', description: '允许查看报名记录' },
|
||||
{ code: 'registration:update', resource: 'registration', action: 'update', name: '更新报名', description: '允许更新报名信息' },
|
||||
{ code: 'registration:delete', resource: 'registration', action: 'delete', name: '取消报名', description: '允许取消报名' },
|
||||
{ code: 'registration:approve', resource: 'registration', action: 'approve', name: '审核报名', description: '允许审核报名' },
|
||||
|
||||
// 参赛作品
|
||||
{ code: 'work:create', resource: 'work', action: 'create', name: '上传作品', description: '允许上传参赛作品' },
|
||||
{ code: 'work:read', resource: 'work', action: 'read', name: '查看作品', description: '允许查看参赛作品' },
|
||||
{ code: 'work:update', resource: 'work', action: 'update', name: '更新作品', description: '允许更新作品信息' },
|
||||
{ code: 'work:delete', resource: 'work', action: 'delete', name: '删除作品', description: '允许删除作品' },
|
||||
{ code: 'work:submit', resource: 'work', action: 'submit', name: '提交作品', description: '允许提交作品' },
|
||||
|
||||
// 作品评审(评委端)
|
||||
{ code: 'review:read', resource: 'review', action: 'read', name: '查看评审任务', description: '允许查看待评审作品' },
|
||||
{ code: 'review:score', resource: 'review', action: 'score', name: '评审打分', description: '允许对作品打分' },
|
||||
|
||||
// 赛事公告
|
||||
{ code: 'notice:create', resource: 'notice', action: 'create', name: '创建公告', description: '允许创建赛事公告' },
|
||||
{ code: 'notice:read', resource: 'notice', action: 'read', name: '查看公告', description: '允许查看赛事公告' },
|
||||
{ code: 'notice:update', resource: 'notice', action: 'update', name: '更新公告', description: '允许更新公告信息' },
|
||||
{ code: 'notice:delete', resource: 'notice', action: 'delete', name: '删除公告', description: '允许删除公告' },
|
||||
|
||||
// 作业管理
|
||||
{ code: 'homework:create', resource: 'homework', action: 'create', name: '创建作业', description: '允许创建作业' },
|
||||
{ code: 'homework:read', resource: 'homework', action: 'read', name: '查看作业', description: '允许查看作业列表' },
|
||||
{ code: 'homework:update', resource: 'homework', action: 'update', name: '更新作业', description: '允许更新作业信息' },
|
||||
{ code: 'homework:delete', resource: 'homework', action: 'delete', name: '删除作业', description: '允许删除作业' },
|
||||
{ code: 'homework:publish', resource: 'homework', action: 'publish', name: '发布作业', description: '允许发布作业' },
|
||||
|
||||
// 作业提交
|
||||
{ code: 'homework-submission:create', resource: 'homework-submission', action: 'create', name: '提交作业', description: '允许提交作业' },
|
||||
{ code: 'homework-submission:read', resource: 'homework-submission', action: 'read', name: '查看作业提交', description: '允许查看作业提交记录' },
|
||||
{ code: 'homework-submission:update', resource: 'homework-submission', action: 'update', name: '更新作业提交', description: '允许更新提交的作业' },
|
||||
|
||||
// 作业评审规则
|
||||
{ code: 'homework-review-rule:create', resource: 'homework-review-rule', action: 'create', name: '创建作业评审规则', description: '允许创建作业评审规则' },
|
||||
{ code: 'homework-review-rule:read', resource: 'homework-review-rule', action: 'read', name: '查看作业评审规则', description: '允许查看作业评审规则' },
|
||||
{ code: 'homework-review-rule:update', resource: 'homework-review-rule', action: 'update', name: '更新作业评审规则', description: '允许更新作业评审规则' },
|
||||
{ code: 'homework-review-rule:delete', resource: 'homework-review-rule', action: 'delete', name: '删除作业评审规则', description: '允许删除作业评审规则' },
|
||||
|
||||
// 作业评分
|
||||
{ code: 'homework-score:create', resource: 'homework-score', action: 'create', name: '作业评分', description: '允许对作业评分' },
|
||||
{ code: 'homework-score:read', resource: 'homework-score', action: 'read', name: '查看作业评分', description: '允许查看作业评分' },
|
||||
|
||||
// 字典管理
|
||||
{ code: 'dict:create', resource: 'dict', action: 'create', name: '创建字典', description: '允许创建新字典' },
|
||||
{ code: 'dict:read', resource: 'dict', action: 'read', name: '查看字典', description: '允许查看字典列表和详情' },
|
||||
{ code: 'dict:update', resource: 'dict', action: 'update', name: '更新字典', description: '允许更新字典信息' },
|
||||
{ code: 'dict:delete', resource: 'dict', action: 'delete', name: '删除字典', description: '允许删除字典' },
|
||||
|
||||
// 系统配置
|
||||
{ code: 'config:create', resource: 'config', action: 'create', name: '创建配置', description: '允许创建新配置' },
|
||||
{ code: 'config:read', resource: 'config', action: 'read', name: '查看配置', description: '允许查看配置列表和详情' },
|
||||
{ code: 'config:update', resource: 'config', action: 'update', name: '更新配置', description: '允许更新配置信息' },
|
||||
{ code: 'config:delete', resource: 'config', action: 'delete', name: '删除配置', description: '允许删除配置' },
|
||||
|
||||
// 日志管理
|
||||
{ code: 'log:read', resource: 'log', action: 'read', name: '查看日志', description: '允许查看系统日志' },
|
||||
{ code: 'log:delete', resource: 'log', action: 'delete', name: '删除日志', description: '允许删除系统日志' },
|
||||
|
||||
// 赛事活动(学校端)
|
||||
{ code: 'activity:read', resource: 'activity', action: 'read', name: '查看赛事活动', description: '允许查看已发布的赛事活动' },
|
||||
{ code: 'activity:guidance', resource: 'activity', action: 'guidance', name: '指导学生', description: '允许指导学生参赛' },
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 角色定义和权限映射
|
||||
// ============================================
|
||||
|
||||
// 超级租户角色
|
||||
const superTenantRoles = [
|
||||
{
|
||||
code: 'super_admin',
|
||||
name: '超级管理员',
|
||||
description: '系统超级管理员,管理赛事和系统配置',
|
||||
permissions: [
|
||||
// 工作台
|
||||
'workbench:read',
|
||||
// 系统管理
|
||||
'user:create', 'user:read', 'user:update', 'user:delete',
|
||||
'role:create', 'role:read', 'role:update', 'role:delete', 'role:assign',
|
||||
'permission:read',
|
||||
'menu:read',
|
||||
'tenant:create', 'tenant:read', 'tenant:update', 'tenant:delete',
|
||||
'dict:create', 'dict:read', 'dict:update', 'dict:delete',
|
||||
'config:create', 'config:read', 'config:update', 'config:delete',
|
||||
'log:read', 'log:delete',
|
||||
// 赛事管理
|
||||
'contest:create', 'contest:read', 'contest:update', 'contest:delete', 'contest:publish', 'contest:finish',
|
||||
'review-rule:create', 'review-rule:read', 'review-rule:update', 'review-rule:delete',
|
||||
'judge:create', 'judge:read', 'judge:update', 'judge:delete', 'judge:assign',
|
||||
'registration:read', 'registration:approve',
|
||||
'work:read',
|
||||
'notice:create', 'notice:read', 'notice:update', 'notice:delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'judge',
|
||||
name: '评委',
|
||||
description: '赛事评委,可以评审作品',
|
||||
permissions: [
|
||||
'workbench:read',
|
||||
'activity:read', // 查看赛事活动
|
||||
'work:read', // 查看待评审作品
|
||||
'review:read', // 查看评审任务
|
||||
'review:score', // 评审打分
|
||||
'notice:read', // 查看公告
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 普通租户(学校)角色
|
||||
const normalTenantRoles = [
|
||||
{
|
||||
code: 'school_admin',
|
||||
name: '学校管理员',
|
||||
description: '学校管理员,管理学校信息、教师、学生等',
|
||||
permissions: [
|
||||
'workbench:read',
|
||||
'user:create', 'user:read', 'user:update', 'user:delete',
|
||||
'role:read',
|
||||
'permission:read',
|
||||
'menu:read',
|
||||
// 学校管理
|
||||
'school:create', 'school:read', 'school:update', 'school:delete',
|
||||
'department:create', 'department:read', 'department:update', 'department:delete',
|
||||
'grade:create', 'grade:read', 'grade:update', 'grade:delete',
|
||||
'class:create', 'class:read', 'class:update', 'class:delete',
|
||||
'teacher:create', 'teacher:read', 'teacher:update', 'teacher:delete',
|
||||
'student:create', 'student:read', 'student:update', 'student:delete',
|
||||
// 赛事活动
|
||||
'activity:read',
|
||||
'notice:read',
|
||||
// 可以查看报名和作品
|
||||
'registration:read',
|
||||
'work:read',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'teacher',
|
||||
name: '教师',
|
||||
description: '教师角色,可以报名赛事、指导学生、管理作业',
|
||||
permissions: [
|
||||
'workbench:read',
|
||||
// 查看基础信息
|
||||
'grade:read',
|
||||
'class:read',
|
||||
'student:read',
|
||||
// 赛事活动
|
||||
'activity:read', // 查看赛事活动列表
|
||||
'activity:guidance', // 指导学生参赛
|
||||
'notice:read', // 查看赛事公告
|
||||
'registration:create', 'registration:read', 'registration:update', 'registration:delete', // 报名管理
|
||||
'work:create', 'work:read', 'work:update', 'work:submit', // 指导学生上传作品
|
||||
// 作业管理
|
||||
'homework:create', 'homework:read', 'homework:update', 'homework:delete', 'homework:publish',
|
||||
'homework-submission:read',
|
||||
'homework-review-rule:create', 'homework-review-rule:read', 'homework-review-rule:update', 'homework-review-rule:delete',
|
||||
'homework-score:create', 'homework-score:read',
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'student',
|
||||
name: '学生',
|
||||
description: '学生角色,可以查看赛事、上传作品、提交作业',
|
||||
permissions: [
|
||||
'workbench:read',
|
||||
// 赛事活动
|
||||
'activity:read', // 查看赛事活动列表
|
||||
'notice:read', // 查看赛事公告
|
||||
'registration:read', // 查看自己的报名记录
|
||||
'work:create', 'work:read', 'work:update', 'work:submit', // 上传/管理自己的作品
|
||||
// 作业
|
||||
'homework:read', // 查看作业
|
||||
'homework-submission:create', 'homework-submission:read', 'homework-submission:update', // 提交作业
|
||||
'homework-score:read', // 查看自己的作业评分
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 初始化函数
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 为租户创建权限
|
||||
*/
|
||||
async function createPermissions(tenantId: number, permissionCodes: string[]) {
|
||||
const createdPermissions: { [code: string]: number } = {};
|
||||
|
||||
for (const code of permissionCodes) {
|
||||
const permDef = allPermissions.find(p => p.code === code);
|
||||
if (!permDef) {
|
||||
console.log(` ⚠️ 权限定义不存在: ${code}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
let permission = await prisma.permission.findFirst({
|
||||
where: { tenantId, code },
|
||||
});
|
||||
|
||||
if (!permission) {
|
||||
permission = await prisma.permission.create({
|
||||
data: {
|
||||
tenantId,
|
||||
code: permDef.code,
|
||||
resource: permDef.resource,
|
||||
action: permDef.action,
|
||||
name: permDef.name,
|
||||
description: permDef.description,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(` ✓ 创建权限: ${code}`);
|
||||
}
|
||||
|
||||
createdPermissions[code] = permission.id;
|
||||
}
|
||||
|
||||
return createdPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为租户创建角色并分配权限
|
||||
*/
|
||||
async function createRoleWithPermissions(
|
||||
tenantId: number,
|
||||
roleConfig: { code: string; name: string; description: string; permissions: string[] },
|
||||
permissionMap: { [code: string]: number }
|
||||
) {
|
||||
// 创建或获取角色
|
||||
let role = await prisma.role.findFirst({
|
||||
where: { tenantId, code: roleConfig.code },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
role = await prisma.role.create({
|
||||
data: {
|
||||
tenantId,
|
||||
code: roleConfig.code,
|
||||
name: roleConfig.name,
|
||||
description: roleConfig.description,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
console.log(` ✓ 创建角色: ${roleConfig.name} (${roleConfig.code})`);
|
||||
} else {
|
||||
// 更新角色信息
|
||||
role = await prisma.role.update({
|
||||
where: { id: role.id },
|
||||
data: {
|
||||
name: roleConfig.name,
|
||||
description: roleConfig.description,
|
||||
},
|
||||
});
|
||||
console.log(` ✓ 更新角色: ${roleConfig.name} (${roleConfig.code})`);
|
||||
}
|
||||
|
||||
// 分配权限
|
||||
const existingRolePermissions = await prisma.rolePermission.findMany({
|
||||
where: { roleId: role.id },
|
||||
select: { permissionId: true },
|
||||
});
|
||||
const existingPermissionIds = new Set(existingRolePermissions.map(rp => rp.permissionId));
|
||||
|
||||
let addedCount = 0;
|
||||
for (const permCode of roleConfig.permissions) {
|
||||
const permissionId = permissionMap[permCode];
|
||||
if (!permissionId) {
|
||||
console.log(` ⚠️ 权限不存在: ${permCode}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingPermissionIds.has(permissionId)) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: role.id,
|
||||
permissionId,
|
||||
},
|
||||
});
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
console.log(` 添加了 ${addedCount} 个权限`);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化超级租户的角色和权限
|
||||
*/
|
||||
async function initSuperTenantRoles() {
|
||||
console.log('\n🚀 开始初始化超级租户角色和权限...\n');
|
||||
|
||||
// 查找超级租户
|
||||
const superTenant = await prisma.tenant.findFirst({
|
||||
where: { isSuper: 1, validState: 1 },
|
||||
});
|
||||
|
||||
if (!superTenant) {
|
||||
console.error('❌ 超级租户不存在!请先运行 init:super-tenant');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到超级租户: ${superTenant.name} (${superTenant.code})\n`);
|
||||
|
||||
// 收集所有需要的权限码
|
||||
const allPermissionCodes = new Set<string>();
|
||||
superTenantRoles.forEach(role => {
|
||||
role.permissions.forEach(code => allPermissionCodes.add(code));
|
||||
});
|
||||
|
||||
// 创建权限
|
||||
console.log('📝 创建权限...');
|
||||
const permissionMap = await createPermissions(superTenant.id, Array.from(allPermissionCodes));
|
||||
console.log(`✅ 共 ${Object.keys(permissionMap).length} 个权限\n`);
|
||||
|
||||
// 创建角色
|
||||
console.log('👥 创建角色...');
|
||||
for (const roleConfig of superTenantRoles) {
|
||||
await createRoleWithPermissions(superTenant.id, roleConfig, permissionMap);
|
||||
}
|
||||
|
||||
console.log('\n✅ 超级租户角色和权限初始化完成!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化普通租户的角色和权限
|
||||
*/
|
||||
async function initNormalTenantRoles(tenantCode: string) {
|
||||
console.log(`\n🚀 开始初始化租户 "${tenantCode}" 的角色和权限...\n`);
|
||||
|
||||
// 查找租户
|
||||
const tenant = await prisma.tenant.findFirst({
|
||||
where: { code: tenantCode, validState: 1 },
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
console.error(`❌ 租户 "${tenantCode}" 不存在!`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tenant.isSuper === 1) {
|
||||
console.log('⚠️ 这是超级租户,请使用 --super 选项');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
|
||||
// 收集所有需要的权限码
|
||||
const allPermissionCodes = new Set<string>();
|
||||
normalTenantRoles.forEach(role => {
|
||||
role.permissions.forEach(code => allPermissionCodes.add(code));
|
||||
});
|
||||
|
||||
// 创建权限
|
||||
console.log('📝 创建权限...');
|
||||
const permissionMap = await createPermissions(tenant.id, Array.from(allPermissionCodes));
|
||||
console.log(`✅ 共 ${Object.keys(permissionMap).length} 个权限\n`);
|
||||
|
||||
// 创建角色
|
||||
console.log('👥 创建角色...');
|
||||
for (const roleConfig of normalTenantRoles) {
|
||||
await createRoleWithPermissions(tenant.id, roleConfig, permissionMap);
|
||||
}
|
||||
|
||||
// 输出角色信息
|
||||
console.log('\n📊 角色权限概览:');
|
||||
for (const roleConfig of normalTenantRoles) {
|
||||
console.log(` ${roleConfig.name} (${roleConfig.code}): ${roleConfig.permissions.length} 个权限`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ 租户 "${tenantCode}" 角色和权限初始化完成!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有普通租户的角色和权限
|
||||
*/
|
||||
async function initAllNormalTenantRoles() {
|
||||
console.log('\n🚀 开始初始化所有普通租户的角色和权限...\n');
|
||||
|
||||
// 查找所有普通租户
|
||||
const normalTenants = await prisma.tenant.findMany({
|
||||
where: { isSuper: { not: 1 }, validState: 1 },
|
||||
});
|
||||
|
||||
if (normalTenants.length === 0) {
|
||||
console.log('⚠️ 没有找到普通租户');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到 ${normalTenants.length} 个普通租户\n`);
|
||||
|
||||
for (const tenant of normalTenants) {
|
||||
await initNormalTenantRoles(tenant.code);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('\n✅ 所有普通租户角色和权限初始化完成!');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 主函数
|
||||
// ============================================
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const isSuper = args.includes('--super');
|
||||
const isAll = args.includes('--all');
|
||||
const tenantCode = args.find(arg => !arg.startsWith('--'));
|
||||
|
||||
try {
|
||||
if (isSuper) {
|
||||
await initSuperTenantRoles();
|
||||
} else if (isAll) {
|
||||
await initAllNormalTenantRoles();
|
||||
} else if (tenantCode) {
|
||||
await initNormalTenantRoles(tenantCode);
|
||||
} else {
|
||||
console.log('使用方法:');
|
||||
console.log(' 初始化超级租户角色: ts-node scripts/init-roles-permissions.ts --super');
|
||||
console.log(' 初始化指定租户角色: ts-node scripts/init-roles-permissions.ts <租户编码>');
|
||||
console.log(' 初始化所有普通租户: ts-node scripts/init-roles-permissions.ts --all');
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('\n🎉 脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -230,7 +230,7 @@ async function main() {
|
||||
icon: 'TeamOutlined',
|
||||
component: 'system/tenants/Index',
|
||||
parentId: systemMenu.id,
|
||||
permission: 'tenant:read',
|
||||
permission: 'tenant:update', // 只有超级租户才有此权限,普通租户只有 tenant:read
|
||||
sort: 7,
|
||||
validState: 1,
|
||||
},
|
||||
|
||||
@ -254,6 +254,563 @@ const permissions = [
|
||||
name: '修改用户密码',
|
||||
description: '允许修改用户密码',
|
||||
},
|
||||
|
||||
// 学校管理权限
|
||||
{
|
||||
code: 'school:create',
|
||||
resource: 'school',
|
||||
action: 'create',
|
||||
name: '创建学校',
|
||||
description: '允许创建学校信息',
|
||||
},
|
||||
{
|
||||
code: 'school:read',
|
||||
resource: 'school',
|
||||
action: 'read',
|
||||
name: '查看学校',
|
||||
description: '允许查看学校信息',
|
||||
},
|
||||
{
|
||||
code: 'school:update',
|
||||
resource: 'school',
|
||||
action: 'update',
|
||||
name: '更新学校',
|
||||
description: '允许更新学校信息',
|
||||
},
|
||||
{
|
||||
code: 'school:delete',
|
||||
resource: 'school',
|
||||
action: 'delete',
|
||||
name: '删除学校',
|
||||
description: '允许删除学校信息',
|
||||
},
|
||||
{
|
||||
code: 'department:create',
|
||||
resource: 'department',
|
||||
action: 'create',
|
||||
name: '创建部门',
|
||||
description: '允许创建部门',
|
||||
},
|
||||
{
|
||||
code: 'department:read',
|
||||
resource: 'department',
|
||||
action: 'read',
|
||||
name: '查看部门',
|
||||
description: '允许查看部门列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'department:update',
|
||||
resource: 'department',
|
||||
action: 'update',
|
||||
name: '更新部门',
|
||||
description: '允许更新部门信息',
|
||||
},
|
||||
{
|
||||
code: 'department:delete',
|
||||
resource: 'department',
|
||||
action: 'delete',
|
||||
name: '删除部门',
|
||||
description: '允许删除部门',
|
||||
},
|
||||
{
|
||||
code: 'grade:create',
|
||||
resource: 'grade',
|
||||
action: 'create',
|
||||
name: '创建年级',
|
||||
description: '允许创建年级',
|
||||
},
|
||||
{
|
||||
code: 'grade:read',
|
||||
resource: 'grade',
|
||||
action: 'read',
|
||||
name: '查看年级',
|
||||
description: '允许查看年级列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'grade:update',
|
||||
resource: 'grade',
|
||||
action: 'update',
|
||||
name: '更新年级',
|
||||
description: '允许更新年级信息',
|
||||
},
|
||||
{
|
||||
code: 'grade:delete',
|
||||
resource: 'grade',
|
||||
action: 'delete',
|
||||
name: '删除年级',
|
||||
description: '允许删除年级',
|
||||
},
|
||||
{
|
||||
code: 'class:create',
|
||||
resource: 'class',
|
||||
action: 'create',
|
||||
name: '创建班级',
|
||||
description: '允许创建班级',
|
||||
},
|
||||
{
|
||||
code: 'class:read',
|
||||
resource: 'class',
|
||||
action: 'read',
|
||||
name: '查看班级',
|
||||
description: '允许查看班级列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'class:update',
|
||||
resource: 'class',
|
||||
action: 'update',
|
||||
name: '更新班级',
|
||||
description: '允许更新班级信息',
|
||||
},
|
||||
{
|
||||
code: 'class:delete',
|
||||
resource: 'class',
|
||||
action: 'delete',
|
||||
name: '删除班级',
|
||||
description: '允许删除班级',
|
||||
},
|
||||
{
|
||||
code: 'teacher:create',
|
||||
resource: 'teacher',
|
||||
action: 'create',
|
||||
name: '创建教师',
|
||||
description: '允许创建教师',
|
||||
},
|
||||
{
|
||||
code: 'teacher:read',
|
||||
resource: 'teacher',
|
||||
action: 'read',
|
||||
name: '查看教师',
|
||||
description: '允许查看教师列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'teacher:update',
|
||||
resource: 'teacher',
|
||||
action: 'update',
|
||||
name: '更新教师',
|
||||
description: '允许更新教师信息',
|
||||
},
|
||||
{
|
||||
code: 'teacher:delete',
|
||||
resource: 'teacher',
|
||||
action: 'delete',
|
||||
name: '删除教师',
|
||||
description: '允许删除教师',
|
||||
},
|
||||
{
|
||||
code: 'student:create',
|
||||
resource: 'student',
|
||||
action: 'create',
|
||||
name: '创建学生',
|
||||
description: '允许创建学生',
|
||||
},
|
||||
{
|
||||
code: 'student:read',
|
||||
resource: 'student',
|
||||
action: 'read',
|
||||
name: '查看学生',
|
||||
description: '允许查看学生列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'student:update',
|
||||
resource: 'student',
|
||||
action: 'update',
|
||||
name: '更新学生',
|
||||
description: '允许更新学生信息',
|
||||
},
|
||||
{
|
||||
code: 'student:delete',
|
||||
resource: 'student',
|
||||
action: 'delete',
|
||||
name: '删除学生',
|
||||
description: '允许删除学生',
|
||||
},
|
||||
|
||||
// 赛事管理权限
|
||||
{
|
||||
code: 'contest:create',
|
||||
resource: 'contest',
|
||||
action: 'create',
|
||||
name: '创建比赛',
|
||||
description: '允许创建比赛',
|
||||
},
|
||||
{
|
||||
code: 'contest:read',
|
||||
resource: 'contest',
|
||||
action: 'read',
|
||||
name: '查看比赛',
|
||||
description: '允许查看比赛列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'contest:update',
|
||||
resource: 'contest',
|
||||
action: 'update',
|
||||
name: '更新比赛',
|
||||
description: '允许更新比赛信息',
|
||||
},
|
||||
{
|
||||
code: 'contest:delete',
|
||||
resource: 'contest',
|
||||
action: 'delete',
|
||||
name: '删除比赛',
|
||||
description: '允许删除比赛',
|
||||
},
|
||||
{
|
||||
code: 'contest:publish',
|
||||
resource: 'contest',
|
||||
action: 'publish',
|
||||
name: '发布比赛',
|
||||
description: '允许发布比赛',
|
||||
},
|
||||
{
|
||||
code: 'contest:team:create',
|
||||
resource: 'contest:team',
|
||||
action: 'create',
|
||||
name: '创建团队',
|
||||
description: '允许创建比赛团队',
|
||||
},
|
||||
{
|
||||
code: 'contest:team:read',
|
||||
resource: 'contest:team',
|
||||
action: 'read',
|
||||
name: '查看团队',
|
||||
description: '允许查看团队列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'contest:team:update',
|
||||
resource: 'contest:team',
|
||||
action: 'update',
|
||||
name: '更新团队',
|
||||
description: '允许更新团队信息',
|
||||
},
|
||||
{
|
||||
code: 'contest:team:delete',
|
||||
resource: 'contest:team',
|
||||
action: 'delete',
|
||||
name: '删除团队',
|
||||
description: '允许删除团队',
|
||||
},
|
||||
{
|
||||
code: 'contest:team:manage',
|
||||
resource: 'contest:team',
|
||||
action: 'manage',
|
||||
name: '管理团队成员',
|
||||
description: '允许管理团队成员',
|
||||
},
|
||||
{
|
||||
code: 'contest:review:create',
|
||||
resource: 'contest:review',
|
||||
action: 'create',
|
||||
name: '创建评审规则',
|
||||
description: '允许创建评审规则',
|
||||
},
|
||||
{
|
||||
code: 'contest:review:read',
|
||||
resource: 'contest:review',
|
||||
action: 'read',
|
||||
name: '查看评审',
|
||||
description: '允许查看评审规则和评审记录',
|
||||
},
|
||||
{
|
||||
code: 'contest:review:update',
|
||||
resource: 'contest:review',
|
||||
action: 'update',
|
||||
name: '更新评审规则',
|
||||
description: '允许更新评审规则',
|
||||
},
|
||||
{
|
||||
code: 'contest:review:delete',
|
||||
resource: 'contest:review',
|
||||
action: 'delete',
|
||||
name: '删除评审规则',
|
||||
description: '允许删除评审规则',
|
||||
},
|
||||
{
|
||||
code: 'contest:review:assign',
|
||||
resource: 'contest:review',
|
||||
action: 'assign',
|
||||
name: '分配评审任务',
|
||||
description: '允许分配评审任务给评委',
|
||||
},
|
||||
{
|
||||
code: 'contest:review:score',
|
||||
resource: 'contest:review',
|
||||
action: 'score',
|
||||
name: '评审打分',
|
||||
description: '允许对作品进行评审打分',
|
||||
},
|
||||
{
|
||||
code: 'contest:judge:create',
|
||||
resource: 'contest:judge',
|
||||
action: 'create',
|
||||
name: '添加评委',
|
||||
description: '允许添加比赛评委',
|
||||
},
|
||||
{
|
||||
code: 'contest:judge:read',
|
||||
resource: 'contest:judge',
|
||||
action: 'read',
|
||||
name: '查看评委',
|
||||
description: '允许查看评委列表',
|
||||
},
|
||||
{
|
||||
code: 'contest:judge:update',
|
||||
resource: 'contest:judge',
|
||||
action: 'update',
|
||||
name: '更新评委',
|
||||
description: '允许更新评委信息',
|
||||
},
|
||||
{
|
||||
code: 'contest:judge:delete',
|
||||
resource: 'contest:judge',
|
||||
action: 'delete',
|
||||
name: '删除评委',
|
||||
description: '允许删除评委',
|
||||
},
|
||||
{
|
||||
code: 'contest:work:create',
|
||||
resource: 'contest:work',
|
||||
action: 'create',
|
||||
name: '创建作品',
|
||||
description: '允许创建参赛作品',
|
||||
},
|
||||
{
|
||||
code: 'contest:work:read',
|
||||
resource: 'contest:work',
|
||||
action: 'read',
|
||||
name: '查看作品',
|
||||
description: '允许查看作品列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'contest:work:update',
|
||||
resource: 'contest:work',
|
||||
action: 'update',
|
||||
name: '更新作品',
|
||||
description: '允许更新作品信息',
|
||||
},
|
||||
{
|
||||
code: 'contest:work:delete',
|
||||
resource: 'contest:work',
|
||||
action: 'delete',
|
||||
name: '删除作品',
|
||||
description: '允许删除作品',
|
||||
},
|
||||
{
|
||||
code: 'contest:work:submit',
|
||||
resource: 'contest:work',
|
||||
action: 'submit',
|
||||
name: '提交作品',
|
||||
description: '允许提交作品',
|
||||
},
|
||||
{
|
||||
code: 'contest:work:review',
|
||||
resource: 'contest:work',
|
||||
action: 'review',
|
||||
name: '审核作品',
|
||||
description: '允许审核作品状态',
|
||||
},
|
||||
{
|
||||
code: 'contest:registration:create',
|
||||
resource: 'contest:registration',
|
||||
action: 'create',
|
||||
name: '创建报名',
|
||||
description: '允许创建报名记录',
|
||||
},
|
||||
{
|
||||
code: 'contest:registration:read',
|
||||
resource: 'contest:registration',
|
||||
action: 'read',
|
||||
name: '查看报名',
|
||||
description: '允许查看报名列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'contest:registration:update',
|
||||
resource: 'contest:registration',
|
||||
action: 'update',
|
||||
name: '更新报名',
|
||||
description: '允许更新报名信息',
|
||||
},
|
||||
{
|
||||
code: 'contest:registration:delete',
|
||||
resource: 'contest:registration',
|
||||
action: 'delete',
|
||||
name: '删除报名',
|
||||
description: '允许删除报名记录',
|
||||
},
|
||||
{
|
||||
code: 'contest:registration:approve',
|
||||
resource: 'contest:registration',
|
||||
action: 'approve',
|
||||
name: '审核报名',
|
||||
description: '允许审核报名(通过/拒绝)',
|
||||
},
|
||||
{
|
||||
code: 'contest:notice:create',
|
||||
resource: 'contest:notice',
|
||||
action: 'create',
|
||||
name: '创建公告',
|
||||
description: '允许创建比赛公告',
|
||||
},
|
||||
{
|
||||
code: 'contest:notice:read',
|
||||
resource: 'contest:notice',
|
||||
action: 'read',
|
||||
name: '查看公告',
|
||||
description: '允许查看公告列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'contest:notice:update',
|
||||
resource: 'contest:notice',
|
||||
action: 'update',
|
||||
name: '更新公告',
|
||||
description: '允许更新公告信息',
|
||||
},
|
||||
{
|
||||
code: 'contest:notice:delete',
|
||||
resource: 'contest:notice',
|
||||
action: 'delete',
|
||||
name: '删除公告',
|
||||
description: '允许删除公告',
|
||||
},
|
||||
{
|
||||
code: 'contest:notice:publish',
|
||||
resource: 'contest:notice',
|
||||
action: 'publish',
|
||||
name: '发布公告',
|
||||
description: '允许发布公告',
|
||||
},
|
||||
|
||||
// 作业管理权限
|
||||
{
|
||||
code: 'homework:create',
|
||||
resource: 'homework',
|
||||
action: 'create',
|
||||
name: '创建作业',
|
||||
description: '允许创建作业',
|
||||
},
|
||||
{
|
||||
code: 'homework:read',
|
||||
resource: 'homework',
|
||||
action: 'read',
|
||||
name: '查看作业',
|
||||
description: '允许查看作业列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'homework:update',
|
||||
resource: 'homework',
|
||||
action: 'update',
|
||||
name: '更新作业',
|
||||
description: '允许更新作业信息',
|
||||
},
|
||||
{
|
||||
code: 'homework:delete',
|
||||
resource: 'homework',
|
||||
action: 'delete',
|
||||
name: '删除作业',
|
||||
description: '允许删除作业',
|
||||
},
|
||||
|
||||
// 租户查看权限(所有租户都可以查看租户列表,用于发布赛事选择公开范围等)
|
||||
{
|
||||
code: 'tenant:read',
|
||||
resource: 'tenant',
|
||||
action: 'read',
|
||||
name: '查看租户',
|
||||
description: '允许查看租户列表和详情',
|
||||
},
|
||||
|
||||
// 评委管理权限(简化前缀)
|
||||
{
|
||||
code: 'judge:create',
|
||||
resource: 'judge',
|
||||
action: 'create',
|
||||
name: '添加评委',
|
||||
description: '允许添加评委',
|
||||
},
|
||||
{
|
||||
code: 'judge:read',
|
||||
resource: 'judge',
|
||||
action: 'read',
|
||||
name: '查看评委',
|
||||
description: '允许查看评委列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'judge:update',
|
||||
resource: 'judge',
|
||||
action: 'update',
|
||||
name: '更新评委',
|
||||
description: '允许更新评委信息',
|
||||
},
|
||||
{
|
||||
code: 'judge:delete',
|
||||
resource: 'judge',
|
||||
action: 'delete',
|
||||
name: '删除评委',
|
||||
description: '允许删除评委',
|
||||
},
|
||||
|
||||
// 公告管理权限(简化前缀)
|
||||
{
|
||||
code: 'notice:create',
|
||||
resource: 'notice',
|
||||
action: 'create',
|
||||
name: '创建公告',
|
||||
description: '允许创建公告',
|
||||
},
|
||||
{
|
||||
code: 'notice:read',
|
||||
resource: 'notice',
|
||||
action: 'read',
|
||||
name: '查看公告',
|
||||
description: '允许查看公告列表和详情',
|
||||
},
|
||||
{
|
||||
code: 'notice:update',
|
||||
resource: 'notice',
|
||||
action: 'update',
|
||||
name: '更新公告',
|
||||
description: '允许更新公告信息',
|
||||
},
|
||||
{
|
||||
code: 'notice:delete',
|
||||
resource: 'notice',
|
||||
action: 'delete',
|
||||
name: '删除公告',
|
||||
description: '允许删除公告',
|
||||
},
|
||||
|
||||
// 作品管理权限(简化前缀)
|
||||
{
|
||||
code: 'work:read',
|
||||
resource: 'work',
|
||||
action: 'read',
|
||||
name: '查看作品',
|
||||
description: '允许查看参赛作品',
|
||||
},
|
||||
];
|
||||
|
||||
// 超级管理员专属权限(只有超级租户才能拥有)
|
||||
const superAdminPermissions = [
|
||||
{
|
||||
code: 'tenant:create',
|
||||
resource: 'tenant',
|
||||
action: 'create',
|
||||
name: '创建租户',
|
||||
description: '允许创建租户(仅超级管理员)',
|
||||
},
|
||||
{
|
||||
code: 'tenant:update',
|
||||
resource: 'tenant',
|
||||
action: 'update',
|
||||
name: '更新租户',
|
||||
description: '允许更新租户信息(仅超级管理员)',
|
||||
},
|
||||
{
|
||||
code: 'tenant:delete',
|
||||
resource: 'tenant',
|
||||
action: 'delete',
|
||||
name: '删除租户',
|
||||
description: '允许删除租户(仅超级管理员)',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@ -280,7 +837,8 @@ async function initTenantAdminPermissionsOnly(tenantCode: string) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
const isSuperTenant = tenant.isSuper === 1;
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})${isSuperTenant ? ' [超级租户]' : ''}\n`);
|
||||
|
||||
// 2. 检查 admin 角色是否存在
|
||||
console.log(`👤 步骤 2: 检查 admin 角色是否存在...`);
|
||||
@ -301,10 +859,15 @@ async function initTenantAdminPermissionsOnly(tenantCode: string) {
|
||||
console.log(`✅ 找到 admin 角色: ${adminRole.name} (${adminRole.code})\n`);
|
||||
|
||||
// 3. 初始化租户权限(如果不存在则创建)
|
||||
console.log(`📝 步骤 3: 初始化租户权限...`);
|
||||
// 超级租户拥有所有权限,普通租户只拥有基础权限
|
||||
const allPermissions = isSuperTenant
|
||||
? [...permissions, ...superAdminPermissions]
|
||||
: permissions;
|
||||
|
||||
console.log(`📝 步骤 3: 初始化租户权限...${isSuperTenant ? '(包含超级管理员权限)' : ''}`);
|
||||
const createdPermissions = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
for (const perm of allPermissions) {
|
||||
// 检查权限是否已存在
|
||||
const existingPermission = await prisma.permission.findFirst({
|
||||
where: {
|
||||
@ -448,7 +1011,8 @@ async function initTenantAdmin(tenantCode: string) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
const isSuperTenant = tenant.isSuper === 1;
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})${isSuperTenant ? ' [超级租户]' : ''}\n`);
|
||||
|
||||
// 2. 检查是否已存在 admin 用户
|
||||
console.log(`👤 步骤 2: 检查 admin 用户是否已存在...`);
|
||||
@ -468,10 +1032,15 @@ async function initTenantAdmin(tenantCode: string) {
|
||||
}
|
||||
|
||||
// 3. 初始化租户权限(如果不存在则创建)
|
||||
console.log(`📝 步骤 3: 初始化租户权限...`);
|
||||
// 超级租户拥有所有权限,普通租户只拥有基础权限
|
||||
const allPermissions = isSuperTenant
|
||||
? [...permissions, ...superAdminPermissions]
|
||||
: permissions;
|
||||
|
||||
console.log(`📝 步骤 3: 初始化租户权限...${isSuperTenant ? '(包含超级管理员权限)' : ''}`);
|
||||
const createdPermissions = [];
|
||||
|
||||
for (const perm of permissions) {
|
||||
for (const perm of allPermissions) {
|
||||
// 检查权限是否已存在
|
||||
const existingPermission = await prisma.permission.findFirst({
|
||||
where: {
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 把"我的作业"移到"作业管理"下...\n');
|
||||
|
||||
// 1. 查找作业管理父菜单
|
||||
const homeworkParent = await prisma.menu.findFirst({
|
||||
where: { name: '作业管理', parentId: null }
|
||||
});
|
||||
|
||||
if (!homeworkParent) {
|
||||
console.log('❌ 未找到作业管理菜单');
|
||||
return;
|
||||
}
|
||||
console.log('✅ 找到作业管理菜单 ID:', homeworkParent.id);
|
||||
|
||||
// 2. 查找我的作业菜单
|
||||
const studentHomework = await prisma.menu.findFirst({
|
||||
where: { path: '/student-homework' }
|
||||
});
|
||||
|
||||
if (!studentHomework) {
|
||||
console.log('❌ 未找到我的作业菜单');
|
||||
return;
|
||||
}
|
||||
console.log('✅ 找到我的作业菜单 ID:', studentHomework.id);
|
||||
|
||||
// 3. 更新我的作业菜单,设置为作业管理的子菜单
|
||||
await prisma.menu.update({
|
||||
where: { id: studentHomework.id },
|
||||
data: {
|
||||
parentId: homeworkParent.id,
|
||||
path: '/homework/student',
|
||||
sort: 0, // 排在最前面
|
||||
}
|
||||
});
|
||||
console.log('✅ 已更新我的作业为作业管理的子菜单');
|
||||
|
||||
// 4. 显示最终作业管理菜单结构
|
||||
console.log('\n📋 作业管理最终菜单结构:');
|
||||
const children = await prisma.menu.findMany({
|
||||
where: { parentId: homeworkParent.id },
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
|
||||
console.log(`- ${homeworkParent.name} (${homeworkParent.path})`);
|
||||
children.forEach(c => {
|
||||
console.log(` - ${c.name} (${c.path}) | component: ${c.component || '(null)'} | permission: ${c.permission}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ 完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,102 +0,0 @@
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function queryContestSubmitTime() {
|
||||
try {
|
||||
const contestName = '3D打印作品大赛2';
|
||||
|
||||
const contest = await prisma.contest.findFirst({
|
||||
where: {
|
||||
contestName: {
|
||||
contains: contestName,
|
||||
},
|
||||
validState: 1,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
contestName: true,
|
||||
submitStartTime: true,
|
||||
submitEndTime: true,
|
||||
submitRule: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!contest) {
|
||||
console.log(`未找到名为"${contestName}"的比赛`);
|
||||
|
||||
// 尝试查找包含"3D打印"的比赛
|
||||
const contests = await prisma.contest.findMany({
|
||||
where: {
|
||||
contestName: {
|
||||
contains: '3D打印',
|
||||
},
|
||||
validState: 1,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
contestName: true,
|
||||
submitStartTime: true,
|
||||
submitEndTime: true,
|
||||
},
|
||||
orderBy: {
|
||||
createTime: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
if (contests.length > 0) {
|
||||
console.log('\n找到以下包含"3D打印"的比赛:');
|
||||
contests.forEach((c) => {
|
||||
console.log(`\n比赛名称: ${c.contestName}`);
|
||||
console.log(`作品提交开始时间: ${c.submitStartTime}`);
|
||||
console.log(`作品提交结束时间: ${c.submitEndTime}`);
|
||||
});
|
||||
} else {
|
||||
console.log('未找到任何包含"3D打印"的比赛');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('比赛信息');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`比赛ID: ${contest.id}`);
|
||||
console.log(`比赛名称: ${contest.contestName}`);
|
||||
console.log(`\n作品提交时间:`);
|
||||
console.log(` 开始时间: ${contest.submitStartTime}`);
|
||||
console.log(` 结束时间: ${contest.submitEndTime}`);
|
||||
console.log(` 提交规则: ${contest.submitRule === 'once' ? '单次提交' : '可多次提交'}`);
|
||||
console.log(`\n比赛时间:`);
|
||||
console.log(` 开始时间: ${contest.startTime}`);
|
||||
console.log(` 结束时间: ${contest.endTime}`);
|
||||
console.log('='.repeat(60));
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
queryContestSubmitTime();
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const result = await prisma.menu.updateMany({
|
||||
where: { path: '/student-activities/list' },
|
||||
data: { name: '活动列表' }
|
||||
});
|
||||
console.log('✅ 已更新', result.count, '条记录');
|
||||
|
||||
// 验证
|
||||
const menu = await prisma.menu.findFirst({
|
||||
where: { path: '/student-activities/list' }
|
||||
});
|
||||
if (menu) {
|
||||
console.log('更新后:', menu.name, '|', menu.path);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@ -1,38 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const contestMenu = await prisma.menu.findFirst({
|
||||
where: { name: '赛事管理', parentId: null }
|
||||
});
|
||||
|
||||
if (!contestMenu) {
|
||||
console.log('未找到赛事管理菜单');
|
||||
return;
|
||||
}
|
||||
|
||||
const children = await prisma.menu.findMany({
|
||||
where: { parentId: contestMenu.id },
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
|
||||
console.log('📋 赛事管理菜单结构:');
|
||||
console.log(`- ${contestMenu.name} (ID: ${contestMenu.id}, path: ${contestMenu.path})`);
|
||||
children.forEach(c => {
|
||||
console.log(` - ${c.name}`);
|
||||
console.log(` path: ${c.path}`);
|
||||
console.log(` component: ${c.component}`);
|
||||
console.log(` permission: ${c.permission}`);
|
||||
});
|
||||
|
||||
// 检查是否有评委管理和评审进度
|
||||
console.log('\n📋 检查缺失的菜单:');
|
||||
const judgeMenu = children.find(c => c.name === '评委管理');
|
||||
const reviewProgressMenu = children.find(c => c.name === '评审进度');
|
||||
|
||||
console.log('评委管理:', judgeMenu ? '存在' : '❌ 不存在');
|
||||
console.log('评审进度:', reviewProgressMenu ? '存在' : '❌ 不存在');
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@ -1,31 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
// 作业管理菜单
|
||||
const homeworkParent = await prisma.menu.findFirst({
|
||||
where: { name: '作业管理', parentId: null }
|
||||
});
|
||||
|
||||
if (!homeworkParent) {
|
||||
console.log('未找到作业管理菜单');
|
||||
return;
|
||||
}
|
||||
|
||||
const children = await prisma.menu.findMany({
|
||||
where: { parentId: homeworkParent.id },
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
|
||||
console.log('📋 作业管理菜单结构:');
|
||||
console.log(`- ${homeworkParent.name} (${homeworkParent.path})`);
|
||||
children.forEach(c => {
|
||||
console.log(` - ${c.name}`);
|
||||
console.log(` path: ${c.path}`);
|
||||
console.log(` component: ${c.component}`);
|
||||
console.log(` permission: ${c.permission}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@ -1,156 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 更新赛事活动菜单结构...\n');
|
||||
|
||||
// 1. 删除赛事管理下的赛事活动菜单
|
||||
console.log('📝 步骤1: 移除赛事管理下的赛事活动菜单');
|
||||
const oldActivityMenu = await prisma.menu.findFirst({
|
||||
where: { path: '/contests/activities' }
|
||||
});
|
||||
|
||||
if (oldActivityMenu) {
|
||||
// 先删除租户菜单关联
|
||||
await prisma.tenantMenu.deleteMany({
|
||||
where: { menuId: oldActivityMenu.id }
|
||||
});
|
||||
// 再删除菜单
|
||||
await prisma.menu.delete({
|
||||
where: { id: oldActivityMenu.id }
|
||||
});
|
||||
console.log(' ✅ 已删除旧的赛事活动菜单');
|
||||
} else {
|
||||
console.log(' ⏭️ 旧菜单不存在,跳过');
|
||||
}
|
||||
|
||||
// 2. 更新独立的赛事活动菜单(改为父菜单,不需要 component)
|
||||
console.log('\n📝 步骤2: 更新赛事活动为父级菜单');
|
||||
let activityParent = await prisma.menu.findFirst({
|
||||
where: { path: '/student-activities' }
|
||||
});
|
||||
|
||||
if (activityParent) {
|
||||
await prisma.menu.update({
|
||||
where: { id: activityParent.id },
|
||||
data: {
|
||||
component: null, // 父菜单不需要组件
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 已更新为父级菜单');
|
||||
} else {
|
||||
// 如果不存在,创建它
|
||||
activityParent = await prisma.menu.create({
|
||||
data: {
|
||||
name: '赛事活动',
|
||||
path: '/student-activities',
|
||||
component: null,
|
||||
icon: 'TrophyOutlined',
|
||||
permission: 'contest:activity:read',
|
||||
sort: 10,
|
||||
parentId: null,
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 已创建父级菜单');
|
||||
}
|
||||
|
||||
// 3. 创建三个子菜单
|
||||
console.log('\n📝 步骤3: 创建子菜单');
|
||||
|
||||
const subMenus = [
|
||||
{
|
||||
name: '我的指导',
|
||||
path: '/student-activities/guidance',
|
||||
component: 'activities/Guidance',
|
||||
permission: 'contest:activity:read',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
name: '评审作品',
|
||||
path: '/student-activities/review',
|
||||
component: 'activities/Review',
|
||||
permission: 'contest:activity:read',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
name: '预设评语',
|
||||
path: '/student-activities/comments',
|
||||
component: 'activities/Comments',
|
||||
permission: 'contest:activity:read',
|
||||
sort: 3,
|
||||
},
|
||||
];
|
||||
|
||||
for (const menuData of subMenus) {
|
||||
const existing = await prisma.menu.findFirst({
|
||||
where: { path: menuData.path }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(` ⏭️ ${menuData.name} 已存在`);
|
||||
} else {
|
||||
await prisma.menu.create({
|
||||
data: {
|
||||
...menuData,
|
||||
parentId: activityParent.id,
|
||||
validState: 1,
|
||||
}
|
||||
});
|
||||
console.log(` ✅ 已创建: ${menuData.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 为租户分配新菜单
|
||||
console.log('\n📝 步骤4: 分配菜单给租户');
|
||||
const tenants = await prisma.tenant.findMany({
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
|
||||
const allMenus = await prisma.menu.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: activityParent.id },
|
||||
{ parentId: activityParent.id }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
for (const tenant of tenants) {
|
||||
for (const menu of allMenus) {
|
||||
const existing = await prisma.tenantMenu.findFirst({
|
||||
where: { tenantId: tenant.id, menuId: menu.id }
|
||||
});
|
||||
if (!existing) {
|
||||
await prisma.tenantMenu.create({
|
||||
data: { tenantId: tenant.id, menuId: menu.id }
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(` ✅ 已分配给租户: ${tenant.name}`);
|
||||
}
|
||||
|
||||
// 5. 显示最终菜单结构
|
||||
console.log('\n📋 最终菜单结构:');
|
||||
const finalMenus = await prisma.menu.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: activityParent.id },
|
||||
{ parentId: activityParent.id }
|
||||
]
|
||||
},
|
||||
orderBy: { sort: 'asc' }
|
||||
});
|
||||
|
||||
finalMenus.forEach(m => {
|
||||
const prefix = m.parentId ? ' ' : '';
|
||||
console.log(`${prefix}- ${m.name} (${m.path})`);
|
||||
});
|
||||
|
||||
console.log('\n✅ 菜单更新完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@ -1,120 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// 加载环境变量(必须在其他导入之前)
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
// 根据 NODE_ENV 加载对应的环境配置文件
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
// scripts 目录的父目录就是 backend 目录
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
// 尝试加载环境特定的配置文件
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
// 如果环境特定文件不存在,尝试加载默认的 .env 文件
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
// 验证必要的环境变量
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ 错误: 未找到 DATABASE_URL 环境变量');
|
||||
console.error(` 请确保存在以下文件之一:`);
|
||||
console.error(` - ${envPath}`);
|
||||
console.error(` - ${path.resolve(backendDir, '.env')}`);
|
||||
console.error(` 或者设置 NODE_ENV 环境变量(当前: ${nodeEnv})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function updatePassword() {
|
||||
try {
|
||||
const tenantCode = 'super';
|
||||
const username = 'admin';
|
||||
const newPassword = process.argv[2] || 'cms@admin'; // 支持命令行参数传入新密码
|
||||
|
||||
console.log(`🔐 开始修改租户 "${tenantCode}" 的 admin 用户密码...\n`);
|
||||
|
||||
// 1. 查找租户
|
||||
console.log(`📋 步骤 1: 查找租户 "${tenantCode}"...`);
|
||||
const tenant = await prisma.tenant.findUnique({
|
||||
where: { code: tenantCode },
|
||||
});
|
||||
|
||||
if (!tenant) {
|
||||
console.error(`❌ 错误: 租户 "${tenantCode}" 不存在!`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ 找到租户: ${tenant.name} (${tenant.code})\n`);
|
||||
|
||||
// 2. 查找用户
|
||||
console.log(`👤 步骤 2: 查找用户 "${username}"...`);
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
tenantId: tenant.id,
|
||||
username: username,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
console.error(
|
||||
`❌ 错误: 租户 "${tenantCode}" 下不存在用户 "${username}"!`,
|
||||
);
|
||||
console.error(` 请先创建该用户`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ 找到用户: ${existingUser.username} (${existingUser.nickname})\n`,
|
||||
);
|
||||
|
||||
// 3. 加密新密码
|
||||
console.log(`🔒 步骤 3: 加密新密码...`);
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
console.log(`✅ 密码加密完成\n`);
|
||||
|
||||
// 4. 更新密码
|
||||
console.log(`💾 步骤 4: 更新用户密码...`);
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: existingUser.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ 密码修改成功!\n`);
|
||||
console.log(`📊 更新结果:`);
|
||||
console.log(` 租户名称: ${tenant.name}`);
|
||||
console.log(` 租户编码: ${tenant.code}`);
|
||||
console.log(` 用户ID: ${updatedUser.id}`);
|
||||
console.log(` 用户名: ${updatedUser.username}`);
|
||||
console.log(` 昵称: ${updatedUser.nickname}`);
|
||||
console.log(` 新密码: ${newPassword}`);
|
||||
console.log(` 修改时间: ${updatedUser.modifyTime}\n`);
|
||||
} catch (error) {
|
||||
console.error('❌ 修改密码时发生错误:');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 执行脚本
|
||||
updatePassword()
|
||||
.then(() => {
|
||||
console.log('🎉 密码修改脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 密码修改脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,115 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🔧 更新学生角色权限...\n');
|
||||
|
||||
// 获取学生角色
|
||||
const studentRole = await prisma.role.findFirst({
|
||||
where: { code: 'student' }
|
||||
});
|
||||
|
||||
if (!studentRole) {
|
||||
console.log('❌ 未找到学生角色');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到学生角色: ${studentRole.name} (ID: ${studentRole.id})`);
|
||||
|
||||
// 需要移除的权限
|
||||
const permissionsToRemove = [
|
||||
'contest:read', // 赛事列表权限
|
||||
'contest:work:read', // 作品管理权限
|
||||
'contest:work:update', // 作品更新权限
|
||||
'work:submit', // 提交作品权限
|
||||
'homework:update', // 作业更新权限
|
||||
'homework:delete', // 作业删除权限
|
||||
];
|
||||
|
||||
// 需要保留/添加的权限
|
||||
const permissionsToKeep = [
|
||||
'workbench:read', // 工作台
|
||||
'contest:activity:read', // 赛事活动(新权限)
|
||||
'homework:read', // 查看作业
|
||||
];
|
||||
|
||||
// 移除不需要的权限
|
||||
console.log('\n📝 移除不需要的权限...');
|
||||
for (const permCode of permissionsToRemove) {
|
||||
const perm = await prisma.permission.findFirst({
|
||||
where: { tenantId: studentRole.tenantId, code: permCode }
|
||||
});
|
||||
|
||||
if (perm) {
|
||||
const deleted = await prisma.rolePermission.deleteMany({
|
||||
where: { roleId: studentRole.id, permissionId: perm.id }
|
||||
});
|
||||
if (deleted.count > 0) {
|
||||
console.log(` ✅ 已移除: ${permCode}`);
|
||||
} else {
|
||||
console.log(` ⏭️ ${permCode} 未分配,跳过`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建赛事活动权限(如果不存在)
|
||||
console.log('\n📝 创建/分配赛事活动权限...');
|
||||
let activityPerm = await prisma.permission.findFirst({
|
||||
where: { tenantId: studentRole.tenantId, code: 'contest:activity:read' }
|
||||
});
|
||||
|
||||
if (!activityPerm) {
|
||||
activityPerm = await prisma.permission.create({
|
||||
data: {
|
||||
tenantId: studentRole.tenantId,
|
||||
code: 'contest:activity:read',
|
||||
name: '查看赛事活动',
|
||||
resource: 'contest:activity',
|
||||
action: 'read',
|
||||
description: '学生查看赛事活动列表',
|
||||
}
|
||||
});
|
||||
console.log(' ✅ 创建权限: contest:activity:read');
|
||||
}
|
||||
|
||||
// 分配赛事活动权限给学生
|
||||
const existingActivityPerm = await prisma.rolePermission.findFirst({
|
||||
where: { roleId: studentRole.id, permissionId: activityPerm.id }
|
||||
});
|
||||
if (!existingActivityPerm) {
|
||||
await prisma.rolePermission.create({
|
||||
data: { roleId: studentRole.id, permissionId: activityPerm.id }
|
||||
});
|
||||
console.log(' ✅ 已分配给学生角色: contest:activity:read');
|
||||
}
|
||||
|
||||
// 更新赛事活动菜单的权限
|
||||
console.log('\n📝 更新菜单权限配置...');
|
||||
const activityMenu = await prisma.menu.findFirst({
|
||||
where: { name: '赛事活动' }
|
||||
});
|
||||
if (activityMenu) {
|
||||
await prisma.menu.update({
|
||||
where: { id: activityMenu.id },
|
||||
data: { permission: 'contest:activity:read' }
|
||||
});
|
||||
console.log(' ✅ 赛事活动菜单权限更新为: contest:activity:read');
|
||||
}
|
||||
|
||||
// 显示最终的学生权限
|
||||
console.log('\n📋 学生角色最终权限:');
|
||||
const finalPerms = await prisma.rolePermission.findMany({
|
||||
where: { roleId: studentRole.id },
|
||||
include: { permission: true }
|
||||
});
|
||||
finalPerms.forEach(rp => {
|
||||
console.log(` - ${rp.permission.code}: ${rp.permission.name}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ 学生权限更新完成!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
78
backend/scripts/update-tenant-menu-permission.ts
Normal file
78
backend/scripts/update-tenant-menu-permission.ts
Normal file
@ -0,0 +1,78 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const envFile = `.env.${nodeEnv}`;
|
||||
const backendDir = path.resolve(__dirname, '..');
|
||||
const envPath = path.resolve(backendDir, envFile);
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
dotenv.config({ path: path.resolve(backendDir, '.env') });
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('DATABASE_URL not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function updateTenantMenuPermission() {
|
||||
try {
|
||||
console.log('🚀 开始更新租户管理菜单权限...\n');
|
||||
|
||||
// 查找租户管理菜单
|
||||
const tenantMenu = await prisma.menu.findFirst({
|
||||
where: {
|
||||
name: '租户管理',
|
||||
path: '/system/tenants',
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenantMenu) {
|
||||
console.log('❌ 租户管理菜单不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`找到租户管理菜单: ID=${tenantMenu.id}, 当前权限=${tenantMenu.permission}`);
|
||||
|
||||
if (tenantMenu.permission === 'tenant:update') {
|
||||
console.log('✅ 菜单权限已经是 tenant:update,无需更新');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新菜单权限
|
||||
await prisma.menu.update({
|
||||
where: { id: tenantMenu.id },
|
||||
data: {
|
||||
permission: 'tenant:update',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ 菜单权限已更新为 tenant:update');
|
||||
console.log('\n说明:');
|
||||
console.log(' - 普通租户只有 tenant:read 权限,可以读取租户列表(用于发布赛事选择公开范围)');
|
||||
console.log(' - 只有超级租户才有 tenant:update 权限,才能看到租户管理菜单');
|
||||
} catch (error) {
|
||||
console.error('❌ 更新失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
updateTenantMenuPermission()
|
||||
.then(() => {
|
||||
console.log('\n🎉 脚本执行完成!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -17,6 +17,11 @@ export class ContestsService {
|
||||
* 检查比赛是否对租户可见
|
||||
*/
|
||||
private isContestVisibleToTenant(contest: any, tenantId: number): boolean {
|
||||
// 如果赛事未发布,对租户不可见
|
||||
if (contest.contestState !== 'published') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果contestTenants为null,表示所有租户可见
|
||||
if (!contest.contestTenants) {
|
||||
return true;
|
||||
@ -173,6 +178,7 @@ export class ContestsService {
|
||||
registrations: true,
|
||||
works: true,
|
||||
teams: true,
|
||||
judges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -232,6 +238,7 @@ export class ContestsService {
|
||||
registrations: true,
|
||||
works: true,
|
||||
teams: true,
|
||||
judges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -239,28 +246,60 @@ export class ContestsService {
|
||||
this.prisma.contest.count({ where }),
|
||||
]);
|
||||
|
||||
// 如果指定了租户ID,进行应用层过滤
|
||||
// 如果指定了租户ID,进行应用层过滤(超级租户不过滤,可以看到所有赛事)
|
||||
let filteredList = allList;
|
||||
let filteredTotal = allTotal;
|
||||
|
||||
if (tenantId) {
|
||||
filteredList = allList.filter((contest) =>
|
||||
this.isContestVisibleToTenant(contest, tenantId),
|
||||
);
|
||||
// 重新计算总数(简化处理,实际应该用原生SQL)
|
||||
filteredTotal = filteredList.length;
|
||||
// 限制返回数量
|
||||
filteredList = filteredList.slice(0, pageSize);
|
||||
// 检查是否为超级租户
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { isSuper: true },
|
||||
});
|
||||
const isSuperTenant = tenant?.isSuper === 1;
|
||||
|
||||
// 超级租户可以看到所有赛事,普通租户只能看到已发布且在公开范围内的赛事
|
||||
if (!isSuperTenant) {
|
||||
filteredList = allList.filter((contest) =>
|
||||
this.isContestVisibleToTenant(contest, tenantId),
|
||||
);
|
||||
// 重新计算总数(简化处理,实际应该用原生SQL)
|
||||
filteredTotal = filteredList.length;
|
||||
// 限制返回数量
|
||||
filteredList = filteredList.slice(0, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 contestTenants JSON 字符串为数组
|
||||
const parsedList = filteredList.map((contest) => ({
|
||||
...contest,
|
||||
contestTenants: this.parseContestTenants(contest.contestTenants),
|
||||
}));
|
||||
|
||||
return {
|
||||
list: filteredList,
|
||||
list: parsedList,
|
||||
total: filteredTotal,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 contestTenants JSON 字符串为数组
|
||||
*/
|
||||
private parseContestTenants(contestTenants: any): number[] | null {
|
||||
if (!contestTenants) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Array.isArray(contestTenants)
|
||||
? contestTenants
|
||||
: JSON.parse(contestTenants as string);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我参与的赛事列表
|
||||
* @param role 角色:student-学生报名的赛事, teacher-教师指导的赛事, judge-评委评审的赛事
|
||||
@ -389,6 +428,7 @@ export class ContestsService {
|
||||
registrations: true,
|
||||
works: true,
|
||||
teams: true,
|
||||
judges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -437,12 +477,24 @@ export class ContestsService {
|
||||
throw new NotFoundException('比赛不存在');
|
||||
}
|
||||
|
||||
// 租户过滤:检查比赛是否对租户可见
|
||||
if (tenantId && !this.isContestVisibleToTenant(contest, tenantId)) {
|
||||
throw new NotFoundException('比赛不存在或无权访问');
|
||||
// 租户过滤:检查比赛是否对租户可见(超级租户可以查看所有赛事)
|
||||
if (tenantId) {
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { isSuper: true },
|
||||
});
|
||||
const isSuperTenant = tenant?.isSuper === 1;
|
||||
|
||||
if (!isSuperTenant && !this.isContestVisibleToTenant(contest, tenantId)) {
|
||||
throw new NotFoundException('比赛不存在或无权访问');
|
||||
}
|
||||
}
|
||||
|
||||
return contest;
|
||||
// 解析 contestTenants JSON 字符串为数组
|
||||
return {
|
||||
...contest,
|
||||
contestTenants: this.parseContestTenants(contest.contestTenants),
|
||||
};
|
||||
}
|
||||
|
||||
async update(
|
||||
@ -655,6 +707,7 @@ export class ContestsService {
|
||||
registrations: true,
|
||||
works: true,
|
||||
teams: true,
|
||||
judges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -779,6 +832,7 @@ export class ContestsService {
|
||||
registrations: true,
|
||||
works: true,
|
||||
teams: true,
|
||||
judges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -822,6 +876,7 @@ export class ContestsService {
|
||||
registrations: true,
|
||||
works: true,
|
||||
teams: true,
|
||||
judges: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -40,5 +40,13 @@ export class QueryWorkDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
workNo?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
username?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -172,6 +172,8 @@ export class WorksService {
|
||||
registrationId,
|
||||
status,
|
||||
title,
|
||||
workNo,
|
||||
username,
|
||||
} = queryDto;
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
@ -202,6 +204,31 @@ export class WorksService {
|
||||
};
|
||||
}
|
||||
|
||||
if (workNo) {
|
||||
where.workNo = {
|
||||
contains: workNo,
|
||||
};
|
||||
}
|
||||
|
||||
if (username) {
|
||||
where.OR = [
|
||||
{
|
||||
submitterAccountNo: {
|
||||
contains: username,
|
||||
},
|
||||
},
|
||||
{
|
||||
registration: {
|
||||
user: {
|
||||
username: {
|
||||
contains: username,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
this.prisma.contestWork.findMany({
|
||||
where,
|
||||
@ -215,6 +242,12 @@ export class WorksService {
|
||||
select: {
|
||||
id: true,
|
||||
contestName: true,
|
||||
reviewRuleId: true,
|
||||
reviewRule: {
|
||||
select: {
|
||||
judgeCount: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
registration: {
|
||||
@ -226,9 +259,33 @@ export class WorksService {
|
||||
nickname: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
teamName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
attachments: true,
|
||||
scores: {
|
||||
where: { validState: 1 },
|
||||
select: {
|
||||
id: true,
|
||||
totalScore: true,
|
||||
},
|
||||
},
|
||||
assignments: {
|
||||
include: {
|
||||
judge: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
nickname: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
scores: true,
|
||||
@ -240,8 +297,32 @@ export class WorksService {
|
||||
this.prisma.contestWork.count({ where }),
|
||||
]);
|
||||
|
||||
// 计算评审统计数据
|
||||
const enrichedList = list.map((work) => {
|
||||
const reviewedCount = work._count?.scores || 0;
|
||||
const totalJudgesCount =
|
||||
work.contest?.reviewRule?.judgeCount || work._count?.assignments || 0;
|
||||
|
||||
// 计算平均分
|
||||
let averageScore: number | null = null;
|
||||
if (work.scores && work.scores.length > 0) {
|
||||
const totalScore = work.scores.reduce(
|
||||
(sum, score) => sum + Number(score.totalScore || 0),
|
||||
0,
|
||||
);
|
||||
averageScore = totalScore / work.scores.length;
|
||||
}
|
||||
|
||||
return {
|
||||
...work,
|
||||
reviewedCount,
|
||||
totalJudgesCount,
|
||||
averageScore,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
list,
|
||||
list: enrichedList,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsEnum, IsInt } from 'class-validator';
|
||||
import { IsString, IsOptional, IsEnum } from 'class-validator';
|
||||
import { Gender, UserStatus } from '../../users/dto/create-user.dto';
|
||||
|
||||
export class CreateJudgeDto {
|
||||
@ -8,8 +8,8 @@ export class CreateJudgeDto {
|
||||
@IsEnum(Gender)
|
||||
gender: Gender;
|
||||
|
||||
@IsInt()
|
||||
tenantId: number;
|
||||
@IsString()
|
||||
organization: string;
|
||||
|
||||
@IsString()
|
||||
phone: string;
|
||||
|
||||
@ -13,10 +13,9 @@ export class QueryJudgeDto {
|
||||
@Type(() => Number)
|
||||
pageSize?: number = 10;
|
||||
|
||||
@IsInt()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
tenantId?: number;
|
||||
organization?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsEnum, IsInt } from 'class-validator';
|
||||
import { IsString, IsOptional, IsEnum } from 'class-validator';
|
||||
import { Gender, UserStatus } from '../../users/dto/create-user.dto';
|
||||
|
||||
export class UpdateJudgeDto {
|
||||
@ -10,9 +10,9 @@ export class UpdateJudgeDto {
|
||||
@IsOptional()
|
||||
gender?: Gender;
|
||||
|
||||
@IsInt()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
tenantId?: number;
|
||||
organization?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
@ -15,6 +15,24 @@ const JUDGE_ROLE_CODE = 'judge';
|
||||
export class JudgesManagementService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* 获取超级租户ID(评委统一存储在超级租户下)
|
||||
*/
|
||||
private async getSuperTenantId(): Promise<number> {
|
||||
const superTenant = await this.prisma.tenant.findFirst({
|
||||
where: {
|
||||
isSuper: 1,
|
||||
validState: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!superTenant) {
|
||||
throw new BadRequestException('系统未配置超级租户');
|
||||
}
|
||||
|
||||
return superTenant.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保评委角色存在,如果不存在则创建
|
||||
*/
|
||||
@ -44,10 +62,13 @@ export class JudgesManagementService {
|
||||
* 创建评委
|
||||
*/
|
||||
async create(createJudgeDto: CreateJudgeDto, creatorId?: number) {
|
||||
const { tenantId, password, ...userData } = createJudgeDto;
|
||||
const { organization, password, ...userData } = createJudgeDto;
|
||||
|
||||
// 评委统一存储在超级租户下
|
||||
const superTenantId = await this.getSuperTenantId();
|
||||
|
||||
// 确保评委角色存在
|
||||
const judgeRoleId = await this.ensureJudgeRole(tenantId);
|
||||
const judgeRoleId = await this.ensureJudgeRole(superTenantId);
|
||||
|
||||
// 生成用户名(如果没有提供)
|
||||
let username = createJudgeDto.username;
|
||||
@ -56,11 +77,11 @@ export class JudgesManagementService {
|
||||
username = createJudgeDto.phone;
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
// 检查用户名是否已存在(在超级租户下)
|
||||
const existingUser = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
tenantId,
|
||||
tenantId: superTenantId,
|
||||
},
|
||||
});
|
||||
|
||||
@ -76,7 +97,8 @@ export class JudgesManagementService {
|
||||
data: {
|
||||
...userData,
|
||||
username,
|
||||
tenantId,
|
||||
tenantId: superTenantId,
|
||||
organization, // 使用独立的所属单位字段
|
||||
password: hashedPassword,
|
||||
status: createJudgeDto.status || 'enabled',
|
||||
creator: creatorId,
|
||||
@ -87,12 +109,6 @@ export class JudgesManagementService {
|
||||
},
|
||||
},
|
||||
include: {
|
||||
tenant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
include: {
|
||||
role: true,
|
||||
@ -108,7 +124,7 @@ export class JudgesManagementService {
|
||||
* 查询评委列表
|
||||
*/
|
||||
async findAll(queryDto: QueryJudgeDto) {
|
||||
const { page = 1, pageSize = 10, tenantId, nickname, username, status } = queryDto;
|
||||
const { page = 1, pageSize = 10, organization, nickname, username, status } = queryDto;
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件:必须有评委角色
|
||||
@ -123,8 +139,10 @@ export class JudgesManagementService {
|
||||
},
|
||||
};
|
||||
|
||||
if (tenantId) {
|
||||
where.tenantId = tenantId;
|
||||
if (organization) {
|
||||
where.organization = {
|
||||
contains: organization,
|
||||
};
|
||||
}
|
||||
|
||||
if (nickname) {
|
||||
@ -149,12 +167,6 @@ export class JudgesManagementService {
|
||||
skip,
|
||||
take: pageSize,
|
||||
include: {
|
||||
tenant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
include: {
|
||||
role: true,
|
||||
@ -210,12 +222,6 @@ export class JudgesManagementService {
|
||||
},
|
||||
},
|
||||
include: {
|
||||
tenant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
include: {
|
||||
role: true,
|
||||
@ -264,19 +270,10 @@ export class JudgesManagementService {
|
||||
data.modifier = modifierId;
|
||||
}
|
||||
|
||||
// 移除 tenantId,不允许修改租户
|
||||
delete data.tenantId;
|
||||
|
||||
return this.prisma.user.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: {
|
||||
tenant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
include: {
|
||||
role: true,
|
||||
@ -329,14 +326,6 @@ export class JudgesManagementService {
|
||||
data: {
|
||||
status,
|
||||
},
|
||||
include: {
|
||||
tenant: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -71,30 +71,7 @@ export class MenusService {
|
||||
* @returns 过滤后的菜单树
|
||||
*/
|
||||
async findUserMenus(userId: number, tenantId: number) {
|
||||
// 检查是否为超级租户
|
||||
const tenant = await this.prisma.tenant.findUnique({
|
||||
where: { id: tenantId },
|
||||
select: { isSuper: true },
|
||||
});
|
||||
const isSuperTenant = tenant?.isSuper === 1;
|
||||
|
||||
// 获取用户信息和角色
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 检查用户是否有超级管理员角色
|
||||
const userRoles = user?.roles?.map((ur: any) => ur.role.code) || [];
|
||||
const isSuperAdmin = userRoles.includes('super_admin');
|
||||
|
||||
// 获取用户的所有权限(即使是超级租户,普通用户也需要权限检查)
|
||||
// 获取用户的所有权限
|
||||
const userPermissions = await this.authService.getUserPermissions(userId);
|
||||
|
||||
// 获取租户分配的菜单ID
|
||||
@ -135,19 +112,10 @@ export class MenusService {
|
||||
const menuTree = buildTree(allMenus);
|
||||
|
||||
// 过滤菜单:只有用户拥有菜单对应权限的菜单才会显示
|
||||
// 超级管理员显示所有菜单,普通用户根据权限过滤
|
||||
// 所有用户都根据权限过滤菜单
|
||||
const filterMenus = (menus: any[]): any[] => {
|
||||
return menus
|
||||
.map((menu) => {
|
||||
// 超级管理员显示所有菜单
|
||||
if (isSuperAdmin) {
|
||||
const filtered = { ...menu };
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
filtered.children = filterMenus(menu.children);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// 先递归处理子菜单
|
||||
let filteredChildren: any[] = [];
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
|
||||
1
backend/tmpclaude-0d23-cwd
Normal file
1
backend/tmpclaude-0d23-cwd
Normal file
@ -0,0 +1 @@
|
||||
/c/Users/82788/Desktop/work/competition-management-system/backend
|
||||
1
backend/tmpclaude-16b9-cwd
Normal file
1
backend/tmpclaude-16b9-cwd
Normal file
@ -0,0 +1 @@
|
||||
/c/Users/82788/Desktop/work/competition-management-system/backend
|
||||
1
backend/tmpclaude-3238-cwd
Normal file
1
backend/tmpclaude-3238-cwd
Normal file
@ -0,0 +1 @@
|
||||
/c/Users/82788/Desktop/work/competition-management-system/backend
|
||||
1
backend/tmpclaude-9916-cwd
Normal file
1
backend/tmpclaude-9916-cwd
Normal file
@ -0,0 +1 @@
|
||||
/c/Users/82788/Desktop/work/competition-management-system/backend
|
||||
1
backend/tmpclaude-bac0-cwd
Normal file
1
backend/tmpclaude-bac0-cwd
Normal file
@ -0,0 +1 @@
|
||||
/c/Users/82788/Desktop/work/competition-management-system/backend
|
||||
1
backend/tmpclaude-cd6c-cwd
Normal file
1
backend/tmpclaude-cd6c-cwd
Normal file
@ -0,0 +1 @@
|
||||
/c/Users/82788/Desktop/work/competition-management-system/backend
|
||||
1
backend/tmpclaude-dcee-cwd
Normal file
1
backend/tmpclaude-dcee-cwd
Normal file
@ -0,0 +1 @@
|
||||
/c/Users/82788/Desktop/work/competition-management-system/backend
|
||||
@ -329,10 +329,24 @@ export interface ContestWork {
|
||||
contest?: Contest;
|
||||
registration?: ContestRegistration;
|
||||
attachments?: ContestWorkAttachment[];
|
||||
assignments?: Array<{
|
||||
id: number;
|
||||
judgeId: number;
|
||||
status: string;
|
||||
judge?: {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
};
|
||||
}>;
|
||||
_count?: {
|
||||
scores: number;
|
||||
assignments: number;
|
||||
};
|
||||
// 评审统计字段(由后端计算返回)
|
||||
reviewedCount?: number;
|
||||
totalJudgesCount?: number;
|
||||
averageScore?: number | null;
|
||||
}
|
||||
|
||||
export interface ContestWorkAttachment {
|
||||
@ -365,6 +379,8 @@ export interface QueryWorkParams extends PaginationParams {
|
||||
registrationId?: number;
|
||||
status?: "submitted" | "locked" | "reviewing" | "rejected" | "accepted";
|
||||
title?: string;
|
||||
workNo?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
// ==================== 评审相关类型 ====================
|
||||
@ -1063,6 +1079,17 @@ export const reviewsApi = {
|
||||
>(`/contests/reviews/work/${workId}/final-score`);
|
||||
return response;
|
||||
},
|
||||
|
||||
// 替换评委
|
||||
replaceJudge: async (
|
||||
assignmentId: number,
|
||||
newJudgeId: number
|
||||
): Promise<void> => {
|
||||
await request.post<any, void>(
|
||||
`/contests/reviews/replace-judge`,
|
||||
{ assignmentId, newJudgeId }
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// 公告管理
|
||||
|
||||
@ -10,16 +10,12 @@ export interface Judge {
|
||||
gender?: 'male' | 'female';
|
||||
avatar?: string;
|
||||
status?: 'enabled' | 'disabled';
|
||||
tenantId?: number;
|
||||
organization?: string;
|
||||
validState?: number;
|
||||
creator?: number;
|
||||
modifier?: number;
|
||||
createTime?: string;
|
||||
modifyTime?: string;
|
||||
tenant?: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
roles?: Array<{
|
||||
id: number;
|
||||
role: {
|
||||
@ -39,7 +35,7 @@ export interface Judge {
|
||||
}
|
||||
|
||||
export interface QueryJudgeParams extends PaginationParams {
|
||||
tenantId?: number;
|
||||
organization?: string;
|
||||
nickname?: string;
|
||||
username?: string;
|
||||
status?: 'enabled' | 'disabled';
|
||||
@ -48,7 +44,7 @@ export interface QueryJudgeParams extends PaginationParams {
|
||||
export interface CreateJudgeForm {
|
||||
nickname: string;
|
||||
gender: 'male' | 'female';
|
||||
tenantId: number;
|
||||
organization: string;
|
||||
phone: string;
|
||||
password: string;
|
||||
username?: string;
|
||||
@ -60,6 +56,7 @@ export interface CreateJudgeForm {
|
||||
export interface UpdateJudgeForm {
|
||||
nickname?: string;
|
||||
gender?: 'male' | 'female';
|
||||
organization?: string;
|
||||
phone?: string;
|
||||
password?: string;
|
||||
username?: string;
|
||||
@ -153,3 +150,4 @@ export const judgesManagementApi = {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@ -110,6 +110,28 @@ const baseRoutes: RouteRecordRaw[] = [
|
||||
permissions: ["contest:registration:read"],
|
||||
},
|
||||
},
|
||||
// 评审进度详情路由
|
||||
{
|
||||
path: "contests/reviews/:id/progress",
|
||||
name: "ReviewProgressDetail",
|
||||
component: () => import("@/views/contests/reviews/ProgressDetail.vue"),
|
||||
meta: {
|
||||
title: "评审进度详情",
|
||||
requiresAuth: true,
|
||||
permissions: ["review:read"],
|
||||
},
|
||||
},
|
||||
// 参赛作品详情列表路由
|
||||
{
|
||||
path: "contests/works/:id/list",
|
||||
name: "WorksDetail",
|
||||
component: () => import("@/views/contests/works/WorksDetail.vue"),
|
||||
meta: {
|
||||
title: "参赛作品详情",
|
||||
requiresAuth: true,
|
||||
permissions: ["work:read"],
|
||||
},
|
||||
},
|
||||
// 作业提交记录路由
|
||||
{
|
||||
path: "homework/submissions",
|
||||
|
||||
@ -30,9 +30,14 @@ const componentMap: Record<string, () => Promise<any>> = {
|
||||
"contests/registrations/Index": () =>
|
||||
import("@/views/contests/registrations/Index.vue"),
|
||||
"contests/works/Index": () => import("@/views/contests/works/Index.vue"),
|
||||
"contests/works/WorksDetail": () =>
|
||||
import("@/views/contests/works/WorksDetail.vue"),
|
||||
"contests/reviews/Index": () => import("@/views/contests/reviews/Index.vue"),
|
||||
"contests/reviews/Tasks": () => import("@/views/contests/reviews/Tasks.vue"),
|
||||
"contests/reviews/Progress": () =>
|
||||
import("@/views/contests/reviews/Progress.vue"),
|
||||
"contests/reviews/ProgressDetail": () =>
|
||||
import("@/views/contests/reviews/ProgressDetail.vue"),
|
||||
"contests/judges/Index": () => import("@/views/contests/judges/Index.vue"),
|
||||
"contests/results/Index": () => import("@/views/contests/results/Index.vue"),
|
||||
"contests/notices/Index": () => import("@/views/contests/notices/Index.vue"),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="contests-page">
|
||||
<a-card class="mb-4">
|
||||
<template #title>比赛管理</template>
|
||||
<template #title>赛事列表</template>
|
||||
<template #extra>
|
||||
<a-button
|
||||
v-permission="'contest:create'"
|
||||
@ -21,15 +21,15 @@
|
||||
class="search-form"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="比赛名称">
|
||||
<a-form-item label="赛事名称">
|
||||
<a-input
|
||||
v-model:value="searchParams.contestName"
|
||||
placeholder="请输入比赛名称"
|
||||
placeholder="请输入赛事名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="发布状态">
|
||||
<a-form-item label="赛事状态">
|
||||
<a-select
|
||||
v-model:value="searchParams.contestState"
|
||||
placeholder="请选择状态"
|
||||
@ -40,23 +40,12 @@
|
||||
<a-select-option value="unpublished">未发布</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="赛事状态">
|
||||
<a-select
|
||||
v-model:value="searchParams.status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="ongoing">进行中</a-select-option>
|
||||
<a-select-option value="finished">已完结</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="比赛类型">
|
||||
<a-form-item label="赛事类型">
|
||||
<a-select
|
||||
v-model:value="searchParams.contestType"
|
||||
placeholder="请选择类型"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="individual">个人赛</a-select-option>
|
||||
<a-select-option value="team">团队赛</a-select-option>
|
||||
@ -83,9 +72,9 @@
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'contestName'">
|
||||
{{ record.contestName }}
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'contestType'">
|
||||
<a-tag
|
||||
@ -101,16 +90,33 @@
|
||||
{{ record.contestState === "published" ? "已发布" : "未发布" }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'ongoing' ? 'processing' : 'orange'">
|
||||
{{ record.status === "ongoing" ? "进行中" : "已完结" }}
|
||||
</a-tag>
|
||||
<template v-else-if="column.key === 'publicScope'">
|
||||
<template
|
||||
v-if="record.contestTenants && record.contestTenants.length > 0"
|
||||
>
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<div v-for="tenantId in record.contestTenants" :key="tenantId">
|
||||
{{ getTenantName(tenantId) }}
|
||||
</div>
|
||||
</template>
|
||||
<a-tag>{{ record.contestTenants.length }}个机构</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'timeRange'">
|
||||
<div>
|
||||
<div>开始:{{ formatDateTime(record.startTime) }}</div>
|
||||
<div>结束:{{ formatDateTime(record.endTime) }}</div>
|
||||
<template v-else-if="column.key === 'judges'">
|
||||
<a-tag v-if="record._count?.judges > 0" color="blue">
|
||||
{{ record._count.judges }}人
|
||||
</a-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'contestTime'">
|
||||
<div v-if="record.startTime || record.endTime">
|
||||
<div>{{ formatDate(record.startTime) }}</div>
|
||||
<div>至 {{ formatDate(record.endTime) }}</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
@ -119,7 +125,7 @@
|
||||
v-permission="'contest:publish'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handlePublish(record)"
|
||||
@click="handlePublishClick(record)"
|
||||
>
|
||||
{{ record.contestState === "published" ? "取消发布" : "发布" }}
|
||||
</a-button>
|
||||
@ -143,13 +149,15 @@
|
||||
编辑
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm
|
||||
<a-button
|
||||
v-permission="'contest:delete'"
|
||||
title="确定要删除这个比赛吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleDeleteClick(record)"
|
||||
>
|
||||
<a-button type="link" danger size="small">删除</a-button>
|
||||
</a-popconfirm>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
@ -171,12 +179,70 @@
|
||||
@success="handleJudgeAddSuccess"
|
||||
/>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 发布弹框 -->
|
||||
<a-modal
|
||||
v-model:open="publishModalVisible"
|
||||
title="发布赛事"
|
||||
:confirm-loading="publishLoading"
|
||||
@ok="handlePublishConfirm"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="选择公开范围(可见机构)" required>
|
||||
<a-select
|
||||
v-model:value="selectedTenants"
|
||||
mode="multiple"
|
||||
placeholder="请选择公开范围"
|
||||
style="width: 100%"
|
||||
:options="tenantOptions"
|
||||
:filter-option="filterTenantOption"
|
||||
show-search
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-alert
|
||||
type="warning"
|
||||
message="发布后,只有选中的机构可以看到此赛事"
|
||||
show-icon
|
||||
class="mt-2"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 取消发布确认弹框 -->
|
||||
<a-modal
|
||||
v-model:open="unpublishModalVisible"
|
||||
title="取消发布"
|
||||
:confirm-loading="publishLoading"
|
||||
@ok="handleUnpublishConfirm"
|
||||
>
|
||||
<p>确定要取消发布赛事「{{ currentPublishContest?.contestName }}」吗?</p>
|
||||
<a-alert
|
||||
type="warning"
|
||||
message="取消发布后,所有机构将无法看到此赛事"
|
||||
show-icon
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<!-- 删除确认弹框 -->
|
||||
<a-modal
|
||||
v-model:open="deleteModalVisible"
|
||||
title="删除赛事"
|
||||
:confirm-loading="deleteLoading"
|
||||
@ok="handleDeleteConfirm"
|
||||
>
|
||||
<p>确定要删除赛事「{{ currentDeleteContest?.contestName }}」吗?</p>
|
||||
<a-alert
|
||||
type="error"
|
||||
message="删除后数据将无法恢复,请谨慎操作!"
|
||||
show-icon
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { ref } from "vue"
|
||||
import { ref, onMounted } from "vue"
|
||||
import { message } from "ant-design-vue"
|
||||
import {
|
||||
PlusOutlined,
|
||||
@ -189,6 +255,7 @@ import {
|
||||
type Contest,
|
||||
type QueryContestParams,
|
||||
} from "@/api/contests"
|
||||
import { tenantsApi, type Tenant } from "@/api/tenants"
|
||||
import AddJudgeDrawer from "./components/AddJudgeDrawer.vue"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
@ -210,73 +277,61 @@ const {
|
||||
requestFn: contestsApi.getList,
|
||||
defaultSearchParams: {} as QueryContestParams,
|
||||
defaultPageSize: 10,
|
||||
errorMessage: "获取比赛列表失败",
|
||||
errorMessage: "获取赛事列表失败",
|
||||
})
|
||||
|
||||
// 租户列表
|
||||
const tenants = ref<Tenant[]>([])
|
||||
const tenantOptions = ref<{ label: string; value: number }[]>([])
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{
|
||||
title: "比赛名称",
|
||||
key: "contestName",
|
||||
title: "赛事名称",
|
||||
dataIndex: "contestName",
|
||||
key: "contestName",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "比赛类型",
|
||||
key: "contestType",
|
||||
dataIndex: "contestType",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "发布状态",
|
||||
key: "contestState",
|
||||
dataIndex: "contestState",
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: "赛事状态",
|
||||
key: "status",
|
||||
dataIndex: "status",
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: "时间范围",
|
||||
key: "timeRange",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "报名时间",
|
||||
key: "registerTime",
|
||||
width: 250,
|
||||
customRender: ({ record }: { record: Contest }) => {
|
||||
return `${formatDateTime(record.registerStartTime)} - ${formatDateTime(
|
||||
record.registerEndTime
|
||||
)}`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "统计",
|
||||
key: "statistics",
|
||||
width: 150,
|
||||
customRender: ({ record }: { record: Contest }) => {
|
||||
const count = record._count || { registrations: 0, works: 0 }
|
||||
return `报名: ${count.registrations || 0} | 作品: ${count.works || 0}`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 320,
|
||||
fixed: "right" as const,
|
||||
},
|
||||
{ title: "赛事类型", key: "contestType", width: 100 },
|
||||
{ title: "赛事状态", key: "contestState", width: 100 },
|
||||
{ title: "公开范围", key: "publicScope", width: 120 },
|
||||
{ title: "评委", key: "judges", width: 80 },
|
||||
{ title: "赛事时间", key: "contestTime", width: 180 },
|
||||
{ title: "操作", key: "action", width: 260, fixed: "right" as const },
|
||||
]
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr?: string) => {
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-"
|
||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
|
||||
// 获取租户名称
|
||||
const getTenantName = (tenantId: number) => {
|
||||
const tenant = tenants.value.find((t) => t.id === tenantId)
|
||||
return tenant?.name || `机构${tenantId}`
|
||||
}
|
||||
|
||||
// 租户选项过滤
|
||||
const filterTenantOption = (input: string, option: any) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
|
||||
// 获取租户列表
|
||||
const fetchTenants = async () => {
|
||||
try {
|
||||
const response = await tenantsApi.getList({ page: 1, pageSize: 100 })
|
||||
tenants.value = response.list
|
||||
tenantOptions.value = response.list.map((t) => ({
|
||||
label: t.name,
|
||||
value: t.id,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error("获取租户列表失败", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
search()
|
||||
@ -295,7 +350,7 @@ const handleAdd = () => {
|
||||
// 编辑
|
||||
const handleEdit = (id: number) => {
|
||||
if (!id) {
|
||||
message.warning("比赛ID不存在")
|
||||
message.warning("赛事ID不存在")
|
||||
return
|
||||
}
|
||||
const path = `/${tenantCode}/contests/${id}/edit`
|
||||
@ -313,13 +368,12 @@ const currentContest = ref<Contest | null>(null)
|
||||
// 添加评委
|
||||
const handleAddJudge = async (id: number) => {
|
||||
currentContestId.value = id
|
||||
// 获取比赛详情以获取评审规则中的评委数量
|
||||
try {
|
||||
const contest = await contestsApi.getDetail(id)
|
||||
currentContest.value = contest
|
||||
judgeDrawerVisible.value = true
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取比赛信息失败")
|
||||
message.error(error?.response?.data?.message || "获取赛事信息失败")
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,37 +387,130 @@ const handleJudgeDrawerClose = () => {
|
||||
// 评委添加成功回调
|
||||
const handleJudgeAddSuccess = () => {
|
||||
message.success("添加评委成功")
|
||||
fetchList() // 刷新列表
|
||||
fetchList()
|
||||
handleJudgeDrawerClose()
|
||||
}
|
||||
|
||||
// 发布/撤回
|
||||
const handlePublish = async (record: Contest) => {
|
||||
try {
|
||||
const newState =
|
||||
record.contestState === "published" ? "unpublished" : "published"
|
||||
await contestsApi.publish(record.id, newState)
|
||||
message.success(newState === "published" ? "发布成功" : "撤回成功")
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "操作失败")
|
||||
// 发布相关
|
||||
const publishModalVisible = ref(false)
|
||||
const unpublishModalVisible = ref(false)
|
||||
const publishLoading = ref(false)
|
||||
const currentPublishContest = ref<Contest | null>(null)
|
||||
const selectedTenants = ref<number[]>([])
|
||||
|
||||
// 点击发布/取消发布按钮
|
||||
const handlePublishClick = async (record: Contest) => {
|
||||
currentPublishContest.value = record
|
||||
if (record.contestState === "published") {
|
||||
// 取消发布
|
||||
unpublishModalVisible.value = true
|
||||
} else {
|
||||
// 发布 - 打开选择租户弹框
|
||||
// 从后端获取最新数据,确保 contestTenants 正确解析
|
||||
try {
|
||||
const contest = await contestsApi.getDetail(record.id)
|
||||
// 确保 contestTenants 是数字数组,处理 null、undefined 和字符串数组的情况
|
||||
if (
|
||||
Array.isArray(contest.contestTenants) &&
|
||||
contest.contestTenants.length > 0
|
||||
) {
|
||||
selectedTenants.value = contest.contestTenants
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => !isNaN(id))
|
||||
} else {
|
||||
selectedTenants.value = []
|
||||
}
|
||||
publishModalVisible.value = true
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取赛事信息失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id: number) => {
|
||||
// 确认发布
|
||||
const handlePublishConfirm = async () => {
|
||||
if (selectedTenants.value.length === 0) {
|
||||
message.warning("请至少选择一个公开范围")
|
||||
return
|
||||
}
|
||||
|
||||
publishLoading.value = true
|
||||
try {
|
||||
await contestsApi.delete(id)
|
||||
// 先更新公开范围
|
||||
await contestsApi.update(currentPublishContest.value!.id, {
|
||||
contestTenants: selectedTenants.value,
|
||||
})
|
||||
// 再发布
|
||||
await contestsApi.publish(currentPublishContest.value!.id, "published")
|
||||
message.success("发布成功")
|
||||
publishModalVisible.value = false
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "发布失败")
|
||||
} finally {
|
||||
publishLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认取消发布
|
||||
const handleUnpublishConfirm = async () => {
|
||||
publishLoading.value = true
|
||||
try {
|
||||
await contestsApi.publish(currentPublishContest.value!.id, "unpublished")
|
||||
message.success("取消发布成功")
|
||||
unpublishModalVisible.value = false
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "取消发布失败")
|
||||
} finally {
|
||||
publishLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除相关
|
||||
const deleteModalVisible = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
const currentDeleteContest = ref<Contest | null>(null)
|
||||
|
||||
// 点击删除按钮
|
||||
const handleDeleteClick = (record: Contest) => {
|
||||
currentDeleteContest.value = record
|
||||
deleteModalVisible.value = true
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const handleDeleteConfirm = async () => {
|
||||
deleteLoading.value = true
|
||||
try {
|
||||
await contestsApi.delete(currentDeleteContest.value!.id)
|
||||
message.success("删除成功")
|
||||
deleteModalVisible.value = false
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "删除失败")
|
||||
} finally {
|
||||
deleteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTenants()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -12,22 +12,14 @@
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="hasTenantReadPermission" label="所属单位">
|
||||
<a-select
|
||||
v-model:value="searchParams.tenantId"
|
||||
placeholder="请选择所属单位"
|
||||
<a-form-item label="所属单位">
|
||||
<a-input
|
||||
v-model:value="searchParams.organization"
|
||||
placeholder="请输入所属单位"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
:loading="tenantsLoading"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="tenant in tenantsList"
|
||||
:key="tenant.id"
|
||||
:value="tenant.id"
|
||||
>
|
||||
{{ tenant.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
@ -62,8 +54,8 @@
|
||||
@change="handleJudgeTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'tenant'">
|
||||
{{ record.tenant?.name || "-" }}
|
||||
<template v-if="column.key === 'organization'">
|
||||
{{ record.organization || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'enabled' ? 'success' : 'error'">
|
||||
@ -77,7 +69,7 @@
|
||||
<!-- 已选评委区域 -->
|
||||
<a-card size="small">
|
||||
<template #title>
|
||||
已选/{{ judgeCount || 0 }}
|
||||
已选 {{ selectedJudges.length }} / {{ judgeCount || 0 }}
|
||||
<span
|
||||
v-if="selectedJudges.length > (judgeCount || 0)"
|
||||
class="warning-text"
|
||||
@ -97,7 +89,7 @@
|
||||
{{ item.nickname }}({{ item.username }})
|
||||
</template>
|
||||
<template #description>
|
||||
{{ item.tenant?.name || "-" }}
|
||||
{{ item.organization || "-" }}
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
@ -141,9 +133,7 @@ import { message } from "ant-design-vue"
|
||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||
import type { Judge, QueryJudgeParams } from "@/api/judges-management"
|
||||
import { judgesManagementApi } from "@/api/judges-management"
|
||||
import { judgesApi, type ContestJudge } from "@/api/contests"
|
||||
import { tenantsApi, type Tenant } from "@/api/tenants"
|
||||
import { useAuthStore } from "@/stores/auth"
|
||||
import { judgesApi } from "@/api/contests"
|
||||
import type { Contest } from "@/api/contests"
|
||||
|
||||
interface Props {
|
||||
@ -156,24 +146,17 @@ const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 评委数量(从评审规则中获取)
|
||||
const judgeCount = computed(() => {
|
||||
return props.contest.reviewRule?.judgeCount || 0
|
||||
})
|
||||
|
||||
// 是否有租户读取权限
|
||||
const hasTenantReadPermission = computed(() => {
|
||||
return authStore.hasPermission("tenant:read")
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive<QueryJudgeParams>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
nickname: undefined,
|
||||
tenantId: undefined,
|
||||
organization: undefined,
|
||||
})
|
||||
|
||||
// 全部评委列表
|
||||
@ -192,10 +175,6 @@ const selectedJudgeIds = ref<number[]>([])
|
||||
const selectedJudges = ref<Judge[]>([])
|
||||
const selectedJudgesLoading = ref(false)
|
||||
|
||||
// 租户列表
|
||||
const tenantsList = ref<Tenant[]>([])
|
||||
const tenantsLoading = ref(false)
|
||||
|
||||
// 提交加载状态
|
||||
const submitLoading = ref(false)
|
||||
|
||||
@ -215,7 +194,8 @@ const judgeColumns = [
|
||||
},
|
||||
{
|
||||
title: "所属单位",
|
||||
key: "tenant",
|
||||
dataIndex: "organization",
|
||||
key: "organization",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
@ -239,7 +219,7 @@ const loadJudges = async () => {
|
||||
page: judgePagination.current,
|
||||
pageSize: judgePagination.pageSize,
|
||||
nickname: searchParams.nickname || undefined,
|
||||
tenantId: searchParams.tenantId || undefined,
|
||||
organization: searchParams.organization || undefined,
|
||||
}
|
||||
const res = await judgesManagementApi.getList(params)
|
||||
judgeList.value = res.list
|
||||
@ -276,34 +256,13 @@ const loadSelectedJudges = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载租户列表
|
||||
const loadTenants = async () => {
|
||||
if (!hasTenantReadPermission.value) {
|
||||
return
|
||||
}
|
||||
tenantsLoading.value = true
|
||||
try {
|
||||
const res = await tenantsApi.getList({ page: 1, pageSize: 100 })
|
||||
tenantsList.value = res.list
|
||||
} catch (error: any) {
|
||||
if (error?.response?.status !== 403) {
|
||||
console.error("加载租户列表失败", error)
|
||||
}
|
||||
} finally {
|
||||
tenantsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查评委是否已选
|
||||
const isJudgeSelected = (judgeId: number): boolean => {
|
||||
return selectedJudgeIds.value.includes(judgeId)
|
||||
}
|
||||
|
||||
// 评委选择变化
|
||||
const handleJudgeSelectionChange = (
|
||||
selectedKeys: number[],
|
||||
selectedRows: Judge[]
|
||||
) => {
|
||||
const handleJudgeSelectionChange = (selectedKeys: number[]) => {
|
||||
// 找出新增的评委(在 selectedKeys 中但不在 selectedJudgeIds 中)
|
||||
const newSelectedIds = selectedKeys.filter(
|
||||
(id) => !selectedJudgeIds.value.includes(id)
|
||||
@ -347,7 +306,7 @@ const handleSearch = () => {
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchParams.nickname = undefined
|
||||
searchParams.tenantId = undefined
|
||||
searchParams.organization = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
@ -414,9 +373,9 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => searchParams.tenantId,
|
||||
() => searchParams.organization,
|
||||
() => {
|
||||
if (searchParams.tenantId === undefined) {
|
||||
if (searchParams.organization === undefined || searchParams.organization === "") {
|
||||
handleSearch()
|
||||
}
|
||||
}
|
||||
@ -425,7 +384,6 @@ watch(
|
||||
onMounted(() => {
|
||||
loadJudges()
|
||||
loadSelectedJudges()
|
||||
loadTenants()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -54,22 +54,13 @@
|
||||
class="search-form"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item v-if="hasTenantReadPermission" label="所属单位">
|
||||
<a-select
|
||||
v-model:value="searchParams.tenantId"
|
||||
placeholder="请选择所属单位"
|
||||
<a-form-item label="所属单位">
|
||||
<a-input
|
||||
v-model:value="searchParams.organization"
|
||||
placeholder="请输入所属单位"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
:loading="tenantsLoading"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="tenant in tenantsList"
|
||||
:key="tenant.id"
|
||||
:value="tenant.id"
|
||||
>
|
||||
{{ tenant.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="姓名">
|
||||
<a-input
|
||||
@ -124,8 +115,8 @@
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'tenant'">
|
||||
{{ record.tenant?.name || "-" }}
|
||||
<template v-else-if="column.key === 'organization'">
|
||||
{{ record.organization || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'gender'">
|
||||
<span v-if="record.gender === 'male'">男</span>
|
||||
@ -220,54 +211,11 @@
|
||||
<a-radio value="female">女</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="hasTenantReadPermission"
|
||||
label="所属单位"
|
||||
name="tenantId"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="form.tenantId"
|
||||
placeholder="请选择所属单位"
|
||||
:loading="tenantsLoading"
|
||||
:disabled="isEditing"
|
||||
show-search
|
||||
:filter-option="(input: string, option: any) => {
|
||||
const label = option?.children?.[0]?.children || option?.label || ''
|
||||
return String(label).toLowerCase().includes(input.toLowerCase())
|
||||
}"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="tenant in tenantsList"
|
||||
:key="tenant.id"
|
||||
:value="tenant.id"
|
||||
>
|
||||
{{ tenant.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div
|
||||
v-if="
|
||||
!isEditing &&
|
||||
form.tenantId &&
|
||||
form.tenantId !== authStore.user?.tenantId
|
||||
"
|
||||
class="tenant-warning"
|
||||
>
|
||||
<a-alert
|
||||
message="注意"
|
||||
description="选择的租户与当前租户不同,创建的用户将属于当前租户"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-else label="所属单位">
|
||||
<a-form-item label="所属单位" name="organization">
|
||||
<a-input
|
||||
:value="
|
||||
tenantsList.find((t) => t.id === authStore.user?.tenantId)
|
||||
?.name || '当前租户'
|
||||
"
|
||||
disabled
|
||||
v-model:value="form.organization"
|
||||
placeholder="请输入所属单位"
|
||||
:maxlength="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="联系方式" name="phone">
|
||||
@ -312,18 +260,15 @@ import {
|
||||
} from "@ant-design/icons-vue"
|
||||
import { useListRequest } from "@/composables/useListRequest"
|
||||
import { contestsApi } from "@/api/contests"
|
||||
import { tenantsApi, type Tenant } from "@/api/tenants"
|
||||
import {
|
||||
judgesManagementApi,
|
||||
type Judge,
|
||||
type QueryJudgeParams,
|
||||
} from "@/api/judges-management"
|
||||
import { useAuthStore } from "@/stores/auth"
|
||||
|
||||
const route = useRoute()
|
||||
const contestId = route.params.id ? Number(route.params.id) : null
|
||||
const tenantCode = route.params.tenantCode as string
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 检查 contestId 是否有效
|
||||
const isValidContestId =
|
||||
@ -352,10 +297,6 @@ const {
|
||||
errorMessage: "获取评委列表失败",
|
||||
})
|
||||
|
||||
// 租户列表
|
||||
const tenantsList = ref<Tenant[]>([])
|
||||
const tenantsLoading = ref(false)
|
||||
|
||||
// 比赛信息
|
||||
const contestName = ref("")
|
||||
|
||||
@ -379,14 +320,14 @@ const form = reactive<{
|
||||
username: string
|
||||
nickname: string
|
||||
gender: "male" | "female" | undefined
|
||||
tenantId: number | undefined
|
||||
organization: string
|
||||
phone: string
|
||||
password: string
|
||||
}>({
|
||||
username: "",
|
||||
nickname: "",
|
||||
gender: undefined,
|
||||
tenantId: undefined,
|
||||
organization: "",
|
||||
phone: "",
|
||||
password: "",
|
||||
})
|
||||
@ -398,7 +339,7 @@ const rules = computed(() => ({
|
||||
: [{ required: true, message: "请输入账号", trigger: "blur" }],
|
||||
nickname: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
gender: [{ required: true, message: "请选择性别", trigger: "change" }],
|
||||
tenantId: [{ required: true, message: "请选择所属单位", trigger: "change" }],
|
||||
organization: [{ required: true, message: "请输入所属单位", trigger: "blur" }],
|
||||
phone: [{ required: true, message: "请输入联系方式", trigger: "blur" }],
|
||||
password: isEditing.value
|
||||
? []
|
||||
@ -414,7 +355,8 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: "所属单位",
|
||||
key: "tenant",
|
||||
key: "organization",
|
||||
dataIndex: "organization",
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
@ -458,42 +400,6 @@ const columns = [
|
||||
},
|
||||
]
|
||||
|
||||
// 是否有租户读取权限
|
||||
const hasTenantReadPermission = computed(() => {
|
||||
return authStore.hasPermission("tenant:read")
|
||||
})
|
||||
|
||||
// 加载租户列表
|
||||
const loadTenants = async () => {
|
||||
// 如果没有权限,跳过加载
|
||||
if (!hasTenantReadPermission.value) {
|
||||
return
|
||||
}
|
||||
|
||||
tenantsLoading.value = true
|
||||
try {
|
||||
// 使用 pageSize 获取租户列表(最大 100)
|
||||
// 如果租户数量很多,可以考虑分页加载或使用后端提供的"获取全部"接口
|
||||
const res = await tenantsApi.getList({ page: 1, pageSize: 100 })
|
||||
tenantsList.value = res.list
|
||||
|
||||
// 如果返回的数据量等于 pageSize,可能还有更多数据
|
||||
if (res.list.length === 10000 && res.total > 10000) {
|
||||
console.warn(`租户数量较多(${res.total}),当前仅显示前10000个`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 如果是权限错误,静默处理(不显示错误消息,因为 request.ts 已经显示了)
|
||||
if (error?.response?.status === 403) {
|
||||
console.warn("缺少租户读取权限,租户筛选功能不可用")
|
||||
} else {
|
||||
console.error("加载租户列表失败", error)
|
||||
message.error("加载租户列表失败,请稍后重试")
|
||||
}
|
||||
} finally {
|
||||
tenantsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载比赛信息
|
||||
const loadContestInfo = async () => {
|
||||
// 如果 contestId 无效,跳过加载
|
||||
@ -526,15 +432,9 @@ const handleAdd = () => {
|
||||
form.username = ""
|
||||
form.nickname = ""
|
||||
form.gender = undefined
|
||||
// 默认使用当前用户的租户ID
|
||||
form.tenantId = authStore.user?.tenantId
|
||||
form.organization = ""
|
||||
form.phone = ""
|
||||
form.password = ""
|
||||
|
||||
// 如果有权限但租户列表为空,尝试加载
|
||||
if (hasTenantReadPermission.value && tenantsList.value.length === 0) {
|
||||
loadTenants()
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑
|
||||
@ -545,7 +445,7 @@ const handleEdit = (record: Judge) => {
|
||||
form.username = record.username || ""
|
||||
form.nickname = record.nickname || ""
|
||||
form.gender = record.gender as "male" | "female" | undefined
|
||||
form.tenantId = record.tenantId
|
||||
form.organization = record.organization || ""
|
||||
form.phone = record.phone || ""
|
||||
form.password = ""
|
||||
}
|
||||
@ -601,6 +501,7 @@ const handleSubmit = async () => {
|
||||
await judgesManagementApi.update(editingId.value, {
|
||||
nickname: form.nickname,
|
||||
gender: form.gender,
|
||||
organization: form.organization,
|
||||
phone: form.phone,
|
||||
...(form.password && { password: form.password }),
|
||||
})
|
||||
@ -611,7 +512,7 @@ const handleSubmit = async () => {
|
||||
username: form.username,
|
||||
nickname: form.nickname,
|
||||
gender: form.gender!,
|
||||
tenantId: form.tenantId!,
|
||||
organization: form.organization,
|
||||
phone: form.phone,
|
||||
password: form.password,
|
||||
status: "enabled",
|
||||
@ -641,7 +542,6 @@ const handleCancel = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTenants()
|
||||
loadContestInfo()
|
||||
})
|
||||
</script>
|
||||
@ -650,8 +550,4 @@ onMounted(() => {
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tenant-warning {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,645 +1,228 @@
|
||||
<template>
|
||||
<div class="review-progress-page">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item>
|
||||
<router-link :to="`/contests`">赛事管理</router-link>
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>
|
||||
<router-link :to="`/contests/${contestId}`">{{
|
||||
progressData?.contest?.contestName || "赛事详情"
|
||||
}}</router-link>
|
||||
</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>评审进度</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="fetchProgress" :loading="loading">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button @click="$router.back()">返回</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 总体进度概览 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="作品总数"
|
||||
:value="progressData?.summary?.totalWorks || 0"
|
||||
>
|
||||
<template #suffix>件</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="评委总数"
|
||||
:value="progressData?.summary?.totalJudges || 0"
|
||||
>
|
||||
<template #suffix>人</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="待分配作品"
|
||||
:value="progressData?.summary?.unassignedWorksCount || 0"
|
||||
:value-style="{
|
||||
color:
|
||||
(progressData?.summary?.unassignedWorksCount || 0) > 0
|
||||
? '#cf1322'
|
||||
: '#3f8600',
|
||||
}"
|
||||
>
|
||||
<template #suffix>件</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-statistic
|
||||
title="待评审数"
|
||||
:value="progressData?.summary?.pendingScoresCount || 0"
|
||||
:value-style="{
|
||||
color:
|
||||
(progressData?.summary?.pendingScoresCount || 0) > 0
|
||||
? '#faad14'
|
||||
: '#3f8600',
|
||||
}"
|
||||
>
|
||||
<template #suffix>条</template>
|
||||
</a-statistic>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<a-card title="评审进度" class="progress-card" size="small">
|
||||
<a-row :gutter="[24, 16]">
|
||||
<a-col :span="8">
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">作品分配进度</div>
|
||||
<a-progress
|
||||
:percent="progressData?.progress?.assignmentProgress || 0"
|
||||
:stroke-color="{ '0%': '#108ee9', '100%': '#87d068' }"
|
||||
/>
|
||||
<div class="progress-desc">
|
||||
{{ progressData?.summary?.assignedWorksCount || 0 }} /
|
||||
{{ progressData?.summary?.totalWorks || 0 }} 件已分配
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">评审完成进度</div>
|
||||
<a-progress
|
||||
:percent="progressData?.progress?.scoringProgress || 0"
|
||||
:stroke-color="{ '0%': '#108ee9', '100%': '#87d068' }"
|
||||
/>
|
||||
<div class="progress-desc">
|
||||
{{ progressData?.summary?.totalScores || 0 }} /
|
||||
{{ progressData?.summary?.totalAssignments || 0 }} 条已评分
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="progress-item">
|
||||
<div class="progress-label">总体完成进度</div>
|
||||
<a-progress
|
||||
:percent="progressData?.progress?.overallProgress || 0"
|
||||
:stroke-color="{ '0%': '#108ee9', '100%': '#87d068' }"
|
||||
/>
|
||||
<div class="progress-desc">
|
||||
{{ progressData?.summary?.scoredWorksCount || 0 }} /
|
||||
{{ progressData?.summary?.totalWorks || 0 }} 件已评审
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 评委进度表格 -->
|
||||
<a-card title="评委工作进度" class="judge-progress-card" size="small">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button
|
||||
v-permission="'review:assign'"
|
||||
type="primary"
|
||||
@click="handleAutoAssign"
|
||||
:loading="autoAssignLoading"
|
||||
>
|
||||
<template #icon><ThunderboltOutlined /></template>
|
||||
自动分配
|
||||
</a-button>
|
||||
<a-button
|
||||
v-permission="'review:assign'"
|
||||
@click="showBatchAssignModal = true"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
批量分配
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
:columns="judgeColumns"
|
||||
:data-source="progressData?.judgeProgress || []"
|
||||
:pagination="false"
|
||||
row-key="judgeId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'judgeName'">
|
||||
<a-space>
|
||||
<a-avatar size="small">{{
|
||||
(record.judgeName || "?")[0]
|
||||
}}</a-avatar>
|
||||
{{ record.judgeName || "-" }}
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'progress'">
|
||||
<a-progress
|
||||
:percent="record.progress"
|
||||
size="small"
|
||||
:status="record.progress === 100 ? 'success' : 'active'"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'pendingCount'">
|
||||
<a-tag :color="record.pendingCount > 0 ? 'orange' : 'green'">
|
||||
{{ record.pendingCount }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 待分配作品和待评审列表 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-card title="待分配作品" size="small" class="list-card">
|
||||
<template #extra>
|
||||
<a-tag color="red">{{
|
||||
progressData?.summary?.unassignedWorksCount || 0
|
||||
}}</a-tag>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="progressData?.unassignedWorks || []"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta
|
||||
:title="item.title"
|
||||
:description="`编号: ${item.workNo || '-'}`"
|
||||
>
|
||||
<template #avatar>
|
||||
<FileOutlined style="font-size: 20px; color: #52c41a" />
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleQuickAssign(item)"
|
||||
>
|
||||
分配
|
||||
</a-button>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
<template
|
||||
#header
|
||||
v-if="(progressData?.unassignedWorks?.length || 0) === 0"
|
||||
>
|
||||
<a-empty description="所有作品已分配" />
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="待评审分配" size="small" class="list-card">
|
||||
<template #extra>
|
||||
<a-tag color="orange">{{
|
||||
progressData?.summary?.pendingScoresCount || 0
|
||||
}}</a-tag>
|
||||
</template>
|
||||
<a-list
|
||||
:data-source="progressData?.pendingAssignments || []"
|
||||
size="small"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta
|
||||
:title="item.workTitle"
|
||||
:description="`评委: ${item.judgeName}`"
|
||||
>
|
||||
<template #avatar>
|
||||
<ClockCircleOutlined
|
||||
style="font-size: 20px; color: #faad14"
|
||||
/>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<span class="assign-time">{{
|
||||
formatDateTime(item.assignmentTime)
|
||||
}}</span>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
<template
|
||||
#header
|
||||
v-if="(progressData?.pendingAssignments?.length || 0) === 0"
|
||||
>
|
||||
<a-empty description="所有分配已评审完成" />
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 评审时间信息 -->
|
||||
<a-card title="评审时间" size="small" class="time-card">
|
||||
<a-descriptions :column="3">
|
||||
<a-descriptions-item label="评审开始时间">
|
||||
{{ formatDateTime(progressData?.contest?.reviewStartTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="评审结束时间">
|
||||
{{ formatDateTime(progressData?.contest?.reviewEndTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="评审状态">
|
||||
<a-tag :color="getReviewStatusColor()">{{
|
||||
getReviewStatusText()
|
||||
}}</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
<a-card class="mb-4">
|
||||
<template #title>评审进度</template>
|
||||
</a-card>
|
||||
|
||||
<!-- 批量分配弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showBatchAssignModal"
|
||||
title="批量分配作品"
|
||||
:confirm-loading="batchAssignLoading"
|
||||
width="700px"
|
||||
@ok="handleBatchAssign"
|
||||
@cancel="showBatchAssignModal = false"
|
||||
>
|
||||
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
|
||||
<a-form-item label="选择作品" required>
|
||||
<a-select
|
||||
v-model:value="batchAssignForm.workIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择要分配的作品"
|
||||
style="width: 100%"
|
||||
:max-tag-count="3"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="work in availableWorks"
|
||||
:key="work.id"
|
||||
:value="work.id"
|
||||
>
|
||||
{{ work.title }} ({{ work.workNo || "-" }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="选择评委" required>
|
||||
<a-select
|
||||
v-model:value="batchAssignForm.judgeIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择评委"
|
||||
style="width: 100%"
|
||||
:max-tag-count="3"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="judge in judges"
|
||||
:key="judge.judgeId"
|
||||
:value="judge.judgeId"
|
||||
>
|
||||
{{ judge.judge?.nickname || judge.judge?.username }}
|
||||
<span v-if="judge.specialty">({{ judge.specialty }})</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<!-- Tab栏 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="contest-tabs" @change="handleTabChange">
|
||||
<a-tab-pane key="individual" tab="个人赛" />
|
||||
<a-tab-pane key="team" tab="团队赛" />
|
||||
</a-tabs>
|
||||
|
||||
<!-- 快速分配弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showQuickAssignModal"
|
||||
title="分配作品给评委"
|
||||
:confirm-loading="quickAssignLoading"
|
||||
width="500px"
|
||||
@ok="handleQuickAssignSubmit"
|
||||
@cancel="showQuickAssignModal = false"
|
||||
<!-- 搜索表单 -->
|
||||
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||
<a-form-item label="赛事名称">
|
||||
<a-input
|
||||
v-model:value="searchParams.contestName"
|
||||
placeholder="请输入赛事名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="评审状态">
|
||||
<a-select
|
||||
v-model:value="searchParams.reviewStatus"
|
||||
placeholder="请选择评审状态"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
>
|
||||
<a-select-option value="not_started">未开始</a-select-option>
|
||||
<a-select-option value="in_progress">进行中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<a-form-item label="作品">
|
||||
<span
|
||||
>{{ quickAssignWork?.title }} ({{
|
||||
quickAssignWork?.workNo || "-"
|
||||
}})</span
|
||||
>
|
||||
</a-form-item>
|
||||
<a-form-item label="选择评委" required>
|
||||
<a-select
|
||||
v-model:value="quickAssignJudgeIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择评委"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="judge in judges"
|
||||
:key="judge.judgeId"
|
||||
:value="judge.judgeId"
|
||||
>
|
||||
{{ judge.judge?.nickname || judge.judge?.username }}
|
||||
<span v-if="judge.specialty">({{ judge.specialty }})</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewStatus'">
|
||||
<a-tag :color="getReviewStatusColor(record)">
|
||||
{{ getReviewStatusText(record) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewedCount'">
|
||||
{{ record.reviewedCount || 0 }} / {{ record.totalWorksCount || 0 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewTime'">
|
||||
<div v-if="record.reviewStartTime || record.reviewEndTime">
|
||||
<div>{{ formatDate(record.reviewStartTime) }}</div>
|
||||
<div>至 {{ formatDate(record.reviewEndTime) }}</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="handleViewDetail(record)">
|
||||
查看详情
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { ref, reactive, onMounted } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { message } from "ant-design-vue"
|
||||
import {
|
||||
ReloadOutlined,
|
||||
ThunderboltOutlined,
|
||||
PlusOutlined,
|
||||
FileOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
reviewsApi,
|
||||
judgesApi,
|
||||
worksApi,
|
||||
type ReviewProgress,
|
||||
type ContestJudge,
|
||||
type ContestWork,
|
||||
type UnassignedWork,
|
||||
} from "@/api/contests"
|
||||
import { contestsApi, type Contest } from "@/api/contests"
|
||||
|
||||
const route = useRoute()
|
||||
const contestId = Number(route.params.id)
|
||||
const router = useRouter()
|
||||
const tenantCode = route.params.tenantCode as string
|
||||
|
||||
// Tab状态
|
||||
const activeTab = ref<"individual" | "team">("individual")
|
||||
|
||||
// 列表状态
|
||||
const loading = ref(false)
|
||||
const progressData = ref<ReviewProgress | null>(null)
|
||||
const judges = ref<ContestJudge[]>([])
|
||||
const availableWorks = ref<ContestWork[]>([])
|
||||
|
||||
// 自动分配
|
||||
const autoAssignLoading = ref(false)
|
||||
|
||||
// 批量分配
|
||||
const showBatchAssignModal = ref(false)
|
||||
const batchAssignLoading = ref(false)
|
||||
const batchAssignForm = reactive({
|
||||
workIds: [] as number[],
|
||||
judgeIds: [] as number[],
|
||||
const dataSource = ref<Contest[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 快速分配
|
||||
const showQuickAssignModal = ref(false)
|
||||
const quickAssignLoading = ref(false)
|
||||
const quickAssignWork = ref<UnassignedWork | null>(null)
|
||||
const quickAssignJudgeIds = ref<number[]>([])
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
contestName: "",
|
||||
reviewStatus: undefined as string | undefined,
|
||||
})
|
||||
|
||||
// 评委表格列
|
||||
const judgeColumns = [
|
||||
{
|
||||
title: "评委",
|
||||
key: "judgeName",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "专长",
|
||||
dataIndex: "specialty",
|
||||
key: "specialty",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "权重",
|
||||
dataIndex: "weight",
|
||||
key: "weight",
|
||||
width: 80,
|
||||
customRender: ({ record }: { record: any }) => record.weight || 1,
|
||||
},
|
||||
{
|
||||
title: "已分配",
|
||||
dataIndex: "assignedCount",
|
||||
key: "assignedCount",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "已评分",
|
||||
dataIndex: "scoredCount",
|
||||
key: "scoredCount",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "待评审",
|
||||
key: "pendingCount",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "完成进度",
|
||||
key: "progress",
|
||||
width: 200,
|
||||
},
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{ title: "赛事名称", dataIndex: "contestName", key: "contestName", width: 200 },
|
||||
{ title: "评审状态", key: "reviewStatus", width: 100 },
|
||||
{ title: "已评审/作品数", key: "reviewedCount", width: 130 },
|
||||
{ title: "评审时间", key: "reviewTime", width: 180 },
|
||||
{ title: "操作", key: "action", width: 100, fixed: "right" as const },
|
||||
]
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr?: string) => {
|
||||
// 获取评审状态颜色
|
||||
const getReviewStatusColor = (record: Contest) => {
|
||||
const now = new Date()
|
||||
const start = record.reviewStartTime ? new Date(record.reviewStartTime) : null
|
||||
const end = record.reviewEndTime ? new Date(record.reviewEndTime) : null
|
||||
|
||||
if (!start || now < start) return "default"
|
||||
if (end && now > end) return "success"
|
||||
return "processing"
|
||||
}
|
||||
|
||||
// 获取评审状态文本
|
||||
const getReviewStatusText = (record: Contest) => {
|
||||
const now = new Date()
|
||||
const start = record.reviewStartTime ? new Date(record.reviewStartTime) : null
|
||||
const end = record.reviewEndTime ? new Date(record.reviewEndTime) : null
|
||||
|
||||
if (!start || now < start) return "未开始"
|
||||
if (end && now > end) return "已完成"
|
||||
return "进行中"
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-"
|
||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
|
||||
// 获取评审状态颜色
|
||||
const getReviewStatusColor = () => {
|
||||
if (!progressData.value?.contest) return "default"
|
||||
const now = new Date()
|
||||
const start = new Date(progressData.value.contest.reviewStartTime)
|
||||
const end = new Date(progressData.value.contest.reviewEndTime)
|
||||
|
||||
if (now < start) return "default"
|
||||
if (now > end) return "red"
|
||||
return "green"
|
||||
}
|
||||
|
||||
// 获取评审状态文本
|
||||
const getReviewStatusText = () => {
|
||||
if (!progressData.value?.contest) return "未知"
|
||||
const now = new Date()
|
||||
const start = new Date(progressData.value.contest.reviewStartTime)
|
||||
const end = new Date(progressData.value.contest.reviewEndTime)
|
||||
|
||||
if (now < start) return "未开始"
|
||||
if (now > end) return "已结束"
|
||||
return "进行中"
|
||||
}
|
||||
|
||||
// 加载评审进度
|
||||
const fetchProgress = async () => {
|
||||
// 获取列表数据
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
progressData.value = await reviewsApi.getReviewProgress(contestId)
|
||||
const contestType = activeTab.value === "individual" ? "individual" : "team"
|
||||
const response = await contestsApi.getList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
contestName: searchParams.contestName || undefined,
|
||||
contestType,
|
||||
})
|
||||
dataSource.value = response.list
|
||||
pagination.total = response.total
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取评审进度失败")
|
||||
message.error(error?.response?.data?.message || "获取列表失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载评委列表
|
||||
const fetchJudges = async () => {
|
||||
try {
|
||||
judges.value = await judgesApi.getList(contestId)
|
||||
} catch (error) {
|
||||
console.error("获取评委列表失败", error)
|
||||
}
|
||||
// Tab切换
|
||||
const handleTabChange = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 加载作品列表
|
||||
const fetchWorks = async () => {
|
||||
try {
|
||||
const response = await worksApi.getList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
contestId,
|
||||
})
|
||||
availableWorks.value = response.list
|
||||
} catch (error) {
|
||||
console.error("获取作品列表失败", error)
|
||||
}
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 自动分配
|
||||
const handleAutoAssign = async () => {
|
||||
autoAssignLoading.value = true
|
||||
try {
|
||||
const result = await reviewsApi.autoAssignWorks(contestId)
|
||||
message.success(result.message)
|
||||
fetchProgress()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "自动分配失败")
|
||||
} finally {
|
||||
autoAssignLoading.value = false
|
||||
}
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchParams.contestName = ""
|
||||
searchParams.reviewStatus = undefined
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 批量分配
|
||||
const handleBatchAssign = async () => {
|
||||
if (
|
||||
batchAssignForm.workIds.length === 0 ||
|
||||
batchAssignForm.judgeIds.length === 0
|
||||
) {
|
||||
message.warning("请选择作品和评委")
|
||||
return
|
||||
}
|
||||
|
||||
batchAssignLoading.value = true
|
||||
try {
|
||||
const result = await reviewsApi.batchAssignWorks(contestId, batchAssignForm)
|
||||
message.success(`成功分配 ${result.created} 条,跳过 ${result.skipped} 条`)
|
||||
showBatchAssignModal.value = false
|
||||
batchAssignForm.workIds = []
|
||||
batchAssignForm.judgeIds = []
|
||||
fetchProgress()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "批量分配失败")
|
||||
} finally {
|
||||
batchAssignLoading.value = false
|
||||
}
|
||||
// 表格分页变化
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 快速分配
|
||||
const handleQuickAssign = (work: UnassignedWork) => {
|
||||
quickAssignWork.value = work
|
||||
quickAssignJudgeIds.value = []
|
||||
showQuickAssignModal.value = true
|
||||
}
|
||||
|
||||
// 提交快速分配
|
||||
const handleQuickAssignSubmit = async () => {
|
||||
if (!quickAssignWork.value || quickAssignJudgeIds.value.length === 0) {
|
||||
message.warning("请选择评委")
|
||||
return
|
||||
}
|
||||
|
||||
quickAssignLoading.value = true
|
||||
try {
|
||||
const result = await reviewsApi.batchAssignWorks(contestId, {
|
||||
workIds: [quickAssignWork.value.id],
|
||||
judgeIds: quickAssignJudgeIds.value,
|
||||
})
|
||||
message.success(`成功分配给 ${result.created} 位评委`)
|
||||
showQuickAssignModal.value = false
|
||||
fetchProgress()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "分配失败")
|
||||
} finally {
|
||||
quickAssignLoading.value = false
|
||||
}
|
||||
// 查看详情
|
||||
const handleViewDetail = (record: Contest) => {
|
||||
router.push(`/${tenantCode}/contests/reviews/${record.id}/progress?type=${activeTab.value}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProgress()
|
||||
fetchJudges()
|
||||
fetchWorks()
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.review-progress-page {
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
.contest-tabs {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.judge-progress-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
margin-bottom: 16px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.time-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.assign-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
602
frontend/src/views/contests/reviews/ProgressDetail.vue
Normal file
602
frontend/src/views/contests/reviews/ProgressDetail.vue
Normal file
@ -0,0 +1,602 @@
|
||||
<template>
|
||||
<div class="progress-detail-page">
|
||||
<a-card class="mb-4">
|
||||
<template #title>
|
||||
<a-space>
|
||||
<a-button type="text" @click="handleBack">
|
||||
<template #icon><ArrowLeftOutlined /></template>
|
||||
</a-button>
|
||||
<span>{{ contestName }}作品评审进度</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleStartReview">
|
||||
开始评审
|
||||
</a-button>
|
||||
<a-button @click="handleEndReview">
|
||||
结束评审
|
||||
</a-button>
|
||||
<a-button @click="handleNotSubmitted">
|
||||
{{ contestType === 'team' ? '未提交作品队伍' : '未提交作品选手' }}
|
||||
</a-button>
|
||||
<a-button>
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
导出
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-card>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||
<a-form-item label="作品编号">
|
||||
<a-input
|
||||
v-model:value="searchParams.workNo"
|
||||
placeholder="请输入作品编号"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="报名账号">
|
||||
<a-input
|
||||
v-model:value="searchParams.username"
|
||||
placeholder="请输入报名账号"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="评审进度">
|
||||
<a-select
|
||||
v-model:value="searchParams.reviewProgress"
|
||||
placeholder="请选择评审进度"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
>
|
||||
<a-select-option value="not_reviewed">未评审</a-select-option>
|
||||
<a-select-option value="in_progress">评审中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'username'">
|
||||
{{ record.submitterAccountNo || record.registration?.user?.username || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'judgeScore'">
|
||||
<span v-if="record.averageScore !== undefined && record.averageScore !== null">
|
||||
{{ record.averageScore.toFixed(2) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewProgress'">
|
||||
<a-tag :color="getProgressColor(record)">
|
||||
{{ getProgressText(record) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="handleViewScores(record)">
|
||||
查看
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 评审详情抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="scoreDrawerVisible"
|
||||
title="评审详情"
|
||||
placement="right"
|
||||
width="700"
|
||||
>
|
||||
<a-table
|
||||
:columns="scoreColumns"
|
||||
:data-source="scoreList"
|
||||
:loading="scoreLoading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'judgeName'">
|
||||
{{ record.judge?.nickname || record.judge?.username || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'phone'">
|
||||
{{ record.judge?.phone || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'tenant'">
|
||||
{{ record.judge?.tenant?.name || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'score'">
|
||||
<span v-if="record.score !== undefined && record.score !== null">
|
||||
{{ record.score }}
|
||||
</span>
|
||||
<span v-else class="text-gray">未评分</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'scoreTime'">
|
||||
{{ formatDate(record.scoreTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="handleReplaceJudge(record)">
|
||||
替换评委
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 评委替换抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="replaceJudgeDrawerVisible"
|
||||
title="评委替换"
|
||||
placement="right"
|
||||
width="700"
|
||||
:footer-style="{ textAlign: 'right' }"
|
||||
>
|
||||
<!-- 搜索 -->
|
||||
<a-form layout="inline" class="mb-3">
|
||||
<a-form-item label="姓名">
|
||||
<a-input
|
||||
v-model:value="judgeSearchParams.nickname"
|
||||
placeholder="请输入姓名"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="机构信息">
|
||||
<a-input
|
||||
v-model:value="judgeSearchParams.tenantName"
|
||||
placeholder="请输入机构信息"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearchJudges">搜索</a-button>
|
||||
<a-button class="ml-2" @click="handleResetJudgeSearch">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 评委列表 -->
|
||||
<a-table
|
||||
:columns="judgeSelectColumns"
|
||||
:data-source="judgeList"
|
||||
:loading="judgeListLoading"
|
||||
:pagination="judgePagination"
|
||||
:row-selection="judgeRowSelection"
|
||||
row-key="id"
|
||||
size="small"
|
||||
@change="handleJudgeTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'judgeName'">
|
||||
{{ record.nickname || record.username || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'assignedCount'">
|
||||
{{ record.contestJudges?.length || 0 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'tenant'">
|
||||
{{ record.tenant?.name || "-" }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="replaceJudgeDrawerVisible = false">取消</a-button>
|
||||
<a-button type="primary" :loading="replaceLoading" @click="handleConfirmReplace">
|
||||
确定
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 未提交作品弹框 -->
|
||||
<a-modal
|
||||
v-model:open="notSubmittedModalVisible"
|
||||
:title="contestType === 'team' ? '未提交作品队伍' : '未提交作品选手'"
|
||||
width="700px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-table
|
||||
:columns="notSubmittedColumns"
|
||||
:data-source="notSubmittedList"
|
||||
:loading="notSubmittedLoading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'name'">
|
||||
{{ contestType === 'team' ? record.teamName : (record.user?.nickname || '-') }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'username'">
|
||||
{{ record.user?.username || "-" }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { message } from "ant-design-vue"
|
||||
import type { TableProps } from "ant-design-vue"
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
import dayjs from "dayjs"
|
||||
import { contestsApi, worksApi, reviewsApi, type ContestWork } from "@/api/contests"
|
||||
import { judgesManagementApi, type Judge } from "@/api/judges-management"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const contestId = Number(route.params.id)
|
||||
const contestType = (route.query.type as string) || "individual"
|
||||
|
||||
// 赛事名称
|
||||
const contestName = ref("")
|
||||
|
||||
// 列表状态
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<ContestWork[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
workNo: "",
|
||||
username: "",
|
||||
reviewProgress: undefined as string | undefined,
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{ title: "作品编号", dataIndex: "workNo", key: "workNo", width: 120 },
|
||||
{ title: "报名账号", key: "username", width: 150 },
|
||||
{ title: "评委评分", key: "judgeScore", width: 100 },
|
||||
{ title: "评审进度", key: "reviewProgress", width: 100 },
|
||||
{ title: "操作", key: "action", width: 80, fixed: "right" as const },
|
||||
]
|
||||
|
||||
// 评分详情抽屉
|
||||
const scoreDrawerVisible = ref(false)
|
||||
const scoreLoading = ref(false)
|
||||
const scoreList = ref<any[]>([])
|
||||
const currentWork = ref<ContestWork | null>(null)
|
||||
|
||||
// 评分详情列
|
||||
const scoreColumns = [
|
||||
{ title: "评委姓名", key: "judgeName", width: 100 },
|
||||
{ title: "联系方式", key: "phone", width: 120 },
|
||||
{ title: "机构信息", key: "tenant", width: 150 },
|
||||
{ title: "评分", key: "score", width: 80 },
|
||||
{ title: "评分时间", key: "scoreTime", width: 150 },
|
||||
{ title: "操作", key: "action", width: 100 },
|
||||
]
|
||||
|
||||
// 替换评委抽屉
|
||||
const replaceJudgeDrawerVisible = ref(false)
|
||||
const replaceLoading = ref(false)
|
||||
const currentReplaceScore = ref<any>(null)
|
||||
const judgeList = ref<Judge[]>([])
|
||||
const judgeListLoading = ref(false)
|
||||
const judgePagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
const judgeSearchParams = reactive({
|
||||
nickname: "",
|
||||
tenantName: "",
|
||||
})
|
||||
const selectedJudgeKeys = ref<number[]>([])
|
||||
const selectedJudgeRow = ref<Judge | null>(null)
|
||||
|
||||
// 评委选择列
|
||||
const judgeSelectColumns = [
|
||||
{ title: "评委姓名", key: "judgeName", width: 120 },
|
||||
{ title: "已分配赛事数", key: "assignedCount", width: 120 },
|
||||
{ title: "机构信息", key: "tenant", width: 150 },
|
||||
]
|
||||
|
||||
// 评委选择行配置(单选)
|
||||
const judgeRowSelection = computed<TableProps["rowSelection"]>(() => ({
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedJudgeKeys.value,
|
||||
onChange: (keys: any, rows: Judge[]) => {
|
||||
selectedJudgeKeys.value = keys
|
||||
selectedJudgeRow.value = rows[0] || null
|
||||
},
|
||||
}))
|
||||
|
||||
// 未提交作品弹框
|
||||
const notSubmittedModalVisible = ref(false)
|
||||
const notSubmittedLoading = ref(false)
|
||||
const notSubmittedList = ref<any[]>([])
|
||||
|
||||
// 未提交作品列
|
||||
const notSubmittedColumns = computed(() => {
|
||||
if (contestType === "team") {
|
||||
return [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{ title: "队伍名称", key: "name", width: 150 },
|
||||
{ title: "报名账号", key: "username", width: 150 },
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{ title: "姓名", key: "name", width: 120 },
|
||||
{ title: "报名账号", key: "username", width: 150 },
|
||||
]
|
||||
})
|
||||
|
||||
// 获取评审进度颜色
|
||||
const getProgressColor = (record: ContestWork) => {
|
||||
if (!record.reviewedCount || record.reviewedCount === 0) return "default"
|
||||
if (record.reviewedCount >= (record.totalJudgesCount || 1)) return "success"
|
||||
return "processing"
|
||||
}
|
||||
|
||||
// 获取评审进度文本
|
||||
const getProgressText = (record: ContestWork) => {
|
||||
if (!record.reviewedCount || record.reviewedCount === 0) return "未评审"
|
||||
if (record.reviewedCount >= (record.totalJudgesCount || 1)) return "已完成"
|
||||
return `${record.reviewedCount}/${record.totalJudgesCount || 0}`
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-"
|
||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
|
||||
// 获取赛事信息
|
||||
const fetchContestInfo = async () => {
|
||||
try {
|
||||
const contest = await contestsApi.getDetail(contestId)
|
||||
contestName.value = contest.contestName
|
||||
} catch (error) {
|
||||
console.error("获取赛事信息失败", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取作品列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await worksApi.getList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
contestId,
|
||||
workNo: searchParams.workNo || undefined,
|
||||
username: searchParams.username || undefined,
|
||||
})
|
||||
dataSource.value = response.list
|
||||
pagination.total = response.total
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取作品列表失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取评委列表(全部评委)
|
||||
const fetchJudgeList = async () => {
|
||||
judgeListLoading.value = true
|
||||
try {
|
||||
const response = await judgesManagementApi.getList({
|
||||
page: judgePagination.current,
|
||||
pageSize: judgePagination.pageSize,
|
||||
nickname: judgeSearchParams.nickname || undefined,
|
||||
})
|
||||
judgeList.value = response.list
|
||||
judgePagination.total = response.total
|
||||
} catch (error) {
|
||||
message.error("获取评委列表失败")
|
||||
} finally {
|
||||
judgeListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchParams.workNo = ""
|
||||
searchParams.username = ""
|
||||
searchParams.reviewProgress = undefined
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 表格分页变化
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 开始评审
|
||||
const handleStartReview = async () => {
|
||||
try {
|
||||
await contestsApi.update(contestId, {
|
||||
reviewStartTime: new Date().toISOString(),
|
||||
})
|
||||
message.success("评审已开始")
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "操作失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 结束评审
|
||||
const handleEndReview = async () => {
|
||||
try {
|
||||
await contestsApi.update(contestId, {
|
||||
reviewEndTime: new Date().toISOString(),
|
||||
})
|
||||
message.success("评审已结束")
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "操作失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 查看未提交作品
|
||||
const handleNotSubmitted = async () => {
|
||||
notSubmittedModalVisible.value = true
|
||||
notSubmittedLoading.value = true
|
||||
try {
|
||||
// TODO: 调用获取未提交作品的API
|
||||
notSubmittedList.value = []
|
||||
} catch (error) {
|
||||
message.error("获取未提交作品列表失败")
|
||||
} finally {
|
||||
notSubmittedLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看评分详情
|
||||
const handleViewScores = async (record: ContestWork) => {
|
||||
currentWork.value = record
|
||||
scoreDrawerVisible.value = true
|
||||
scoreLoading.value = true
|
||||
try {
|
||||
// TODO: 调用获取作品评分列表的API
|
||||
const scores = await reviewsApi.getWorkScores(record.id)
|
||||
scoreList.value = scores
|
||||
} catch (error) {
|
||||
message.error("获取评分详情失败")
|
||||
scoreList.value = []
|
||||
} finally {
|
||||
scoreLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 替换评委
|
||||
const handleReplaceJudge = (record: any) => {
|
||||
currentReplaceScore.value = record
|
||||
selectedJudgeKeys.value = []
|
||||
selectedJudgeRow.value = null
|
||||
replaceJudgeDrawerVisible.value = true
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 搜索评委
|
||||
const handleSearchJudges = () => {
|
||||
judgePagination.current = 1
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 重置评委搜索
|
||||
const handleResetJudgeSearch = () => {
|
||||
judgeSearchParams.nickname = ""
|
||||
judgeSearchParams.tenantName = ""
|
||||
judgePagination.current = 1
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 评委表格分页变化
|
||||
const handleJudgeTableChange = (pag: any) => {
|
||||
judgePagination.current = pag.current
|
||||
judgePagination.pageSize = pag.pageSize
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 确认替换评委
|
||||
const handleConfirmReplace = async () => {
|
||||
if (!selectedJudgeRow.value) {
|
||||
message.warning("请选择评委")
|
||||
return
|
||||
}
|
||||
|
||||
replaceLoading.value = true
|
||||
try {
|
||||
// TODO: 调用替换评委的API
|
||||
await reviewsApi.replaceJudge(currentReplaceScore.value.id, selectedJudgeRow.value.id)
|
||||
message.success("替换成功")
|
||||
replaceJudgeDrawerVisible.value = false
|
||||
// 刷新评分列表
|
||||
if (currentWork.value) {
|
||||
handleViewScores(currentWork.value)
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "替换失败")
|
||||
} finally {
|
||||
replaceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchContestInfo()
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.progress-detail-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
278
frontend/src/views/contests/reviews/Tasks.vue
Normal file
278
frontend/src/views/contests/reviews/Tasks.vue
Normal file
@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="review-tasks-page">
|
||||
<a-card class="mb-4">
|
||||
<template #title>我的评审任务</template>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="fetchList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</template>
|
||||
</a-card>
|
||||
|
||||
<!-- Tab栏 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="contest-tabs" @change="handleTabChange">
|
||||
<a-tab-pane key="pending" tab="待评审" />
|
||||
<a-tab-pane key="completed" tab="已评审" />
|
||||
</a-tabs>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||
<a-form-item label="赛事名称">
|
||||
<a-input
|
||||
v-model:value="searchParams.contestName"
|
||||
placeholder="请输入赛事名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'contestType'">
|
||||
<a-tag :color="record.contestType === 'individual' ? 'blue' : 'green'">
|
||||
{{ record.contestType === 'individual' ? '个人赛' : '团队赛' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewProgress'">
|
||||
<a-progress
|
||||
:percent="getReviewProgress(record)"
|
||||
:status="getProgressStatus(record)"
|
||||
size="small"
|
||||
/>
|
||||
<span class="progress-text">{{ record.reviewedCount || 0 }} / {{ record.totalWorksCount || 0 }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reviewTime'">
|
||||
<div v-if="record.reviewStartTime || record.reviewEndTime">
|
||||
<div>{{ formatDate(record.reviewStartTime) }}</div>
|
||||
<div>至 {{ formatDate(record.reviewEndTime) }}</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record)">
|
||||
{{ getStatusText(record) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:disabled="!canReview(record)"
|
||||
@click="handleStartReview(record)"
|
||||
>
|
||||
{{ activeTab === 'pending' ? '开始评审' : '查看详情' }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty v-if="!loading && dataSource.length === 0" description="暂无评审任务" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { message } from "ant-design-vue"
|
||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||
import dayjs from "dayjs"
|
||||
import { contestsApi, type Contest } from "@/api/contests"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tenantCode = route.params.tenantCode as string
|
||||
|
||||
// Tab状态
|
||||
const activeTab = ref<"pending" | "completed">("pending")
|
||||
|
||||
// 列表状态
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Contest[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
contestName: "",
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns = computed(() => [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{ title: "赛事名称", dataIndex: "contestName", key: "contestName", width: 200 },
|
||||
{ title: "赛事类型", key: "contestType", width: 100 },
|
||||
{ title: "评审进度", key: "reviewProgress", width: 180 },
|
||||
{ title: "评审时间", key: "reviewTime", width: 180 },
|
||||
{ title: "状态", key: "status", width: 100 },
|
||||
{ title: "操作", key: "action", width: 120, fixed: "right" as const },
|
||||
])
|
||||
|
||||
// 计算评审进度百分比
|
||||
const getReviewProgress = (record: Contest) => {
|
||||
if (!record.totalWorksCount || record.totalWorksCount === 0) return 0
|
||||
return Math.round(((record.reviewedCount || 0) / record.totalWorksCount) * 100)
|
||||
}
|
||||
|
||||
// 获取进度条状态
|
||||
const getProgressStatus = (record: Contest) => {
|
||||
const progress = getReviewProgress(record)
|
||||
if (progress === 100) return "success"
|
||||
if (progress > 0) return "active"
|
||||
return "normal"
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (record: Contest) => {
|
||||
const now = new Date()
|
||||
const start = record.reviewStartTime ? new Date(record.reviewStartTime) : null
|
||||
const end = record.reviewEndTime ? new Date(record.reviewEndTime) : null
|
||||
|
||||
if (!start || now < start) return "default"
|
||||
if (end && now > end) return "success"
|
||||
return "processing"
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (record: Contest) => {
|
||||
const now = new Date()
|
||||
const start = record.reviewStartTime ? new Date(record.reviewStartTime) : null
|
||||
const end = record.reviewEndTime ? new Date(record.reviewEndTime) : null
|
||||
|
||||
if (!start || now < start) return "未开始"
|
||||
if (end && now > end) return "已结束"
|
||||
return "进行中"
|
||||
}
|
||||
|
||||
// 判断是否可以评审
|
||||
const canReview = (record: Contest) => {
|
||||
const now = new Date()
|
||||
const start = record.reviewStartTime ? new Date(record.reviewStartTime) : null
|
||||
const end = record.reviewEndTime ? new Date(record.reviewEndTime) : null
|
||||
|
||||
if (!start) return false
|
||||
if (now < start) return false
|
||||
if (end && now > end) return false
|
||||
return true
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-"
|
||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
|
||||
// 获取列表数据
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 这里需要调用专门的评委任务API
|
||||
// 暂时使用赛事列表API,后续需要替换为评委专属API
|
||||
const response = await contestsApi.getList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
contestName: searchParams.contestName || undefined,
|
||||
})
|
||||
dataSource.value = response.list
|
||||
pagination.total = response.total
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取评审任务失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
const handleTabChange = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchParams.contestName = ""
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 表格分页变化
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 开始评审/查看详情
|
||||
const handleStartReview = (record: Contest) => {
|
||||
// 跳转到作品评审详情页面
|
||||
router.push(`/${tenantCode}/contests/works/${record.id}/list`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.review-tasks-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contest-tabs {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -1,811 +1,202 @@
|
||||
<template>
|
||||
<div class="works-page">
|
||||
<a-card>
|
||||
<template #title>作品管理</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button
|
||||
v-permission="'contest:read'"
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
>
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
导出作品
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form
|
||||
:model="searchParams"
|
||||
layout="inline"
|
||||
class="search-form"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="比赛">
|
||||
<a-select
|
||||
v-model:value="searchParams.contestId"
|
||||
placeholder="请选择比赛"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="contest in contests"
|
||||
:key="contest.id"
|
||||
:value="contest.id"
|
||||
>
|
||||
{{ contest.contestName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="作品状态">
|
||||
<a-select
|
||||
v-model:value="searchParams.status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="submitted">已提交</a-select-option>
|
||||
<a-select-option value="locked">已锁定</a-select-option>
|
||||
<a-select-option value="reviewing">评审中</a-select-option>
|
||||
<a-select-option value="accepted">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="作品标题">
|
||||
<a-input
|
||||
v-model:value="searchParams.title"
|
||||
placeholder="请输入作品标题"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'contestName'">
|
||||
<a @click="handleViewContest(record.contestId)">{{
|
||||
record.contest?.contestName || "-"
|
||||
}}</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'title'">
|
||||
<a @click="handleViewDetail(record)">{{ record.title }}</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'submitter'">
|
||||
{{
|
||||
record.registration?.user?.nickname ||
|
||||
record.submitterAccountNo ||
|
||||
"-"
|
||||
}}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'version'">
|
||||
<a-tag v-if="record.isLatest" color="success"
|
||||
>v{{ record.version }} (最新)</a-tag
|
||||
>
|
||||
<a-tag v-else>v{{ record.version }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'scoreInfo'">
|
||||
<template v-if="record._count?.scores > 0">
|
||||
<a-tag color="blue">{{ record._count.scores }}人已评</a-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleViewDetail(record)"
|
||||
>
|
||||
查看
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.registrationId"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleViewVersions(record.registrationId)"
|
||||
>
|
||||
<HistoryOutlined /> 版本
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'submitted'"
|
||||
v-permission="'contest:update'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleLockWork(record.id)"
|
||||
>
|
||||
锁定
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-card class="mb-4">
|
||||
<template #title>参赛作品</template>
|
||||
</a-card>
|
||||
|
||||
<!-- 作品详情抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="detailDrawerVisible"
|
||||
title="作品详情"
|
||||
width="700"
|
||||
placement="right"
|
||||
<!-- Tab栏 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="contest-tabs" @change="handleTabChange">
|
||||
<a-tab-pane key="individual" tab="个人赛" />
|
||||
<a-tab-pane key="team" tab="团队赛" />
|
||||
</a-tabs>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||
<a-form-item label="赛事名称">
|
||||
<a-input
|
||||
v-model:value="searchParams.contestName"
|
||||
placeholder="请输入赛事名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template v-if="currentWork">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="作品编号" :span="2">
|
||||
{{ currentWork.workNo || "-" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="作品标题" :span="2">
|
||||
{{ currentWork.title }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属比赛" :span="2">
|
||||
{{ currentWork.contest?.contestName || "-" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交人">
|
||||
{{
|
||||
currentWork.registration?.user?.nickname ||
|
||||
currentWork.submitterAccountNo ||
|
||||
"-"
|
||||
}}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交来源">
|
||||
{{ currentWork.submitSource || "-" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="作品状态">
|
||||
<a-tag :color="getStatusColor(currentWork.status)">
|
||||
{{ getStatusText(currentWork.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="版本">
|
||||
<a-tag v-if="currentWork.isLatest" color="success"
|
||||
>v{{ currentWork.version }} (最新)</a-tag
|
||||
>
|
||||
<a-tag v-else>v{{ currentWork.version }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交时间" :span="2">
|
||||
{{ formatDateTime(currentWork.submitTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
v-if="currentWork.description"
|
||||
label="作品描述"
|
||||
:span="2"
|
||||
>
|
||||
<div v-html="currentWork.description"></div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
v-if="currentWork.previewUrl"
|
||||
label="预览链接"
|
||||
:span="2"
|
||||
>
|
||||
<a :href="currentWork.previewUrl" target="_blank">{{
|
||||
currentWork.previewUrl
|
||||
}}</a>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 附件列表 -->
|
||||
<a-divider orientation="left">附件列表</a-divider>
|
||||
<a-table
|
||||
:columns="attachmentColumns"
|
||||
:data-source="currentWork.attachments || []"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'fileName'">
|
||||
<a :href="record.fileUrl" target="_blank">
|
||||
<FileOutlined /> {{ record.fileName }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'size'">
|
||||
{{ formatFileSize(record.size) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 评分信息 -->
|
||||
<a-divider orientation="left">评分信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-statistic
|
||||
title="评分人数"
|
||||
:value="currentWork._count?.scores || 0"
|
||||
suffix="人"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-statistic
|
||||
title="分配评委"
|
||||
:value="currentWork._count?.assignments || 0"
|
||||
suffix="人"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-button type="primary" @click="handleViewScores"
|
||||
>查看评分详情</a-button
|
||||
>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- AI元数据 -->
|
||||
<template v-if="currentWork.aiModelMeta">
|
||||
<a-divider orientation="left">AI模型信息</a-divider>
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="模型名称">
|
||||
{{ currentWork.aiModelMeta.modelName || "-" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="模型版本">
|
||||
{{ currentWork.aiModelMeta.modelVersion || "-" }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
v-if="currentWork.aiModelMeta.parameters"
|
||||
label="参数"
|
||||
:span="2"
|
||||
>
|
||||
<pre style="margin: 0; white-space: pre-wrap">{{
|
||||
JSON.stringify(currentWork.aiModelMeta.parameters, null, 2)
|
||||
}}</pre>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'registrationCount'">
|
||||
{{ activeTab === 'team' ? (record._count?.teams || 0) : (record._count?.registrations || 0) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'worksCount'">
|
||||
{{ record._count?.works || 0 }} / {{ activeTab === 'team' ? (record._count?.teams || 0) : (record._count?.registrations || 0) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'contestTime'">
|
||||
<div v-if="record.startTime || record.endTime">
|
||||
<div>{{ formatDate(record.startTime) }}</div>
|
||||
<div>至 {{ formatDate(record.endTime) }}</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="handleViewDetail(record)">
|
||||
查看详情
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 版本历史弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="versionModalVisible"
|
||||
title="版本历史"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<a-spin :spinning="versionLoading">
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="version in versionHistory"
|
||||
:key="version.id"
|
||||
:color="version.isLatest ? 'green' : 'gray'"
|
||||
>
|
||||
<template #dot v-if="version.isLatest">
|
||||
<CheckCircleOutlined style="font-size: 16px; color: #52c41a" />
|
||||
</template>
|
||||
<a-card
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:bodyStyle="{ padding: '12px' }"
|
||||
>
|
||||
<a-row :gutter="16" align="middle">
|
||||
<a-col :span="4">
|
||||
<a-tag :color="version.isLatest ? 'success' : 'default'">
|
||||
v{{ version.version
|
||||
}}{{ version.isLatest ? " (最新)" : "" }}
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<span style="color: #666">{{ version.title }}</span>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-tag :color="getStatusColor(version.status)">
|
||||
{{ getStatusText(version.status) }}
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
{{ formatDateTime(version.submitTime) }}
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleViewVersionDetail(version)"
|
||||
>
|
||||
查看
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-empty
|
||||
v-if="versionHistory.length === 0 && !versionLoading"
|
||||
description="暂无版本记录"
|
||||
/>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
|
||||
<!-- 评分详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="scoresModalVisible"
|
||||
title="评分详情"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<a-spin :spinning="scoresLoading">
|
||||
<a-table
|
||||
:columns="scoreColumns"
|
||||
:data-source="workScores"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'judge'">
|
||||
{{ record.judge?.nickname || record.judgeName || "-" }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'totalScore'">
|
||||
<a-tag color="blue" style="font-size: 14px">{{
|
||||
record.totalScore
|
||||
}}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dimensionScores'">
|
||||
<template v-if="record.dimensionScores">
|
||||
<a-space wrap>
|
||||
<span
|
||||
v-for="(score, name) in parseDimensionScores(
|
||||
record.dimensionScores
|
||||
)"
|
||||
:key="name"
|
||||
>
|
||||
{{ name }}: {{ score }}
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty
|
||||
v-if="workScores.length === 0 && !scoresLoading"
|
||||
description="暂无评分记录"
|
||||
/>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { ref, reactive, computed, onMounted } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { message } from "ant-design-vue"
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
DownloadOutlined,
|
||||
HistoryOutlined,
|
||||
FileOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
import { useListRequest } from "@/composables/useListRequest"
|
||||
import {
|
||||
worksApi,
|
||||
contestsApi,
|
||||
reviewsApi,
|
||||
type ContestWork,
|
||||
type QueryWorkParams,
|
||||
type ContestWorkScore,
|
||||
} from "@/api/contests"
|
||||
import type { Contest } from "@/api/contests"
|
||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||
import dayjs from "dayjs"
|
||||
import { contestsApi, type Contest } from "@/api/contests"
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tenantCode = route.params.tenantCode as string
|
||||
|
||||
// 从URL获取contestId参数
|
||||
const urlContestId = route.params.id ? Number(route.params.id) : undefined
|
||||
// Tab状态
|
||||
const activeTab = ref<"individual" | "team">("individual")
|
||||
|
||||
// 使用列表请求组合函数
|
||||
const {
|
||||
loading,
|
||||
dataSource,
|
||||
pagination,
|
||||
searchParams,
|
||||
fetchList,
|
||||
resetSearch,
|
||||
search,
|
||||
handleTableChange,
|
||||
} = useListRequest<ContestWork, QueryWorkParams>({
|
||||
requestFn: worksApi.getList,
|
||||
defaultSearchParams: { contestId: urlContestId } as QueryWorkParams,
|
||||
defaultPageSize: 10,
|
||||
errorMessage: "获取作品列表失败",
|
||||
// 列表状态
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Contest[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
const contests = ref<Contest[]>([])
|
||||
const exportLoading = ref(false)
|
||||
|
||||
// 详情抽屉
|
||||
const detailDrawerVisible = ref(false)
|
||||
const currentWork = ref<ContestWork | null>(null)
|
||||
|
||||
// 版本历史
|
||||
const versionModalVisible = ref(false)
|
||||
const versionLoading = ref(false)
|
||||
const versionHistory = ref<ContestWork[]>([])
|
||||
|
||||
// 评分详情
|
||||
const scoresModalVisible = ref(false)
|
||||
const scoresLoading = ref(false)
|
||||
const workScores = ref<ContestWorkScore[]>([])
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
contestName: "",
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
const columns = computed(() => [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{ title: "赛事名称", dataIndex: "contestName", key: "contestName", width: 200 },
|
||||
{
|
||||
title: "比赛名称",
|
||||
key: "contestName",
|
||||
dataIndex: ["contest", "contestName"],
|
||||
width: 180,
|
||||
title: activeTab.value === "team" ? "报名队伍数" : "报名人数",
|
||||
key: "registrationCount",
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: "作品编号",
|
||||
key: "workNo",
|
||||
dataIndex: "workNo",
|
||||
width: 120,
|
||||
title: "已递交/应递交作品数",
|
||||
key: "worksCount",
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: "作品标题",
|
||||
key: "title",
|
||||
dataIndex: "title",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "提交人",
|
||||
key: "submitter",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "版本",
|
||||
key: "version",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
dataIndex: "status",
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: "评分",
|
||||
key: "scoreInfo",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "提交时间",
|
||||
key: "submitTime",
|
||||
dataIndex: "submitTime",
|
||||
width: 150,
|
||||
customRender: ({ record }: { record: ContestWork }) => {
|
||||
return dayjs(record.submitTime).format("YYYY-MM-DD HH:mm")
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 160,
|
||||
fixed: "right" as const,
|
||||
},
|
||||
]
|
||||
{ title: "赛事时间", key: "contestTime", width: 180 },
|
||||
{ title: "操作", key: "action", width: 100, fixed: "right" as const },
|
||||
])
|
||||
|
||||
// 附件表格列
|
||||
const attachmentColumns = [
|
||||
{
|
||||
title: "文件名",
|
||||
key: "fileName",
|
||||
dataIndex: "fileName",
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
key: "fileType",
|
||||
dataIndex: "fileType",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "大小",
|
||||
key: "size",
|
||||
dataIndex: "size",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
key: "createTime",
|
||||
dataIndex: "createTime",
|
||||
width: 160,
|
||||
customRender: ({ record }: any) => {
|
||||
return record.createTime
|
||||
? dayjs(record.createTime).format("YYYY-MM-DD HH:mm")
|
||||
: "-"
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 评分表格列
|
||||
const scoreColumns = [
|
||||
{
|
||||
title: "评委",
|
||||
key: "judge",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: "总分",
|
||||
key: "totalScore",
|
||||
dataIndex: "totalScore",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "维度评分",
|
||||
key: "dimensionScores",
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: "评语",
|
||||
key: "comments",
|
||||
dataIndex: "comments",
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: "评分时间",
|
||||
key: "scoreTime",
|
||||
dataIndex: "scoreTime",
|
||||
width: 160,
|
||||
customRender: ({ record }: { record: ContestWorkScore }) => {
|
||||
return record.scoreTime
|
||||
? dayjs(record.scoreTime).format("YYYY-MM-DD HH:mm")
|
||||
: "-"
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr?: string) => {
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-"
|
||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size?: string | number) => {
|
||||
if (!size) return "-"
|
||||
const bytes = typeof size === "string" ? parseInt(size) : size
|
||||
if (isNaN(bytes)) return size
|
||||
if (bytes < 1024) return bytes + " B"
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
||||
}
|
||||
|
||||
// 解析维度评分
|
||||
const parseDimensionScores = (dimensionScores: any) => {
|
||||
if (!dimensionScores) return {}
|
||||
// 获取列表数据
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
return typeof dimensionScores === "string"
|
||||
? JSON.parse(dimensionScores)
|
||||
: dimensionScores
|
||||
} catch {
|
||||
return {}
|
||||
const contestType = activeTab.value === "individual" ? "individual" : "team"
|
||||
const response = await contestsApi.getList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
contestName: searchParams.contestName || undefined,
|
||||
contestType,
|
||||
})
|
||||
dataSource.value = response.list
|
||||
pagination.total = response.total
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取列表失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
switch (status) {
|
||||
case "accepted":
|
||||
return "success"
|
||||
case "rejected":
|
||||
return "error"
|
||||
case "reviewing":
|
||||
return "processing"
|
||||
case "locked":
|
||||
return "warning"
|
||||
default:
|
||||
return "default"
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
switch (status) {
|
||||
case "accepted":
|
||||
return "已通过"
|
||||
case "rejected":
|
||||
return "已拒绝"
|
||||
case "reviewing":
|
||||
return "评审中"
|
||||
case "locked":
|
||||
return "已锁定"
|
||||
default:
|
||||
return "已提交"
|
||||
}
|
||||
}
|
||||
|
||||
// 加载比赛列表
|
||||
const fetchContests = async () => {
|
||||
try {
|
||||
const response = await contestsApi.getList({ page: 1, pageSize: 100 })
|
||||
contests.value = response.list
|
||||
} catch (error) {
|
||||
console.error("获取比赛列表失败", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索选项过滤
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
// Tab切换
|
||||
const handleTabChange = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
search()
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
resetSearch()
|
||||
searchParams.contestName = ""
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 表格分页变化
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = async (record: ContestWork) => {
|
||||
try {
|
||||
const detail = await worksApi.getDetail(record.id)
|
||||
currentWork.value = detail
|
||||
detailDrawerVisible.value = true
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取作品详情失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 查看版本历史
|
||||
const handleViewVersions = async (registrationId: number) => {
|
||||
versionModalVisible.value = true
|
||||
versionLoading.value = true
|
||||
|
||||
try {
|
||||
const versions = await worksApi.getVersions(registrationId)
|
||||
versionHistory.value = versions.sort((a, b) => b.version - a.version)
|
||||
} catch (error: any) {
|
||||
message.error("获取版本历史失败")
|
||||
versionHistory.value = []
|
||||
} finally {
|
||||
versionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看版本详情
|
||||
const handleViewVersionDetail = async (version: ContestWork) => {
|
||||
versionModalVisible.value = false
|
||||
try {
|
||||
const detail = await worksApi.getDetail(version.id)
|
||||
currentWork.value = detail
|
||||
detailDrawerVisible.value = true
|
||||
} catch (error: any) {
|
||||
message.error("获取版本详情失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 查看评分详情
|
||||
const handleViewScores = async () => {
|
||||
if (!currentWork.value) return
|
||||
|
||||
scoresModalVisible.value = true
|
||||
scoresLoading.value = true
|
||||
|
||||
try {
|
||||
const scores = await reviewsApi.getWorkScores(currentWork.value.id)
|
||||
workScores.value = scores
|
||||
} catch (error: any) {
|
||||
message.error("获取评分详情失败")
|
||||
workScores.value = []
|
||||
} finally {
|
||||
scoresLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看比赛
|
||||
const handleViewContest = (contestId: number) => {
|
||||
router.push(`/${tenantCode}/contests/${contestId}`)
|
||||
}
|
||||
|
||||
// 锁定作品
|
||||
const handleLockWork = async (workId: number) => {
|
||||
// TODO: 实现锁定作品功能(需要后端接口)
|
||||
message.info("锁定作品功能待实现")
|
||||
}
|
||||
|
||||
// 导出作品
|
||||
const handleExport = async () => {
|
||||
if (!searchParams.contestId) {
|
||||
message.warning("请先选择一个比赛")
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const response = await worksApi.getList({
|
||||
contestId: searchParams.contestId,
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
})
|
||||
|
||||
if (response.list.length === 0) {
|
||||
message.warning("暂无作品数据")
|
||||
return
|
||||
}
|
||||
|
||||
const headers = [
|
||||
"作品ID",
|
||||
"作品编号",
|
||||
"作品标题",
|
||||
"提交人",
|
||||
"版本",
|
||||
"状态",
|
||||
"提交时间",
|
||||
"评分人数",
|
||||
]
|
||||
const rows = response.list.map((item) => [
|
||||
item.id,
|
||||
item.workNo || "",
|
||||
item.title,
|
||||
item.registration?.user?.nickname || item.submitterAccountNo || "",
|
||||
`v${item.version}${item.isLatest ? "(最新)" : ""}`,
|
||||
getStatusText(item.status),
|
||||
formatDateTime(item.submitTime),
|
||||
item._count?.scores || 0,
|
||||
])
|
||||
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
...rows.map((row) => row.map((cell) => `"${cell}"`).join(",")),
|
||||
].join("\n")
|
||||
|
||||
const blob = new Blob(["\ufeff" + csvContent], {
|
||||
type: "text/csv;charset=utf-8",
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement("a")
|
||||
link.href = url
|
||||
link.download = `作品列表_${dayjs().format("YYYYMMDD_HHmmss")}.csv`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
message.success("导出成功")
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "导出失败")
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
const handleViewDetail = (record: Contest) => {
|
||||
router.push(`/${tenantCode}/contests/works/${record.id}/list?type=${activeTab.value}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchContests()
|
||||
if (urlContestId) {
|
||||
searchParams.contestId = urlContestId
|
||||
}
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.works-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contest-tabs {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
632
frontend/src/views/contests/works/WorksDetail.vue
Normal file
632
frontend/src/views/contests/works/WorksDetail.vue
Normal file
@ -0,0 +1,632 @@
|
||||
<template>
|
||||
<div class="works-detail-page">
|
||||
<a-card class="mb-4">
|
||||
<template #title>
|
||||
<a-space>
|
||||
<a-button type="text" @click="handleBack">
|
||||
<template #icon><ArrowLeftOutlined /></template>
|
||||
</a-button>
|
||||
<span>{{ contestName }}参赛作品</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
@click="handleBatchAssign"
|
||||
>
|
||||
分配评委
|
||||
</a-button>
|
||||
</template>
|
||||
</a-card>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||
<a-form-item :label="contestType === 'team' ? '队伍名称' : '选手名称'">
|
||||
<a-input
|
||||
v-model:value="searchParams.name"
|
||||
:placeholder="contestType === 'team' ? '请输入队伍名称' : '请输入选手名称'"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="报名账号">
|
||||
<a-input
|
||||
v-model:value="searchParams.username"
|
||||
placeholder="请输入报名账号"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="作品编号">
|
||||
<a-input
|
||||
v-model:value="searchParams.workNo"
|
||||
placeholder="请输入作品编号"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="分配状态">
|
||||
<a-select
|
||||
v-model:value="searchParams.assignStatus"
|
||||
placeholder="请选择"
|
||||
allow-clear
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="assigned">已分配</a-select-option>
|
||||
<a-select-option value="unassigned">未分配</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="递交时间">
|
||||
<a-range-picker
|
||||
v-model:value="searchParams.submitTimeRange"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="机构">
|
||||
<a-select
|
||||
v-model:value="searchParams.tenantId"
|
||||
placeholder="请选择机构"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="tenant in tenants"
|
||||
:key="tenant.id"
|
||||
:value="tenant.id"
|
||||
>
|
||||
{{ tenant.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'index'">
|
||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'workNo'">
|
||||
<a @click="handleViewWork(record)">{{ record.workNo || '-' }}</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'username'">
|
||||
{{ record.submitterAccountNo || record.registration?.user?.username || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'name'">
|
||||
<template v-if="contestType === 'team'">
|
||||
{{ record.registration?.team?.teamName || '-' }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ record.registration?.user?.nickname || '-' }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'submitTime'">
|
||||
{{ formatDate(record.submitTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'assignStatus'">
|
||||
<a-tag v-if="record._count?.assignments > 0" color="success">已分配</a-tag>
|
||||
<a-tag v-else color="default">未分配</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'judges'">
|
||||
<template v-if="record.assignments && record.assignments.length > 0">
|
||||
<a-space wrap>
|
||||
<a-tag v-for="assignment in record.assignments" :key="assignment.id">
|
||||
{{ assignment.judge?.nickname || assignment.judge?.username || '-' }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" @click="handleAssignJudge(record)">
|
||||
分配评委
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 作品详情弹框 -->
|
||||
<a-modal
|
||||
v-model:open="workModalVisible"
|
||||
title="作品详情"
|
||||
width="700px"
|
||||
:footer="null"
|
||||
>
|
||||
<template v-if="currentWork">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="作品编号" :span="2">
|
||||
{{ currentWork.workNo || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="作品标题" :span="2">
|
||||
{{ currentWork.title }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="所属比赛" :span="2">
|
||||
{{ currentWork.contest?.contestName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交人">
|
||||
{{ currentWork.registration?.user?.nickname || currentWork.submitterAccountNo || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="报名账号">
|
||||
{{ currentWork.registration?.user?.username || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交时间" :span="2">
|
||||
{{ formatDate(currentWork.submitTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="currentWork.description" label="作品描述" :span="2">
|
||||
<div v-html="currentWork.description"></div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="currentWork.previewUrl" label="预览链接" :span="2">
|
||||
<a :href="currentWork.previewUrl" target="_blank">{{ currentWork.previewUrl }}</a>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 附件列表 -->
|
||||
<a-divider orientation="left">附件列表</a-divider>
|
||||
<a-table
|
||||
:columns="attachmentColumns"
|
||||
:data-source="currentWork.attachments || []"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'fileName'">
|
||||
<a :href="record.fileUrl" target="_blank">
|
||||
<FileOutlined /> {{ record.fileName }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'size'">
|
||||
{{ formatFileSize(record.size) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- 分配评委弹框 -->
|
||||
<a-modal
|
||||
v-model:open="assignModalVisible"
|
||||
title="分配评委"
|
||||
width="800px"
|
||||
:confirm-loading="assignLoading"
|
||||
@ok="handleConfirmAssign"
|
||||
>
|
||||
<!-- 搜索 -->
|
||||
<a-form layout="inline" class="mb-3">
|
||||
<a-form-item label="评委姓名">
|
||||
<a-input
|
||||
v-model:value="judgeSearchParams.nickname"
|
||||
placeholder="请输入姓名"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="机构">
|
||||
<a-input
|
||||
v-model:value="judgeSearchParams.tenantName"
|
||||
placeholder="请输入机构"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearchJudges">搜索</a-button>
|
||||
<a-button class="ml-2" @click="handleResetJudgeSearch">重置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 评委列表 -->
|
||||
<a-table
|
||||
:columns="judgeColumns"
|
||||
:data-source="judgeList"
|
||||
:loading="judgeListLoading"
|
||||
:pagination="judgePagination"
|
||||
:row-selection="judgeRowSelection"
|
||||
row-key="id"
|
||||
size="small"
|
||||
@change="handleJudgeTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'judgeName'">
|
||||
{{ record.judge?.nickname || record.judge?.username || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'phone'">
|
||||
{{ record.judge?.phone || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'tenant'">
|
||||
{{ record.judge?.tenant?.name || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'assignedCount'">
|
||||
{{ record._count?.assignedContestWorks || 0 }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { message } from "ant-design-vue"
|
||||
import type { TableProps } from "ant-design-vue"
|
||||
import type { Dayjs } from "dayjs"
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
FileOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
contestsApi,
|
||||
worksApi,
|
||||
reviewsApi,
|
||||
judgesApi,
|
||||
type ContestWork,
|
||||
type ContestJudge,
|
||||
} from "@/api/contests"
|
||||
|
||||
interface Tenant {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const contestId = Number(route.params.id)
|
||||
const contestType = (route.query.type as string) || "individual"
|
||||
|
||||
// 赛事名称
|
||||
const contestName = ref("")
|
||||
|
||||
// 租户列表
|
||||
const tenants = ref<Tenant[]>([])
|
||||
|
||||
// 列表状态
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<ContestWork[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
name: "",
|
||||
username: "",
|
||||
workNo: "",
|
||||
assignStatus: undefined as string | undefined,
|
||||
submitTimeRange: null as [Dayjs, Dayjs] | null,
|
||||
tenantId: undefined as number | undefined,
|
||||
})
|
||||
|
||||
// 选中的行
|
||||
const selectedRowKeys = ref<number[]>([])
|
||||
const selectedRows = ref<ContestWork[]>([])
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = computed<TableProps["rowSelection"]>(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys: any, rows: ContestWork[]) => {
|
||||
selectedRowKeys.value = keys
|
||||
selectedRows.value = rows
|
||||
},
|
||||
}))
|
||||
|
||||
// 表格列定义
|
||||
const columns = computed(() => [
|
||||
{ title: "序号", key: "index", width: 70 },
|
||||
{ title: "作品编号", key: "workNo", width: 150 },
|
||||
{ title: "报名账号", key: "username", width: 120 },
|
||||
{
|
||||
title: contestType === "team" ? "队伍名称" : "选手姓名",
|
||||
key: "name",
|
||||
width: 150
|
||||
},
|
||||
{ title: "递交时间", key: "submitTime", width: 160 },
|
||||
{ title: "分配状态", key: "assignStatus", width: 100 },
|
||||
{ title: "评委", key: "judges", width: 200 },
|
||||
{ title: "操作", key: "action", width: 100, fixed: "right" as const },
|
||||
])
|
||||
|
||||
// 附件表格列
|
||||
const attachmentColumns = [
|
||||
{ title: "文件名", key: "fileName", dataIndex: "fileName", width: 300 },
|
||||
{ title: "类型", key: "fileType", dataIndex: "fileType", width: 100 },
|
||||
{ title: "大小", key: "size", dataIndex: "size", width: 100 },
|
||||
]
|
||||
|
||||
// 作品详情弹框
|
||||
const workModalVisible = ref(false)
|
||||
const currentWork = ref<ContestWork | null>(null)
|
||||
|
||||
// 分配评委弹框
|
||||
const assignModalVisible = ref(false)
|
||||
const assignLoading = ref(false)
|
||||
const currentAssignWork = ref<ContestWork | null>(null)
|
||||
const isBatchAssign = ref(false)
|
||||
|
||||
// 评委列表
|
||||
const judgeList = ref<ContestJudge[]>([])
|
||||
const judgeListLoading = ref(false)
|
||||
const judgePagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
const judgeSearchParams = reactive({
|
||||
nickname: "",
|
||||
tenantName: "",
|
||||
})
|
||||
const selectedJudgeKeys = ref<number[]>([])
|
||||
const selectedJudgeRows = ref<ContestJudge[]>([])
|
||||
|
||||
// 评委行选择配置
|
||||
const judgeRowSelection = computed<TableProps["rowSelection"]>(() => ({
|
||||
selectedRowKeys: selectedJudgeKeys.value,
|
||||
onChange: (keys: any, rows: ContestJudge[]) => {
|
||||
selectedJudgeKeys.value = keys
|
||||
selectedJudgeRows.value = rows
|
||||
},
|
||||
}))
|
||||
|
||||
// 评委表格列
|
||||
const judgeColumns = [
|
||||
{ title: "评委姓名", key: "judgeName", width: 120 },
|
||||
{ title: "联系方式", key: "phone", width: 130 },
|
||||
{ title: "机构", key: "tenant", width: 150 },
|
||||
{ title: "已分配作品数", key: "assignedCount", width: 120 },
|
||||
]
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-"
|
||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size?: string | number) => {
|
||||
if (!size) return "-"
|
||||
const bytes = typeof size === "string" ? parseInt(size) : size
|
||||
if (isNaN(bytes)) return size
|
||||
if (bytes < 1024) return bytes + " B"
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
||||
}
|
||||
|
||||
// 搜索选项过滤
|
||||
const filterOption = (input: string, option: any) => {
|
||||
return option.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
|
||||
// 获取赛事信息
|
||||
const fetchContestInfo = async () => {
|
||||
try {
|
||||
const contest = await contestsApi.getDetail(contestId)
|
||||
contestName.value = contest.contestName
|
||||
} catch (error) {
|
||||
console.error("获取赛事信息失败", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取租户列表
|
||||
const fetchTenants = async () => {
|
||||
try {
|
||||
// TODO: 需要后端提供租户列表接口
|
||||
tenants.value = []
|
||||
} catch (error) {
|
||||
console.error("获取租户列表失败", error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取作品列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await worksApi.getList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
contestId,
|
||||
workNo: searchParams.workNo || undefined,
|
||||
username: searchParams.username || undefined,
|
||||
})
|
||||
dataSource.value = response.list
|
||||
pagination.total = response.total
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取作品列表失败")
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取赛事评委列表
|
||||
const fetchJudgeList = async () => {
|
||||
judgeListLoading.value = true
|
||||
try {
|
||||
const response = await judgesApi.getList(contestId)
|
||||
judgeList.value = response
|
||||
judgePagination.total = response.length
|
||||
} catch (error) {
|
||||
message.error("获取评委列表失败")
|
||||
} finally {
|
||||
judgeListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchParams.name = ""
|
||||
searchParams.username = ""
|
||||
searchParams.workNo = ""
|
||||
searchParams.assignStatus = undefined
|
||||
searchParams.submitTimeRange = null
|
||||
searchParams.tenantId = undefined
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 表格分页变化
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 查看作品详情
|
||||
const handleViewWork = async (record: ContestWork) => {
|
||||
try {
|
||||
const detail = await worksApi.getDetail(record.id)
|
||||
currentWork.value = detail
|
||||
workModalVisible.value = true
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "获取作品详情失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 单个分配评委
|
||||
const handleAssignJudge = (record: ContestWork) => {
|
||||
currentAssignWork.value = record
|
||||
isBatchAssign.value = false
|
||||
selectedJudgeKeys.value = []
|
||||
selectedJudgeRows.value = []
|
||||
assignModalVisible.value = true
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 批量分配评委
|
||||
const handleBatchAssign = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning("请先选择作品")
|
||||
return
|
||||
}
|
||||
isBatchAssign.value = true
|
||||
selectedJudgeKeys.value = []
|
||||
selectedJudgeRows.value = []
|
||||
assignModalVisible.value = true
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 搜索评委
|
||||
const handleSearchJudges = () => {
|
||||
judgePagination.current = 1
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 重置评委搜索
|
||||
const handleResetJudgeSearch = () => {
|
||||
judgeSearchParams.nickname = ""
|
||||
judgeSearchParams.tenantName = ""
|
||||
judgePagination.current = 1
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 评委表格分页变化
|
||||
const handleJudgeTableChange = (pag: any) => {
|
||||
judgePagination.current = pag.current
|
||||
judgePagination.pageSize = pag.pageSize
|
||||
fetchJudgeList()
|
||||
}
|
||||
|
||||
// 确认分配评委
|
||||
const handleConfirmAssign = async () => {
|
||||
if (selectedJudgeRows.value.length === 0) {
|
||||
message.warning("请选择评委")
|
||||
return
|
||||
}
|
||||
|
||||
assignLoading.value = true
|
||||
try {
|
||||
const judgeIds = selectedJudgeRows.value.map(j => j.judgeId)
|
||||
|
||||
if (isBatchAssign.value) {
|
||||
// 批量分配
|
||||
await reviewsApi.batchAssignWorks(contestId, {
|
||||
workIds: selectedRowKeys.value,
|
||||
judgeIds,
|
||||
})
|
||||
message.success("批量分配成功")
|
||||
selectedRowKeys.value = []
|
||||
selectedRows.value = []
|
||||
} else if (currentAssignWork.value) {
|
||||
// 单个分配
|
||||
await reviewsApi.assignWork(contestId, {
|
||||
workId: currentAssignWork.value.id,
|
||||
judgeIds,
|
||||
})
|
||||
message.success("分配成功")
|
||||
}
|
||||
|
||||
assignModalVisible.value = false
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || "分配失败")
|
||||
} finally {
|
||||
assignLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchContestInfo()
|
||||
fetchTenants()
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.works-detail-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@ -2,16 +2,48 @@
|
||||
<div class="error-page">
|
||||
<a-result status="404" title="404" sub-title="抱歉,您访问的页面不存在">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="$router.push({ path: '/' })">
|
||||
返回首页
|
||||
</a-button>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleGoHome">
|
||||
返回首页
|
||||
</a-button>
|
||||
<a-button v-if="isAuthenticated" @click="handleLogout">
|
||||
退出登录
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 404 page
|
||||
import { computed } from "vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { useAuthStore } from "@/stores/auth"
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
|
||||
const handleGoHome = () => {
|
||||
const tenantCode = authStore.user?.tenantCode || route.params.tenantCode
|
||||
if (tenantCode) {
|
||||
router.push(`/${tenantCode}`)
|
||||
} else {
|
||||
router.push("/")
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
const tenantCode = authStore.user?.tenantCode || route.params.tenantCode
|
||||
await authStore.logout()
|
||||
if (tenantCode) {
|
||||
router.push(`/${tenantCode}/login`)
|
||||
} else {
|
||||
router.push("/login")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user