2026-01-09 18:14:35 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<a-drawer
|
|
|
|
|
|
v-model:open="visible"
|
|
|
|
|
|
title="参赛作品"
|
|
|
|
|
|
placement="right"
|
2026-01-15 09:28:22 +08:00
|
|
|
|
width="600px"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
:footer-style="{ textAlign: 'right', padding: '16px 24px' }"
|
|
|
|
|
|
@close="handleCancel"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #title>
|
|
|
|
|
|
<div class="drawer-title">
|
|
|
|
|
|
<span>参赛作品</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<div v-if="!work" class="empty-container">
|
|
|
|
|
|
<a-empty description="暂无作品" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="work-detail">
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<!-- 作品预览卡片 -->
|
|
|
|
|
|
<div class="work-preview-card">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="preview-wrapper"
|
|
|
|
|
|
@mouseenter="showActions = true"
|
|
|
|
|
|
@mouseleave="showActions = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 预览图 -->
|
|
|
|
|
|
<img
|
|
|
|
|
|
v-if="previewImageUrl"
|
|
|
|
|
|
:src="previewImageUrl"
|
|
|
|
|
|
alt="作品预览"
|
|
|
|
|
|
class="preview-image"
|
|
|
|
|
|
@error="handlePreviewError"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div v-else class="preview-placeholder">
|
|
|
|
|
|
<FileImageOutlined class="placeholder-icon" />
|
|
|
|
|
|
<span>暂无预览图</span>
|
|
|
|
|
|
</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<!-- 悬浮操作按钮 -->
|
|
|
|
|
|
<transition name="fade">
|
|
|
|
|
|
<div v-show="showActions" class="actions-overlay">
|
|
|
|
|
|
<div class="actions-buttons">
|
2026-01-09 18:14:35 +08:00
|
|
|
|
<a-button
|
2026-01-15 09:28:22 +08:00
|
|
|
|
v-if="workFile && is3DModelFile(workFile)"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
type="primary"
|
2026-01-15 09:28:22 +08:00
|
|
|
|
@click="handlePreview3DModel(workFile)"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
>
|
|
|
|
|
|
<template #icon><EyeOutlined /></template>
|
2026-01-15 09:28:22 +08:00
|
|
|
|
预览模型
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="workFile"
|
|
|
|
|
|
@click="handleDownloadWork"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #icon><DownloadOutlined /></template>
|
|
|
|
|
|
下载作品
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-15 09:28:22 +08:00
|
|
|
|
</transition>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 作品信息 -->
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<div class="work-info-section">
|
|
|
|
|
|
<div class="info-row">
|
|
|
|
|
|
<span class="info-label">作品名称</span>
|
|
|
|
|
|
<span class="info-value">{{ work.title }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-row">
|
|
|
|
|
|
<span class="info-label">作品介绍</span>
|
|
|
|
|
|
<span class="info-value description">{{ work.description || "暂无介绍" }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-row">
|
|
|
|
|
|
<span class="info-label">作品编号</span>
|
|
|
|
|
|
<span class="info-value">{{ work.workNo || "-" }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-row">
|
|
|
|
|
|
<span class="info-label">提交时间</span>
|
|
|
|
|
|
<span class="info-value">{{ formatDateTime(work.submitTime) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-row">
|
|
|
|
|
|
<span class="info-label">作品状态</span>
|
|
|
|
|
|
<a-tag :color="getStatusColor(work.status)">
|
|
|
|
|
|
{{ getStatusText(work.status) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="work.version" class="info-row">
|
|
|
|
|
|
<span class="info-label">版本号</span>
|
|
|
|
|
|
<span class="info-value">v{{ work.version }}</span>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 上传的附件 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="work.attachments && work.attachments.length > 0"
|
2026-01-15 09:28:22 +08:00
|
|
|
|
class="attachments-section"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
>
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<div class="section-title">上传附件</div>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
<div class="attachments-list">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="attachment in work.attachments"
|
|
|
|
|
|
:key="attachment.id"
|
|
|
|
|
|
class="attachment-item"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="attachment-info">
|
|
|
|
|
|
<FileOutlined class="attachment-icon" />
|
|
|
|
|
|
<span class="attachment-name">{{ attachment.fileName }}</span>
|
|
|
|
|
|
<span v-if="attachment.size" class="attachment-size">
|
|
|
|
|
|
({{ formatFileSize(attachment.size) }})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
@click="handleDownloadAttachment(attachment)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #icon><DownloadOutlined /></template>
|
|
|
|
|
|
下载
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
2026-01-15 09:28:22 +08:00
|
|
|
|
<a-button @click="handleCancel">关闭</a-button>
|
2026-01-09 18:14:35 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</a-drawer>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, watch, computed } from "vue"
|
2026-01-14 14:29:16 +08:00
|
|
|
|
import { useRouter, useRoute } from "vue-router"
|
2026-01-09 18:14:35 +08:00
|
|
|
|
import { message } from "ant-design-vue"
|
|
|
|
|
|
import {
|
|
|
|
|
|
FileOutlined,
|
2026-01-15 09:28:22 +08:00
|
|
|
|
FileImageOutlined,
|
2026-01-09 18:14:35 +08:00
|
|
|
|
DownloadOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
} from "@ant-design/icons-vue"
|
|
|
|
|
|
import { worksApi, registrationsApi, type ContestWork } from "@/api/contests"
|
|
|
|
|
|
import { useAuthStore } from "@/stores/auth"
|
|
|
|
|
|
import dayjs from "dayjs"
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
open: boolean
|
|
|
|
|
|
contestId: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Emits {
|
|
|
|
|
|
(e: "update:open", value: boolean): void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
|
|
|
2026-01-14 14:29:16 +08:00
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const route = useRoute()
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
const visible = ref(false)
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const work = ref<ContestWork | null>(null)
|
2026-01-15 09:28:22 +08:00
|
|
|
|
const showActions = ref(false)
|
|
|
|
|
|
const previewError = ref(false)
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听抽屉打开状态
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.open,
|
|
|
|
|
|
async (newVal) => {
|
|
|
|
|
|
visible.value = newVal
|
|
|
|
|
|
if (newVal) {
|
2026-01-15 09:28:22 +08:00
|
|
|
|
previewError.value = false
|
2026-01-09 18:14:35 +08:00
|
|
|
|
await fetchUserWork()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
work.value = null
|
2026-01-15 09:28:22 +08:00
|
|
|
|
showActions.value = false
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(visible, (newVal) => {
|
|
|
|
|
|
emit("update:open", newVal)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前用户的作品
|
|
|
|
|
|
const fetchUserWork = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
work.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
const userId = authStore.user?.id
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
|
message.error("用户未登录,无法查看作品")
|
|
|
|
|
|
visible.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 先获取用户的报名记录
|
|
|
|
|
|
const registrationResponse = await registrationsApi.getList({
|
|
|
|
|
|
contestId: props.contestId,
|
|
|
|
|
|
userId: userId,
|
|
|
|
|
|
registrationType: "individual",
|
|
|
|
|
|
registrationState: "passed",
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (registrationResponse.list.length === 0) {
|
|
|
|
|
|
message.warning("您尚未报名该赛事或报名未通过,无法查看作品")
|
|
|
|
|
|
visible.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const registrationId = registrationResponse.list[0].id
|
|
|
|
|
|
|
|
|
|
|
|
// 获取该报名的所有作品版本,取最新版本
|
|
|
|
|
|
const works = await worksApi.getVersions(registrationId)
|
|
|
|
|
|
if (works && works.length > 0) {
|
|
|
|
|
|
// 找到最新版本的作品
|
|
|
|
|
|
const latestWork = works.find((w) => w.isLatest) || works[0]
|
|
|
|
|
|
work.value = latestWork
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.warning("您尚未提交作品")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
message.error(error?.response?.data?.message || "获取作品信息失败")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 预览图 URL
|
|
|
|
|
|
const previewImageUrl = computed(() => {
|
|
|
|
|
|
if (!work.value || previewError.value) return null
|
|
|
|
|
|
|
|
|
|
|
|
// 优先使用 previewUrl
|
|
|
|
|
|
if (work.value.previewUrl) {
|
|
|
|
|
|
return getFileUrl(work.value.previewUrl)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 作品文件(只取第一个)
|
|
|
|
|
|
const workFile = computed(() => {
|
|
|
|
|
|
if (!work.value) return null
|
|
|
|
|
|
let files = work.value.files || []
|
|
|
|
|
|
|
2026-01-12 20:04:11 +08:00
|
|
|
|
// 如果 files 是字符串(JSON),需要解析
|
2026-01-15 09:28:22 +08:00
|
|
|
|
if (typeof files === "string") {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
try {
|
|
|
|
|
|
files = JSON.parse(files)
|
2026-01-12 20:04:11 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
return null
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 20:04:11 +08:00
|
|
|
|
if (!Array.isArray(files) || files.length === 0) return null
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-12 20:04:11 +08:00
|
|
|
|
// 处理可能是对象 {fileUrl: string} 或字符串的情况
|
|
|
|
|
|
const firstFile = files[0]
|
2026-01-15 09:28:22 +08:00
|
|
|
|
return typeof firstFile === "object" && firstFile?.fileUrl
|
2026-01-12 20:04:11 +08:00
|
|
|
|
? firstFile.fileUrl
|
|
|
|
|
|
: firstFile
|
2026-01-09 18:14:35 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否为3D模型文件
|
2026-01-12 20:04:11 +08:00
|
|
|
|
const is3DModelFile = (fileUrl: string): boolean => {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
const modelExtensions = [
|
|
|
|
|
|
".glb",
|
|
|
|
|
|
".gltf",
|
|
|
|
|
|
".obj",
|
|
|
|
|
|
".fbx",
|
|
|
|
|
|
".3ds",
|
|
|
|
|
|
".dae",
|
|
|
|
|
|
".stl",
|
|
|
|
|
|
".ply",
|
2026-01-15 09:28:22 +08:00
|
|
|
|
".zip",
|
2026-01-09 18:14:35 +08:00
|
|
|
|
]
|
|
|
|
|
|
const lowerUrl = fileUrl.toLowerCase()
|
|
|
|
|
|
return modelExtensions.some((ext) => lowerUrl.includes(ext))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取文件URL(处理相对路径)
|
|
|
|
|
|
const getFileUrl = (fileUrl: string): string => {
|
|
|
|
|
|
if (!fileUrl) return ""
|
|
|
|
|
|
// 如果已经是完整URL,直接返回
|
|
|
|
|
|
if (fileUrl.startsWith("http://") || fileUrl.startsWith("https://")) {
|
|
|
|
|
|
return fileUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果是相对路径,拼接API基础URL
|
|
|
|
|
|
const baseURL = import.meta.env.VITE_API_BASE_URL || ""
|
|
|
|
|
|
if (fileUrl.startsWith("/api") && baseURL.includes("/api")) {
|
|
|
|
|
|
const urlWithoutApi = baseURL.replace(/\/api$/, "")
|
|
|
|
|
|
return `${urlWithoutApi}${fileUrl}`
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${baseURL}${fileUrl.startsWith("/") ? "" : "/"}${fileUrl}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 预览图加载错误
|
|
|
|
|
|
const handlePreviewError = () => {
|
|
|
|
|
|
previewError.value = true
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 预览3D模型
|
|
|
|
|
|
const handlePreview3DModel = (fileUrl: string) => {
|
|
|
|
|
|
if (!fileUrl) {
|
|
|
|
|
|
message.error("文件路径无效")
|
|
|
|
|
|
return
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
2026-01-15 09:28:22 +08:00
|
|
|
|
const fullUrl = getFileUrl(fileUrl)
|
|
|
|
|
|
const tenantCode = route.params.tenantCode as string
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
path: `/${tenantCode}/workbench/model-viewer`,
|
|
|
|
|
|
query: { url: fullUrl },
|
|
|
|
|
|
})
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 下载作品
|
|
|
|
|
|
const handleDownloadWork = () => {
|
|
|
|
|
|
if (!workFile.value) {
|
|
|
|
|
|
message.error("无作品文件")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const fileUrl = getFileUrl(workFile.value)
|
|
|
|
|
|
const link = document.createElement("a")
|
|
|
|
|
|
link.href = fileUrl
|
|
|
|
|
|
link.download = work.value?.title || "作品"
|
|
|
|
|
|
link.target = "_blank"
|
|
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
message.success("开始下载作品")
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 下载附件
|
|
|
|
|
|
const handleDownloadAttachment = async (attachment: any) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fileUrl = getFileUrl(attachment.fileUrl)
|
|
|
|
|
|
const link = document.createElement("a")
|
|
|
|
|
|
link.href = fileUrl
|
|
|
|
|
|
link.download = attachment.fileName
|
|
|
|
|
|
link.target = "_blank"
|
|
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
message.success("开始下载附件")
|
2026-01-15 09:28:22 +08:00
|
|
|
|
} catch {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
message.error("下载附件失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化日期时间
|
|
|
|
|
|
const formatDateTime = (dateStr?: string) => {
|
|
|
|
|
|
if (!dateStr) return "-"
|
|
|
|
|
|
return dayjs(dateStr).format("YYYY-MM-DD HH:mm:ss")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化文件大小
|
|
|
|
|
|
const formatFileSize = (size: string | number): string => {
|
|
|
|
|
|
if (!size) return "-"
|
|
|
|
|
|
const numSize = typeof size === "string" ? parseFloat(size) : size
|
|
|
|
|
|
if (numSize < 1024) {
|
|
|
|
|
|
return `${numSize} B`
|
|
|
|
|
|
} else if (numSize < 1024 * 1024) {
|
|
|
|
|
|
return `${(numSize / 1024).toFixed(2)} KB`
|
|
|
|
|
|
} else if (numSize < 1024 * 1024 * 1024) {
|
|
|
|
|
|
return `${(numSize / (1024 * 1024)).toFixed(2)} MB`
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return `${(numSize / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取状态颜色
|
|
|
|
|
|
const getStatusColor = (
|
|
|
|
|
|
status?: "submitted" | "locked" | "reviewing" | "rejected" | "accepted"
|
|
|
|
|
|
): string => {
|
|
|
|
|
|
const colorMap: Record<string, string> = {
|
|
|
|
|
|
submitted: "blue",
|
|
|
|
|
|
locked: "default",
|
|
|
|
|
|
reviewing: "processing",
|
|
|
|
|
|
rejected: "error",
|
|
|
|
|
|
accepted: "success",
|
|
|
|
|
|
}
|
|
|
|
|
|
return colorMap[status || "submitted"] || "default"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取状态文本
|
|
|
|
|
|
const getStatusText = (
|
|
|
|
|
|
status?: "submitted" | "locked" | "reviewing" | "rejected" | "accepted"
|
|
|
|
|
|
): string => {
|
|
|
|
|
|
const textMap: Record<string, string> = {
|
|
|
|
|
|
submitted: "已提交",
|
|
|
|
|
|
locked: "已锁定",
|
|
|
|
|
|
reviewing: "评审中",
|
|
|
|
|
|
rejected: "已拒绝",
|
|
|
|
|
|
accepted: "已通过",
|
|
|
|
|
|
}
|
|
|
|
|
|
return textMap[status || "submitted"] || "未知"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 取消
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
visible.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.drawer-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-container {
|
|
|
|
|
|
padding: 40px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.work-detail {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 作品预览卡片
|
|
|
|
|
|
.work-preview-card {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.preview-wrapper {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
border-radius: 12px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
overflow: hidden;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
background: #f5f5f5;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.preview-image {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.preview-placeholder {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.25);
|
|
|
|
|
|
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
2026-01-09 18:14:35 +08:00
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.placeholder-icon {
|
|
|
|
|
|
font-size: 64px;
|
|
|
|
|
|
margin-bottom: 16px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
span {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 悬浮操作层
|
|
|
|
|
|
.actions-overlay {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
position: absolute;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
2026-01-09 18:14:35 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.actions-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-btn) {
|
|
|
|
|
|
min-width: 140px;
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 淡入淡出动画
|
|
|
|
|
|
.fade-enter-active,
|
|
|
|
|
|
.fade-leave-active {
|
|
|
|
|
|
transition: opacity 0.2s ease;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
.fade-enter-from,
|
|
|
|
|
|
.fade-leave-to {
|
|
|
|
|
|
opacity: 0;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 09:28:22 +08:00
|
|
|
|
// 作品信息区域
|
|
|
|
|
|
.work-info-section {
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-row {
|
2026-01-09 18:14:35 +08:00
|
|
|
|
display: flex;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
|
|
|
|
&:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-label {
|
2026-01-15 09:28:22 +08:00
|
|
|
|
width: 80px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
|
|
|
font-size: 14px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value {
|
2026-01-15 09:28:22 +08:00
|
|
|
|
flex: 1;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
2026-01-15 09:28:22 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
|
|
|
|
|
|
&.description {
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 附件区域
|
|
|
|
|
|
.attachments-section {
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
|
|
|
|
|
margin-bottom: 12px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.attachments-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
gap: 8px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.attachment-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #d9d9d9;
|
2026-01-15 09:28:22 +08:00
|
|
|
|
border-radius: 8px;
|
2026-01-09 18:14:35 +08:00
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: #1890ff;
|
|
|
|
|
|
background: #f0f7ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.attachment-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.attachment-icon {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.attachment-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.85);
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.attachment-size {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|