feat: 成果发布列表评审未完成时禁用发布成果入口

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-15 14:21:43 +08:00
parent 6c3b2f4a1d
commit ea40131084

View File

@ -8,12 +8,8 @@
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="stats-row"> <div class="stats-row">
<div <div v-for="item in statsItems" :key="item.key" :class="['stat-card', { active: activeFilter === item.key }]"
v-for="item in statsItems" @click="handleStatClick(item.key)">
:key="item.key"
:class="['stat-card', { active: activeFilter === item.key }]"
@click="handleStatClick(item.key)"
>
<div class="stat-icon" :style="{ background: item.bgColor }"> <div class="stat-icon" :style="{ background: item.bgColor }">
<component :is="item.icon" :style="{ color: item.color }" /> <component :is="item.icon" :style="{ color: item.color }" />
</div> </div>
@ -28,46 +24,31 @@
<div class="filter-bar"> <div class="filter-bar">
<a-form layout="inline" :model="superSearch" @finish="handleSuperSearch"> <a-form layout="inline" :model="superSearch" @finish="handleSuperSearch">
<a-form-item label="所属活动"> <a-form-item label="所属活动">
<a-select <a-select v-model:value="superSearch.contestId" placeholder="全部活动" allow-clear show-search
v-model:value="superSearch.contestId" :filter-option="filterContestOption" style="width: 200px" :options="contestOptions" />
placeholder="全部活动"
allow-clear
show-search
:filter-option="filterContestOption"
style="width: 200px"
:options="contestOptions"
/>
</a-form-item> </a-form-item>
<a-form-item label="发布状态"> <a-form-item label="发布状态">
<a-select <a-select v-model:value="superSearch.resultState" placeholder="全部" allow-clear style="width: 110px">
v-model:value="superSearch.resultState"
placeholder="全部"
allow-clear
style="width: 110px"
>
<a-select-option value="published">已发布</a-select-option> <a-select-option value="published">已发布</a-select-option>
<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 <a-select v-model:value="superSearch.creatorTenantId" placeholder="全部机构" allow-clear show-search
v-model:value="superSearch.creatorTenantId" :filter-option="filterTenantOption" style="width: 160px" :options="tenantOptions" />
placeholder="全部机构"
allow-clear
show-search
:filter-option="filterTenantOption"
style="width: 160px"
:options="tenantOptions"
/>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-space> <a-space>
<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 @click="handleSuperReset"> <a-button @click="handleSuperReset">
<template #icon><ReloadOutlined /></template> <template #icon>
<ReloadOutlined />
</template>
重置 重置
</a-button> </a-button>
</a-space> </a-space>
@ -76,26 +57,15 @@
</div> </div>
<!-- 表格 --> <!-- 表格 -->
<a-table <a-table :columns="superColumns" :data-source="superDataSource" :loading="superLoading"
:columns="superColumns" :pagination="superPagination" row-key="id" @change="handleSuperTableChange" class="data-table">
:data-source="superDataSource"
:loading="superLoading"
:pagination="superPagination"
row-key="id"
@change="handleSuperTableChange"
class="data-table"
>
<template #bodyCell="{ column, record, index }"> <template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'index'"> <template v-if="column.key === 'index'">
{{ (superPagination.current - 1) * superPagination.pageSize + index + 1 }} {{ (superPagination.current - 1) * superPagination.pageSize + index + 1 }}
</template> </template>
<template v-else-if="column.key === 'contestName'"> <template v-else-if="column.key === 'contestName'">
<a-button <a-button type="link" size="small" class="contest-link"
type="link" @click="router.push(`/${tenantCode}/contests/${record.id}/overview`)">
size="small"
class="contest-link"
@click="router.push(`/${tenantCode}/contests/${record.id}/overview`)"
>
{{ record.contestName }} {{ record.contestName }}
</a-button> </a-button>
</template> </template>
@ -137,7 +107,8 @@
<!-- 统计概览 --> <!-- 统计概览 -->
<div class="stats-row"> <div class="stats-row">
<div v-for="item in orgStatsItems" :key="item.key" :class="['stat-card', { active: orgActiveFilter === item.key }]" @click="handleOrgStatClick(item.key)"> <div v-for="item in orgStatsItems" :key="item.key"
:class="['stat-card', { active: orgActiveFilter === item.key }]" @click="handleOrgStatClick(item.key)">
<div class="stat-icon" :style="{ background: item.bgColor }"> <div class="stat-icon" :style="{ background: item.bgColor }">
<component :is="item.icon" :style="{ color: item.color }" /> <component :is="item.icon" :style="{ color: item.color }" />
</div> </div>
@ -155,25 +126,33 @@
<a-input v-model:value="searchParams.contestName" placeholder="请输入活动名称" allow-clear style="width: 200px" /> <a-input v-model:value="searchParams.contestName" placeholder="请输入活动名称" allow-clear style="width: 200px" />
</a-form-item> </a-form-item>
<a-form-item label="活动类型"> <a-form-item label="活动类型">
<a-select v-model:value="searchParams.contestType" placeholder="全部" allow-clear style="width: 120px" @change="handleSearch"> <a-select v-model:value="searchParams.contestType" placeholder="全部" allow-clear style="width: 120px"
@change="handleSearch">
<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>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="发布状态"> <a-form-item label="发布状态">
<a-select v-model:value="orgResultState" placeholder="全部" allow-clear style="width: 110px" @change="handleSearch"> <a-select v-model:value="orgResultState" placeholder="全部" allow-clear style="width: 110px"
@change="handleSearch">
<a-select-option value="published">已发布</a-select-option> <a-select-option value="published">已发布</a-select-option>
<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> <a-form-item>
<a-button type="primary" html-type="submit"><template #icon><SearchOutlined /></template> 搜索</a-button> <a-button type="primary" html-type="submit"><template #icon>
<a-button style="margin-left: 8px" @click="handleReset"><template #icon><ReloadOutlined /></template> 重置</a-button> <SearchOutlined />
</template> 搜索</a-button>
<a-button style="margin-left: 8px" @click="handleReset"><template #icon>
<ReloadOutlined />
</template>
重置</a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </div>
<a-table :columns="orgColumns" :data-source="dataSource" :loading="loading" :pagination="pagination" row-key="id" @change="handleTableChange" class="data-table"> <a-table :columns="orgColumns" :data-source="dataSource" :loading="loading" :pagination="pagination" row-key="id"
@change="handleTableChange" class="data-table">
<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 }}
@ -182,7 +161,8 @@
<a @click="handleViewDetail(record)">{{ record.contestName }}</a> <a @click="handleViewDetail(record)">{{ record.contestName }}</a>
</template> </template>
<template v-else-if="column.key === 'contestType'"> <template v-else-if="column.key === 'contestType'">
<a-tag :color="record.contestType === 'individual' ? 'blue' : 'green'">{{ record.contestType === 'individual' ? '个人' : '团队' }}</a-tag> <a-tag :color="record.contestType === 'individual' ? 'blue' : 'green'">{{ record.contestType ===
'individual' ? '个人' : '团队' }}</a-tag>
</template> </template>
<template v-else-if="column.key === 'registrationCount'"> <template v-else-if="column.key === 'registrationCount'">
{{ record._count?.registrations || 0 }} {{ record._count?.registrations || 0 }}
@ -195,8 +175,17 @@
<a-tag v-else color="default">未发布</a-tag> <a-tag v-else color="default">未发布</a-tag>
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-button v-if="record.resultState === 'published'" type="link" size="small" @click="handleViewDetail(record)">查看成果</a-button> <a-button v-if="record.resultState === 'published'" type="link" size="small"
<a-button v-else type="link" size="small" style="color: #10b981" @click="handleViewDetail(record)">发布成果</a-button> @click="handleViewDetail(record)">查看成果</a-button>
<template v-else-if="canPublishResults(record)">
<a-button type="link" size="small" style="color: #10b981"
@click="handleViewDetail(record)">发布成果</a-button>
</template>
<a-tooltip v-else placement="top" title="评审未完成,请待全部作品完成评审后再发布成果">
<span class="publish-results-btn-wrap">
<a-button type="link" size="small" style="color: #777" disabled>发布成果</a-button>
</span>
</a-tooltip>
</template> </template>
</template> </template>
</a-table> </a-table>
@ -232,6 +221,16 @@ const isSuperAdmin = computed(() => authStore.hasAnyRole(['super_admin']))
const formatDate = (d?: string) => d ? dayjs(d).format("YYYY-MM-DD HH:mm") : "-" const formatDate = (d?: string) => d ? dayjs(d).format("YYYY-MM-DD HH:mm") : "-"
/**
* 是否允许进入成果发布详情发布成果入口
* 与活动列表接口的 reviewedCount / totalWorksCount 一致有作品且已完成评审的作品数 作品总数
*/
function canPublishResults(record: Contest): boolean {
const totalWorks = Number(record.totalWorksCount ?? record._count?.works ?? 0)
const reviewed = Number(record.reviewedCount ?? 0)
return totalWorks > 0 && reviewed >= totalWorks
}
// ============================================= // =============================================
// //
// ============================================= // =============================================
@ -455,11 +454,26 @@ $primary: #6366f1;
border: none; border: none;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
:deep(.ant-card-head) { border-bottom: none; .ant-card-head-title { font-size: 18px; font-weight: 600; } }
:deep(.ant-card-body) { padding: 0; } :deep(.ant-card-head) {
border-bottom: none;
.ant-card-head-title {
font-size: 18px;
font-weight: 600;
}
}
:deep(.ant-card-body) {
padding: 0;
}
} }
.stats-row { display: flex; gap: 12px; margin-bottom: 16px; } .stats-row {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.stat-card { .stat-card {
flex: 1; flex: 1;
@ -473,10 +487,43 @@ $primary: #6366f1;
cursor: pointer; cursor: pointer;
border: 2px solid transparent; border: 2px solid transparent;
transition: all 0.2s; transition: all 0.2s;
&:hover { box-shadow: 0 4px 16px rgba($primary, 0.12); }
&.active { border-color: $primary; background: rgba($primary, 0.02); } &:hover {
.stat-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; } box-shadow: 0 4px 16px rgba($primary, 0.12);
.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; } } }
&.active {
border-color: $primary;
background: rgba($primary, 0.02);
}
.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;
}
}
} }
.filter-bar { .filter-bar {
@ -493,13 +540,35 @@ $primary: #6366f1;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
overflow: hidden; 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-thead>tr>th {
.ant-table-pagination { padding: 16px; margin: 0; } background: #fafafa;
font-weight: 600;
}
.ant-table-tbody>tr:hover>td {
background: rgba($primary, 0.03);
}
.ant-table-pagination {
padding: 16px;
margin: 0;
}
} }
} }
.contest-link { padding: 0; text-align: left; } .contest-link {
.text-muted { color: #d1d5db; } padding: 0;
text-align: left;
}
.text-muted {
color: #d1d5db;
}
/* 包裹禁用链接按钮,便于 Tooltip 触发且布局不塌缩 */
.publish-results-btn-wrap {
display: inline-block;
cursor: not-allowed;
}
</style> </style>