feat: 评审规则嵌套活动详情、评委人数与提交流程联动
- 活动详情接口补充嵌套 reviewRule(含 judgeCount) - 添加评委抽屉:人数提示对齐评审规则页,满足评委数量方可提交 - 作品分配评委:与规则人数一致,标题与确定按钮逻辑对齐 - 评审规则:去最高最低仅当评委数量大于阈值时可选用 Made-with: Cursor
This commit is contained in:
parent
dfccb07f5a
commit
ba93872922
@ -21,7 +21,9 @@ import com.lesingle.modules.biz.contest.mapper.ContestMapper;
|
|||||||
import com.lesingle.modules.biz.contest.mapper.ContestRegistrationMapper;
|
import com.lesingle.modules.biz.contest.mapper.ContestRegistrationMapper;
|
||||||
import com.lesingle.modules.biz.contest.mapper.ContestWorkMapper;
|
import com.lesingle.modules.biz.contest.mapper.ContestWorkMapper;
|
||||||
import com.lesingle.modules.biz.contest.service.IContestService;
|
import com.lesingle.modules.biz.contest.service.IContestService;
|
||||||
|
import com.lesingle.modules.biz.review.entity.BizContestReviewRule;
|
||||||
import com.lesingle.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
import com.lesingle.modules.biz.review.entity.BizContestWorkJudgeAssignment;
|
||||||
|
import com.lesingle.modules.biz.review.mapper.ContestReviewRuleMapper;
|
||||||
import com.lesingle.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
import com.lesingle.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||||
import com.lesingle.modules.sys.entity.SysTenant;
|
import com.lesingle.modules.sys.entity.SysTenant;
|
||||||
import com.lesingle.modules.sys.mapper.SysTenantMapper;
|
import com.lesingle.modules.sys.mapper.SysTenantMapper;
|
||||||
@ -46,6 +48,7 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
private final ContestRegistrationMapper contestRegistrationMapper;
|
private final ContestRegistrationMapper contestRegistrationMapper;
|
||||||
private final ContestWorkMapper contestWorkMapper;
|
private final ContestWorkMapper contestWorkMapper;
|
||||||
private final ContestWorkJudgeAssignmentMapper contestWorkJudgeAssignmentMapper;
|
private final ContestWorkJudgeAssignmentMapper contestWorkJudgeAssignmentMapper;
|
||||||
|
private final ContestReviewRuleMapper contestReviewRuleMapper;
|
||||||
private final SysTenantMapper sysTenantMapper;
|
private final SysTenantMapper sysTenantMapper;
|
||||||
|
|
||||||
// 支持两种日期格式:ISO 格式 (T 分隔) 和空格分隔格式
|
// 支持两种日期格式:ISO 格式 (T 分隔) 和空格分隔格式
|
||||||
@ -302,6 +305,22 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
|||||||
result.put("contestTenantInfos", tenantInfoList);
|
result.put("contestTenantInfos", tenantInfoList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 嵌套评审规则(与评委端活动详情一致,供前端「每作品评委数」等使用)
|
||||||
|
if (contest.getReviewRuleId() != null) {
|
||||||
|
BizContestReviewRule rule = contestReviewRuleMapper.selectById(contest.getReviewRuleId());
|
||||||
|
if (rule != null) {
|
||||||
|
Map<String, Object> ruleMap = new LinkedHashMap<>();
|
||||||
|
ruleMap.put("id", rule.getId());
|
||||||
|
ruleMap.put("tenantId", rule.getTenantId());
|
||||||
|
ruleMap.put("ruleName", rule.getRuleName());
|
||||||
|
ruleMap.put("ruleDescription", rule.getRuleDescription());
|
||||||
|
ruleMap.put("judgeCount", rule.getJudgeCount());
|
||||||
|
ruleMap.put("dimensions", rule.getDimensions());
|
||||||
|
ruleMap.put("calculationRule", rule.getCalculationRule());
|
||||||
|
result.put("reviewRule", ruleMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,15 +55,21 @@
|
|||||||
<!-- 已选评委区域 -->
|
<!-- 已选评委区域 -->
|
||||||
<a-card size="small">
|
<a-card size="small">
|
||||||
<template #title>
|
<template #title>
|
||||||
<template v-if="judgeCount > 0">
|
<template v-if="perWorkJudgeCount > 0">
|
||||||
已选 {{ selectedJudges.length }} / {{ judgeCount }}
|
已选 {{ selectedJudges.length }} / {{ perWorkJudgeCount }}
|
||||||
<span v-if="selectedJudges.length > judgeCount" class="warning-text">
|
<span
|
||||||
(超出{{ selectedJudges.length - judgeCount }}人)
|
v-if="selectedJudges.length > perWorkJudgeCount"
|
||||||
|
class="warning-text"
|
||||||
|
>
|
||||||
|
(超出{{ selectedJudges.length - perWorkJudgeCount }}人)
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
已选 {{ selectedJudges.length }}
|
已选 {{ selectedJudges.length }}
|
||||||
<span class="hint-text">(评审规则未配置人数)</span>
|
<span v-if="!linkedReviewRule" class="hint-text"
|
||||||
|
>(活动未关联评审规则)</span
|
||||||
|
>
|
||||||
|
<span v-else class="hint-text">(评审规则中「评委数量」未设置)</span>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<a-list :data-source="selectedJudges" :loading="selectedJudgesLoading" size="small">
|
<a-list :data-source="selectedJudges" :loading="selectedJudgesLoading" size="small">
|
||||||
@ -94,7 +100,12 @@
|
|||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button @click="handleCancel">取消</a-button>
|
<a-button @click="handleCancel">取消</a-button>
|
||||||
<a-button type="primary" :loading="submitLoading" :disabled="submitLoading" @click="handleSubmit">
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="submitLoading"
|
||||||
|
:disabled="submitLoading || !canSubmitJudges"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
确定
|
确定
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
@ -108,7 +119,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 } from "@/api/contests"
|
import { judgesApi, reviewRulesApi } from "@/api/contests"
|
||||||
import type { Contest } from "@/api/contests"
|
import type { Contest } from "@/api/contests"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -121,11 +132,48 @@ const emit = defineEmits<{
|
|||||||
success: []
|
success: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 评委数量(从评审规则中获取)
|
/** 活动是否已选择评审规则(reviewRuleId) */
|
||||||
const judgeCount = computed(() => {
|
const linkedReviewRule = computed(
|
||||||
return props.contest.reviewRule?.judgeCount || 0
|
() => props.contest.reviewRuleId != null || props.contest.reviewRule?.id != null
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 从接口额外拉取的规则上的评委数量(兼容旧版活动详情未嵌套 reviewRule) */
|
||||||
|
const resolvedRuleJudgeCount = ref<number | null>(null)
|
||||||
|
|
||||||
|
/** 与「评审规则」页「评委数量」一致:每位作品需要的评委人数 */
|
||||||
|
const perWorkJudgeCount = computed(() => {
|
||||||
|
const fromDetail = props.contest.reviewRule?.judgeCount
|
||||||
|
if (typeof fromDetail === "number" && fromDetail > 0) {
|
||||||
|
return fromDetail
|
||||||
|
}
|
||||||
|
const r = resolvedRuleJudgeCount.value
|
||||||
|
if (typeof r === "number" && r > 0) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 打开抽屉时若详情无嵌套人数则补拉评审规则详情 */
|
||||||
|
async function ensureRuleJudgeCount() {
|
||||||
|
resolvedRuleJudgeCount.value = null
|
||||||
|
const rid = props.contest.reviewRuleId ?? props.contest.reviewRule?.id
|
||||||
|
if (!rid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nested = props.contest.reviewRule?.judgeCount
|
||||||
|
if (typeof nested === "number" && nested > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rule = await reviewRulesApi.getDetail(rid)
|
||||||
|
if (typeof rule.judgeCount === "number" && rule.judgeCount > 0) {
|
||||||
|
resolvedRuleJudgeCount.value = rule.judgeCount
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 忽略 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索参数
|
// 搜索参数
|
||||||
const searchParams = reactive<QueryJudgeParams>({
|
const searchParams = reactive<QueryJudgeParams>({
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -150,6 +198,15 @@ const selectedJudgeIds = ref<number[]>([])
|
|||||||
const selectedJudges = ref<Judge[]>([])
|
const selectedJudges = ref<Judge[]>([])
|
||||||
const selectedJudgesLoading = ref(false)
|
const selectedJudgesLoading = ref(false)
|
||||||
|
|
||||||
|
/** 活动侧已选评委 ≥ 评审规则「评委数量」时才允许提交;规则未配置人数时不拦截 */
|
||||||
|
const canSubmitJudges = computed(() => {
|
||||||
|
const need = perWorkJudgeCount.value
|
||||||
|
if (need <= 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return selectedJudges.value.length >= need
|
||||||
|
})
|
||||||
|
|
||||||
// 提交加载状态
|
// 提交加载状态
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
@ -295,6 +352,13 @@ const handleJudgeTableChange = (pag: any) => {
|
|||||||
|
|
||||||
// 提交(与后端 assigned 对比:删除取消勾选的关联行 + 新增勾选项)
|
// 提交(与后端 assigned 对比:删除取消勾选的关联行 + 新增勾选项)
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
const need = perWorkJudgeCount.value
|
||||||
|
if (need > 0 && selectedJudges.value.length < need) {
|
||||||
|
message.warning(
|
||||||
|
`活动评委至少需要 ${need} 人(与评审规则中的「评委数量」一致)`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
const { assigned } = await judgesApi.getList(props.contestId)
|
const { assigned } = await judgesApi.getList(props.contestId)
|
||||||
@ -368,6 +432,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
ensureRuleJudgeCount()
|
||||||
loadJudges()
|
loadJudges()
|
||||||
loadSelectedJudges()
|
loadSelectedJudges()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,51 +3,34 @@
|
|||||||
<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:update'" type="primary" @click="handleAdd">新建规则</a-button>
|
||||||
v-permission="'contest:update'"
|
|
||||||
type="primary"
|
|
||||||
@click="handleAdd"
|
|
||||||
>新建规则</a-button
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<a-form
|
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||||
:model="searchParams"
|
|
||||||
layout="inline"
|
|
||||||
class="search-form"
|
|
||||||
@finish="handleSearch"
|
|
||||||
>
|
|
||||||
<a-form-item label="规则名称">
|
<a-form-item label="规则名称">
|
||||||
<a-input
|
<a-input v-model:value="searchParams.ruleName" placeholder="请输入规则名称" allow-clear style="width: 200px" />
|
||||||
v-model:value="searchParams.ruleName"
|
|
||||||
placeholder="请输入规则名称"
|
|
||||||
allow-clear
|
|
||||||
style="width: 200px"
|
|
||||||
/>
|
|
||||||
</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">
|
||||||
<template #icon><SearchOutlined /></template>
|
<template #icon>
|
||||||
|
<SearchOutlined />
|
||||||
|
</template>
|
||||||
搜索
|
搜索
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button style="margin-left: 8px" @click="handleReset">
|
<a-button style="margin-left: 8px" @click="handleReset">
|
||||||
<template #icon><ReloadOutlined /></template>
|
<template #icon>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</template>
|
||||||
重置
|
重置
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<a-table
|
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination" row-key="id"
|
||||||
:columns="columns"
|
@change="handleTableChange">
|
||||||
:data-source="dataSource"
|
|
||||||
:loading="loading"
|
|
||||||
:pagination="pagination"
|
|
||||||
row-key="id"
|
|
||||||
@change="handleTableChange"
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column, record, index }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<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 }}
|
||||||
@ -69,25 +52,18 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'contests'">
|
<template v-else-if="column.key === 'contests'">
|
||||||
<span v-if="record.contests && record.contests.length > 0">
|
<span v-if="record.contests && record.contests.length > 0">
|
||||||
{{ record.contests.map((c: any) => c.contestName).join("、") }}
|
{{record.contests.map((c: any) => c.contestName).join("、")}}
|
||||||
</span>
|
</span>
|
||||||
<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-space>
|
||||||
<a-button
|
<a-button v-permission="'contest:update'" type="link" size="small" @click="handleEdit(record)">
|
||||||
v-permission="'contest:update'"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handleEdit(record)"
|
|
||||||
>
|
|
||||||
编辑
|
编辑
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-popconfirm
|
<a-popconfirm v-permission="'contest:update'"
|
||||||
v-permission="'contest:update'"
|
|
||||||
:title="record.contests?.length > 0 ? `该规则已关联${record.contests.length}个活动,确定删除吗?` : '确定要删除这个评审规则吗?'"
|
:title="record.contests?.length > 0 ? `该规则已关联${record.contests.length}个活动,确定删除吗?` : '确定要删除这个评审规则吗?'"
|
||||||
@confirm="handleDelete(record.id)"
|
@confirm="handleDelete(record.id)">
|
||||||
>
|
|
||||||
<a-button type="link" danger size="small">删除</a-button>
|
<a-button type="link" danger size="small">删除</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-space>
|
</a-space>
|
||||||
@ -96,117 +72,60 @@
|
|||||||
</a-table>
|
</a-table>
|
||||||
|
|
||||||
<!-- 新增/编辑评审规则抽屉 -->
|
<!-- 新增/编辑评审规则抽屉 -->
|
||||||
<a-drawer
|
<a-drawer v-model:open="modalVisible" :title="isEditing ? '编辑规则' : '新建规则'" placement="right" width="600px"
|
||||||
v-model:open="modalVisible"
|
:footer-style="{ textAlign: 'right' }">
|
||||||
:title="isEditing ? '编辑规则' : '新建规则'"
|
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||||
placement="right"
|
|
||||||
width="600px"
|
|
||||||
:footer-style="{ textAlign: 'right' }"
|
|
||||||
>
|
|
||||||
<a-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
:label-col="{ span: 6 }"
|
|
||||||
:wrapper-col="{ span: 18 }"
|
|
||||||
>
|
|
||||||
<a-form-item label="规则名称" name="ruleName">
|
<a-form-item label="规则名称" name="ruleName">
|
||||||
<a-input
|
<a-input v-model:value="form.ruleName" placeholder="请输入规则名称" :maxlength="100" />
|
||||||
v-model:value="form.ruleName"
|
|
||||||
placeholder="请输入规则名称"
|
|
||||||
:maxlength="100"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="规则说明" name="ruleDescription">
|
<a-form-item label="规则说明" name="ruleDescription">
|
||||||
<a-textarea
|
<a-textarea v-model:value="form.ruleDescription" placeholder="请输入规则说明" :rows="4" :maxlength="500" />
|
||||||
v-model:value="form.ruleDescription"
|
|
||||||
placeholder="请输入规则说明"
|
|
||||||
:rows="4"
|
|
||||||
:maxlength="500"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="评委数量" name="judgeCount">
|
<a-form-item label="评委数量" name="judgeCount">
|
||||||
<a-input-number
|
<a-input-number v-model:value="form.judgeCount" placeholder="请输入评委数量" :min="1" style="width: 100%" />
|
||||||
v-model:value="form.judgeCount"
|
|
||||||
placeholder="请输入评委数量"
|
|
||||||
:min="1"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="评分标准" name="dimensions" required>
|
<a-form-item label="评分标准" name="dimensions" required>
|
||||||
<div class="scoring-standards">
|
<div class="scoring-standards">
|
||||||
<div
|
<div v-for="(dimension, index) in form.dimensions" :key="index" class="scoring-standard-item">
|
||||||
v-for="(dimension, index) in form.dimensions"
|
|
||||||
:key="index"
|
|
||||||
class="scoring-standard-item"
|
|
||||||
>
|
|
||||||
<a-row :gutter="16" class="scoring-input-row">
|
<a-row :gutter="16" class="scoring-input-row">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-input
|
<a-input v-model:value="dimension.name" placeholder="请输入评分项,如创意分、技术分" :maxlength="50" />
|
||||||
v-model:value="dimension.name"
|
|
||||||
placeholder="请输入评分项,如创意分、技术分"
|
|
||||||
:maxlength="50"
|
|
||||||
/>
|
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="8">
|
<a-col :span="8">
|
||||||
<a-input-number
|
<a-input-number v-model:value="dimension.percentage" placeholder="请输入分值" :min="0" :max="100"
|
||||||
v-model:value="dimension.percentage"
|
:precision="2" style="width: 100%">
|
||||||
placeholder="请输入分值"
|
|
||||||
:min="0"
|
|
||||||
:max="100"
|
|
||||||
:precision="2"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<template #addonAfter>%</template>
|
<template #addonAfter>%</template>
|
||||||
</a-input-number>
|
</a-input-number>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col
|
<a-col :span="4" style="
|
||||||
:span="4"
|
|
||||||
style="
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
"
|
">
|
||||||
>
|
<a-button v-if="form.dimensions.length > 1" type="text" danger @click="removeDimension(index)"
|
||||||
<a-button
|
style="padding: 0; height: auto">
|
||||||
v-if="form.dimensions.length > 1"
|
<template #icon>
|
||||||
type="text"
|
<DeleteOutlined />
|
||||||
danger
|
</template>
|
||||||
@click="removeDimension(index)"
|
|
||||||
style="padding: 0; height: auto"
|
|
||||||
>
|
|
||||||
<template #icon><DeleteOutlined /></template>
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-textarea
|
<a-textarea v-model:value="dimension.description" placeholder="请输入评分标准说明" :rows="2" :maxlength="150"
|
||||||
v-model:value="dimension.description"
|
show-count style="margin-top: 12px" />
|
||||||
placeholder="请输入评分标准说明"
|
|
||||||
:rows="2"
|
|
||||||
:maxlength="150"
|
|
||||||
show-count
|
|
||||||
style="margin-top: 12px"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<a-button type="dashed" style="width: 100%" @click="addDimension">
|
<a-button type="dashed" style="width: 100%" @click="addDimension">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
添加评分标准
|
添加评分标准
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="分数计算" name="calculationRule">
|
<a-form-item label="分数计算" name="calculationRule">
|
||||||
<a-radio-group
|
<a-radio-group v-model:value="form.calculationRule" style="width: 100%">
|
||||||
v-model:value="form.calculationRule"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<a-space direction="vertical" :size="8" style="width: 100%">
|
<a-space direction="vertical" :size="8" style="width: 100%">
|
||||||
<a-radio
|
<a-radio v-for="option in availableCalculationOptions" :key="option.value" :value="option.value"
|
||||||
v-for="option in availableCalculationOptions"
|
:disabled="option.disabled" style="display: block; margin-bottom: 8px">
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
:disabled="option.disabled"
|
|
||||||
style="display: block; margin-bottom: 8px"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</a-radio>
|
</a-radio>
|
||||||
</a-space>
|
</a-space>
|
||||||
@ -214,12 +133,8 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-button style="margin-right: 8px" @click="handleCancel"
|
<a-button style="margin-right: 8px" @click="handleCancel">取消</a-button>
|
||||||
>取消</a-button
|
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</a-button>
|
||||||
>
|
|
||||||
<a-button type="primary" :loading="submitLoading" @click="handleSubmit"
|
|
||||||
>确定</a-button
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
</div>
|
</div>
|
||||||
@ -286,25 +201,21 @@ const calculationOptions = [
|
|||||||
{ value: "removeMin", label: "去掉最低分的均值" },
|
{ value: "removeMin", label: "去掉最低分的均值" },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 根据评委数量动态计算可用的分数计算选项
|
// 根据评委数量动态计算可用的分数计算选项(去掉最高最低需评委数 > 4)
|
||||||
const availableCalculationOptions = computed(() => {
|
const availableCalculationOptions = computed(() => {
|
||||||
const judgeCount = form.judgeCount || 0
|
const judgeCount = form.judgeCount ?? 0
|
||||||
if (judgeCount === 1) {
|
return calculationOptions.map((opt) => {
|
||||||
return calculationOptions.map((opt) => ({
|
let disabled = false
|
||||||
...opt,
|
if (judgeCount === 1) {
|
||||||
disabled: opt.value !== "average",
|
disabled = opt.value !== "average"
|
||||||
}))
|
} else if (judgeCount === 2) {
|
||||||
} else if (judgeCount === 2) {
|
disabled = opt.value === "removeMin" || opt.value === "removeMaxMin"
|
||||||
return calculationOptions.map((opt) => ({
|
} else {
|
||||||
...opt,
|
disabled =
|
||||||
disabled: opt.value === "removeMin",
|
opt.value === "removeMaxMin" ? judgeCount <= 3 : false
|
||||||
}))
|
}
|
||||||
} else {
|
return { ...opt, disabled }
|
||||||
return calculationOptions.map((opt) => ({
|
})
|
||||||
...opt,
|
|
||||||
disabled: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听评委数量变化,自动调整分数计算选项
|
// 监听评委数量变化,自动调整分数计算选项
|
||||||
@ -315,6 +226,12 @@ watch(
|
|||||||
form.calculationRule = "average"
|
form.calculationRule = "average"
|
||||||
} else if (newVal === 2 && form.calculationRule === "removeMin") {
|
} else if (newVal === 2 && form.calculationRule === "removeMin") {
|
||||||
form.calculationRule = "average"
|
form.calculationRule = "average"
|
||||||
|
} else if (
|
||||||
|
newVal !== undefined &&
|
||||||
|
newVal <= 4 &&
|
||||||
|
form.calculationRule === "removeMaxMin"
|
||||||
|
) {
|
||||||
|
form.calculationRule = "average"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -472,6 +389,13 @@ const handleEdit = async (record: ReviewRule) => {
|
|||||||
calculationRule = "removeMin"
|
calculationRule = "removeMin"
|
||||||
}
|
}
|
||||||
form.calculationRule = calculationRule
|
form.calculationRule = calculationRule
|
||||||
|
// 评委≤4 时不允许「去最高最低」,回退为全部均值
|
||||||
|
if (
|
||||||
|
(form.judgeCount ?? 0) <= 4 &&
|
||||||
|
form.calculationRule === "removeMaxMin"
|
||||||
|
) {
|
||||||
|
form.calculationRule = "average"
|
||||||
|
}
|
||||||
|
|
||||||
// 解析评分标准
|
// 解析评分标准
|
||||||
try {
|
try {
|
||||||
@ -482,28 +406,28 @@ const handleEdit = async (record: ReviewRule) => {
|
|||||||
form.dimensions =
|
form.dimensions =
|
||||||
Array.isArray(dims) && dims.length > 0
|
Array.isArray(dims) && dims.length > 0
|
||||||
? dims.map((d: any) => {
|
? dims.map((d: any) => {
|
||||||
// 兼容旧数据格式
|
// 兼容旧数据格式
|
||||||
if (d.percentage !== undefined) {
|
if (d.percentage !== undefined) {
|
||||||
return {
|
return {
|
||||||
name: d.name || "",
|
name: d.name || "",
|
||||||
percentage: d.percentage || 0,
|
percentage: d.percentage || 0,
|
||||||
description: d.description || "",
|
description: d.description || "",
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 旧格式:maxScore 和 weight,转换为 percentage
|
|
||||||
const total = dims.reduce(
|
|
||||||
(sum: number, item: any) => sum + (item.weight || 1),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
const percentage =
|
|
||||||
total > 0 ? ((d.weight || 1) / total) * 100 : 0
|
|
||||||
return {
|
|
||||||
name: d.name || "",
|
|
||||||
percentage: percentage,
|
|
||||||
description: d.description || "",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
|
// 旧格式:maxScore 和 weight,转换为 percentage
|
||||||
|
const total = dims.reduce(
|
||||||
|
(sum: number, item: any) => sum + (item.weight || 1),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const percentage =
|
||||||
|
total > 0 ? ((d.weight || 1) / total) * 100 : 0
|
||||||
|
return {
|
||||||
|
name: d.name || "",
|
||||||
|
percentage: percentage,
|
||||||
|
description: d.description || "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
: [{ name: "", percentage: 0, description: "" }]
|
: [{ name: "", percentage: 0, description: "" }]
|
||||||
} catch {
|
} catch {
|
||||||
form.dimensions = [{ name: "", percentage: 0, description: "" }]
|
form.dimensions = [{ name: "", percentage: 0, description: "" }]
|
||||||
@ -570,7 +494,7 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
message.error(
|
message.error(
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
(isEditing.value ? "编辑失败" : "创建失败")
|
(isEditing.value ? "编辑失败" : "创建失败")
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
@ -589,24 +513,62 @@ $primary: #6366f1;
|
|||||||
|
|
||||||
.review-rules-page {
|
.review-rules-page {
|
||||||
:deep(.ant-card) {
|
:deep(.ant-card) {
|
||||||
border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 16px;
|
border: none;
|
||||||
.ant-card-head { border-bottom: none; .ant-card-head-title { font-size: 18px; font-weight: 600; } }
|
border-radius: 12px;
|
||||||
.ant-card-body { padding: 0; }
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
.ant-card-head-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-table-wrapper) {
|
:deep(.ant-table-wrapper) {
|
||||||
background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden;
|
background: #fff;
|
||||||
.ant-table-thead > tr > th { background: #fafafa; font-weight: 600; }
|
border-radius: 12px;
|
||||||
.ant-table-tbody > tr:hover > td { background: rgba($primary, 0.03); }
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
.ant-table-pagination { padding: 16px; margin: 0; }
|
overflow: hidden;
|
||||||
|
|
||||||
|
.ant-table-thead>tr>th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody>tr:hover>td {
|
||||||
|
background: rgba($primary, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-pagination {
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-desc { font-size: 12px; color: #6b7280; }
|
.text-desc {
|
||||||
.text-muted { color: #d1d5db; }
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
margin-bottom: 16px; padding: 20px 24px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
margin-bottom: 16px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoring-standards {
|
.scoring-standards {
|
||||||
|
|||||||
@ -208,7 +208,18 @@
|
|||||||
<!-- 已选评委区域 -->
|
<!-- 已选评委区域 -->
|
||||||
<a-card size="small">
|
<a-card size="small">
|
||||||
<template #title>
|
<template #title>
|
||||||
已选 {{ selectedJudgeRows.length }} 位评委
|
<template v-if="perWorkJudgeCount > 0">
|
||||||
|
已选 {{ selectedJudgeRows.length }} / {{ perWorkJudgeCount }} 位评委
|
||||||
|
<span
|
||||||
|
v-if="selectedJudgeRows.length > perWorkJudgeCount"
|
||||||
|
class="assign-warning-text"
|
||||||
|
>
|
||||||
|
(超出{{ selectedJudgeRows.length - perWorkJudgeCount }}人)
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
已选 {{ selectedJudgeRows.length }} 位评委
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<a-list :data-source="selectedJudgeRows" size="small">
|
<a-list :data-source="selectedJudgeRows" size="small">
|
||||||
<template #renderItem="{ item }">
|
<template #renderItem="{ item }">
|
||||||
@ -241,7 +252,12 @@
|
|||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button @click="handleAssignDrawerClose">取消</a-button>
|
<a-button @click="handleAssignDrawerClose">取消</a-button>
|
||||||
<a-button type="primary" :loading="assignLoading" @click="handleConfirmAssign">
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="assignLoading"
|
||||||
|
:disabled="assignLoading || !canConfirmAssign"
|
||||||
|
@click="handleConfirmAssign"
|
||||||
|
>
|
||||||
确定
|
确定
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
@ -273,6 +289,8 @@ import {
|
|||||||
worksApi,
|
worksApi,
|
||||||
reviewsApi,
|
reviewsApi,
|
||||||
judgesApi,
|
judgesApi,
|
||||||
|
reviewRulesApi,
|
||||||
|
type Contest,
|
||||||
type ContestWork,
|
type ContestWork,
|
||||||
type ContestJudge,
|
type ContestJudge,
|
||||||
} from "@/api/contests"
|
} from "@/api/contests"
|
||||||
@ -303,8 +321,22 @@ const fetchDetailStats = async () => {
|
|||||||
try { detailStats.value = await worksApi.getStats(contestId) } catch { /* */ }
|
try { detailStats.value = await worksApi.getStats(contestId) } catch { /* */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 活动名称
|
// 活动名称与详情(评审规则「评委数量」与添加评委侧一致)
|
||||||
const contestName = ref("")
|
const contestName = ref("")
|
||||||
|
const contestDetail = ref<Contest | null>(null)
|
||||||
|
const resolvedRuleJudgeCount = ref<number | null>(null)
|
||||||
|
|
||||||
|
const perWorkJudgeCount = computed(() => {
|
||||||
|
const fromDetail = contestDetail.value?.reviewRule?.judgeCount
|
||||||
|
if (typeof fromDetail === "number" && fromDetail > 0) {
|
||||||
|
return fromDetail
|
||||||
|
}
|
||||||
|
const r = resolvedRuleJudgeCount.value
|
||||||
|
if (typeof r === "number" && r > 0) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
// 租户列表
|
// 租户列表
|
||||||
const tenants = ref<Tenant[]>([])
|
const tenants = ref<Tenant[]>([])
|
||||||
@ -381,6 +413,15 @@ const judgeSearchParams = reactive({
|
|||||||
const selectedJudgeKeys = ref<number[]>([])
|
const selectedJudgeKeys = ref<number[]>([])
|
||||||
const selectedJudgeRows = ref<ContestJudge[]>([])
|
const selectedJudgeRows = ref<ContestJudge[]>([])
|
||||||
|
|
||||||
|
/** 分配所选评委人数 ≥ 规则要求时才可确定(规则未配置人数时不拦截) */
|
||||||
|
const canConfirmAssign = computed(() => {
|
||||||
|
const need = perWorkJudgeCount.value
|
||||||
|
if (need <= 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return selectedJudgeRows.value.length >= need
|
||||||
|
})
|
||||||
|
|
||||||
/** 将作品上已分配但不在活动显式名单中的评委并入列表,避免误删历史分配 */
|
/** 将作品上已分配但不在活动显式名单中的评委并入列表,避免误删历史分配 */
|
||||||
function mergeOrphanJudges(record: ContestWork, base: ContestJudge[]): ContestJudge[] {
|
function mergeOrphanJudges(record: ContestWork, base: ContestJudge[]): ContestJudge[] {
|
||||||
const seen = new Set(base.map((j) => j.judgeId))
|
const seen = new Set(base.map((j) => j.judgeId))
|
||||||
@ -488,6 +529,24 @@ const fetchContestInfo = async () => {
|
|||||||
try {
|
try {
|
||||||
const contest = await contestsApi.getDetail(contestId)
|
const contest = await contestsApi.getDetail(contestId)
|
||||||
contestName.value = contest.contestName
|
contestName.value = contest.contestName
|
||||||
|
contestDetail.value = contest
|
||||||
|
resolvedRuleJudgeCount.value = null
|
||||||
|
const rid = contest.reviewRuleId ?? contest.reviewRule?.id
|
||||||
|
if (!rid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nested = contest.reviewRule?.judgeCount
|
||||||
|
if (typeof nested === "number" && nested > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rule = await reviewRulesApi.getDetail(rid)
|
||||||
|
if (typeof rule.judgeCount === "number" && rule.judgeCount > 0) {
|
||||||
|
resolvedRuleJudgeCount.value = rule.judgeCount
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 忽略 */
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取活动信息失败", error)
|
console.error("获取活动信息失败", error)
|
||||||
}
|
}
|
||||||
@ -647,6 +706,14 @@ const handleJudgeTableChange = (pag: any) => {
|
|||||||
|
|
||||||
// 确认分配评委(可与列表同步:移除的评委在后端删除分配记录)
|
// 确认分配评委(可与列表同步:移除的评委在后端删除分配记录)
|
||||||
const handleConfirmAssign = async () => {
|
const handleConfirmAssign = async () => {
|
||||||
|
const need = perWorkJudgeCount.value
|
||||||
|
if (need > 0 && selectedJudgeRows.value.length < need) {
|
||||||
|
message.warning(
|
||||||
|
`每位作品至少需要 ${need} 位评委(与评审规则中的「评委数量」一致)`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isBatchAssign.value && selectedJudgeRows.value.length === 0) {
|
if (isBatchAssign.value && selectedJudgeRows.value.length === 0) {
|
||||||
message.warning("批量分配请至少选择一名评委")
|
message.warning("批量分配请至少选择一名评委")
|
||||||
return
|
return
|
||||||
@ -826,6 +893,11 @@ $primary: #6366f1;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assign-judge-drawer {
|
.assign-judge-drawer {
|
||||||
|
.assign-warning-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.drawer-footer {
|
.drawer-footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user