feat: 成果详情未完成计算得分时展示引导并隐藏列表
Made-with: Cursor
This commit is contained in:
parent
4ba2af18f6
commit
aca472aa54
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user