feat: 成果详情未完成计算得分时展示引导并隐藏列表

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-16 17:08:12 +08:00
parent 4ba2af18f6
commit aca472aa54

View File

@ -20,24 +20,36 @@
<!-- 统计摘要 -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon" style="background: rgba(99,102,241,0.1)"><file-text-outlined style="color: #6366f1" /></div>
<div class="stat-info"><span class="stat-count">{{ summary.totalWorks }}</span><span class="stat-label">总作品</span></div>
<div class="stat-icon" style="background: rgba(99,102,241,0.1)"><file-text-outlined style="color: #6366f1" />
</div>
<div class="stat-info"><span class="stat-count">{{ summary.totalWorks }}</span><span
class="stat-label">总作品</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: rgba(59,130,246,0.1)"><check-circle-outlined style="color: #3b82f6" /></div>
<div class="stat-info"><span class="stat-count">{{ summary.scoredWorks }}</span><span class="stat-label">已评分</span></div>
<div class="stat-icon" style="background: rgba(59,130,246,0.1)"><check-circle-outlined style="color: #3b82f6" />
</div>
<div class="stat-info"><span class="stat-count">{{ summary.scoredWorks }}</span><span
class="stat-label">已评分</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: rgba(16,185,129,0.1)"><ordered-list-outlined style="color: #10b981" /></div>
<div class="stat-info"><span class="stat-count">{{ summary.rankedWorks }}</span><span class="stat-label">已排名</span></div>
<div class="stat-icon" style="background: rgba(16,185,129,0.1)"><ordered-list-outlined style="color: #10b981" />
</div>
<div class="stat-info"><span class="stat-count">{{ summary.rankedWorks }}</span><span
class="stat-label">已排名</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: rgba(245,158,11,0.1)"><trophy-outlined style="color: #f59e0b" /></div>
<div class="stat-info"><span class="stat-count">{{ summary.awardedWorks }}</span><span class="stat-label">已设奖</span></div>
<div class="stat-info"><span class="stat-count">{{ summary.awardedWorks }}</span><span
class="stat-label">已设奖</span>
</div>
</div>
<div class="stat-card" v-if="summary.avgScore">
<div class="stat-icon" style="background: rgba(139,92,246,0.1)"><fund-outlined style="color: #8b5cf6" /></div>
<div class="stat-info"><span class="stat-count">{{ summary.avgScore }}</span><span class="stat-label">平均分</span></div>
<div class="stat-info"><span class="stat-count">{{ summary.avgScore }}</span><span class="stat-label">平均分</span>
</div>
</div>
</div>
@ -59,6 +71,26 @@
</a-space>
</div>
<!-- 未完成第一步计算得分操作引导不展示列表 -->
<div v-if="!showResultsList" class="step-guide-card">
<div class="step-guide-icon">
<calculator-outlined />
</div>
<h3 class="step-guide-title">请先完成第一步计算得分</h3>
<p class="step-guide-desc">
成果列表依赖各作品的最终得分请先点击下方按钮系统将根据评审规则汇总评分并写入最终得分完成后再进行排名设奖与发布
</p>
<ol class="step-guide-steps">
<li>点击第一步计算得分或下方按钮汇总评委评分为最终得分</li>
<li>再依次完成第二步排名第三步设奖见上方操作区</li>
</ol>
<a-button type="primary" size="large" :loading="calcScoreLoading" @click="handleCalculateScores">
<template #icon><calculator-outlined /></template>
开始计算得分
</a-button>
</div>
<template v-else>
<!-- 搜索 -->
<div class="filter-bar">
<a-form :model="searchParams" layout="inline" @finish="handleSearch">
@ -69,20 +101,23 @@
<a-input v-model:value="searchParams.accountNo" placeholder="请输入报名账号" allow-clear style="width: 150px" />
</a-form-item>
<a-form-item label="奖项">
<a-select v-model:value="searchParams.awardLevel" placeholder="全部" allow-clear style="width: 120px" @change="handleSearch">
<a-select v-model:value="searchParams.awardLevel" placeholder="全部" allow-clear style="width: 120px"
@change="handleSearch">
<a-select-option v-for="opt in awardFilterOptions" :key="opt" :value="opt">{{ opt }}</a-select-option>
<a-select-option value="_none">无奖项</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit"><template #icon><search-outlined /></template> 搜索</a-button>
<a-button style="margin-left: 8px" @click="handleReset"><template #icon><reload-outlined /></template> 重置</a-button>
<a-button style="margin-left: 8px" @click="handleReset"><template #icon><reload-outlined /></template>
重置</a-button>
</a-form-item>
</a-form>
</div>
<!-- 数据表格 -->
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination" row-key="id" @change="handleTableChange" class="data-table">
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination" row-key="id"
@change="handleTableChange" class="data-table">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'index'">
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
@ -116,11 +151,13 @@
{{ record.submitterAccountNo || record.registration?.user?.username || '-' }}
</template>
<template v-else-if="column.key === 'action'">
<a-button v-if="!isSuperAdmin && contestInfo?.resultState !== 'published'" type="link" size="small" @click="openSetAward(record)">设奖</a-button>
<a-button v-if="!isSuperAdmin && contestInfo?.resultState !== 'published'" type="link" size="small"
@click="openSetAward(record)">设奖</a-button>
<a-button type="link" size="small" @click="handleViewWorkDetail(record)">查看</a-button>
</template>
</template>
</a-table>
</template>
<!-- 作品详情弹框 -->
<WorkDetailModal v-model:open="workDetailModalVisible" :work-id="currentWorkId" />
@ -129,16 +166,19 @@
<a-modal v-model:open="setAwardVisible" title="设置奖项" @ok="handleSetAward" :confirm-loading="setAwardLoading">
<a-form layout="vertical" style="margin-top: 16px">
<a-form-item label="作品">
<span>{{ currentAwardWork?.workNo }} {{ currentAwardWork?.registration?.user?.nickname || currentAwardWork?.registration?.team?.teamName }}</span>
<span>{{ currentAwardWork?.workNo }} {{ currentAwardWork?.registration?.user?.nickname ||
currentAwardWork?.registration?.team?.teamName }}</span>
</a-form-item>
<a-form-item label="奖项名称" required>
<a-select v-model:value="awardForm.awardName" placeholder="选择或输入奖项名称" mode="combobox" :options="existingAwardOptions" />
<a-select v-model:value="awardForm.awardName" placeholder="选择或输入奖项名称" mode="combobox"
:options="existingAwardOptions" />
</a-form-item>
</a-form>
</a-modal>
<!-- 自动设置奖项弹窗 -->
<a-modal v-model:open="autoAwardVisible" title="按排名自动设置奖项" @ok="handleAutoSetAwards" :confirm-loading="autoAwardLoading" width="520px">
<a-modal v-model:open="autoAwardVisible" title="按排名自动设置奖项" @ok="handleAutoSetAwards"
:confirm-loading="autoAwardLoading" width="520px">
<p style="color: #6b7280; font-size: 13px; margin-bottom: 16px">
自定义奖项名称和获奖人数系统将按排名从高到低依次分配
</p>
@ -204,6 +244,13 @@ const contestInfo = ref<any>(null)
const summary = ref({ totalWorks: 0, scoredWorks: 0, rankedWorks: 0, awardedWorks: 0, unscoredWorks: 0, avgScore: null as string | null })
const canPublish = computed(() => summary.value.rankedWorks > 0)
/** 机构端且未发布时,需先完成「第一步:计算得分」后再展示作品列表与筛选 */
const showResultsList = computed(() => {
if (isSuperAdmin.value) return true
if (contestInfo.value?.resultState === 'published') return true
return summary.value.scoredWorks > 0
})
//
const awardFilterOptions = computed(() => {
const names = new Set<string>()
@ -311,7 +358,7 @@ const handleCalculateScores = async () => {
calcScoreLoading.value = true
try {
const res = await resultsApi.calculateScores(contestId)
message.success(res.message)
message.success(res.message || '计算成功')
fetchList()
fetchSummary()
} catch (e: any) { message.error(e?.response?.data?.message || '计算失败') }
@ -323,7 +370,7 @@ const handleCalculateRankings = async () => {
calcRankLoading.value = true
try {
const res = await resultsApi.calculateRankings(contestId)
message.success(res.message)
message.success(res.message || '计算成功')
fetchList()
fetchSummary()
} catch (e: any) { message.error(e?.response?.data?.message || '计算失败') }
@ -394,58 +441,215 @@ const handlePublish = () => {
})
}
onMounted(() => { fetchSummary(); fetchList() })
onMounted(async () => {
await fetchSummary()
if (showResultsList.value) {
await fetchList()
}
})
</script>
<style scoped lang="scss">
$primary: #6366f1;
.page-header {
display: flex; justify-content: space-between; align-items: center;
padding: 16px 24px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 16px;
.header-left { display: flex; align-items: center; gap: 8px; }
.page-title { font-size: 18px; font-weight: 600; color: #1e1b4b; }
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1e1b4b;
}
}
.stats-row {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.stats-row { display: flex; gap: 12px; margin-bottom: 16px; }
.stat-card {
flex: 1; display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06);
.stat-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.stat-info { display: flex; flex-direction: column;
.stat-count { font-size: 18px; font-weight: 700; color: #1e1b4b; line-height: 1.2; }
.stat-label { font-size: 12px; color: #9ca3af; }
flex: 1;
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
.stat-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.stat-info {
display: flex;
flex-direction: column;
.stat-count {
font-size: 18px;
font-weight: 700;
color: #1e1b4b;
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: #9ca3af;
}
}
}
.action-bar {
padding: 16px 20px; background: rgba($primary, 0.03); border: 1px dashed rgba($primary, 0.15);
border-radius: 12px; margin-bottom: 16px;
padding: 16px 20px;
background: rgba($primary, 0.03);
border: 1px dashed rgba($primary, 0.15);
border-radius: 12px;
margin-bottom: 16px;
}
.filter-bar { padding: 20px 24px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); margin-bottom: 16px; }
.step-guide-card {
padding: 40px 32px 48px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
text-align: center;
margin-bottom: 16px;
border: 1px solid rgba($primary, 0.12);
.step-guide-icon {
width: 64px;
height: 64px;
margin: 0 auto 20px;
border-radius: 16px;
background: rgba($primary, 0.1);
color: $primary;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.step-guide-title {
margin: 0 0 12px;
font-size: 18px;
font-weight: 600;
color: #1e1b4b;
}
.step-guide-desc {
margin: 0 auto 20px;
max-width: 520px;
font-size: 14px;
line-height: 1.65;
color: #6b7280;
}
.step-guide-steps {
margin: 0 auto 28px;
max-width: 480px;
text-align: left;
padding-left: 20px;
font-size: 13px;
line-height: 1.8;
color: #4b5563;
}
}
.filter-bar {
padding: 20px 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
}
.data-table {
: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; }
: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;
}
}
}
.rank-badge {
display: inline-flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 50%; font-size: 13px; font-weight: 700; background: #f3f4f6; color: #374151;
&.gold { background: linear-gradient(135deg, #fbbf24, #f59e0b); color: #fff; }
&.silver { background: linear-gradient(135deg, #d1d5db, #9ca3af); color: #fff; }
&.bronze { background: linear-gradient(135deg, #f59e0b, #d97706); color: #fff; }
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 13px;
font-weight: 700;
background: #f3f4f6;
color: #374151;
&.gold {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: #fff;
}
.score { font-weight: 700; color: #10b981; }
.text-muted { color: #d1d5db; }
&.silver {
background: linear-gradient(135deg, #d1d5db, #9ca3af);
color: #fff;
}
&.bronze {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #fff;
}
}
.score {
font-weight: 700;
color: #10b981;
}
.text-muted {
color: #d1d5db;
}
.award-tiers {
.award-tier-row {
display: flex; gap: 8px; align-items: center; margin-bottom: 8px;
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
}
</style>