From 252ba922666555b237afedf91012ee141038a349 Mon Sep 17 00:00:00 2001 From: zhangxiaohua <827885272@qq.com> Date: Thu, 15 Jan 2026 18:00:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BD=9C=E5=93=81=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/router/index.ts | 8 +- .../contests/components/WorkDetailModal.vue | 505 ++++++++++++++++++ .../src/views/contests/results/Detail.vue | 21 +- .../views/contests/reviews/ProgressDetail.vue | 22 +- .../src/views/contests/works/WorksDetail.vue | 373 +++++++------ frontend/src/views/model/ModelViewer.vue | 30 +- 6 files changed, 795 insertions(+), 164 deletions(-) create mode 100644 frontend/src/views/contests/components/WorkDetailModal.vue diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 3cc7891..93969cc 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -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 } } diff --git a/frontend/src/views/contests/components/WorkDetailModal.vue b/frontend/src/views/contests/components/WorkDetailModal.vue new file mode 100644 index 0000000..c66a545 --- /dev/null +++ b/frontend/src/views/contests/components/WorkDetailModal.vue @@ -0,0 +1,505 @@ + + + + + + + {{ formatDateTime(workDetail.submitTime) }} + + + + + 作品介绍 + + + + + 暂无介绍 + + + + + + 作品详情 + + + + + + 暂无预览图 + + + + + + + 3D模型预览 + + + + + + + + + + 作品附件 + + + + + 附件名称: + {{ attachment.fileName?.split('.')[0] || attachment.fileName }} + + + + {{ attachment.fileName }} + + + + + + + + + + + + + 评审记录 + + + + + + + 作品评分: + + {{ record.score }} 分 + + 未评审 + + + 评委老师: + {{ record.judge?.nickname || record.judge?.username || '-' }} + + + 评分时间: + + {{ record.scoreTime ? formatDateTime(record.scoreTime) : '-' }} + + + + 老师评语: + {{ record.comment }} + + + + + + + + + + + + + + + + + diff --git a/frontend/src/views/contests/results/Detail.vue b/frontend/src/views/contests/results/Detail.vue index 01ce5bd..f09842a 100644 --- a/frontend/src/views/contests/results/Detail.vue +++ b/frontend/src/views/contests/results/Detail.vue @@ -68,6 +68,9 @@ {{ (pagination.current - 1) * pagination.pageSize + index + 1 }} + + {{ record.workNo || "-" }} + {{ Number(record.finalScore).toFixed(2) }} @@ -94,6 +97,12 @@ + + + @@ -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(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" diff --git a/frontend/src/views/contests/reviews/ProgressDetail.vue b/frontend/src/views/contests/reviews/ProgressDetail.vue index d11da0b..faad85c 100644 --- a/frontend/src/views/contests/reviews/ProgressDetail.vue +++ b/frontend/src/views/contests/reviews/ProgressDetail.vue @@ -83,6 +83,9 @@ {{ (pagination.current - 1) * pagination.pageSize + index + 1 }} + + {{ record.workNo || "-" }} + {{ record.submitterAccountNo || record.registration?.user?.username || "-" }} @@ -214,6 +217,12 @@ + + + (null) + // 列表状态 const loading = ref(false) const dataSource = ref([]) @@ -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 { diff --git a/frontend/src/views/contests/works/WorksDetail.vue b/frontend/src/views/contests/works/WorksDetail.vue index b1581ac..a474b80 100644 --- a/frontend/src/views/contests/works/WorksDetail.vue +++ b/frontend/src/views/contests/works/WorksDetail.vue @@ -21,11 +21,18 @@ - + @@ -108,31 +115,44 @@ {{ (pagination.current - 1) * pagination.pageSize + index + 1 }} - {{ record.workNo || '-' }} + {{ record.workNo || "-" }} - {{ record.submitterAccountNo || record.registration?.user?.username || '-' }} + {{ + record.submitterAccountNo || + record.registration?.user?.username || + "-" + }} - {{ record.registration?.team?.teamName || '-' }} + {{ record.registration?.team?.teamName || "-" }} - {{ record.registration?.user?.nickname || '-' }} + {{ record.registration?.user?.nickname || "-" }} {{ formatDate(record.submitTime) }} - 已分配 + 已分配 未分配 - - {{ assignment.judge?.nickname || assignment.judge?.username || '-' }} + + {{ + assignment.judge?.nickname || + assignment.judge?.username || + "-" + }} @@ -147,122 +167,141 @@ - - - - - {{ currentWork.workNo || '-' }} - - - {{ currentWork.title }} - - - {{ currentWork.contest?.contestName || '-' }} - - - {{ currentWork.registration?.user?.nickname || currentWork.submitterAccountNo || '-' }} - - - {{ currentWork.registration?.user?.username || '-' }} - - - {{ formatDate(currentWork.submitTime) }} - - - - - - {{ currentWork.previewUrl }} - - + :work-id="currentWorkId" + /> - - 附件列表 - - - - - {{ record.fileName }} - - - - {{ formatFileSize(record.size) }} - - - - - - - - + - - - - - - - - - - 搜索 - 重置 - - + + + + + + + + + + + + + + 搜索 + + + + 重置 + + + + - - - - - {{ record.judge?.nickname || record.judge?.username || '-' }} + + + 全部评委 + + + + {{ record.judge?.nickname || record.judge?.username || "-" }} + + + {{ record.judge?.phone || "-" }} + + + {{ record.judge?.organization || record.judge?.tenant?.name || "-" }} + + + {{ record._count?.assignedContestWorks || 0 }} + + + + + + + + + 已选 {{ selectedJudgeRows.length }} 位评委 - - {{ record.judge?.phone || '-' }} - - - {{ record.judge?.tenant?.name || '-' }} - - - {{ record._count?.assignedContestWorks || 0 }} - - - - + + + + + + {{ item.judge?.nickname || item.judge?.username || "-" }} + + + {{ item.judge?.organization || item.judge?.tenant?.name || "-" }} + + + + + 移除 + + + + + + + + + + + + + + @@ -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(null) +const currentWorkId = ref(null) // 分配评委弹框 const assignModalVisible = ref(false) @@ -384,14 +416,40 @@ const judgeSearchParams = reactive({ const selectedJudgeKeys = ref([]) const selectedJudgeRows = ref([]) -// 评委行选择配置 -const judgeRowSelection = computed(() => ({ - 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(() => { }) - diff --git a/frontend/src/views/model/ModelViewer.vue b/frontend/src/views/model/ModelViewer.vue index 15b91d6..6a2f173 100644 --- a/frontend/src/views/model/ModelViewer.vue +++ b/frontend/src/views/model/ModelViewer.vue @@ -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() + } } // 重置相机视角