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.ContestWorkMapper;
|
||||
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.mapper.ContestReviewRuleMapper;
|
||||
import com.lesingle.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper;
|
||||
import com.lesingle.modules.sys.entity.SysTenant;
|
||||
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 ContestWorkMapper contestWorkMapper;
|
||||
private final ContestWorkJudgeAssignmentMapper contestWorkJudgeAssignmentMapper;
|
||||
private final ContestReviewRuleMapper contestReviewRuleMapper;
|
||||
private final SysTenantMapper sysTenantMapper;
|
||||
|
||||
// 支持两种日期格式:ISO 格式 (T 分隔) 和空格分隔格式
|
||||
@ -302,6 +305,22 @@ public class ContestServiceImpl extends ServiceImpl<ContestMapper, BizContest> i
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -55,15 +55,21 @@
|
||||
<!-- 已选评委区域 -->
|
||||
<a-card size="small">
|
||||
<template #title>
|
||||
<template v-if="judgeCount > 0">
|
||||
已选 {{ selectedJudges.length }} / {{ judgeCount }}
|
||||
<span v-if="selectedJudges.length > judgeCount" class="warning-text">
|
||||
(超出{{ selectedJudges.length - judgeCount }}人)
|
||||
<template v-if="perWorkJudgeCount > 0">
|
||||
已选 {{ selectedJudges.length }} / {{ perWorkJudgeCount }}
|
||||
<span
|
||||
v-if="selectedJudges.length > perWorkJudgeCount"
|
||||
class="warning-text"
|
||||
>
|
||||
(超出{{ selectedJudges.length - perWorkJudgeCount }}人)
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
已选 {{ selectedJudges.length }}
|
||||
<span class="hint-text">(评审规则未配置人数)</span>
|
||||
<span v-if="!linkedReviewRule" class="hint-text"
|
||||
>(活动未关联评审规则)</span
|
||||
>
|
||||
<span v-else class="hint-text">(评审规则中「评委数量」未设置)</span>
|
||||
</template>
|
||||
</template>
|
||||
<a-list :data-source="selectedJudges" :loading="selectedJudgesLoading" size="small">
|
||||
@ -94,7 +100,12 @@
|
||||
<div class="drawer-footer">
|
||||
<a-space>
|
||||
<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-space>
|
||||
@ -108,7 +119,7 @@ import { message } from "ant-design-vue"
|
||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
||||
import type { Judge, QueryJudgeParams } from "@/api/judges-management"
|
||||
import { judgesManagementApi } from "@/api/judges-management"
|
||||
import { judgesApi } from "@/api/contests"
|
||||
import { judgesApi, reviewRulesApi } from "@/api/contests"
|
||||
import type { Contest } from "@/api/contests"
|
||||
|
||||
interface Props {
|
||||
@ -121,11 +132,48 @@ const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
// 评委数量(从评审规则中获取)
|
||||
const judgeCount = computed(() => {
|
||||
return props.contest.reviewRule?.judgeCount || 0
|
||||
/** 活动是否已选择评审规则(reviewRuleId) */
|
||||
const linkedReviewRule = computed(
|
||||
() => 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>({
|
||||
page: 1,
|
||||
@ -150,6 +198,15 @@ const selectedJudgeIds = ref<number[]>([])
|
||||
const selectedJudges = ref<Judge[]>([])
|
||||
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)
|
||||
|
||||
@ -295,6 +352,13 @@ const handleJudgeTableChange = (pag: any) => {
|
||||
|
||||
// 提交(与后端 assigned 对比:删除取消勾选的关联行 + 新增勾选项)
|
||||
const handleSubmit = async () => {
|
||||
const need = perWorkJudgeCount.value
|
||||
if (need > 0 && selectedJudges.value.length < need) {
|
||||
message.warning(
|
||||
`活动评委至少需要 ${need} 人(与评审规则中的「评委数量」一致)`,
|
||||
)
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const { assigned } = await judgesApi.getList(props.contestId)
|
||||
@ -368,6 +432,7 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
ensureRuleJudgeCount()
|
||||
loadJudges()
|
||||
loadSelectedJudges()
|
||||
})
|
||||
|
||||
@ -3,51 +3,34 @@
|
||||
<a-card class="mb-4">
|
||||
<template #title>评审规则</template>
|
||||
<template #extra>
|
||||
<a-button
|
||||
v-permission="'contest:update'"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>新建规则</a-button
|
||||
>
|
||||
<a-button v-permission="'contest:update'" type="primary" @click="handleAdd">新建规则</a-button>
|
||||
</template>
|
||||
</a-card>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<a-form
|
||||
:model="searchParams"
|
||||
layout="inline"
|
||||
class="search-form"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||
<a-form-item label="规则名称">
|
||||
<a-input
|
||||
v-model:value="searchParams.ruleName"
|
||||
placeholder="请输入规则名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
<a-input v-model:value="searchParams.ruleName" placeholder="请输入规则名称" allow-clear style="width: 200px" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button style="margin-left: 8px" @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<a-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 }}
|
||||
@ -69,25 +52,18 @@
|
||||
</template>
|
||||
<template v-else-if="column.key === 'contests'">
|
||||
<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 v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-permission="'contest:update'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
<a-button v-permission="'contest:update'" type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
v-permission="'contest:update'"
|
||||
<a-popconfirm v-permission="'contest:update'"
|
||||
: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-popconfirm>
|
||||
</a-space>
|
||||
@ -96,117 +72,60 @@
|
||||
</a-table>
|
||||
|
||||
<!-- 新增/编辑评审规则抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="modalVisible"
|
||||
:title="isEditing ? '编辑规则' : '新建规则'"
|
||||
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-drawer v-model:open="modalVisible" :title="isEditing ? '编辑规则' : '新建规则'" 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-input
|
||||
v-model:value="form.ruleName"
|
||||
placeholder="请输入规则名称"
|
||||
:maxlength="100"
|
||||
/>
|
||||
<a-input v-model:value="form.ruleName" placeholder="请输入规则名称" :maxlength="100" />
|
||||
</a-form-item>
|
||||
<a-form-item label="规则说明" name="ruleDescription">
|
||||
<a-textarea
|
||||
v-model:value="form.ruleDescription"
|
||||
placeholder="请输入规则说明"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
/>
|
||||
<a-textarea v-model:value="form.ruleDescription" placeholder="请输入规则说明" :rows="4" :maxlength="500" />
|
||||
</a-form-item>
|
||||
<a-form-item label="评委数量" name="judgeCount">
|
||||
<a-input-number
|
||||
v-model:value="form.judgeCount"
|
||||
placeholder="请输入评委数量"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-input-number v-model:value="form.judgeCount" placeholder="请输入评委数量" :min="1" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="评分标准" name="dimensions" required>
|
||||
<div class="scoring-standards">
|
||||
<div
|
||||
v-for="(dimension, index) in form.dimensions"
|
||||
:key="index"
|
||||
class="scoring-standard-item"
|
||||
>
|
||||
<div v-for="(dimension, index) in form.dimensions" :key="index" class="scoring-standard-item">
|
||||
<a-row :gutter="16" class="scoring-input-row">
|
||||
<a-col :span="12">
|
||||
<a-input
|
||||
v-model:value="dimension.name"
|
||||
placeholder="请输入评分项,如创意分、技术分"
|
||||
:maxlength="50"
|
||||
/>
|
||||
<a-input v-model:value="dimension.name" placeholder="请输入评分项,如创意分、技术分" :maxlength="50" />
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-input-number
|
||||
v-model:value="dimension.percentage"
|
||||
placeholder="请输入分值"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-input-number v-model:value="dimension.percentage" placeholder="请输入分值" :min="0" :max="100"
|
||||
:precision="2" style="width: 100%">
|
||||
<template #addonAfter>%</template>
|
||||
</a-input-number>
|
||||
</a-col>
|
||||
<a-col
|
||||
:span="4"
|
||||
style="
|
||||
<a-col :span="4" style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<a-button
|
||||
v-if="form.dimensions.length > 1"
|
||||
type="text"
|
||||
danger
|
||||
@click="removeDimension(index)"
|
||||
style="padding: 0; height: auto"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
">
|
||||
<a-button v-if="form.dimensions.length > 1" type="text" danger @click="removeDimension(index)"
|
||||
style="padding: 0; height: auto">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-textarea
|
||||
v-model:value="dimension.description"
|
||||
placeholder="请输入评分标准说明"
|
||||
:rows="2"
|
||||
:maxlength="150"
|
||||
show-count
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
<a-textarea v-model:value="dimension.description" placeholder="请输入评分标准说明" :rows="2" :maxlength="150"
|
||||
show-count style="margin-top: 12px" />
|
||||
</div>
|
||||
<a-button type="dashed" style="width: 100%" @click="addDimension">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加评分标准
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="分数计算" name="calculationRule">
|
||||
<a-radio-group
|
||||
v-model:value="form.calculationRule"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-radio-group v-model:value="form.calculationRule" style="width: 100%">
|
||||
<a-space direction="vertical" :size="8" style="width: 100%">
|
||||
<a-radio
|
||||
v-for="option in availableCalculationOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:disabled="option.disabled"
|
||||
style="display: block; margin-bottom: 8px"
|
||||
>
|
||||
<a-radio v-for="option in availableCalculationOptions" :key="option.value" :value="option.value"
|
||||
:disabled="option.disabled" style="display: block; margin-bottom: 8px">
|
||||
{{ option.label }}
|
||||
</a-radio>
|
||||
</a-space>
|
||||
@ -214,12 +133,8 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button style="margin-right: 8px" @click="handleCancel"
|
||||
>取消</a-button
|
||||
>
|
||||
<a-button type="primary" :loading="submitLoading" @click="handleSubmit"
|
||||
>确定</a-button
|
||||
>
|
||||
<a-button style="margin-right: 8px" @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</a-button>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</div>
|
||||
@ -286,25 +201,21 @@ const calculationOptions = [
|
||||
{ value: "removeMin", label: "去掉最低分的均值" },
|
||||
]
|
||||
|
||||
// 根据评委数量动态计算可用的分数计算选项
|
||||
// 根据评委数量动态计算可用的分数计算选项(去掉最高最低需评委数 > 4)
|
||||
const availableCalculationOptions = computed(() => {
|
||||
const judgeCount = form.judgeCount || 0
|
||||
const judgeCount = form.judgeCount ?? 0
|
||||
return calculationOptions.map((opt) => {
|
||||
let disabled = false
|
||||
if (judgeCount === 1) {
|
||||
return calculationOptions.map((opt) => ({
|
||||
...opt,
|
||||
disabled: opt.value !== "average",
|
||||
}))
|
||||
disabled = opt.value !== "average"
|
||||
} else if (judgeCount === 2) {
|
||||
return calculationOptions.map((opt) => ({
|
||||
...opt,
|
||||
disabled: opt.value === "removeMin",
|
||||
}))
|
||||
disabled = opt.value === "removeMin" || opt.value === "removeMaxMin"
|
||||
} else {
|
||||
return calculationOptions.map((opt) => ({
|
||||
...opt,
|
||||
disabled: false,
|
||||
}))
|
||||
disabled =
|
||||
opt.value === "removeMaxMin" ? judgeCount <= 3 : false
|
||||
}
|
||||
return { ...opt, disabled }
|
||||
})
|
||||
})
|
||||
|
||||
// 监听评委数量变化,自动调整分数计算选项
|
||||
@ -315,6 +226,12 @@ watch(
|
||||
form.calculationRule = "average"
|
||||
} else if (newVal === 2 && form.calculationRule === "removeMin") {
|
||||
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"
|
||||
}
|
||||
form.calculationRule = calculationRule
|
||||
// 评委≤4 时不允许「去最高最低」,回退为全部均值
|
||||
if (
|
||||
(form.judgeCount ?? 0) <= 4 &&
|
||||
form.calculationRule === "removeMaxMin"
|
||||
) {
|
||||
form.calculationRule = "average"
|
||||
}
|
||||
|
||||
// 解析评分标准
|
||||
try {
|
||||
@ -589,24 +513,62 @@ $primary: #6366f1;
|
||||
|
||||
.review-rules-page {
|
||||
:deep(.ant-card) {
|
||||
border: none; border-radius: 12px; 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; }
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
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) {
|
||||
background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); 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; }
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
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-muted { color: #d1d5db; }
|
||||
.text-desc {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@ -208,8 +208,19 @@
|
||||
<!-- 已选评委区域 -->
|
||||
<a-card size="small">
|
||||
<template #title>
|
||||
<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>
|
||||
<a-list :data-source="selectedJudgeRows" size="small">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
@ -241,7 +252,12 @@
|
||||
<div class="drawer-footer">
|
||||
<a-space>
|
||||
<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-space>
|
||||
@ -273,6 +289,8 @@ import {
|
||||
worksApi,
|
||||
reviewsApi,
|
||||
judgesApi,
|
||||
reviewRulesApi,
|
||||
type Contest,
|
||||
type ContestWork,
|
||||
type ContestJudge,
|
||||
} from "@/api/contests"
|
||||
@ -303,8 +321,22 @@ const fetchDetailStats = async () => {
|
||||
try { detailStats.value = await worksApi.getStats(contestId) } catch { /* */ }
|
||||
}
|
||||
|
||||
// 活动名称
|
||||
// 活动名称与详情(评审规则「评委数量」与添加评委侧一致)
|
||||
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[]>([])
|
||||
@ -381,6 +413,15 @@ const judgeSearchParams = reactive({
|
||||
const selectedJudgeKeys = ref<number[]>([])
|
||||
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[] {
|
||||
const seen = new Set(base.map((j) => j.judgeId))
|
||||
@ -488,6 +529,24 @@ const fetchContestInfo = async () => {
|
||||
try {
|
||||
const contest = await contestsApi.getDetail(contestId)
|
||||
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) {
|
||||
console.error("获取活动信息失败", error)
|
||||
}
|
||||
@ -647,6 +706,14 @@ const handleJudgeTableChange = (pag: any) => {
|
||||
|
||||
// 确认分配评委(可与列表同步:移除的评委在后端删除分配记录)
|
||||
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) {
|
||||
message.warning("批量分配请至少选择一名评委")
|
||||
return
|
||||
@ -826,6 +893,11 @@ $primary: #6366f1;
|
||||
}
|
||||
|
||||
.assign-judge-drawer {
|
||||
.assign-warning-text {
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user