修改作品详情页

This commit is contained in:
zhangxiaohua 2026-01-15 18:00:42 +08:00
parent 464f5389a4
commit 252ba92266
6 changed files with 795 additions and 164 deletions

View File

@ -381,13 +381,13 @@ router.beforeEach(async (to, _from, next) => {
userTenantCode,
to.path.replace(`/${tenantCodeFromUrl}`, "")
)
next({ path: correctedPath, replace: true })
next({ path: correctedPath, query: to.query, replace: true })
return
}
// 如果URL中没有租户编码添加租户编码
if (!tenantCodeFromUrl) {
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path)
next({ path: correctedPath, replace: true })
next({ path: correctedPath, query: to.query, replace: true })
return
}
}
@ -493,7 +493,7 @@ router.beforeEach(async (to, _from, next) => {
userTenantCode,
to.path.replace(`/${tenantCodeFromUrl}`, "")
)
next({ path: correctedPath, replace: true })
next({ path: correctedPath, query: to.query, replace: true })
return
}
// 如果URL中没有租户编码添加租户编码排除不需要认证的特殊路由
@ -501,7 +501,7 @@ router.beforeEach(async (to, _from, next) => {
const shouldSkipTenantCode = skipTenantCodePaths.some(p => to.path.startsWith(p))
if (!tenantCodeFromUrl && !shouldSkipTenantCode) {
const correctedPath = buildPathWithTenantCode(userTenantCode, to.path)
next({ path: correctedPath, replace: true })
next({ path: correctedPath, query: to.query, replace: true })
return
}
}

View File

@ -0,0 +1,505 @@
<template>
<a-drawer
v-model:open="visible"
:title="drawerTitle"
placement="right"
width="600px"
:footer="null"
class="work-detail-drawer"
>
<a-spin :spinning="loading">
<template v-if="workDetail">
<!-- 提交时间 -->
<div class="submit-time">
{{ formatDateTime(workDetail.submitTime) }}
</div>
<!-- 作品介绍 -->
<div class="section">
<div class="section-title">作品介绍</div>
<div class="section-content description">
<template v-if="workDetail.description">
<div v-html="workDetail.description"></div>
</template>
<span v-else class="empty-text">暂无介绍</span>
</div>
</div>
<!-- 作品详情 - 预览图 -->
<div class="section">
<div class="section-title">作品详情</div>
<div class="preview-container">
<div class="preview-image" @mouseenter="showPreviewBtn = true" @mouseleave="showPreviewBtn = false">
<img
v-if="previewImageUrl"
:src="previewImageUrl"
alt="作品预览"
@error="handleImageError"
/>
<div v-else class="preview-placeholder">
<FileImageOutlined />
<span>暂无预览图</span>
</div>
<!-- 3D预览按钮 -->
<transition name="fade">
<div v-if="showPreviewBtn && hasModelFile" class="preview-btn-overlay" @click="handleView3DModel">
<a-button type="primary">
<template #icon><EyeOutlined /></template>
3D模型预览
</a-button>
</div>
</transition>
</div>
</div>
</div>
<!-- 作品附件 -->
<div class="section">
<div class="section-title">作品附件</div>
<div class="attachments-list">
<template v-if="workDetail.attachments && workDetail.attachments.length > 0">
<div
v-for="attachment in workDetail.attachments"
:key="attachment.id"
class="attachment-item"
>
<div class="attachment-name">
<span class="label">附件名称</span>
<span>{{ attachment.fileName?.split('.')[0] || attachment.fileName }}</span>
</div>
<div class="attachment-file">
<PaperClipOutlined class="file-icon" />
<span class="file-name">{{ attachment.fileName }}</span>
<a-button
type="link"
size="small"
@click="handleDownload(attachment)"
>
<template #icon><DownloadOutlined /></template>
</a-button>
</div>
</div>
</template>
<a-empty v-else description="暂无附件" :image="false" />
</div>
</div>
<!-- 评审记录 -->
<div class="section">
<div class="section-title">评审记录</div>
<div class="review-records">
<template v-if="reviewRecords && reviewRecords.length > 0">
<a-tabs v-model:activeKey="activeReviewTab">
<a-tab-pane
v-for="record in reviewRecords"
:key="record.id"
:tab="record.judge?.nickname || record.judge?.username || '评委'"
>
<div class="review-card">
<div class="review-item">
<span class="review-label">作品评分</span>
<span v-if="record.score !== null && record.score !== undefined" class="review-score">
{{ record.score }}
</span>
<span v-else class="not-reviewed">未评审</span>
</div>
<div class="review-item">
<span class="review-label">评委老师</span>
<span class="review-value">{{ record.judge?.nickname || record.judge?.username || '-' }}</span>
</div>
<div class="review-item">
<span class="review-label">评分时间</span>
<span class="review-value">
{{ record.scoreTime ? formatDateTime(record.scoreTime) : '-' }}
</span>
</div>
<div v-if="record.comment" class="review-item comment">
<span class="review-label">老师评语</span>
<span class="review-value">{{ record.comment }}</span>
</div>
</div>
</a-tab-pane>
</a-tabs>
</template>
<a-empty v-else description="暂无评审记录" :image="false" />
</div>
</div>
</template>
<a-empty v-else-if="!loading" description="暂无数据" />
</a-spin>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, watch, computed } from "vue"
import { useRoute, useRouter } from "vue-router"
import { message } from "ant-design-vue"
import {
FileImageOutlined,
EyeOutlined,
PaperClipOutlined,
DownloadOutlined
} from "@ant-design/icons-vue"
import dayjs from "dayjs"
import { worksApi, reviewsApi, type ContestWork } from "@/api/contests"
interface Props {
open: boolean
workId?: number | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
"update:open": [value: boolean]
}>()
const route = useRoute()
const router = useRouter()
//
const visible = ref(false)
const loading = ref(false)
const workDetail = ref<ContestWork | null>(null)
const reviewRecords = ref<any[]>([])
const activeReviewTab = ref<number | string>("")
const showPreviewBtn = ref(false)
//
const drawerTitle = computed(() => {
if (workDetail.value) {
const workNo = workDetail.value.workNo || ""
const title = workDetail.value.title || ""
return workNo && title ? `${workNo} ${title}` : workNo || title || "作品详情"
}
return "作品详情"
})
// URL
const previewImageUrl = computed(() => {
if (!workDetail.value) return ""
// 使
if (workDetail.value.previewUrl) {
return workDetail.value.previewUrl
}
// files
const imageFromFiles = workDetail.value.files?.find(
(url) => /\.(jpg|jpeg|png|gif|webp)$/i.test(url)
)
if (imageFromFiles) return imageFromFiles
// attachments
const imageAttachment = workDetail.value.attachments?.find(
(att) => att.fileType?.startsWith("image/") || /\.(jpg|jpeg|png|gif|webp)$/i.test(att.fileName || "")
)
return imageAttachment?.fileUrl || ""
})
// URL3DURL
const isModelFile = (urlOrFileName: string): boolean => {
//
const pathWithoutQuery = urlOrFileName.split("?")[0]
return /\.(glb|gltf|obj|fbx|stl|zip)$/i.test(pathWithoutQuery)
}
// 3D
const hasModelFile = computed(() => {
if (!workDetail.value) return false
// files
const hasInFiles = workDetail.value.files?.some((url) => isModelFile(url))
if (hasInFiles) return true
// attachments
const hasInAttachments = workDetail.value.attachments?.some(
(att) => isModelFile(att.fileName || "") || isModelFile(att.fileUrl || "")
)
return hasInAttachments || false
})
// 3DURL
const modelFileUrl = computed(() => {
if (!workDetail.value) return ""
// files
const modelFromFiles = workDetail.value.files?.find((url) => isModelFile(url))
if (modelFromFiles) return modelFromFiles
// attachments
const modelAtt = workDetail.value.attachments?.find(
(att) => isModelFile(att.fileName || "") || isModelFile(att.fileUrl || "")
)
return modelAtt?.fileUrl || ""
})
//
const formatDateTime = (dateStr?: string) => {
if (!dateStr) return "-"
return dayjs(dateStr).format("YYYY.MM.DD HH:mm")
}
//
const fetchWorkDetail = async (id: number) => {
loading.value = true
try {
const detail = await worksApi.getDetail(id)
workDetail.value = detail
//
await fetchReviewRecords(id)
} catch (error: any) {
message.error(error?.response?.data?.message || "获取作品详情失败")
workDetail.value = null
} finally {
loading.value = false
}
}
//
const fetchReviewRecords = async (workId: number) => {
try {
const records = await reviewsApi.getWorkScores(workId)
reviewRecords.value = records || []
if (records && records.length > 0) {
activeReviewTab.value = records[0].id
}
} catch (error) {
console.error("获取评审记录失败", error)
reviewRecords.value = []
}
}
//
const handleImageError = (e: Event) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
}
// 3D
const handleView3DModel = () => {
const tenantCode = route.params.tenantCode as string
console.log("3D模型预览 - modelFileUrl:", modelFileUrl.value)
console.log("3D模型预览 - files:", workDetail.value?.files)
console.log("3D模型预览 - attachments:", workDetail.value?.attachments)
if (modelFileUrl.value) {
const url = `/${tenantCode}/workbench/model-viewer?url=${encodeURIComponent(modelFileUrl.value)}`
console.log("3D模型预览 - 跳转URL:", url)
window.open(url, "_blank")
} else {
message.warning("未找到3D模型文件")
}
}
//
const handleDownload = (attachment: any) => {
if (attachment.fileUrl) {
window.open(attachment.fileUrl, "_blank")
}
}
// open
watch(
() => props.open,
(newVal) => {
visible.value = newVal
if (newVal && props.workId) {
fetchWorkDetail(props.workId)
} else if (!newVal) {
workDetail.value = null
reviewRecords.value = []
}
}
)
// visible
watch(visible, (newVal) => {
emit("update:open", newVal)
})
// workId
watch(
() => props.workId,
(newVal) => {
if (newVal && props.open) {
fetchWorkDetail(newVal)
}
}
)
</script>
<style scoped lang="scss">
.work-detail-drawer {
:deep(.ant-drawer-body) {
padding: 16px 24px;
}
}
.submit-time {
color: #666;
font-size: 14px;
margin-bottom: 20px;
}
.section {
margin-bottom: 24px;
.section-title {
font-size: 15px;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
.section-content {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.description {
background: #fafafa;
padding: 12px 16px;
border-radius: 6px;
}
.empty-text {
color: #999;
}
}
.preview-container {
.preview-image {
position: relative;
width: 100%;
height: 280px;
background: #f5f5f5;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #bbb;
font-size: 14px;
:deep(.anticon) {
font-size: 48px;
margin-bottom: 12px;
}
}
.preview-btn-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
}
}
.attachments-list {
.attachment-item {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.attachment-name {
margin-bottom: 8px;
font-size: 14px;
.label {
color: #0958d9;
}
}
.attachment-file {
display: flex;
align-items: center;
padding: 8px 12px;
background: #fafafa;
border-radius: 6px;
.file-icon {
color: #52c41a;
margin-right: 8px;
}
.file-name {
flex: 1;
color: #333;
font-size: 14px;
}
}
}
}
.review-records {
:deep(.ant-tabs-nav) {
margin-bottom: 16px;
}
.review-card {
background: #fff;
border-left: 3px solid #52c41a;
padding: 16px;
border-radius: 0 8px 8px 0;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
.review-item {
margin-bottom: 12px;
font-size: 14px;
line-height: 1.6;
&:last-child {
margin-bottom: 0;
}
.review-label {
color: #666;
}
.review-value {
color: #333;
}
.review-score {
color: #0958d9;
font-size: 18px;
font-weight: 600;
}
.not-reviewed {
color: #999;
}
&.comment {
.review-value {
color: #52c41a;
}
}
}
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -68,6 +68,9 @@
<template v-if="column.key === 'index'">
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
</template>
<template v-else-if="column.key === 'workNo'">
<a @click="handleViewWorkDetail(record)">{{ record.workNo || "-" }}</a>
</template>
<template v-else-if="column.key === 'finalScore'">
<span v-if="record.finalScore !== null" class="score">
{{ Number(record.finalScore).toFixed(2) }}
@ -94,6 +97,12 @@
</template>
</template>
</a-table>
<!-- 作品详情弹框 -->
<WorkDetailModal
v-model:open="workDetailModalVisible"
:work-id="currentWorkId"
/>
</div>
</template>
@ -107,6 +116,7 @@ import {
ReloadOutlined,
} from "@ant-design/icons-vue"
import { resultsApi } from "@/api/contests"
import WorkDetailModal from "../components/WorkDetailModal.vue"
const route = useRoute()
const router = useRouter()
@ -120,6 +130,10 @@ const contestInfo = ref<{
resultState: string
} | null>(null)
//
const workDetailModalVisible = ref(false)
const currentWorkId = ref<number | null>(null)
//
const loading = ref(false)
const publishLoading = ref(false)
@ -145,7 +159,6 @@ const columns = [
},
{
title: "作品编号",
dataIndex: "workNo",
key: "workNo",
width: 120,
},
@ -231,6 +244,12 @@ const handleBack = () => {
router.push(`/${tenantCode}/contests/results`)
}
//
const handleViewWorkDetail = (record: any) => {
currentWorkId.value = record.id
workDetailModalVisible.value = true
}
// /
const handlePublish = () => {
const isPublished = contestInfo.value?.resultState === "published"

View File

@ -83,6 +83,9 @@
<template v-if="column.key === 'index'">
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
</template>
<template v-else-if="column.key === 'workNo'">
<a @click="handleViewWorkDetail(record)">{{ record.workNo || "-" }}</a>
</template>
<template v-else-if="column.key === 'username'">
{{ record.submitterAccountNo || record.registration?.user?.username || "-" }}
</template>
@ -214,6 +217,12 @@
</template>
</a-drawer>
<!-- 作品详情弹框 -->
<WorkDetailModal
v-model:open="workDetailModalVisible"
:work-id="currentWorkId"
/>
<!-- 未提交作品弹框 -->
<a-modal
v-model:open="notSubmittedModalVisible"
@ -259,6 +268,7 @@ import {
import dayjs from "dayjs"
import { contestsApi, worksApi, reviewsApi, type ContestWork } from "@/api/contests"
import { judgesManagementApi, type Judge } from "@/api/judges-management"
import WorkDetailModal from "../components/WorkDetailModal.vue"
const route = useRoute()
const router = useRouter()
@ -268,6 +278,10 @@ const contestType = (route.query.type as string) || "individual"
//
const contestName = ref("")
//
const workDetailModalVisible = ref(false)
const currentWorkId = ref<number | null>(null)
//
const loading = ref(false)
const dataSource = ref<ContestWork[]>([])
@ -287,7 +301,7 @@ const searchParams = reactive({
//
const columns = [
{ title: "序号", key: "index", width: 70 },
{ title: "作品编号", dataIndex: "workNo", key: "workNo", width: 120 },
{ title: "作品编号", key: "workNo", width: 120 },
{ title: "报名账号", key: "username", width: 150 },
{ title: "评委评分", key: "judgeScore", width: 100 },
{ title: "评审进度", key: "reviewProgress", width: 100 },
@ -461,6 +475,12 @@ const handleBack = () => {
router.back()
}
//
const handleViewWorkDetail = (record: ContestWork) => {
currentWorkId.value = record.id
workDetailModalVisible.value = true
}
//
const handleStartReview = async () => {
try {

View File

@ -21,11 +21,18 @@
</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="contestType === 'team' ? '队伍名称' : '选手名称'">
<a-input
v-model:value="searchParams.name"
:placeholder="contestType === 'team' ? '请输入队伍名称' : '请输入选手名称'"
:placeholder="
contestType === 'team' ? '请输入队伍名称' : '请输入选手名称'
"
allow-clear
style="width: 150px"
/>
@ -108,31 +115,44 @@
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
</template>
<template v-else-if="column.key === 'workNo'">
<a @click="handleViewWork(record)">{{ record.workNo || '-' }}</a>
<a @click="handleViewWork(record)">{{ record.workNo || "-" }}</a>
</template>
<template v-else-if="column.key === 'username'">
{{ record.submitterAccountNo || record.registration?.user?.username || '-' }}
{{
record.submitterAccountNo ||
record.registration?.user?.username ||
"-"
}}
</template>
<template v-else-if="column.key === 'name'">
<template v-if="contestType === 'team'">
{{ record.registration?.team?.teamName || '-' }}
{{ record.registration?.team?.teamName || "-" }}
</template>
<template v-else>
{{ record.registration?.user?.nickname || '-' }}
{{ record.registration?.user?.nickname || "-" }}
</template>
</template>
<template v-else-if="column.key === 'submitTime'">
{{ formatDate(record.submitTime) }}
</template>
<template v-else-if="column.key === 'assignStatus'">
<a-tag v-if="record._count?.assignments > 0" color="success">已分配</a-tag>
<a-tag v-if="record._count?.assignments > 0" color="success"
>已分配</a-tag
>
<a-tag v-else color="default">未分配</a-tag>
</template>
<template v-else-if="column.key === 'judges'">
<template v-if="record.assignments && record.assignments.length > 0">
<a-space wrap>
<a-tag v-for="assignment in record.assignments" :key="assignment.id">
{{ assignment.judge?.nickname || assignment.judge?.username || '-' }}
<a-tag
v-for="assignment in record.assignments"
:key="assignment.id"
>
{{
assignment.judge?.nickname ||
assignment.judge?.username ||
"-"
}}
</a-tag>
</a-space>
</template>
@ -147,122 +167,141 @@
</a-table>
<!-- 作品详情弹框 -->
<a-modal
<WorkDetailModal
v-model:open="workModalVisible"
title="作品详情"
width="700px"
:footer="null"
>
<template v-if="currentWork">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="作品编号" :span="2">
{{ currentWork.workNo || '-' }}
</a-descriptions-item>
<a-descriptions-item label="作品标题" :span="2">
{{ currentWork.title }}
</a-descriptions-item>
<a-descriptions-item label="所属比赛" :span="2">
{{ currentWork.contest?.contestName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="提交人">
{{ currentWork.registration?.user?.nickname || currentWork.submitterAccountNo || '-' }}
</a-descriptions-item>
<a-descriptions-item label="报名账号">
{{ currentWork.registration?.user?.username || '-' }}
</a-descriptions-item>
<a-descriptions-item label="提交时间" :span="2">
{{ formatDate(currentWork.submitTime) }}
</a-descriptions-item>
<a-descriptions-item v-if="currentWork.description" label="作品描述" :span="2">
<div v-html="currentWork.description"></div>
</a-descriptions-item>
<a-descriptions-item v-if="currentWork.previewUrl" label="预览链接" :span="2">
<a :href="currentWork.previewUrl" target="_blank">{{ currentWork.previewUrl }}</a>
</a-descriptions-item>
</a-descriptions>
:work-id="currentWorkId"
/>
<!-- 附件列表 -->
<a-divider orientation="left">附件列表</a-divider>
<a-table
:columns="attachmentColumns"
:data-source="currentWork.attachments || []"
:pagination="false"
size="small"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'">
<a :href="record.fileUrl" target="_blank">
<FileOutlined /> {{ record.fileName }}
</a>
</template>
<template v-else-if="column.key === 'size'">
{{ formatFileSize(record.size) }}
</template>
</template>
</a-table>
</template>
</a-modal>
<!-- 分配评委弹框 -->
<a-modal
<!-- 分配评委抽屉 -->
<a-drawer
v-model:open="assignModalVisible"
title="分配评委"
placement="right"
width="800px"
:confirm-loading="assignLoading"
@ok="handleConfirmAssign"
:footer-style="{ textAlign: 'right' }"
@close="handleAssignDrawerClose"
>
<!-- 搜索 -->
<a-form layout="inline" class="mb-3">
<a-form-item label="评委姓名">
<a-input
v-model:value="judgeSearchParams.nickname"
placeholder="请输入姓名"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item label="机构">
<a-input
v-model:value="judgeSearchParams.tenantName"
placeholder="请输入机构"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearchJudges">搜索</a-button>
<a-button class="ml-2" @click="handleResetJudgeSearch">重置</a-button>
</a-form-item>
</a-form>
<div class="assign-judge-drawer">
<!-- 搜索区域 -->
<a-card class="mb-4" size="small">
<a-form layout="inline" :model="judgeSearchParams" @finish="handleSearchJudges">
<a-form-item label="姓名">
<a-input
v-model:value="judgeSearchParams.nickname"
placeholder="请输入姓名"
allow-clear
style="width: 150px"
@press-enter="handleSearchJudges"
/>
</a-form-item>
<a-form-item label="所属单位">
<a-input
v-model:value="judgeSearchParams.tenantName"
placeholder="请输入所属单位"
allow-clear
style="width: 200px"
@press-enter="handleSearchJudges"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleResetJudgeSearch">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 评委列表 -->
<a-table
:columns="judgeColumns"
:data-source="judgeList"
:loading="judgeListLoading"
:pagination="judgePagination"
:row-selection="judgeRowSelection"
row-key="id"
size="small"
@change="handleJudgeTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'judgeName'">
{{ record.judge?.nickname || record.judge?.username || '-' }}
<!-- 全部评委列表 -->
<a-card class="mb-4" size="small">
<template #title>全部评委</template>
<a-table
:columns="judgeColumns"
:data-source="judgeList"
:loading="judgeListLoading"
:pagination="judgePagination"
:row-selection="{
selectedRowKeys: selectedJudgeKeys,
onChange: handleJudgeSelectionChange,
}"
row-key="id"
size="small"
@change="handleJudgeTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'judgeName'">
{{ record.judge?.nickname || record.judge?.username || "-" }}
</template>
<template v-else-if="column.key === 'phone'">
{{ record.judge?.phone || "-" }}
</template>
<template v-else-if="column.key === 'tenant'">
{{ record.judge?.organization || record.judge?.tenant?.name || "-" }}
</template>
<template v-else-if="column.key === 'assignedCount'">
{{ record._count?.assignedContestWorks || 0 }}
</template>
</template>
</a-table>
</a-card>
<!-- 已选评委区域 -->
<a-card size="small">
<template #title>
已选 {{ selectedJudgeRows.length }} 位评委
</template>
<template v-else-if="column.key === 'phone'">
{{ record.judge?.phone || '-' }}
</template>
<template v-else-if="column.key === 'tenant'">
{{ record.judge?.tenant?.name || '-' }}
</template>
<template v-else-if="column.key === 'assignedCount'">
{{ record._count?.assignedContestWorks || 0 }}
</template>
</template>
</a-table>
</a-modal>
<a-list
:data-source="selectedJudgeRows"
size="small"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
{{ item.judge?.nickname || item.judge?.username || "-" }}
</template>
<template #description>
{{ item.judge?.organization || item.judge?.tenant?.name || "-" }}
</template>
</a-list-item-meta>
<template #actions>
<a-button
type="link"
danger
size="small"
@click="handleRemoveSelectedJudge(item.id)"
>
移除
</a-button>
</template>
</a-list-item>
</template>
<template #empty>
<a-empty description="暂未选择评委" :image="false" />
</template>
</a-list>
</a-card>
<!-- 底部操作按钮 -->
<div class="drawer-footer">
<a-space>
<a-button @click="handleAssignDrawerClose">取消</a-button>
<a-button
type="primary"
:loading="assignLoading"
:disabled="selectedJudgeRows.length === 0"
@click="handleConfirmAssign"
>
确定
</a-button>
</a-space>
</div>
</div>
</a-drawer>
</div>
</template>
@ -276,7 +315,6 @@ import {
ArrowLeftOutlined,
SearchOutlined,
ReloadOutlined,
FileOutlined,
} from "@ant-design/icons-vue"
import dayjs from "dayjs"
import {
@ -287,6 +325,7 @@ import {
type ContestWork,
type ContestJudge,
} from "@/api/contests"
import WorkDetailModal from "../components/WorkDetailModal.vue"
interface Tenant {
id: number
@ -344,7 +383,7 @@ const columns = computed(() => [
{
title: contestType === "team" ? "队伍名称" : "选手姓名",
key: "name",
width: 150
width: 150,
},
{ title: "递交时间", key: "submitTime", width: 160 },
{ title: "分配状态", key: "assignStatus", width: 100 },
@ -352,16 +391,9 @@ const columns = computed(() => [
{ title: "操作", key: "action", width: 100, fixed: "right" as const },
])
//
const attachmentColumns = [
{ title: "文件名", key: "fileName", dataIndex: "fileName", width: 300 },
{ title: "类型", key: "fileType", dataIndex: "fileType", width: 100 },
{ title: "大小", key: "size", dataIndex: "size", width: 100 },
]
//
const workModalVisible = ref(false)
const currentWork = ref<ContestWork | null>(null)
const currentWorkId = ref<number | null>(null)
//
const assignModalVisible = ref(false)
@ -384,14 +416,40 @@ const judgeSearchParams = reactive({
const selectedJudgeKeys = ref<number[]>([])
const selectedJudgeRows = ref<ContestJudge[]>([])
//
const judgeRowSelection = computed<TableProps["rowSelection"]>(() => ({
selectedRowKeys: selectedJudgeKeys.value,
onChange: (keys: any, rows: ContestJudge[]) => {
selectedJudgeKeys.value = keys
selectedJudgeRows.value = rows
},
}))
//
const handleJudgeSelectionChange = (selectedKeys: number[]) => {
//
const newSelectedIds = selectedKeys.filter(
(id) => !selectedJudgeKeys.value.includes(id)
)
//
const removedIds = selectedJudgeKeys.value.filter(
(id) => !selectedKeys.includes(id)
)
// ID
selectedJudgeKeys.value = selectedKeys
//
//
selectedJudgeRows.value = selectedJudgeRows.value.filter(
(judge) => !removedIds.includes(judge.id)
)
//
const newSelectedJudges = judgeList.value.filter((judge) =>
newSelectedIds.includes(judge.id)
)
selectedJudgeRows.value = [...selectedJudgeRows.value, ...newSelectedJudges]
}
//
const handleRemoveSelectedJudge = (judgeId: number) => {
selectedJudgeKeys.value = selectedJudgeKeys.value.filter((id) => id !== judgeId)
selectedJudgeRows.value = selectedJudgeRows.value.filter(
(judge) => judge.id !== judgeId
)
}
//
const judgeColumns = [
@ -407,16 +465,6 @@ const formatDate = (dateStr?: string) => {
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
}
//
const formatFileSize = (size?: string | number) => {
if (!size) return "-"
const bytes = typeof size === "string" ? parseInt(size) : size
if (isNaN(bytes)) return size
if (bytes < 1024) return bytes + " B"
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
}
//
const filterOption = (input: string, option: any) => {
return option.children?.toLowerCase().indexOf(input.toLowerCase()) >= 0
@ -507,14 +555,9 @@ const handleBack = () => {
}
//
const handleViewWork = async (record: ContestWork) => {
try {
const detail = await worksApi.getDetail(record.id)
currentWork.value = detail
workModalVisible.value = true
} catch (error: any) {
message.error(error?.response?.data?.message || "获取作品详情失败")
}
const handleViewWork = (record: ContestWork) => {
currentWorkId.value = record.id
workModalVisible.value = true
}
//
@ -540,6 +583,14 @@ const handleBatchAssign = () => {
fetchJudgeList()
}
//
const handleAssignDrawerClose = () => {
assignModalVisible.value = false
selectedJudgeKeys.value = []
selectedJudgeRows.value = []
currentAssignWork.value = null
}
//
const handleSearchJudges = () => {
judgePagination.current = 1
@ -570,7 +621,7 @@ const handleConfirmAssign = async () => {
assignLoading.value = true
try {
const judgeIds = selectedJudgeRows.value.map(j => j.judgeId)
const judgeIds = selectedJudgeRows.value.map((j) => j.judgeId)
if (isBatchAssign.value) {
//
@ -606,7 +657,7 @@ onMounted(() => {
})
</script>
<style scoped>
<style scoped lang="scss">
.works-detail-page {
padding: 0;
}
@ -629,4 +680,16 @@ onMounted(() => {
.ml-2 {
margin-left: 8px;
}
.assign-judge-drawer {
.drawer-footer {
position: sticky;
bottom: 0;
padding: 16px 0;
background: #fff;
border-top: 1px solid #f0f0f0;
margin-top: 16px;
text-align: right;
}
}
</style>

View File

@ -433,13 +433,37 @@ let fillLight: THREE.DirectionalLight | null = null
let spotLight: THREE.SpotLight | null = null
let gridHelper: THREE.GridHelper | null = null
// URL
const modelUrl = ref((route.query.url as string) || "")
// URL -
const SESSION_KEY = "model-viewer-url"
const getModelUrl = (): string => {
// query
const queryUrl = route.query.url as string
if (queryUrl) {
// sessionStorage便
sessionStorage.setItem(SESSION_KEY, queryUrl)
return queryUrl
}
// query sessionStorage
const storedUrl = sessionStorage.getItem(SESSION_KEY)
if (storedUrl) {
return storedUrl
}
return ""
}
const modelUrl = ref(getModelUrl())
console.log("模型查看器 - URL:", modelUrl.value)
//
const handleBack = () => {
router.back()
// sessionStorage URL使 URL
sessionStorage.removeItem(SESSION_KEY)
//
//
if (window.history.length <= 1) {
window.close()
} else {
router.back()
}
}
//