修改代码
This commit is contained in:
parent
bbdeaac93a
commit
aecd72f9ee
@ -6,7 +6,7 @@
|
|||||||
"component": "workbench/Index",
|
"component": "workbench/Index",
|
||||||
"parentId": null,
|
"parentId": null,
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"permission": null
|
"permission": "workbench:read"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "学校管理",
|
"name": "学校管理",
|
||||||
@ -14,8 +14,8 @@
|
|||||||
"icon": "BankOutlined",
|
"icon": "BankOutlined",
|
||||||
"component": null,
|
"component": null,
|
||||||
"parentId": null,
|
"parentId": null,
|
||||||
"sort": 5,
|
"sort": 2,
|
||||||
"permission": null,
|
"permission": "school:read",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"name": "学校信息",
|
"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": "赛事管理",
|
"name": "赛事管理",
|
||||||
"path": "/contests",
|
"path": "/contests",
|
||||||
"icon": "TrophyOutlined",
|
"icon": "TrophyOutlined",
|
||||||
"component": null,
|
"component": null,
|
||||||
"parentId": null,
|
"parentId": null,
|
||||||
"sort": 6,
|
"sort": 4,
|
||||||
"permission": null,
|
"permission": "contest:create",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"name": "赛事活动",
|
|
||||||
"path": "/contests/activities",
|
|
||||||
"icon": "AppstoreOutlined",
|
|
||||||
"component": "contests/Activities",
|
|
||||||
"sort": 0,
|
|
||||||
"permission": "contest:read"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "赛事列表",
|
"name": "赛事列表",
|
||||||
"path": "/contests",
|
"path": "/contests",
|
||||||
"icon": "UnorderedListOutlined",
|
"icon": "UnorderedListOutlined",
|
||||||
"component": "contests/Index",
|
"component": "contests/Index",
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"permission": "contest:read"
|
"permission": "contest:create"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "评委管理",
|
||||||
|
"path": "/contests/judges",
|
||||||
|
"icon": "SolutionOutlined",
|
||||||
|
"component": "contests/judges/Index",
|
||||||
|
"sort": 2,
|
||||||
|
"permission": "judge:read"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "报名管理",
|
"name": "报名管理",
|
||||||
"path": "/contests/registrations",
|
"path": "/contests/registrations",
|
||||||
"icon": "UserAddOutlined",
|
"icon": "UserAddOutlined",
|
||||||
"component": "contests/registrations/Index",
|
"component": "contests/registrations/Index",
|
||||||
"sort": 2,
|
"sort": 3,
|
||||||
"permission": "contest:registration:read"
|
"permission": "registration:approve"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "作品管理",
|
"name": "作品管理",
|
||||||
"path": "/contests/works",
|
"path": "/contests/works",
|
||||||
"icon": "FileTextOutlined",
|
"icon": "FileTextOutlined",
|
||||||
"component": "contests/works/Index",
|
"component": "contests/works/Index",
|
||||||
"sort": 3,
|
"sort": 4,
|
||||||
"permission": "contest:work:read"
|
"permission": "work:read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "评审任务",
|
||||||
|
"path": "/contests/review-tasks",
|
||||||
|
"icon": "AuditOutlined",
|
||||||
|
"component": "contests/reviews/Tasks",
|
||||||
|
"sort": 5,
|
||||||
|
"permission": "review:read"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "评审规则",
|
"name": "评审规则",
|
||||||
"path": "/contests/reviews",
|
"path": "/contests/reviews",
|
||||||
"icon": "CheckCircleOutlined",
|
"icon": "CheckCircleOutlined",
|
||||||
"component": "contests/reviews/Index",
|
"component": "contests/reviews/Index",
|
||||||
"sort": 4,
|
"sort": 6,
|
||||||
"permission": "contest:review:read"
|
"permission": "review-rule:read"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "赛果发布",
|
"name": "赛果发布",
|
||||||
"path": "/contests/results",
|
"path": "/contests/results",
|
||||||
"icon": "TrophyOutlined",
|
"icon": "TrophyOutlined",
|
||||||
"component": "contests/results/Index",
|
"component": "contests/results/Index",
|
||||||
"sort": 5,
|
"sort": 7,
|
||||||
"permission": "contest:result:read"
|
"permission": "contest:create"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "通知管理",
|
"name": "通知管理",
|
||||||
"path": "/contests/notices",
|
"path": "/contests/notices",
|
||||||
"icon": "BellOutlined",
|
"icon": "BellOutlined",
|
||||||
"component": "contests/notices/Index",
|
"component": "contests/notices/Index",
|
||||||
"sort": 7,
|
"sort": 8,
|
||||||
"permission": "contest:notice:read"
|
"permission": "notice:create"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -140,11 +183,11 @@
|
|||||||
"icon": "FormOutlined",
|
"icon": "FormOutlined",
|
||||||
"component": null,
|
"component": null,
|
||||||
"parentId": null,
|
"parentId": null,
|
||||||
"sort": 7,
|
"sort": 5,
|
||||||
"permission": null,
|
"permission": "homework:read",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"name": "作业管理",
|
"name": "作业列表",
|
||||||
"path": "/homework",
|
"path": "/homework",
|
||||||
"icon": "FileTextOutlined",
|
"icon": "FileTextOutlined",
|
||||||
"component": "homework/Index",
|
"component": "homework/Index",
|
||||||
@ -157,7 +200,7 @@
|
|||||||
"icon": "EditOutlined",
|
"icon": "EditOutlined",
|
||||||
"component": "homework/Submissions",
|
"component": "homework/Submissions",
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"permission": "homework:read"
|
"permission": "homework-submission:read"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "评审规则",
|
"name": "评审规则",
|
||||||
@ -165,7 +208,7 @@
|
|||||||
"icon": "CheckCircleOutlined",
|
"icon": "CheckCircleOutlined",
|
||||||
"component": "homework/ReviewRules",
|
"component": "homework/ReviewRules",
|
||||||
"sort": 3,
|
"sort": 3,
|
||||||
"permission": "homework:read"
|
"permission": "homework-review-rule:read"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -176,7 +219,7 @@
|
|||||||
"component": null,
|
"component": null,
|
||||||
"parentId": null,
|
"parentId": null,
|
||||||
"sort": 10,
|
"sort": 10,
|
||||||
"permission": null,
|
"permission": "user:read",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"name": "用户管理",
|
"name": "用户管理",
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"code": "workbench:read",
|
||||||
|
"resource": "workbench",
|
||||||
|
"action": "read",
|
||||||
|
"name": "查看工作台",
|
||||||
|
"description": "允许查看工作台"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code": "user:create",
|
"code": "user:create",
|
||||||
"resource": "user",
|
"resource": "user",
|
||||||
@ -125,6 +132,517 @@
|
|||||||
"name": "删除菜单",
|
"name": "删除菜单",
|
||||||
"description": "允许删除菜单"
|
"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",
|
"code": "dict:create",
|
||||||
"resource": "dict",
|
"resource": "dict",
|
||||||
@ -196,423 +714,17 @@
|
|||||||
"description": "允许删除系统日志"
|
"description": "允许删除系统日志"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "school:create",
|
"code": "activity:read",
|
||||||
"resource": "school",
|
"resource": "activity",
|
||||||
"action": "create",
|
|
||||||
"name": "创建学校",
|
|
||||||
"description": "允许创建学校信息"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "school:read",
|
|
||||||
"resource": "school",
|
|
||||||
"action": "read",
|
"action": "read",
|
||||||
"name": "查看学校",
|
"name": "查看赛事活动",
|
||||||
"description": "允许查看学校信息"
|
"description": "允许查看已发布的赛事活动"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "school:update",
|
"code": "activity:guidance",
|
||||||
"resource": "school",
|
"resource": "activity",
|
||||||
"action": "update",
|
"action": "guidance",
|
||||||
"name": "更新学校",
|
"name": "指导学生",
|
||||||
"description": "允许更新学校信息"
|
"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": "允许发布公告"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -31,12 +31,17 @@
|
|||||||
"init:admin:permissions": "ts-node scripts/init-admin-permissions.ts",
|
"init:admin:permissions": "ts-node scripts/init-admin-permissions.ts",
|
||||||
"init:menus": "ts-node scripts/init-menus.ts",
|
"init:menus": "ts-node scripts/init-menus.ts",
|
||||||
"init:super-tenant": "ts-node scripts/init-super-tenant.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": "ts-node scripts/init-tenant-admin.ts",
|
||||||
"init:tenant-admin:permissions": "ts-node scripts/init-tenant-admin.ts --permissions-only",
|
"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-permissions": "ts-node scripts/init-tenant-permissions.ts",
|
||||||
"init:tenant-menu-permissions": "ts-node scripts/init-tenant-menu-permissions.ts",
|
"init:tenant-menu-permissions": "ts-node scripts/init-tenant-menu-permissions.ts",
|
||||||
"update:password": "ts-node scripts/update-password.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": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.3.3",
|
"@nestjs/common": "^10.3.3",
|
||||||
|
|||||||
@ -66,6 +66,7 @@ model User {
|
|||||||
phone String? /// 联系方式/手机号
|
phone String? /// 联系方式/手机号
|
||||||
gender String? /// 性别:male-男,female-女
|
gender String? /// 性别:male-男,female-女
|
||||||
avatar String? /// 头像URL
|
avatar String? /// 头像URL
|
||||||
|
organization String? /// 所属单位(用于评委等独立用户)
|
||||||
status String @default("enabled") /// 账号状态:enabled-启用,disabled-停用
|
status String @default("enabled") /// 账号状态:enabled-启用,disabled-停用
|
||||||
validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效
|
validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效
|
||||||
creator Int? @map("creator") /// 创建人ID
|
creator Int? @map("creator") /// 创建人ID
|
||||||
|
|||||||
@ -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 menus = JSON.parse(fs.readFileSync(menusFilePath, 'utf-8'));
|
||||||
|
|
||||||
|
// 超级租户可见的菜单名称
|
||||||
|
const SUPER_TENANT_MENUS = ['工作台', '赛事活动', '赛事管理', '系统管理'];
|
||||||
|
|
||||||
async function initMenus() {
|
async function initMenus() {
|
||||||
try {
|
try {
|
||||||
console.log('🚀 开始初始化菜单数据...\n');
|
console.log('🚀 开始初始化菜单数据...\n');
|
||||||
@ -105,8 +108,10 @@ async function initMenus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 清空现有菜单(重新初始化)
|
// 清空现有菜单(重新初始化)
|
||||||
console.log('🗑️ 清空现有菜单...');
|
console.log('🗑️ 清空现有菜单和租户菜单关联...');
|
||||||
// 先删除所有子菜单,再删除父菜单(避免外键约束问题)
|
// 先删除租户菜单关联
|
||||||
|
await prisma.tenantMenu.deleteMany({});
|
||||||
|
// 再删除所有子菜单,再删除父菜单(避免外键约束问题)
|
||||||
await prisma.menu.deleteMany({
|
await prisma.menu.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
parentId: {
|
parentId: {
|
||||||
@ -163,7 +168,7 @@ async function initMenus() {
|
|||||||
printMenuTree(menu);
|
printMenuTree(menu);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 为所有现有租户分配新菜单
|
// 为所有现有租户分配菜单(区分超级租户和普通租户)
|
||||||
console.log(`\n📋 为所有租户分配菜单...`);
|
console.log(`\n📋 为所有租户分配菜单...`);
|
||||||
const allTenants = await prisma.tenant.findMany({
|
const allTenants = await prisma.tenant.findMany({
|
||||||
where: { validState: 1 },
|
where: { validState: 1 },
|
||||||
@ -174,21 +179,33 @@ async function initMenus() {
|
|||||||
} else {
|
} else {
|
||||||
console.log(` 找到 ${allTenants.length} 个租户\n`);
|
console.log(` 找到 ${allTenants.length} 个租户\n`);
|
||||||
|
|
||||||
for (const tenant of allTenants) {
|
// 获取超级租户专属菜单ID(工作台、赛事管理、系统管理及其子菜单)
|
||||||
// 获取租户已分配的菜单
|
const superTenantMenuIds = new Set<number>();
|
||||||
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) {
|
for (const menu of allMenus) {
|
||||||
if (!existingMenuIds.has(menu.id)) {
|
// 顶级菜单
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
await prisma.tenantMenu.create({
|
||||||
data: {
|
data: {
|
||||||
tenantId: tenant.id,
|
tenantId: tenant.id,
|
||||||
@ -197,17 +214,11 @@ async function initMenus() {
|
|||||||
});
|
});
|
||||||
addedMenuCount++;
|
addedMenuCount++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (addedMenuCount > 0) {
|
const tenantType = isSuperTenant ? '(超级租户)' : '(普通租户)';
|
||||||
console.log(
|
console.log(
|
||||||
` ✓ 租户 "${tenant.name}" (${tenant.code}): 添加了 ${addedMenuCount} 个菜单`,
|
` ✓ 租户 "${tenant.name}" ${tenantType}: 分配了 ${addedMenuCount} 个菜单`,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
` ✓ 租户 "${tenant.name}" (${tenant.code}): 已拥有所有菜单`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
console.log(`\n✅ 菜单分配完成!`);
|
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',
|
icon: 'TeamOutlined',
|
||||||
component: 'system/tenants/Index',
|
component: 'system/tenants/Index',
|
||||||
parentId: systemMenu.id,
|
parentId: systemMenu.id,
|
||||||
permission: 'tenant:read',
|
permission: 'tenant:update', // 只有超级租户才有此权限,普通租户只有 tenant:read
|
||||||
sort: 7,
|
sort: 7,
|
||||||
validState: 1,
|
validState: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -254,6 +254,563 @@ const permissions = [
|
|||||||
name: '修改用户密码',
|
name: '修改用户密码',
|
||||||
description: '允许修改用户密码',
|
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);
|
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 角色是否存在
|
// 2. 检查 admin 角色是否存在
|
||||||
console.log(`👤 步骤 2: 检查 admin 角色是否存在...`);
|
console.log(`👤 步骤 2: 检查 admin 角色是否存在...`);
|
||||||
@ -301,10 +859,15 @@ async function initTenantAdminPermissionsOnly(tenantCode: string) {
|
|||||||
console.log(`✅ 找到 admin 角色: ${adminRole.name} (${adminRole.code})\n`);
|
console.log(`✅ 找到 admin 角色: ${adminRole.name} (${adminRole.code})\n`);
|
||||||
|
|
||||||
// 3. 初始化租户权限(如果不存在则创建)
|
// 3. 初始化租户权限(如果不存在则创建)
|
||||||
console.log(`📝 步骤 3: 初始化租户权限...`);
|
// 超级租户拥有所有权限,普通租户只拥有基础权限
|
||||||
|
const allPermissions = isSuperTenant
|
||||||
|
? [...permissions, ...superAdminPermissions]
|
||||||
|
: permissions;
|
||||||
|
|
||||||
|
console.log(`📝 步骤 3: 初始化租户权限...${isSuperTenant ? '(包含超级管理员权限)' : ''}`);
|
||||||
const createdPermissions = [];
|
const createdPermissions = [];
|
||||||
|
|
||||||
for (const perm of permissions) {
|
for (const perm of allPermissions) {
|
||||||
// 检查权限是否已存在
|
// 检查权限是否已存在
|
||||||
const existingPermission = await prisma.permission.findFirst({
|
const existingPermission = await prisma.permission.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -448,7 +1011,8 @@ async function initTenantAdmin(tenantCode: string) {
|
|||||||
process.exit(1);
|
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 用户
|
// 2. 检查是否已存在 admin 用户
|
||||||
console.log(`👤 步骤 2: 检查 admin 用户是否已存在...`);
|
console.log(`👤 步骤 2: 检查 admin 用户是否已存在...`);
|
||||||
@ -468,10 +1032,15 @@ async function initTenantAdmin(tenantCode: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. 初始化租户权限(如果不存在则创建)
|
// 3. 初始化租户权限(如果不存在则创建)
|
||||||
console.log(`📝 步骤 3: 初始化租户权限...`);
|
// 超级租户拥有所有权限,普通租户只拥有基础权限
|
||||||
|
const allPermissions = isSuperTenant
|
||||||
|
? [...permissions, ...superAdminPermissions]
|
||||||
|
: permissions;
|
||||||
|
|
||||||
|
console.log(`📝 步骤 3: 初始化租户权限...${isSuperTenant ? '(包含超级管理员权限)' : ''}`);
|
||||||
const createdPermissions = [];
|
const createdPermissions = [];
|
||||||
|
|
||||||
for (const perm of permissions) {
|
for (const perm of allPermissions) {
|
||||||
// 检查权限是否已存在
|
// 检查权限是否已存在
|
||||||
const existingPermission = await prisma.permission.findFirst({
|
const existingPermission = await prisma.permission.findFirst({
|
||||||
where: {
|
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 {
|
private isContestVisibleToTenant(contest: any, tenantId: number): boolean {
|
||||||
|
// 如果赛事未发布,对租户不可见
|
||||||
|
if (contest.contestState !== 'published') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果contestTenants为null,表示所有租户可见
|
// 如果contestTenants为null,表示所有租户可见
|
||||||
if (!contest.contestTenants) {
|
if (!contest.contestTenants) {
|
||||||
return true;
|
return true;
|
||||||
@ -173,6 +178,7 @@ export class ContestsService {
|
|||||||
registrations: true,
|
registrations: true,
|
||||||
works: true,
|
works: true,
|
||||||
teams: true,
|
teams: true,
|
||||||
|
judges: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -232,6 +238,7 @@ export class ContestsService {
|
|||||||
registrations: true,
|
registrations: true,
|
||||||
works: true,
|
works: true,
|
||||||
teams: true,
|
teams: true,
|
||||||
|
judges: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -239,11 +246,20 @@ export class ContestsService {
|
|||||||
this.prisma.contest.count({ where }),
|
this.prisma.contest.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 如果指定了租户ID,进行应用层过滤
|
// 如果指定了租户ID,进行应用层过滤(超级租户不过滤,可以看到所有赛事)
|
||||||
let filteredList = allList;
|
let filteredList = allList;
|
||||||
let filteredTotal = allTotal;
|
let filteredTotal = allTotal;
|
||||||
|
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
|
// 检查是否为超级租户
|
||||||
|
const tenant = await this.prisma.tenant.findUnique({
|
||||||
|
where: { id: tenantId },
|
||||||
|
select: { isSuper: true },
|
||||||
|
});
|
||||||
|
const isSuperTenant = tenant?.isSuper === 1;
|
||||||
|
|
||||||
|
// 超级租户可以看到所有赛事,普通租户只能看到已发布且在公开范围内的赛事
|
||||||
|
if (!isSuperTenant) {
|
||||||
filteredList = allList.filter((contest) =>
|
filteredList = allList.filter((contest) =>
|
||||||
this.isContestVisibleToTenant(contest, tenantId),
|
this.isContestVisibleToTenant(contest, tenantId),
|
||||||
);
|
);
|
||||||
@ -252,15 +268,38 @@ export class ContestsService {
|
|||||||
// 限制返回数量
|
// 限制返回数量
|
||||||
filteredList = filteredList.slice(0, pageSize);
|
filteredList = filteredList.slice(0, pageSize);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 contestTenants JSON 字符串为数组
|
||||||
|
const parsedList = filteredList.map((contest) => ({
|
||||||
|
...contest,
|
||||||
|
contestTenants: this.parseContestTenants(contest.contestTenants),
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list: filteredList,
|
list: parsedList,
|
||||||
total: filteredTotal,
|
total: filteredTotal,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
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-评委评审的赛事
|
* @param role 角色:student-学生报名的赛事, teacher-教师指导的赛事, judge-评委评审的赛事
|
||||||
@ -389,6 +428,7 @@ export class ContestsService {
|
|||||||
registrations: true,
|
registrations: true,
|
||||||
works: true,
|
works: true,
|
||||||
teams: true,
|
teams: true,
|
||||||
|
judges: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -437,12 +477,24 @@ export class ContestsService {
|
|||||||
throw new NotFoundException('比赛不存在');
|
throw new NotFoundException('比赛不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 租户过滤:检查比赛是否对租户可见
|
// 租户过滤:检查比赛是否对租户可见(超级租户可以查看所有赛事)
|
||||||
if (tenantId && !this.isContestVisibleToTenant(contest, tenantId)) {
|
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('比赛不存在或无权访问');
|
throw new NotFoundException('比赛不存在或无权访问');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return contest;
|
// 解析 contestTenants JSON 字符串为数组
|
||||||
|
return {
|
||||||
|
...contest,
|
||||||
|
contestTenants: this.parseContestTenants(contest.contestTenants),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
@ -655,6 +707,7 @@ export class ContestsService {
|
|||||||
registrations: true,
|
registrations: true,
|
||||||
works: true,
|
works: true,
|
||||||
teams: true,
|
teams: true,
|
||||||
|
judges: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -779,6 +832,7 @@ export class ContestsService {
|
|||||||
registrations: true,
|
registrations: true,
|
||||||
works: true,
|
works: true,
|
||||||
teams: true,
|
teams: true,
|
||||||
|
judges: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -822,6 +876,7 @@ export class ContestsService {
|
|||||||
registrations: true,
|
registrations: true,
|
||||||
works: true,
|
works: true,
|
||||||
teams: true,
|
teams: true,
|
||||||
|
judges: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -40,5 +40,13 @@ export class QueryWorkDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
workNo?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -172,6 +172,8 @@ export class WorksService {
|
|||||||
registrationId,
|
registrationId,
|
||||||
status,
|
status,
|
||||||
title,
|
title,
|
||||||
|
workNo,
|
||||||
|
username,
|
||||||
} = queryDto;
|
} = queryDto;
|
||||||
const skip = (page - 1) * pageSize;
|
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([
|
const [list, total] = await Promise.all([
|
||||||
this.prisma.contestWork.findMany({
|
this.prisma.contestWork.findMany({
|
||||||
where,
|
where,
|
||||||
@ -215,6 +242,12 @@ export class WorksService {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
contestName: true,
|
contestName: true,
|
||||||
|
reviewRuleId: true,
|
||||||
|
reviewRule: {
|
||||||
|
select: {
|
||||||
|
judgeCount: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
registration: {
|
registration: {
|
||||||
@ -226,9 +259,33 @@ export class WorksService {
|
|||||||
nickname: true,
|
nickname: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
teamName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
attachments: true,
|
attachments: true,
|
||||||
|
scores: {
|
||||||
|
where: { validState: 1 },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
totalScore: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assignments: {
|
||||||
|
include: {
|
||||||
|
judge: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
nickname: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
scores: true,
|
scores: true,
|
||||||
@ -240,8 +297,32 @@ export class WorksService {
|
|||||||
this.prisma.contestWork.count({ where }),
|
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 {
|
return {
|
||||||
list,
|
...work,
|
||||||
|
reviewedCount,
|
||||||
|
totalJudgesCount,
|
||||||
|
averageScore,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: enrichedList,
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
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';
|
import { Gender, UserStatus } from '../../users/dto/create-user.dto';
|
||||||
|
|
||||||
export class CreateJudgeDto {
|
export class CreateJudgeDto {
|
||||||
@ -8,8 +8,8 @@ export class CreateJudgeDto {
|
|||||||
@IsEnum(Gender)
|
@IsEnum(Gender)
|
||||||
gender: Gender;
|
gender: Gender;
|
||||||
|
|
||||||
@IsInt()
|
@IsString()
|
||||||
tenantId: number;
|
organization: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
@ -13,10 +13,9 @@ export class QueryJudgeDto {
|
|||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
pageSize?: number = 10;
|
pageSize?: number = 10;
|
||||||
|
|
||||||
@IsInt()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
organization?: string;
|
||||||
tenantId?: number;
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@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';
|
import { Gender, UserStatus } from '../../users/dto/create-user.dto';
|
||||||
|
|
||||||
export class UpdateJudgeDto {
|
export class UpdateJudgeDto {
|
||||||
@ -10,9 +10,9 @@ export class UpdateJudgeDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
gender?: Gender;
|
gender?: Gender;
|
||||||
|
|
||||||
@IsInt()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
tenantId?: number;
|
organization?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@ -15,6 +15,24 @@ const JUDGE_ROLE_CODE = 'judge';
|
|||||||
export class JudgesManagementService {
|
export class JudgesManagementService {
|
||||||
constructor(private prisma: PrismaService) {}
|
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) {
|
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;
|
let username = createJudgeDto.username;
|
||||||
@ -56,11 +77,11 @@ export class JudgesManagementService {
|
|||||||
username = createJudgeDto.phone;
|
username = createJudgeDto.phone;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户名是否已存在
|
// 检查用户名是否已存在(在超级租户下)
|
||||||
const existingUser = await this.prisma.user.findFirst({
|
const existingUser = await this.prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
tenantId,
|
tenantId: superTenantId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,7 +97,8 @@ export class JudgesManagementService {
|
|||||||
data: {
|
data: {
|
||||||
...userData,
|
...userData,
|
||||||
username,
|
username,
|
||||||
tenantId,
|
tenantId: superTenantId,
|
||||||
|
organization, // 使用独立的所属单位字段
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
status: createJudgeDto.status || 'enabled',
|
status: createJudgeDto.status || 'enabled',
|
||||||
creator: creatorId,
|
creator: creatorId,
|
||||||
@ -87,12 +109,6 @@ export class JudgesManagementService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
tenant: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
roles: {
|
roles: {
|
||||||
include: {
|
include: {
|
||||||
role: true,
|
role: true,
|
||||||
@ -108,7 +124,7 @@ export class JudgesManagementService {
|
|||||||
* 查询评委列表
|
* 查询评委列表
|
||||||
*/
|
*/
|
||||||
async findAll(queryDto: QueryJudgeDto) {
|
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;
|
const skip = (page - 1) * pageSize;
|
||||||
|
|
||||||
// 构建查询条件:必须有评委角色
|
// 构建查询条件:必须有评委角色
|
||||||
@ -123,8 +139,10 @@ export class JudgesManagementService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tenantId) {
|
if (organization) {
|
||||||
where.tenantId = tenantId;
|
where.organization = {
|
||||||
|
contains: organization,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nickname) {
|
if (nickname) {
|
||||||
@ -149,12 +167,6 @@ export class JudgesManagementService {
|
|||||||
skip,
|
skip,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
include: {
|
include: {
|
||||||
tenant: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
roles: {
|
roles: {
|
||||||
include: {
|
include: {
|
||||||
role: true,
|
role: true,
|
||||||
@ -210,12 +222,6 @@ export class JudgesManagementService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
tenant: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
roles: {
|
roles: {
|
||||||
include: {
|
include: {
|
||||||
role: true,
|
role: true,
|
||||||
@ -264,19 +270,10 @@ export class JudgesManagementService {
|
|||||||
data.modifier = modifierId;
|
data.modifier = modifierId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除 tenantId,不允许修改租户
|
|
||||||
delete data.tenantId;
|
|
||||||
|
|
||||||
return this.prisma.user.update({
|
return this.prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data,
|
data,
|
||||||
include: {
|
include: {
|
||||||
tenant: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
roles: {
|
roles: {
|
||||||
include: {
|
include: {
|
||||||
role: true,
|
role: true,
|
||||||
@ -329,14 +326,6 @@ export class JudgesManagementService {
|
|||||||
data: {
|
data: {
|
||||||
status,
|
status,
|
||||||
},
|
},
|
||||||
include: {
|
|
||||||
tenant: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -71,30 +71,7 @@ export class MenusService {
|
|||||||
* @returns 过滤后的菜单树
|
* @returns 过滤后的菜单树
|
||||||
*/
|
*/
|
||||||
async findUserMenus(userId: number, tenantId: number) {
|
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);
|
const userPermissions = await this.authService.getUserPermissions(userId);
|
||||||
|
|
||||||
// 获取租户分配的菜单ID
|
// 获取租户分配的菜单ID
|
||||||
@ -135,19 +112,10 @@ export class MenusService {
|
|||||||
const menuTree = buildTree(allMenus);
|
const menuTree = buildTree(allMenus);
|
||||||
|
|
||||||
// 过滤菜单:只有用户拥有菜单对应权限的菜单才会显示
|
// 过滤菜单:只有用户拥有菜单对应权限的菜单才会显示
|
||||||
// 超级管理员显示所有菜单,普通用户根据权限过滤
|
// 所有用户都根据权限过滤菜单
|
||||||
const filterMenus = (menus: any[]): any[] => {
|
const filterMenus = (menus: any[]): any[] => {
|
||||||
return menus
|
return menus
|
||||||
.map((menu) => {
|
.map((menu) => {
|
||||||
// 超级管理员显示所有菜单
|
|
||||||
if (isSuperAdmin) {
|
|
||||||
const filtered = { ...menu };
|
|
||||||
if (menu.children && menu.children.length > 0) {
|
|
||||||
filtered.children = filterMenus(menu.children);
|
|
||||||
}
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先递归处理子菜单
|
// 先递归处理子菜单
|
||||||
let filteredChildren: any[] = [];
|
let filteredChildren: any[] = [];
|
||||||
if (menu.children && menu.children.length > 0) {
|
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;
|
contest?: Contest;
|
||||||
registration?: ContestRegistration;
|
registration?: ContestRegistration;
|
||||||
attachments?: ContestWorkAttachment[];
|
attachments?: ContestWorkAttachment[];
|
||||||
|
assignments?: Array<{
|
||||||
|
id: number;
|
||||||
|
judgeId: number;
|
||||||
|
status: string;
|
||||||
|
judge?: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
_count?: {
|
_count?: {
|
||||||
scores: number;
|
scores: number;
|
||||||
assignments: number;
|
assignments: number;
|
||||||
};
|
};
|
||||||
|
// 评审统计字段(由后端计算返回)
|
||||||
|
reviewedCount?: number;
|
||||||
|
totalJudgesCount?: number;
|
||||||
|
averageScore?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContestWorkAttachment {
|
export interface ContestWorkAttachment {
|
||||||
@ -365,6 +379,8 @@ export interface QueryWorkParams extends PaginationParams {
|
|||||||
registrationId?: number;
|
registrationId?: number;
|
||||||
status?: "submitted" | "locked" | "reviewing" | "rejected" | "accepted";
|
status?: "submitted" | "locked" | "reviewing" | "rejected" | "accepted";
|
||||||
title?: string;
|
title?: string;
|
||||||
|
workNo?: string;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 评审相关类型 ====================
|
// ==================== 评审相关类型 ====================
|
||||||
@ -1063,6 +1079,17 @@ export const reviewsApi = {
|
|||||||
>(`/contests/reviews/work/${workId}/final-score`);
|
>(`/contests/reviews/work/${workId}/final-score`);
|
||||||
return response;
|
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';
|
gender?: 'male' | 'female';
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
status?: 'enabled' | 'disabled';
|
status?: 'enabled' | 'disabled';
|
||||||
tenantId?: number;
|
organization?: string;
|
||||||
validState?: number;
|
validState?: number;
|
||||||
creator?: number;
|
creator?: number;
|
||||||
modifier?: number;
|
modifier?: number;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
modifyTime?: string;
|
modifyTime?: string;
|
||||||
tenant?: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
roles?: Array<{
|
roles?: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
role: {
|
role: {
|
||||||
@ -39,7 +35,7 @@ export interface Judge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryJudgeParams extends PaginationParams {
|
export interface QueryJudgeParams extends PaginationParams {
|
||||||
tenantId?: number;
|
organization?: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
status?: 'enabled' | 'disabled';
|
status?: 'enabled' | 'disabled';
|
||||||
@ -48,7 +44,7 @@ export interface QueryJudgeParams extends PaginationParams {
|
|||||||
export interface CreateJudgeForm {
|
export interface CreateJudgeForm {
|
||||||
nickname: string;
|
nickname: string;
|
||||||
gender: 'male' | 'female';
|
gender: 'male' | 'female';
|
||||||
tenantId: number;
|
organization: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
password: string;
|
password: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
@ -60,6 +56,7 @@ export interface CreateJudgeForm {
|
|||||||
export interface UpdateJudgeForm {
|
export interface UpdateJudgeForm {
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
gender?: 'male' | 'female';
|
gender?: 'male' | 'female';
|
||||||
|
organization?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
@ -153,3 +150,4 @@ export const judgesManagementApi = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -110,6 +110,28 @@ const baseRoutes: RouteRecordRaw[] = [
|
|||||||
permissions: ["contest:registration:read"],
|
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",
|
path: "homework/submissions",
|
||||||
|
|||||||
@ -30,9 +30,14 @@ const componentMap: Record<string, () => Promise<any>> = {
|
|||||||
"contests/registrations/Index": () =>
|
"contests/registrations/Index": () =>
|
||||||
import("@/views/contests/registrations/Index.vue"),
|
import("@/views/contests/registrations/Index.vue"),
|
||||||
"contests/works/Index": () => import("@/views/contests/works/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/Index": () => import("@/views/contests/reviews/Index.vue"),
|
||||||
|
"contests/reviews/Tasks": () => import("@/views/contests/reviews/Tasks.vue"),
|
||||||
"contests/reviews/Progress": () =>
|
"contests/reviews/Progress": () =>
|
||||||
import("@/views/contests/reviews/Progress.vue"),
|
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/judges/Index": () => import("@/views/contests/judges/Index.vue"),
|
||||||
"contests/results/Index": () => import("@/views/contests/results/Index.vue"),
|
"contests/results/Index": () => import("@/views/contests/results/Index.vue"),
|
||||||
"contests/notices/Index": () => import("@/views/contests/notices/Index.vue"),
|
"contests/notices/Index": () => import("@/views/contests/notices/Index.vue"),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contests-page">
|
<div class="contests-page">
|
||||||
<a-card class="mb-4">
|
<a-card class="mb-4">
|
||||||
<template #title>比赛管理</template>
|
<template #title>赛事列表</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button
|
<a-button
|
||||||
v-permission="'contest:create'"
|
v-permission="'contest:create'"
|
||||||
@ -21,15 +21,15 @@
|
|||||||
class="search-form"
|
class="search-form"
|
||||||
@finish="handleSearch"
|
@finish="handleSearch"
|
||||||
>
|
>
|
||||||
<a-form-item label="比赛名称">
|
<a-form-item label="赛事名称">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="searchParams.contestName"
|
v-model:value="searchParams.contestName"
|
||||||
placeholder="请输入比赛名称"
|
placeholder="请输入赛事名称"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="发布状态">
|
<a-form-item label="赛事状态">
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="searchParams.contestState"
|
v-model:value="searchParams.contestState"
|
||||||
placeholder="请选择状态"
|
placeholder="请选择状态"
|
||||||
@ -40,23 +40,12 @@
|
|||||||
<a-select-option value="unpublished">未发布</a-select-option>
|
<a-select-option value="unpublished">未发布</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="赛事状态">
|
<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-select
|
<a-select
|
||||||
v-model:value="searchParams.contestType"
|
v-model:value="searchParams.contestType"
|
||||||
placeholder="请选择类型"
|
placeholder="请选择类型"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 150px"
|
style="width: 120px"
|
||||||
>
|
>
|
||||||
<a-select-option value="individual">个人赛</a-select-option>
|
<a-select-option value="individual">个人赛</a-select-option>
|
||||||
<a-select-option value="team">团队赛</a-select-option>
|
<a-select-option value="team">团队赛</a-select-option>
|
||||||
@ -83,9 +72,9 @@
|
|||||||
row-key="id"
|
row-key="id"
|
||||||
@change="handleTableChange"
|
@change="handleTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'contestName'">
|
<template v-if="column.key === 'index'">
|
||||||
{{ record.contestName }}
|
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'contestType'">
|
<template v-else-if="column.key === 'contestType'">
|
||||||
<a-tag
|
<a-tag
|
||||||
@ -101,17 +90,34 @@
|
|||||||
{{ record.contestState === "published" ? "已发布" : "未发布" }}
|
{{ record.contestState === "published" ? "已发布" : "未发布" }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'status'">
|
<template v-else-if="column.key === 'publicScope'">
|
||||||
<a-tag :color="record.status === 'ongoing' ? 'processing' : 'orange'">
|
<template
|
||||||
{{ record.status === "ongoing" ? "进行中" : "已完结" }}
|
v-if="record.contestTenants && record.contestTenants.length > 0"
|
||||||
</a-tag>
|
>
|
||||||
</template>
|
<a-tooltip>
|
||||||
<template v-else-if="column.key === 'timeRange'">
|
<template #title>
|
||||||
<div>
|
<div v-for="tenantId in record.contestTenants" :key="tenantId">
|
||||||
<div>开始:{{ formatDateTime(record.startTime) }}</div>
|
{{ getTenantName(tenantId) }}
|
||||||
<div>结束:{{ formatDateTime(record.endTime) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<a-tag>{{ record.contestTenants.length }}个机构</a-tag>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
<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'">
|
<template v-else-if="column.key === 'action'">
|
||||||
<a-space>
|
<a-space>
|
||||||
<!-- 发布/取消发布 -->
|
<!-- 发布/取消发布 -->
|
||||||
@ -119,7 +125,7 @@
|
|||||||
v-permission="'contest:publish'"
|
v-permission="'contest:publish'"
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handlePublish(record)"
|
@click="handlePublishClick(record)"
|
||||||
>
|
>
|
||||||
{{ record.contestState === "published" ? "取消发布" : "发布" }}
|
{{ record.contestState === "published" ? "取消发布" : "发布" }}
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -143,13 +149,15 @@
|
|||||||
编辑
|
编辑
|
||||||
</a-button>
|
</a-button>
|
||||||
<!-- 删除 -->
|
<!-- 删除 -->
|
||||||
<a-popconfirm
|
<a-button
|
||||||
v-permission="'contest:delete'"
|
v-permission="'contest:delete'"
|
||||||
title="确定要删除这个比赛吗?"
|
type="link"
|
||||||
@confirm="handleDelete(record.id)"
|
danger
|
||||||
|
size="small"
|
||||||
|
@click="handleDeleteClick(record)"
|
||||||
>
|
>
|
||||||
<a-button type="link" danger size="small">删除</a-button>
|
删除
|
||||||
</a-popconfirm>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@ -171,12 +179,70 @@
|
|||||||
@success="handleJudgeAddSuccess"
|
@success="handleJudgeAddSuccess"
|
||||||
/>
|
/>
|
||||||
</a-drawer>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter, useRoute } from "vue-router"
|
import { useRouter, useRoute } from "vue-router"
|
||||||
import { ref } from "vue"
|
import { ref, onMounted } from "vue"
|
||||||
import { message } from "ant-design-vue"
|
import { message } from "ant-design-vue"
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@ -189,6 +255,7 @@ import {
|
|||||||
type Contest,
|
type Contest,
|
||||||
type QueryContestParams,
|
type QueryContestParams,
|
||||||
} from "@/api/contests"
|
} from "@/api/contests"
|
||||||
|
import { tenantsApi, type Tenant } from "@/api/tenants"
|
||||||
import AddJudgeDrawer from "./components/AddJudgeDrawer.vue"
|
import AddJudgeDrawer from "./components/AddJudgeDrawer.vue"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
@ -210,73 +277,61 @@ const {
|
|||||||
requestFn: contestsApi.getList,
|
requestFn: contestsApi.getList,
|
||||||
defaultSearchParams: {} as QueryContestParams,
|
defaultSearchParams: {} as QueryContestParams,
|
||||||
defaultPageSize: 10,
|
defaultPageSize: 10,
|
||||||
errorMessage: "获取比赛列表失败",
|
errorMessage: "获取赛事列表失败",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 租户列表
|
||||||
|
const tenants = ref<Tenant[]>([])
|
||||||
|
const tenantOptions = ref<{ label: string; value: number }[]>([])
|
||||||
|
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
const columns = [
|
const columns = [
|
||||||
|
{ title: "序号", key: "index", width: 70 },
|
||||||
{
|
{
|
||||||
title: "比赛名称",
|
title: "赛事名称",
|
||||||
key: "contestName",
|
|
||||||
dataIndex: "contestName",
|
dataIndex: "contestName",
|
||||||
|
key: "contestName",
|
||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
{ title: "赛事类型", key: "contestType", width: 100 },
|
||||||
title: "比赛类型",
|
{ title: "赛事状态", key: "contestState", width: 100 },
|
||||||
key: "contestType",
|
{ title: "公开范围", key: "publicScope", width: 120 },
|
||||||
dataIndex: "contestType",
|
{ title: "评委", key: "judges", width: 80 },
|
||||||
width: 100,
|
{ title: "赛事时间", key: "contestTime", width: 180 },
|
||||||
},
|
{ title: "操作", key: "action", width: 260, fixed: "right" as const },
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 格式化日期时间
|
// 格式化日期
|
||||||
const formatDateTime = (dateStr?: string) => {
|
const formatDate = (dateStr?: string) => {
|
||||||
if (!dateStr) return "-"
|
if (!dateStr) return "-"
|
||||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
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 = () => {
|
const handleSearch = () => {
|
||||||
search()
|
search()
|
||||||
@ -295,7 +350,7 @@ const handleAdd = () => {
|
|||||||
// 编辑
|
// 编辑
|
||||||
const handleEdit = (id: number) => {
|
const handleEdit = (id: number) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
message.warning("比赛ID不存在")
|
message.warning("赛事ID不存在")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const path = `/${tenantCode}/contests/${id}/edit`
|
const path = `/${tenantCode}/contests/${id}/edit`
|
||||||
@ -313,13 +368,12 @@ const currentContest = ref<Contest | null>(null)
|
|||||||
// 添加评委
|
// 添加评委
|
||||||
const handleAddJudge = async (id: number) => {
|
const handleAddJudge = async (id: number) => {
|
||||||
currentContestId.value = id
|
currentContestId.value = id
|
||||||
// 获取比赛详情以获取评审规则中的评委数量
|
|
||||||
try {
|
try {
|
||||||
const contest = await contestsApi.getDetail(id)
|
const contest = await contestsApi.getDetail(id)
|
||||||
currentContest.value = contest
|
currentContest.value = contest
|
||||||
judgeDrawerVisible.value = true
|
judgeDrawerVisible.value = true
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error(error?.response?.data?.message || "获取比赛信息失败")
|
message.error(error?.response?.data?.message || "获取赛事信息失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,37 +387,130 @@ const handleJudgeDrawerClose = () => {
|
|||||||
// 评委添加成功回调
|
// 评委添加成功回调
|
||||||
const handleJudgeAddSuccess = () => {
|
const handleJudgeAddSuccess = () => {
|
||||||
message.success("添加评委成功")
|
message.success("添加评委成功")
|
||||||
fetchList() // 刷新列表
|
fetchList()
|
||||||
handleJudgeDrawerClose()
|
handleJudgeDrawerClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发布/撤回
|
// 发布相关
|
||||||
const handlePublish = async (record: Contest) => {
|
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 {
|
try {
|
||||||
const newState =
|
const contest = await contestsApi.getDetail(record.id)
|
||||||
record.contestState === "published" ? "unpublished" : "published"
|
// 确保 contestTenants 是数字数组,处理 null、undefined 和字符串数组的情况
|
||||||
await contestsApi.publish(record.id, newState)
|
if (
|
||||||
message.success(newState === "published" ? "发布成功" : "撤回成功")
|
Array.isArray(contest.contestTenants) &&
|
||||||
fetchList()
|
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) {
|
} catch (error: any) {
|
||||||
message.error(error?.response?.data?.message || "操作失败")
|
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 {
|
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("删除成功")
|
message.success("删除成功")
|
||||||
|
deleteModalVisible.value = false
|
||||||
fetchList()
|
fetchList()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error(error?.response?.data?.message || "删除失败")
|
message.error(error?.response?.data?.message || "删除失败")
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTenants()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.search-form {
|
.search-form {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -12,22 +12,14 @@
|
|||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="hasTenantReadPermission" label="所属单位">
|
<a-form-item label="所属单位">
|
||||||
<a-select
|
<a-input
|
||||||
v-model:value="searchParams.tenantId"
|
v-model:value="searchParams.organization"
|
||||||
placeholder="请选择所属单位"
|
placeholder="请输入所属单位"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
:loading="tenantsLoading"
|
@press-enter="handleSearch"
|
||||||
>
|
/>
|
||||||
<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>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" html-type="submit">
|
<a-button type="primary" html-type="submit">
|
||||||
@ -62,8 +54,8 @@
|
|||||||
@change="handleJudgeTableChange"
|
@change="handleJudgeTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'tenant'">
|
<template v-if="column.key === 'organization'">
|
||||||
{{ record.tenant?.name || "-" }}
|
{{ record.organization || "-" }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'status'">
|
<template v-else-if="column.key === 'status'">
|
||||||
<a-tag :color="record.status === 'enabled' ? 'success' : 'error'">
|
<a-tag :color="record.status === 'enabled' ? 'success' : 'error'">
|
||||||
@ -77,7 +69,7 @@
|
|||||||
<!-- 已选评委区域 -->
|
<!-- 已选评委区域 -->
|
||||||
<a-card size="small">
|
<a-card size="small">
|
||||||
<template #title>
|
<template #title>
|
||||||
已选/{{ judgeCount || 0 }}
|
已选 {{ selectedJudges.length }} / {{ judgeCount || 0 }}
|
||||||
<span
|
<span
|
||||||
v-if="selectedJudges.length > (judgeCount || 0)"
|
v-if="selectedJudges.length > (judgeCount || 0)"
|
||||||
class="warning-text"
|
class="warning-text"
|
||||||
@ -97,7 +89,7 @@
|
|||||||
{{ item.nickname }}({{ item.username }})
|
{{ item.nickname }}({{ item.username }})
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
{{ item.tenant?.name || "-" }}
|
{{ item.organization || "-" }}
|
||||||
</template>
|
</template>
|
||||||
</a-list-item-meta>
|
</a-list-item-meta>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@ -141,9 +133,7 @@ import { message } from "ant-design-vue"
|
|||||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||||
import type { Judge, QueryJudgeParams } from "@/api/judges-management"
|
import type { Judge, QueryJudgeParams } from "@/api/judges-management"
|
||||||
import { judgesManagementApi } from "@/api/judges-management"
|
import { judgesManagementApi } from "@/api/judges-management"
|
||||||
import { judgesApi, type ContestJudge } from "@/api/contests"
|
import { judgesApi } from "@/api/contests"
|
||||||
import { tenantsApi, type Tenant } from "@/api/tenants"
|
|
||||||
import { useAuthStore } from "@/stores/auth"
|
|
||||||
import type { Contest } from "@/api/contests"
|
import type { Contest } from "@/api/contests"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -156,24 +146,17 @@ const emit = defineEmits<{
|
|||||||
success: []
|
success: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
// 评委数量(从评审规则中获取)
|
// 评委数量(从评审规则中获取)
|
||||||
const judgeCount = computed(() => {
|
const judgeCount = computed(() => {
|
||||||
return props.contest.reviewRule?.judgeCount || 0
|
return props.contest.reviewRule?.judgeCount || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// 是否有租户读取权限
|
|
||||||
const hasTenantReadPermission = computed(() => {
|
|
||||||
return authStore.hasPermission("tenant:read")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 搜索参数
|
// 搜索参数
|
||||||
const searchParams = reactive<QueryJudgeParams>({
|
const searchParams = reactive<QueryJudgeParams>({
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
nickname: undefined,
|
nickname: undefined,
|
||||||
tenantId: undefined,
|
organization: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 全部评委列表
|
// 全部评委列表
|
||||||
@ -192,10 +175,6 @@ const selectedJudgeIds = ref<number[]>([])
|
|||||||
const selectedJudges = ref<Judge[]>([])
|
const selectedJudges = ref<Judge[]>([])
|
||||||
const selectedJudgesLoading = ref(false)
|
const selectedJudgesLoading = ref(false)
|
||||||
|
|
||||||
// 租户列表
|
|
||||||
const tenantsList = ref<Tenant[]>([])
|
|
||||||
const tenantsLoading = ref(false)
|
|
||||||
|
|
||||||
// 提交加载状态
|
// 提交加载状态
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
@ -215,7 +194,8 @@ const judgeColumns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "所属单位",
|
title: "所属单位",
|
||||||
key: "tenant",
|
dataIndex: "organization",
|
||||||
|
key: "organization",
|
||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -239,7 +219,7 @@ const loadJudges = async () => {
|
|||||||
page: judgePagination.current,
|
page: judgePagination.current,
|
||||||
pageSize: judgePagination.pageSize,
|
pageSize: judgePagination.pageSize,
|
||||||
nickname: searchParams.nickname || undefined,
|
nickname: searchParams.nickname || undefined,
|
||||||
tenantId: searchParams.tenantId || undefined,
|
organization: searchParams.organization || undefined,
|
||||||
}
|
}
|
||||||
const res = await judgesManagementApi.getList(params)
|
const res = await judgesManagementApi.getList(params)
|
||||||
judgeList.value = res.list
|
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 => {
|
const isJudgeSelected = (judgeId: number): boolean => {
|
||||||
return selectedJudgeIds.value.includes(judgeId)
|
return selectedJudgeIds.value.includes(judgeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 评委选择变化
|
// 评委选择变化
|
||||||
const handleJudgeSelectionChange = (
|
const handleJudgeSelectionChange = (selectedKeys: number[]) => {
|
||||||
selectedKeys: number[],
|
|
||||||
selectedRows: Judge[]
|
|
||||||
) => {
|
|
||||||
// 找出新增的评委(在 selectedKeys 中但不在 selectedJudgeIds 中)
|
// 找出新增的评委(在 selectedKeys 中但不在 selectedJudgeIds 中)
|
||||||
const newSelectedIds = selectedKeys.filter(
|
const newSelectedIds = selectedKeys.filter(
|
||||||
(id) => !selectedJudgeIds.value.includes(id)
|
(id) => !selectedJudgeIds.value.includes(id)
|
||||||
@ -347,7 +306,7 @@ const handleSearch = () => {
|
|||||||
// 重置搜索
|
// 重置搜索
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
searchParams.nickname = undefined
|
searchParams.nickname = undefined
|
||||||
searchParams.tenantId = undefined
|
searchParams.organization = undefined
|
||||||
handleSearch()
|
handleSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,9 +373,9 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => searchParams.tenantId,
|
() => searchParams.organization,
|
||||||
() => {
|
() => {
|
||||||
if (searchParams.tenantId === undefined) {
|
if (searchParams.organization === undefined || searchParams.organization === "") {
|
||||||
handleSearch()
|
handleSearch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -425,7 +384,6 @@ watch(
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadJudges()
|
loadJudges()
|
||||||
loadSelectedJudges()
|
loadSelectedJudges()
|
||||||
loadTenants()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -54,22 +54,13 @@
|
|||||||
class="search-form"
|
class="search-form"
|
||||||
@finish="handleSearch"
|
@finish="handleSearch"
|
||||||
>
|
>
|
||||||
<a-form-item v-if="hasTenantReadPermission" label="所属单位">
|
<a-form-item label="所属单位">
|
||||||
<a-select
|
<a-input
|
||||||
v-model:value="searchParams.tenantId"
|
v-model:value="searchParams.organization"
|
||||||
placeholder="请选择所属单位"
|
placeholder="请输入所属单位"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
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>
|
||||||
<a-form-item label="姓名">
|
<a-form-item label="姓名">
|
||||||
<a-input
|
<a-input
|
||||||
@ -124,8 +115,8 @@
|
|||||||
<template v-if="column.key === 'index'">
|
<template v-if="column.key === 'index'">
|
||||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'tenant'">
|
<template v-else-if="column.key === 'organization'">
|
||||||
{{ record.tenant?.name || "-" }}
|
{{ record.organization || "-" }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'gender'">
|
<template v-else-if="column.key === 'gender'">
|
||||||
<span v-if="record.gender === 'male'">男</span>
|
<span v-if="record.gender === 'male'">男</span>
|
||||||
@ -220,54 +211,11 @@
|
|||||||
<a-radio value="female">女</a-radio>
|
<a-radio value="female">女</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item
|
<a-form-item label="所属单位" name="organization">
|
||||||
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-input
|
<a-input
|
||||||
:value="
|
v-model:value="form.organization"
|
||||||
tenantsList.find((t) => t.id === authStore.user?.tenantId)
|
placeholder="请输入所属单位"
|
||||||
?.name || '当前租户'
|
:maxlength="100"
|
||||||
"
|
|
||||||
disabled
|
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="联系方式" name="phone">
|
<a-form-item label="联系方式" name="phone">
|
||||||
@ -312,18 +260,15 @@ import {
|
|||||||
} from "@ant-design/icons-vue"
|
} from "@ant-design/icons-vue"
|
||||||
import { useListRequest } from "@/composables/useListRequest"
|
import { useListRequest } from "@/composables/useListRequest"
|
||||||
import { contestsApi } from "@/api/contests"
|
import { contestsApi } from "@/api/contests"
|
||||||
import { tenantsApi, type Tenant } from "@/api/tenants"
|
|
||||||
import {
|
import {
|
||||||
judgesManagementApi,
|
judgesManagementApi,
|
||||||
type Judge,
|
type Judge,
|
||||||
type QueryJudgeParams,
|
type QueryJudgeParams,
|
||||||
} from "@/api/judges-management"
|
} from "@/api/judges-management"
|
||||||
import { useAuthStore } from "@/stores/auth"
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const contestId = route.params.id ? Number(route.params.id) : null
|
const contestId = route.params.id ? Number(route.params.id) : null
|
||||||
const tenantCode = route.params.tenantCode as string
|
const tenantCode = route.params.tenantCode as string
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
// 检查 contestId 是否有效
|
// 检查 contestId 是否有效
|
||||||
const isValidContestId =
|
const isValidContestId =
|
||||||
@ -352,10 +297,6 @@ const {
|
|||||||
errorMessage: "获取评委列表失败",
|
errorMessage: "获取评委列表失败",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 租户列表
|
|
||||||
const tenantsList = ref<Tenant[]>([])
|
|
||||||
const tenantsLoading = ref(false)
|
|
||||||
|
|
||||||
// 比赛信息
|
// 比赛信息
|
||||||
const contestName = ref("")
|
const contestName = ref("")
|
||||||
|
|
||||||
@ -379,14 +320,14 @@ const form = reactive<{
|
|||||||
username: string
|
username: string
|
||||||
nickname: string
|
nickname: string
|
||||||
gender: "male" | "female" | undefined
|
gender: "male" | "female" | undefined
|
||||||
tenantId: number | undefined
|
organization: string
|
||||||
phone: string
|
phone: string
|
||||||
password: string
|
password: string
|
||||||
}>({
|
}>({
|
||||||
username: "",
|
username: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
gender: undefined,
|
gender: undefined,
|
||||||
tenantId: undefined,
|
organization: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
password: "",
|
password: "",
|
||||||
})
|
})
|
||||||
@ -398,7 +339,7 @@ const rules = computed(() => ({
|
|||||||
: [{ required: true, message: "请输入账号", trigger: "blur" }],
|
: [{ required: true, message: "请输入账号", trigger: "blur" }],
|
||||||
nickname: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
nickname: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||||
gender: [{ required: true, message: "请选择性别", trigger: "change" }],
|
gender: [{ required: true, message: "请选择性别", trigger: "change" }],
|
||||||
tenantId: [{ required: true, message: "请选择所属单位", trigger: "change" }],
|
organization: [{ required: true, message: "请输入所属单位", trigger: "blur" }],
|
||||||
phone: [{ required: true, message: "请输入联系方式", trigger: "blur" }],
|
phone: [{ required: true, message: "请输入联系方式", trigger: "blur" }],
|
||||||
password: isEditing.value
|
password: isEditing.value
|
||||||
? []
|
? []
|
||||||
@ -414,7 +355,8 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "所属单位",
|
title: "所属单位",
|
||||||
key: "tenant",
|
key: "organization",
|
||||||
|
dataIndex: "organization",
|
||||||
width: 150,
|
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 () => {
|
const loadContestInfo = async () => {
|
||||||
// 如果 contestId 无效,跳过加载
|
// 如果 contestId 无效,跳过加载
|
||||||
@ -526,15 +432,9 @@ const handleAdd = () => {
|
|||||||
form.username = ""
|
form.username = ""
|
||||||
form.nickname = ""
|
form.nickname = ""
|
||||||
form.gender = undefined
|
form.gender = undefined
|
||||||
// 默认使用当前用户的租户ID
|
form.organization = ""
|
||||||
form.tenantId = authStore.user?.tenantId
|
|
||||||
form.phone = ""
|
form.phone = ""
|
||||||
form.password = ""
|
form.password = ""
|
||||||
|
|
||||||
// 如果有权限但租户列表为空,尝试加载
|
|
||||||
if (hasTenantReadPermission.value && tenantsList.value.length === 0) {
|
|
||||||
loadTenants()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
@ -545,7 +445,7 @@ const handleEdit = (record: Judge) => {
|
|||||||
form.username = record.username || ""
|
form.username = record.username || ""
|
||||||
form.nickname = record.nickname || ""
|
form.nickname = record.nickname || ""
|
||||||
form.gender = record.gender as "male" | "female" | undefined
|
form.gender = record.gender as "male" | "female" | undefined
|
||||||
form.tenantId = record.tenantId
|
form.organization = record.organization || ""
|
||||||
form.phone = record.phone || ""
|
form.phone = record.phone || ""
|
||||||
form.password = ""
|
form.password = ""
|
||||||
}
|
}
|
||||||
@ -601,6 +501,7 @@ const handleSubmit = async () => {
|
|||||||
await judgesManagementApi.update(editingId.value, {
|
await judgesManagementApi.update(editingId.value, {
|
||||||
nickname: form.nickname,
|
nickname: form.nickname,
|
||||||
gender: form.gender,
|
gender: form.gender,
|
||||||
|
organization: form.organization,
|
||||||
phone: form.phone,
|
phone: form.phone,
|
||||||
...(form.password && { password: form.password }),
|
...(form.password && { password: form.password }),
|
||||||
})
|
})
|
||||||
@ -611,7 +512,7 @@ const handleSubmit = async () => {
|
|||||||
username: form.username,
|
username: form.username,
|
||||||
nickname: form.nickname,
|
nickname: form.nickname,
|
||||||
gender: form.gender!,
|
gender: form.gender!,
|
||||||
tenantId: form.tenantId!,
|
organization: form.organization,
|
||||||
phone: form.phone,
|
phone: form.phone,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
status: "enabled",
|
status: "enabled",
|
||||||
@ -641,7 +542,6 @@ const handleCancel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTenants()
|
|
||||||
loadContestInfo()
|
loadContestInfo()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -650,8 +550,4 @@ onMounted(() => {
|
|||||||
.search-form {
|
.search-form {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tenant-warning {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,645 +1,228 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="review-progress-page">
|
<div class="review-progress-page">
|
||||||
<a-card>
|
<a-card class="mb-4">
|
||||||
<template #title>
|
<template #title>评审进度</template>
|
||||||
<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>
|
||||||
|
|
||||||
<!-- 评委进度表格 -->
|
<!-- Tab栏 -->
|
||||||
<a-card title="评委工作进度" class="judge-progress-card" size="small">
|
<a-tabs v-model:activeKey="activeTab" class="contest-tabs" @change="handleTabChange">
|
||||||
<template #extra>
|
<a-tab-pane key="individual" tab="个人赛" />
|
||||||
<a-space>
|
<a-tab-pane key="team" tab="团队赛" />
|
||||||
<a-button
|
</a-tabs>
|
||||||
v-permission="'review:assign'"
|
|
||||||
type="primary"
|
<!-- 搜索表单 -->
|
||||||
@click="handleAutoAssign"
|
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||||
:loading="autoAssignLoading"
|
<a-form-item label="赛事名称">
|
||||||
>
|
<a-input
|
||||||
<template #icon><ThunderboltOutlined /></template>
|
v-model:value="searchParams.contestName"
|
||||||
自动分配
|
placeholder="请输入赛事名称"
|
||||||
</a-button>
|
allow-clear
|
||||||
<a-button
|
style="width: 200px"
|
||||||
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'"
|
|
||||||
/>
|
/>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'index'">
|
||||||
|
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'pendingCount'">
|
<template v-else-if="column.key === 'reviewStatus'">
|
||||||
<a-tag :color="record.pendingCount > 0 ? 'orange' : 'green'">
|
<a-tag :color="getReviewStatusColor(record)">
|
||||||
{{ record.pendingCount }}
|
{{ getReviewStatusText(record) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</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>
|
</template>
|
||||||
</a-table>
|
</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>
|
|
||||||
|
|
||||||
<!-- 批量分配弹窗 -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 快速分配弹窗 -->
|
|
||||||
<a-modal
|
|
||||||
v-model:open="showQuickAssignModal"
|
|
||||||
title="分配作品给评委"
|
|
||||||
:confirm-loading="quickAssignLoading"
|
|
||||||
width="500px"
|
|
||||||
@ok="handleQuickAssignSubmit"
|
|
||||||
@cancel="showQuickAssignModal = false"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, computed } from "vue"
|
import { ref, reactive, onMounted } from "vue"
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import { message } from "ant-design-vue"
|
import { message } from "ant-design-vue"
|
||||||
import {
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||||
ReloadOutlined,
|
|
||||||
ThunderboltOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
FileOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
} from "@ant-design/icons-vue"
|
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import {
|
import { contestsApi, type Contest } from "@/api/contests"
|
||||||
reviewsApi,
|
|
||||||
judgesApi,
|
|
||||||
worksApi,
|
|
||||||
type ReviewProgress,
|
|
||||||
type ContestJudge,
|
|
||||||
type ContestWork,
|
|
||||||
type UnassignedWork,
|
|
||||||
} from "@/api/contests"
|
|
||||||
|
|
||||||
const route = useRoute()
|
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 loading = ref(false)
|
||||||
const progressData = ref<ReviewProgress | null>(null)
|
const dataSource = ref<Contest[]>([])
|
||||||
const judges = ref<ContestJudge[]>([])
|
const pagination = reactive({
|
||||||
const availableWorks = ref<ContestWork[]>([])
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
// 自动分配
|
total: 0,
|
||||||
const autoAssignLoading = ref(false)
|
|
||||||
|
|
||||||
// 批量分配
|
|
||||||
const showBatchAssignModal = ref(false)
|
|
||||||
const batchAssignLoading = ref(false)
|
|
||||||
const batchAssignForm = reactive({
|
|
||||||
workIds: [] as number[],
|
|
||||||
judgeIds: [] as number[],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 快速分配
|
// 搜索参数
|
||||||
const showQuickAssignModal = ref(false)
|
const searchParams = reactive({
|
||||||
const quickAssignLoading = ref(false)
|
contestName: "",
|
||||||
const quickAssignWork = ref<UnassignedWork | null>(null)
|
reviewStatus: undefined as string | undefined,
|
||||||
const quickAssignJudgeIds = ref<number[]>([])
|
})
|
||||||
|
|
||||||
// 评委表格列
|
// 表格列定义
|
||||||
const judgeColumns = [
|
const columns = [
|
||||||
{
|
{ title: "序号", key: "index", width: 70 },
|
||||||
title: "评委",
|
{ title: "赛事名称", dataIndex: "contestName", key: "contestName", width: 200 },
|
||||||
key: "judgeName",
|
{ title: "评审状态", key: "reviewStatus", width: 100 },
|
||||||
width: 200,
|
{ title: "已评审/作品数", key: "reviewedCount", width: 130 },
|
||||||
},
|
{ title: "评审时间", key: "reviewTime", width: 180 },
|
||||||
{
|
{ title: "操作", key: "action", width: 100, fixed: "right" as const },
|
||||||
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 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 "-"
|
if (!dateStr) return "-"
|
||||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取评审状态颜色
|
// 获取列表数据
|
||||||
const getReviewStatusColor = () => {
|
const fetchList = async () => {
|
||||||
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 () => {
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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) {
|
} catch (error: any) {
|
||||||
message.error(error?.response?.data?.message || "获取评审进度失败")
|
message.error(error?.response?.data?.message || "获取列表失败")
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载评委列表
|
// Tab切换
|
||||||
const fetchJudges = async () => {
|
const handleTabChange = () => {
|
||||||
try {
|
pagination.current = 1
|
||||||
judges.value = await judgesApi.getList(contestId)
|
fetchList()
|
||||||
} catch (error) {
|
|
||||||
console.error("获取评委列表失败", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载作品列表
|
// 搜索
|
||||||
const fetchWorks = async () => {
|
const handleSearch = () => {
|
||||||
try {
|
pagination.current = 1
|
||||||
const response = await worksApi.getList({
|
fetchList()
|
||||||
page: 1,
|
|
||||||
pageSize: 100,
|
|
||||||
contestId,
|
|
||||||
})
|
|
||||||
availableWorks.value = response.list
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取作品列表失败", error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动分配
|
// 重置
|
||||||
const handleAutoAssign = async () => {
|
const handleReset = () => {
|
||||||
autoAssignLoading.value = true
|
searchParams.contestName = ""
|
||||||
try {
|
searchParams.reviewStatus = undefined
|
||||||
const result = await reviewsApi.autoAssignWorks(contestId)
|
pagination.current = 1
|
||||||
message.success(result.message)
|
fetchList()
|
||||||
fetchProgress()
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error?.response?.data?.message || "自动分配失败")
|
|
||||||
} finally {
|
|
||||||
autoAssignLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量分配
|
// 表格分页变化
|
||||||
const handleBatchAssign = async () => {
|
const handleTableChange = (pag: any) => {
|
||||||
if (
|
pagination.current = pag.current
|
||||||
batchAssignForm.workIds.length === 0 ||
|
pagination.pageSize = pag.pageSize
|
||||||
batchAssignForm.judgeIds.length === 0
|
fetchList()
|
||||||
) {
|
|
||||||
message.warning("请选择作品和评委")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
batchAssignLoading.value = true
|
// 查看详情
|
||||||
try {
|
const handleViewDetail = (record: Contest) => {
|
||||||
const result = await reviewsApi.batchAssignWorks(contestId, batchAssignForm)
|
router.push(`/${tenantCode}/contests/reviews/${record.id}/progress?type=${activeTab.value}`)
|
||||||
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 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchProgress()
|
fetchList()
|
||||||
fetchJudges()
|
|
||||||
fetchWorks()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.review-progress-page {
|
.review-progress-page {
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-row {
|
.contest-tabs {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
padding: 16px;
|
background: #fff;
|
||||||
background: #fafafa;
|
padding: 0 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-card {
|
.search-form {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
.progress-item {
|
border-radius: 8px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</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,65 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="works-page">
|
<div class="works-page">
|
||||||
<a-card>
|
<a-card class="mb-4">
|
||||||
<template #title>作品管理</template>
|
<template #title>参赛作品</template>
|
||||||
<template #extra>
|
</a-card>
|
||||||
<a-space>
|
|
||||||
<a-button
|
<!-- Tab栏 -->
|
||||||
v-permission="'contest:read'"
|
<a-tabs v-model:activeKey="activeTab" class="contest-tabs" @change="handleTabChange">
|
||||||
@click="handleExport"
|
<a-tab-pane key="individual" tab="个人赛" />
|
||||||
:loading="exportLoading"
|
<a-tab-pane key="team" tab="团队赛" />
|
||||||
>
|
</a-tabs>
|
||||||
<template #icon><DownloadOutlined /></template>
|
|
||||||
导出作品
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<a-form
|
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||||
:model="searchParams"
|
<a-form-item label="赛事名称">
|
||||||
layout="inline"
|
<a-input
|
||||||
class="search-form"
|
v-model:value="searchParams.contestName"
|
||||||
@finish="handleSearch"
|
placeholder="请输入赛事名称"
|
||||||
>
|
|
||||||
<a-form-item label="比赛">
|
|
||||||
<a-select
|
|
||||||
v-model:value="searchParams.contestId"
|
|
||||||
placeholder="请选择比赛"
|
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
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-form-item>
|
<a-form-item>
|
||||||
@ -83,729 +41,162 @@
|
|||||||
row-key="id"
|
row-key="id"
|
||||||
@change="handleTableChange"
|
@change="handleTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'contestName'">
|
<template v-if="column.key === 'index'">
|
||||||
<a @click="handleViewContest(record.contestId)">{{
|
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||||
record.contest?.contestName || "-"
|
|
||||||
}}</a>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'title'">
|
<template v-else-if="column.key === 'registrationCount'">
|
||||||
<a @click="handleViewDetail(record)">{{ record.title }}</a>
|
{{ activeTab === 'team' ? (record._count?.teams || 0) : (record._count?.registrations || 0) }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'status'">
|
<template v-else-if="column.key === 'worksCount'">
|
||||||
<a-tag :color="getStatusColor(record.status)">
|
{{ record._count?.works || 0 }} / {{ activeTab === 'team' ? (record._count?.teams || 0) : (record._count?.registrations || 0) }}
|
||||||
{{ 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>
|
</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>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'action'">
|
<template v-else-if="column.key === 'action'">
|
||||||
<a-space>
|
<a-button type="link" size="small" @click="handleViewDetail(record)">
|
||||||
<a-button
|
查看详情
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handleViewDetail(record)"
|
|
||||||
>
|
|
||||||
查看
|
|
||||||
</a-button>
|
</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>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
|
||||||
|
|
||||||
<!-- 作品详情抽屉 -->
|
|
||||||
<a-drawer
|
|
||||||
v-model:open="detailDrawerVisible"
|
|
||||||
title="作品详情"
|
|
||||||
width="700"
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue"
|
import { ref, reactive, computed, onMounted } from "vue"
|
||||||
import { useRouter, useRoute } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import { message } from "ant-design-vue"
|
import { message } from "ant-design-vue"
|
||||||
import {
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||||
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 dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
import { contestsApi, type Contest } from "@/api/contests"
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const tenantCode = route.params.tenantCode as string
|
const tenantCode = route.params.tenantCode as string
|
||||||
|
|
||||||
// 从URL获取contestId参数
|
// Tab状态
|
||||||
const urlContestId = route.params.id ? Number(route.params.id) : undefined
|
const activeTab = ref<"individual" | "team">("individual")
|
||||||
|
|
||||||
// 使用列表请求组合函数
|
// 列表状态
|
||||||
const {
|
const loading = ref(false)
|
||||||
loading,
|
const dataSource = ref<Contest[]>([])
|
||||||
dataSource,
|
const pagination = reactive({
|
||||||
pagination,
|
current: 1,
|
||||||
searchParams,
|
pageSize: 10,
|
||||||
fetchList,
|
total: 0,
|
||||||
resetSearch,
|
|
||||||
search,
|
|
||||||
handleTableChange,
|
|
||||||
} = useListRequest<ContestWork, QueryWorkParams>({
|
|
||||||
requestFn: worksApi.getList,
|
|
||||||
defaultSearchParams: { contestId: urlContestId } as QueryWorkParams,
|
|
||||||
defaultPageSize: 10,
|
|
||||||
errorMessage: "获取作品列表失败",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const contests = ref<Contest[]>([])
|
// 搜索参数
|
||||||
const exportLoading = ref(false)
|
const searchParams = reactive({
|
||||||
|
contestName: "",
|
||||||
// 详情抽屉
|
})
|
||||||
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 columns = [
|
const columns = computed(() => [
|
||||||
|
{ title: "序号", key: "index", width: 70 },
|
||||||
|
{ title: "赛事名称", dataIndex: "contestName", key: "contestName", width: 200 },
|
||||||
{
|
{
|
||||||
title: "比赛名称",
|
title: activeTab.value === "team" ? "报名队伍数" : "报名人数",
|
||||||
key: "contestName",
|
key: "registrationCount",
|
||||||
dataIndex: ["contest", "contestName"],
|
width: 120
|
||||||
width: 180,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "作品编号",
|
title: "已递交/应递交作品数",
|
||||||
key: "workNo",
|
key: "worksCount",
|
||||||
dataIndex: "workNo",
|
width: 160
|
||||||
width: 120,
|
|
||||||
},
|
},
|
||||||
{
|
{ title: "赛事时间", key: "contestTime", width: 180 },
|
||||||
title: "作品标题",
|
{ title: "操作", key: "action", width: 100, fixed: "right" as const },
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// 附件表格列
|
// 格式化日期
|
||||||
const attachmentColumns = [
|
const formatDate = (dateStr?: string) => {
|
||||||
{
|
|
||||||
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) => {
|
|
||||||
if (!dateStr) return "-"
|
if (!dateStr) return "-"
|
||||||
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化文件大小
|
// 获取列表数据
|
||||||
const formatFileSize = (size?: string | number) => {
|
const fetchList = async () => {
|
||||||
if (!size) return "-"
|
loading.value = true
|
||||||
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 {}
|
|
||||||
try {
|
try {
|
||||||
return typeof dimensionScores === "string"
|
const contestType = activeTab.value === "individual" ? "individual" : "team"
|
||||||
? JSON.parse(dimensionScores)
|
const response = await contestsApi.getList({
|
||||||
: dimensionScores
|
page: pagination.current,
|
||||||
} catch {
|
pageSize: pagination.pageSize,
|
||||||
return {}
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态颜色
|
// Tab切换
|
||||||
const getStatusColor = (status?: string) => {
|
const handleTabChange = () => {
|
||||||
switch (status) {
|
pagination.current = 1
|
||||||
case "accepted":
|
fetchList()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
search()
|
pagination.current = 1
|
||||||
|
fetchList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置搜索
|
// 重置
|
||||||
const handleReset = () => {
|
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) => {
|
const handleViewDetail = (record: Contest) => {
|
||||||
try {
|
router.push(`/${tenantCode}/contests/works/${record.id}/list?type=${activeTab.value}`)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchContests()
|
fetchList()
|
||||||
if (urlContestId) {
|
|
||||||
searchParams.contestId = urlContestId
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.works-page {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-tabs {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</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">
|
<div class="error-page">
|
||||||
<a-result status="404" title="404" sub-title="抱歉,您访问的页面不存在">
|
<a-result status="404" title="404" sub-title="抱歉,您访问的页面不存在">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button type="primary" @click="$router.push({ path: '/' })">
|
<a-space>
|
||||||
|
<a-button type="primary" @click="handleGoHome">
|
||||||
返回首页
|
返回首页
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button v-if="isAuthenticated" @click="handleLogout">
|
||||||
|
退出登录
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</a-result>
|
</a-result>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user