修改样式
This commit is contained in:
parent
5be24bdc31
commit
9d3537ce53
@ -8,7 +8,8 @@
|
||||
"WebFetch(domain:3d.hunyuan.tencent.com)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:cloud.tencent.com)",
|
||||
"WebFetch(domain:cloud.tencent.com.cn)"
|
||||
"WebFetch(domain:cloud.tencent.com.cn)",
|
||||
"WebFetch(domain:ui-ux-pro-max-skill.nextlevelbuilder.io)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,11 +323,11 @@ const handleLogout = async () => {
|
||||
color: var(--sidebar-menu-text-selected, #01412b); // Logo 文字使用主题色
|
||||
margin-bottom: 8px;
|
||||
border-radius: 0; // 保持直角,与图片一致
|
||||
padding: 12px;
|
||||
padding: 20px 12px;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 40px;
|
||||
max-height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
@ -27,165 +27,95 @@
|
||||
<a-empty description="暂无赛事" />
|
||||
</div>
|
||||
|
||||
<div v-else class="contests-list">
|
||||
<div v-for="contest in dataSource" :key="contest.id" class="contest-card">
|
||||
<div class="contest-card-content">
|
||||
<!-- 左侧封面 -->
|
||||
<div class="contest-cover-wrapper">
|
||||
<div class="contest-cover">
|
||||
<img
|
||||
v-if="contest.coverUrl && !imageErrors[contest.id]"
|
||||
:src="contest.coverUrl"
|
||||
:alt="contest.contestName"
|
||||
@error="(e) => handleImageError(e, contest.id)"
|
||||
/>
|
||||
<div
|
||||
v-if="!contest.coverUrl || imageErrors[contest.id]"
|
||||
class="cover-placeholder"
|
||||
<div v-else class="contests-grid">
|
||||
<div
|
||||
v-for="contest in dataSource"
|
||||
:key="contest.id"
|
||||
class="contest-card"
|
||||
@click="handleViewDetail(contest.id)"
|
||||
>
|
||||
<!-- 卡片图标 -->
|
||||
<div class="card-icon">
|
||||
<TrophyOutlined />
|
||||
</div>
|
||||
|
||||
<!-- 卡片标题 -->
|
||||
<div class="card-title">{{ contest.contestName }}</div>
|
||||
|
||||
<!-- 卡片描述 -->
|
||||
<div class="card-desc">
|
||||
赛事时间:{{ formatDate(contest.startTime) }} ~ {{ formatDate(contest.endTime) }}
|
||||
</div>
|
||||
|
||||
<!-- 卡片标签 -->
|
||||
<div class="card-tags">
|
||||
<span class="tag tag-type">
|
||||
{{ contest.contestType === "individual" ? "个人赛" : "团队赛" }}
|
||||
</span>
|
||||
<span class="tag tag-status" :class="{ 'tag-ongoing': contest.status === 'ongoing' }">
|
||||
{{ getStatusText(contest) }}
|
||||
</span>
|
||||
<span v-if="getStageText(contest)" class="tag tag-stage">
|
||||
{{ getStageText(contest) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 - 我的赛事tab显示 -->
|
||||
<div v-if="activeTab === 'my'" class="card-actions" @click.stop>
|
||||
<!-- 学生角色按钮 -->
|
||||
<template v-if="userRole === 'student'">
|
||||
<template v-if="contest.contestType === 'individual'">
|
||||
<a-button
|
||||
v-if="isSubmitting(contest)"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleUploadWork(contest.id)"
|
||||
>
|
||||
<span>赛事封面</span>
|
||||
</div>
|
||||
<a-tag
|
||||
class="contest-type-tag"
|
||||
:color="contest.contestType === 'individual' ? 'blue' : 'green'"
|
||||
>
|
||||
{{ contest.contestType === "individual" ? "个人赛" : "团队赛" }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<!-- 图片下方按钮 - 根据角色显示不同按钮 -->
|
||||
<div v-if="activeTab === 'my'" class="cover-buttons">
|
||||
<!-- 学生角色按钮 -->
|
||||
<template v-if="userRole === 'student'">
|
||||
<template v-if="contest.contestType === 'individual'">
|
||||
<a-button
|
||||
v-if="isSubmitting(contest)"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="handleUploadWork(contest.id)"
|
||||
>
|
||||
上传作品
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
@click.stop="handleViewWorks(contest.id)"
|
||||
>
|
||||
参赛作品
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button
|
||||
size="small"
|
||||
@click.stop="handleViewWorks(contest.id)"
|
||||
>
|
||||
参赛作品
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
@click.stop="handleViewTeam(contest.id)"
|
||||
>
|
||||
我的队伍
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
上传作品
|
||||
</a-button>
|
||||
<a-button size="small" @click="handleViewWorks(contest.id)">
|
||||
参赛作品
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button size="small" @click="handleViewWorks(contest.id)">
|
||||
参赛作品
|
||||
</a-button>
|
||||
<a-button size="small" @click="handleViewTeam(contest.id)">
|
||||
我的队伍
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 教师角色按钮 -->
|
||||
<template v-if="userRole === 'teacher'">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="handleMyGuidance(contest.id)"
|
||||
>
|
||||
我的指导
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 评委角色按钮 -->
|
||||
<template v-if="userRole === 'judge'">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="isReviewEnded(contest)"
|
||||
@click.stop="handleReviewWorks(contest.id)"
|
||||
>
|
||||
评审作品
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="isReviewEnded(contest)"
|
||||
@click.stop="handlePresetComments(contest.id)"
|
||||
>
|
||||
预设评语
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="contest-content">
|
||||
<div class="contest-title">{{ contest.contestName }}</div>
|
||||
<div class="contest-status">
|
||||
<div class="status-row">
|
||||
<a-tag color="success" class="status-tag">
|
||||
{{ getStatusText(contest) }}
|
||||
</a-tag>
|
||||
<span v-if="contest.status === 'ongoing'" class="stage-text">
|
||||
{{ getStageText(contest) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="time-info">
|
||||
<div class="time-item">
|
||||
<span class="time-label">赛事时间:</span>
|
||||
<span class="time-value">
|
||||
{{ formatDate(contest.startTime) }} ~
|
||||
{{ formatDate(contest.endTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isRegistering(contest)" class="time-item">
|
||||
<a-tag color="success" class="stage-tag">报名中</a-tag>
|
||||
<span class="time-label">报名时间:</span>
|
||||
<span class="time-value">
|
||||
{{ formatDate(contest.registerStartTime) }} ~
|
||||
{{ formatDate(contest.registerEndTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isSubmitting(contest)" class="time-item">
|
||||
<a-tag color="success" class="stage-tag">征稿中</a-tag>
|
||||
<span class="time-label">提交作品:</span>
|
||||
<span class="time-value">
|
||||
{{ formatDate(contest.submitStartTime) }} ~
|
||||
{{ formatDate(contest.submitEndTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isReviewing(contest)" class="time-item">
|
||||
<a-tag color="processing" class="stage-tag">评审中</a-tag>
|
||||
<span class="time-label">评审作品:</span>
|
||||
<span class="time-value">
|
||||
{{ formatDate(contest.reviewStartTime) }} ~
|
||||
{{ formatDate(contest.reviewEndTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
contest.resultState === 'published' &&
|
||||
contest.resultPublishTime
|
||||
"
|
||||
class="time-item"
|
||||
>
|
||||
<span class="time-label">结果公布:</span>
|
||||
<span class="time-value">
|
||||
{{ formatDate(contest.resultPublishTime) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作按钮 -->
|
||||
<div class="contest-actions">
|
||||
<a-button type="primary" @click.stop="handleViewDetail(contest.id)">
|
||||
查看活动
|
||||
<!-- 教师角色按钮 -->
|
||||
<template v-if="userRole === 'teacher'">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleMyGuidance(contest.id)"
|
||||
>
|
||||
我的指导
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 评委角色按钮 -->
|
||||
<template v-if="userRole === 'judge'">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="isReviewEnded(contest)"
|
||||
@click="handleReviewWorks(contest.id)"
|
||||
>
|
||||
评审作品
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="isReviewEnded(contest)"
|
||||
@click="handlePresetComments(contest.id)"
|
||||
>
|
||||
预设评语
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -223,6 +153,7 @@
|
||||
import { ref, reactive, computed, onMounted } from "vue"
|
||||
import { useRouter, useRoute } from "vue-router"
|
||||
import { message } from "ant-design-vue"
|
||||
import { TrophyOutlined } from "@ant-design/icons-vue"
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
contestsApi,
|
||||
@ -487,6 +418,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 主题色
|
||||
$primary: #0958d9;
|
||||
$primary-light: #1677ff;
|
||||
|
||||
.contests-activities-page {
|
||||
padding: 24px;
|
||||
|
||||
@ -498,159 +433,115 @@ onMounted(() => {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.contests-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.contests-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
|
||||
.contest-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
|
||||
.contest-card-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contest-cover-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contest-cover {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||
}
|
||||
|
||||
.contest-type-tag {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
margin: 0;
|
||||
.card-icon {
|
||||
background: linear-gradient(135deg, #ff7a45 0%, #fa541c 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-buttons {
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba($primary, 0.08);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 22px;
|
||||
color: $primary;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.contest-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.contest-title {
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contest-status {
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
margin: 0;
|
||||
}
|
||||
.tag-type {
|
||||
background: rgba($primary, 0.08);
|
||||
color: $primary;
|
||||
border: 1px solid rgba($primary, 0.2);
|
||||
}
|
||||
|
||||
.stage-text {
|
||||
.tag-status {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
&.tag-ongoing {
|
||||
background: rgba(82, 196, 26, 0.08);
|
||||
color: #52c41a;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-color: rgba(82, 196, 26, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
|
||||
.time-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.stage-tag {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
color: #8c8c8c;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
.tag-stage {
|
||||
background: rgba(250, 173, 20, 0.08);
|
||||
color: #d48806;
|
||||
border: 1px solid rgba(250, 173, 20, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.contest-actions {
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
v-model:open="visible"
|
||||
title="参赛作品"
|
||||
placement="right"
|
||||
width="850px"
|
||||
width="600px"
|
||||
:footer-style="{ textAlign: 'right', padding: '16px 24px' }"
|
||||
@close="handleCancel"
|
||||
>
|
||||
@ -19,108 +19,87 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="work-detail">
|
||||
<!-- 作品名称 -->
|
||||
<div class="work-section">
|
||||
<div class="section-label">作品名称</div>
|
||||
<div class="section-content">{{ work.title }}</div>
|
||||
</div>
|
||||
<!-- 作品预览卡片 -->
|
||||
<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>
|
||||
|
||||
<!-- 作品介绍 -->
|
||||
<div class="work-section">
|
||||
<div class="section-label">作品介绍</div>
|
||||
<div class="section-content description-text">
|
||||
{{ work.description || "暂无介绍" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参赛作品 -->
|
||||
<div class="work-section">
|
||||
<div class="section-label">参赛作品</div>
|
||||
<div class="work-file-container">
|
||||
<div v-if="workFile" class="work-image-item">
|
||||
<div
|
||||
class="image-wrapper"
|
||||
@mouseenter="handleImageHover(workFile)"
|
||||
@mouseleave="handleImageLeave"
|
||||
>
|
||||
<img
|
||||
v-if="isImageFile(workFile)"
|
||||
:src="getFileUrl(workFile)"
|
||||
alt="作品图片"
|
||||
class="work-image"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div
|
||||
v-else-if="is3DModelFile(workFile)"
|
||||
class="file-placeholder model-file"
|
||||
>
|
||||
<FileOutlined class="file-icon" />
|
||||
<span class="file-name">{{ getFileName(workFile) }}</span>
|
||||
<!-- 悬浮操作按钮 -->
|
||||
<transition name="fade">
|
||||
<div v-show="showActions" class="actions-overlay">
|
||||
<div class="actions-buttons">
|
||||
<a-button
|
||||
v-if="workFile && is3DModelFile(workFile)"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="preview-btn"
|
||||
@click.stop="handlePreview3DModel(workFile)"
|
||||
@click="handlePreview3DModel(workFile)"
|
||||
>
|
||||
<template #icon><EyeOutlined /></template>
|
||||
预览3D模型
|
||||
预览模型
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="workFile"
|
||||
@click="handleDownloadWork"
|
||||
>
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
下载作品
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-else class="file-placeholder">
|
||||
<FileOutlined class="file-icon" />
|
||||
<span class="file-name">{{ getFileName(workFile) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图片预览遮罩 -->
|
||||
<div
|
||||
v-if="previewImage && previewImage === workFile"
|
||||
class="image-preview-overlay"
|
||||
@mouseleave="handleImageLeave"
|
||||
>
|
||||
<img
|
||||
:src="getFileUrl(workFile)"
|
||||
alt="作品预览"
|
||||
class="preview-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-files">暂无作品文件</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作品信息 -->
|
||||
<div class="work-section">
|
||||
<div class="section-label">作品信息</div>
|
||||
<div class="work-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">作品编号:</span>
|
||||
<span class="info-value">{{ work.workNo || "-" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">提交时间:</span>
|
||||
<span class="info-value">{{
|
||||
formatDateTime(work.submitTime)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">作品状态:</span>
|
||||
<a-tag :color="getStatusColor(work.status)">
|
||||
{{ getStatusText(work.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div v-if="work.version" class="info-item">
|
||||
<span class="info-label">版本号:</span>
|
||||
<span class="info-value">v{{ work.version }}</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传的附件 -->
|
||||
<div
|
||||
v-if="work.attachments && work.attachments.length > 0"
|
||||
class="work-section"
|
||||
class="attachments-section"
|
||||
>
|
||||
<div class="section-label">上传附件</div>
|
||||
<div class="section-title">上传附件</div>
|
||||
<div class="attachments-list">
|
||||
<div
|
||||
v-for="attachment in work.attachments"
|
||||
@ -149,11 +128,8 @@
|
||||
</a-spin>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">关闭</a-button>
|
||||
</a-space>
|
||||
<a-button @click="handleCancel">关闭</a-button>
|
||||
</template>
|
||||
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
@ -163,6 +139,7 @@ import { useRouter, useRoute } from "vue-router"
|
||||
import { message } from "ant-design-vue"
|
||||
import {
|
||||
FileOutlined,
|
||||
FileImageOutlined,
|
||||
DownloadOutlined,
|
||||
EyeOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
@ -188,7 +165,8 @@ const authStore = useAuthStore()
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const work = ref<ContestWork | null>(null)
|
||||
const previewImage = ref<string | null>(null)
|
||||
const showActions = ref(false)
|
||||
const previewError = ref(false)
|
||||
|
||||
// 监听抽屉打开状态
|
||||
watch(
|
||||
@ -196,10 +174,11 @@ watch(
|
||||
async (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
previewError.value = false
|
||||
await fetchUserWork()
|
||||
} else {
|
||||
work.value = null
|
||||
previewImage.value = null
|
||||
showActions.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@ -255,13 +234,25 @@ const fetchUserWork = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 预览图 URL
|
||||
const previewImageUrl = computed(() => {
|
||||
if (!work.value || previewError.value) return null
|
||||
|
||||
// 优先使用 previewUrl
|
||||
if (work.value.previewUrl) {
|
||||
return getFileUrl(work.value.previewUrl)
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
// 作品文件(只取第一个)
|
||||
const workFile = computed(() => {
|
||||
if (!work.value) return null
|
||||
let files = work.value.files || []
|
||||
|
||||
// 如果 files 是字符串(JSON),需要解析
|
||||
if (typeof files === 'string') {
|
||||
if (typeof files === "string") {
|
||||
try {
|
||||
files = JSON.parse(files)
|
||||
} catch {
|
||||
@ -273,26 +264,11 @@ const workFile = computed(() => {
|
||||
|
||||
// 处理可能是对象 {fileUrl: string} 或字符串的情况
|
||||
const firstFile = files[0]
|
||||
return typeof firstFile === 'object' && firstFile?.fileUrl
|
||||
return typeof firstFile === "object" && firstFile?.fileUrl
|
||||
? firstFile.fileUrl
|
||||
: firstFile
|
||||
})
|
||||
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (fileUrl: string): boolean => {
|
||||
const imageExtensions = [
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".bmp",
|
||||
".webp",
|
||||
".svg",
|
||||
]
|
||||
const lowerUrl = fileUrl.toLowerCase()
|
||||
return imageExtensions.some((ext) => lowerUrl.includes(ext))
|
||||
}
|
||||
|
||||
// 判断是否为3D模型文件
|
||||
const is3DModelFile = (fileUrl: string): boolean => {
|
||||
const modelExtensions = [
|
||||
@ -304,6 +280,7 @@ const is3DModelFile = (fileUrl: string): boolean => {
|
||||
".dae",
|
||||
".stl",
|
||||
".ply",
|
||||
".zip",
|
||||
]
|
||||
const lowerUrl = fileUrl.toLowerCase()
|
||||
return modelExtensions.some((ext) => lowerUrl.includes(ext))
|
||||
@ -318,82 +295,53 @@ const getFileUrl = (fileUrl: string): string => {
|
||||
}
|
||||
// 如果是相对路径,拼接API基础URL
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || ""
|
||||
// 如果 fileUrl 已经以 /api 开头,且 baseURL 也包含 /api,需要避免重复
|
||||
if (fileUrl.startsWith("/api") && baseURL.includes("/api")) {
|
||||
// fileUrl 已经包含 /api,直接拼接 baseURL 的协议和域名部分
|
||||
const urlWithoutApi = baseURL.replace(/\/api$/, "")
|
||||
return `${urlWithoutApi}${fileUrl}`
|
||||
}
|
||||
// 正常拼接
|
||||
return `${baseURL}${fileUrl.startsWith("/") ? "" : "/"}${fileUrl}`
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const getFileName = (fileUrl: string): string => {
|
||||
if (!fileUrl) return "文件"
|
||||
// 预览图加载错误
|
||||
const handlePreviewError = () => {
|
||||
previewError.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
// 移除查询参数和锚点
|
||||
const urlWithoutQuery = fileUrl.split("?")[0].split("#")[0]
|
||||
|
||||
// 提取文件名
|
||||
const parts = urlWithoutQuery.split("/")
|
||||
let fileName = parts[parts.length - 1] || "文件"
|
||||
|
||||
// 如果文件名是空的或者是特殊字符,返回默认值
|
||||
if (!fileName || fileName.trim() === "" || fileName === "[") {
|
||||
return "文件"
|
||||
}
|
||||
|
||||
// 解码 URL 编码的文件名
|
||||
try {
|
||||
fileName = decodeURIComponent(fileName)
|
||||
} catch {
|
||||
// 如果解码失败,使用原始文件名
|
||||
}
|
||||
|
||||
// 如果文件名太长,截断并添加省略号
|
||||
const maxLength = 20
|
||||
if (fileName.length > maxLength) {
|
||||
const ext = fileName.substring(fileName.lastIndexOf("."))
|
||||
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf("."))
|
||||
if (nameWithoutExt.length > maxLength - ext.length - 3) {
|
||||
return (
|
||||
nameWithoutExt.substring(0, maxLength - ext.length - 3) + "..." + ext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return fileName
|
||||
} catch (error) {
|
||||
console.error("获取文件名失败:", error)
|
||||
return "文件"
|
||||
// 预览3D模型
|
||||
const handlePreview3DModel = (fileUrl: string) => {
|
||||
if (!fileUrl) {
|
||||
message.error("文件路径无效")
|
||||
return
|
||||
}
|
||||
const fullUrl = getFileUrl(fileUrl)
|
||||
const tenantCode = route.params.tenantCode as string
|
||||
router.push({
|
||||
path: `/${tenantCode}/workbench/model-viewer`,
|
||||
query: { url: fullUrl },
|
||||
})
|
||||
}
|
||||
|
||||
// 图片鼠标移入
|
||||
const handleImageHover = (file: string) => {
|
||||
if (isImageFile(file)) {
|
||||
previewImage.value = file
|
||||
// 下载作品
|
||||
const handleDownloadWork = () => {
|
||||
if (!workFile.value) {
|
||||
message.error("无作品文件")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 图片鼠标移出
|
||||
const handleImageLeave = () => {
|
||||
previewImage.value = null
|
||||
}
|
||||
|
||||
// 图片加载错误
|
||||
const handleImageError = (event: Event) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.style.display = "none"
|
||||
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("开始下载作品")
|
||||
}
|
||||
|
||||
// 下载附件
|
||||
const handleDownloadAttachment = async (attachment: any) => {
|
||||
try {
|
||||
const fileUrl = getFileUrl(attachment.fileUrl)
|
||||
// 创建临时链接下载
|
||||
const link = document.createElement("a")
|
||||
link.href = fileUrl
|
||||
link.download = attachment.fileName
|
||||
@ -402,7 +350,7 @@ const handleDownloadAttachment = async (attachment: any) => {
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
message.success("开始下载附件")
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
message.error("下载附件失败")
|
||||
}
|
||||
}
|
||||
@ -456,23 +404,6 @@ const getStatusText = (
|
||||
return textMap[status || "submitted"] || "未知"
|
||||
}
|
||||
|
||||
// 预览3D模型
|
||||
const handlePreview3DModel = (fileUrl: string) => {
|
||||
console.log("handlePreview3DModel called with:", fileUrl)
|
||||
if (!fileUrl) {
|
||||
message.error("文件路径无效")
|
||||
return
|
||||
}
|
||||
const fullUrl = getFileUrl(fileUrl)
|
||||
console.log("预览3D模型,原始URL:", fileUrl, "完整URL:", fullUrl)
|
||||
// 跳转到模型查看器页面
|
||||
const tenantCode = route.params.tenantCode as string
|
||||
router.push({
|
||||
path: `/${tenantCode}/workbench/model-viewer`,
|
||||
query: { url: fullUrl },
|
||||
})
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
@ -494,160 +425,132 @@ const handleCancel = () => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.work-section {
|
||||
// 作品预览卡片
|
||||
.work-preview-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
.preview-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-label {
|
||||
// 悬浮操作层
|
||||
.actions-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.ant-btn) {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 作品信息区域
|
||||
.work-info-section {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
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;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.work-file-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.work-image-item {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #fafafa;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.work-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
position: relative;
|
||||
|
||||
&.model-file {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
margin-bottom: 8px;
|
||||
max-width: 100%;
|
||||
padding: 0 4px;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.image-preview-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.no-files {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.work-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
margin-right: 8px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.attachments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
@ -656,7 +559,7 @@ const handleCancel = () => {
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -8,13 +8,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="viewer-header">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<a-button type="text" class="back-btn" @click="handleBack">
|
||||
<template #icon><ArrowLeftOutlined /></template>
|
||||
</a-button>
|
||||
<span class="title">3D 模型预览</span>
|
||||
<span class="badge">LIVE</span>
|
||||
<span class="live-badge">
|
||||
<span class="pulse-dot"></span>
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-button type="text" class="action-btn" @click="resetCamera">
|
||||
@ -1168,76 +1171,87 @@ $gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
// ==========================================
|
||||
// Header
|
||||
// ==========================================
|
||||
.viewer-header {
|
||||
.page-header {
|
||||
height: 64px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: rgba($surface, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba($primary, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.back-btn {
|
||||
color: $text !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba($primary, 0.3) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
transition: all 0.3s !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
color: $text;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
background: $gradient-primary;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: $success;
|
||||
border-radius: 4px;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
&:hover {
|
||||
background: rgba($primary, 0.2) !important;
|
||||
border-color: $primary !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
background: $gradient-primary;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.live-badge {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
background: rgba($success, 0.1);
|
||||
border: 1px solid rgba($success, 0.3);
|
||||
border-radius: 20px;
|
||||
color: $success;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
.pulse-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: $success;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
color: $text !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba($primary, 0.3) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary, 0.2) !important;
|
||||
border-color: $primary !important;
|
||||
transform: translateY(-1px);
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -210,7 +210,10 @@ const modelCards = computed(() => {
|
||||
const resultUrls = task.value.resultUrls || []
|
||||
|
||||
// 如果是加载中状态且没有预览图,显示1个加载卡片
|
||||
if ((status === "pending" || status === "processing") && previewUrls.length === 0) {
|
||||
if (
|
||||
(status === "pending" || status === "processing") &&
|
||||
previewUrls.length === 0
|
||||
) {
|
||||
return [{ status: status, previewUrl: "" }]
|
||||
}
|
||||
|
||||
@ -436,10 +439,6 @@ $gradient-card: linear-gradient(
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba($surface, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba($primary, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
@ -871,7 +870,11 @@ $gradient-card: linear-gradient(
|
||||
.failed-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, rgba($error, 0.15) 0%, rgba($error, 0.25) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba($error, 0.15) 0%,
|
||||
rgba($error, 0.25) 100%
|
||||
);
|
||||
border: 2px solid rgba($error, 0.3);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
@ -884,7 +887,8 @@ $gradient-card: linear-gradient(
|
||||
}
|
||||
|
||||
@keyframes pulse-error {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba($error, 0.3);
|
||||
}
|
||||
@ -978,19 +982,67 @@ $gradient-card: linear-gradient(
|
||||
// ==========================================
|
||||
// Responsive
|
||||
// ==========================================
|
||||
@media (max-width: 1024px) {
|
||||
.page-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.model-grid {
|
||||
grid-template-columns: repeat(2, 280px);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
width: 280px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
gap: 12px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.live-badge,
|
||||
.pbr-tag {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.model-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
max-width: 320px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.tips-section {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -67,7 +67,6 @@
|
||||
class="history-card"
|
||||
@click="handleViewTask(task)"
|
||||
>
|
||||
<div class="card-glow"></div>
|
||||
<div class="card-preview">
|
||||
<img
|
||||
v-if="task.status === 'completed' && task.previewUrl"
|
||||
@ -102,14 +101,37 @@
|
||||
<div class="status-badge" :class="`status-${task.status}`">
|
||||
{{ getStatusText(task.status) }}
|
||||
</div>
|
||||
|
||||
<!-- 悬停显示的操作按钮 -->
|
||||
<div class="card-actions-overlay" @click.stop>
|
||||
<button
|
||||
v-if="task.status === 'completed'"
|
||||
class="overlay-btn btn-primary"
|
||||
@click="handlePreview(task)"
|
||||
>
|
||||
<EyeOutlined />
|
||||
<span>预览</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="['failed', 'timeout'].includes(task.status)"
|
||||
class="overlay-btn btn-primary"
|
||||
:disabled="task.retryCount >= 3"
|
||||
@click="handleRetry(task)"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
<span>重试</span>
|
||||
</button>
|
||||
<button
|
||||
class="overlay-btn btn-secondary"
|
||||
@click="handleDelete(task)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-info">
|
||||
<div class="card-type">
|
||||
<a-tag :color="task.inputType === 'text' ? 'blue' : 'green'">
|
||||
{{ task.inputType === 'text' ? '文生3D' : '图生3D' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="card-desc" :title="task.inputContent">
|
||||
{{ task.inputContent }}
|
||||
</div>
|
||||
@ -118,30 +140,9 @@
|
||||
<ClockCircleOutlined />
|
||||
{{ formatTime(task.createTime) }}
|
||||
</span>
|
||||
<div class="card-actions" @click.stop>
|
||||
<a-tooltip v-if="task.status === 'completed'" title="预览模型">
|
||||
<div class="action-btn" @click="handlePreview(task)">
|
||||
<EyeOutlined />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="['failed', 'timeout'].includes(task.status)"
|
||||
title="重试"
|
||||
>
|
||||
<div
|
||||
class="action-btn"
|
||||
:class="{ disabled: task.retryCount >= 3 }"
|
||||
@click="handleRetry(task)"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除">
|
||||
<div class="action-btn danger" @click="handleDelete(task)">
|
||||
<DeleteOutlined />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<span class="card-type">
|
||||
{{ task.inputType === 'text' ? '文生3D' : '图生3D' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -472,22 +473,37 @@ $gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba($surface, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba($primary, 0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.back-btn {
|
||||
color: $text !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba($primary, 0.3) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
transition: all 0.3s !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary, 0.2) !important;
|
||||
border-color: $primary !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
background: $gradient-primary;
|
||||
-webkit-background-clip: text;
|
||||
@ -496,33 +512,22 @@ $gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
padding: 4px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
background: rgba($primary, 0.1);
|
||||
border: 1px solid rgba($primary, 0.2);
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
color: $primary;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
color: $text !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba($primary, 0.3) !important;
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary, 0.1) !important;
|
||||
border-color: $primary !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@ -658,53 +663,47 @@ $gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
|
||||
.history-card {
|
||||
background: $surface;
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
// border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
|
||||
.card-glow {
|
||||
opacity: 0.3;
|
||||
.card-preview {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
transform: scale(1.1);
|
||||
.card-preview .preview-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-actions-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: $primary;
|
||||
border-radius: 10px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.card-preview {
|
||||
height: 160px;
|
||||
background: rgba($surface-light, 0.8);
|
||||
// border-radius: 8px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba($surface-light, 0.9) 0%,
|
||||
rgba($primary, 0.05) 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s ease;
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.preview-loading,
|
||||
@ -739,6 +738,62 @@ $gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// 悬停时显示的操作按钮遮罩层
|
||||
.card-actions-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.6) 0%,
|
||||
rgba(0, 0, 0, 0.2) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: 16px;
|
||||
gap: 10px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 10;
|
||||
|
||||
.overlay-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
|
||||
&.btn-primary {
|
||||
background: $primary;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: $primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: $text;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-error {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
@ -812,22 +867,19 @@ $gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
|
||||
.card-info {
|
||||
padding: 16px;
|
||||
background: rgba($primary, 0.15);
|
||||
}
|
||||
|
||||
.card-type {
|
||||
margin-bottom: 8px;
|
||||
background: $surface;
|
||||
border-top: 1px solid rgba($primary, 0.06);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
color: $text;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
@ -844,46 +896,20 @@ $gradient-primary: linear-gradient(135deg, $primary 0%, $primary-light 100%);
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba($primary, 0.1);
|
||||
border: 1px solid rgba($primary, 0.2);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
.card-type {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $primary-light;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary-light, 0.2);
|
||||
border-color: $primary-light;
|
||||
color: $primary-light;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.danger:hover {
|
||||
background: rgba($error, 0.15);
|
||||
border-color: $error;
|
||||
color: $error;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
background: rgba($primary, 0.08);
|
||||
}
|
||||
}
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba($primary, 0.1) 0%,
|
||||
rgba($primary-light, 0.15) 100%
|
||||
);
|
||||
border: 1px solid rgba($primary, 0.2);
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<!-- 文生3D输入 -->
|
||||
<div v-if="inputType === 'text'" class="text-input-section">
|
||||
<div class="input-label">
|
||||
<span class="label-icon">✨</span>
|
||||
<EditOutlined class="label-icon" />
|
||||
<span>创意描述</span>
|
||||
</div>
|
||||
<div class="input-hint">
|
||||
@ -100,7 +100,7 @@
|
||||
<!-- 图生3D上传 -->
|
||||
<div v-else class="image-input-section">
|
||||
<div class="input-label">
|
||||
<span class="label-icon">🖼️</span>
|
||||
<PictureOutlined class="label-icon" />
|
||||
<span>参考图片</span>
|
||||
</div>
|
||||
<div class="input-hint">
|
||||
@ -165,11 +165,7 @@
|
||||
<div class="right-panel">
|
||||
<!-- 介绍区 -->
|
||||
<div class="intro-section">
|
||||
<div class="intro-badge">
|
||||
<span class="badge-dot"></span>
|
||||
<span>AI Powered</span>
|
||||
</div>
|
||||
<h1 class="intro-title">用一句话、一张图<br />创造你的 3D 世界</h1>
|
||||
<h1 class="intro-title">用一句话、一张图,创造你的 3D 世界</h1>
|
||||
<p class="intro-desc">
|
||||
借助先进的 AI 技术,将文字描述或图片瞬间转化为专业级 3D 模型
|
||||
</p>
|
||||
@ -177,7 +173,7 @@
|
||||
<div class="intro-features">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon gradient-1">
|
||||
<span>✨</span>
|
||||
<BulbOutlined />
|
||||
</div>
|
||||
<div class="feature-info">
|
||||
<h3>AI 智能建模</h3>
|
||||
@ -187,7 +183,7 @@
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon gradient-2">
|
||||
<span>👁</span>
|
||||
<EyeOutlined />
|
||||
</div>
|
||||
<div class="feature-info">
|
||||
<h3>实时预览</h3>
|
||||
@ -197,7 +193,7 @@
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon gradient-3">
|
||||
<span>📁</span>
|
||||
<FolderOutlined />
|
||||
</div>
|
||||
<div class="feature-info">
|
||||
<h3>作品管理</h3>
|
||||
@ -207,7 +203,7 @@
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon gradient-4">
|
||||
<span>🔄</span>
|
||||
<SyncOutlined />
|
||||
</div>
|
||||
<div class="feature-info">
|
||||
<h3>迭代优化</h3>
|
||||
@ -256,7 +252,6 @@
|
||||
class="history-card"
|
||||
@click="handleViewTask(task)"
|
||||
>
|
||||
<div class="card-glow"></div>
|
||||
<div class="card-preview">
|
||||
<img
|
||||
v-if="task.status === 'completed' && task.previewUrl"
|
||||
@ -297,6 +292,34 @@
|
||||
<div class="status-badge" :class="`status-${task.status}`">
|
||||
{{ getStatusText(task.status) }}
|
||||
</div>
|
||||
|
||||
<!-- 悬停显示的操作按钮 -->
|
||||
<div class="card-actions-overlay" @click.stop>
|
||||
<button
|
||||
v-if="task.status === 'completed'"
|
||||
class="overlay-btn btn-primary"
|
||||
@click="handlePreview(task)"
|
||||
>
|
||||
<EyeOutlined />
|
||||
<span>预览</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="['failed', 'timeout'].includes(task.status)"
|
||||
class="overlay-btn btn-primary"
|
||||
:disabled="task.retryCount >= 3"
|
||||
@click="handleRetry(task)"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
<span>重试</span>
|
||||
</button>
|
||||
<button
|
||||
class="overlay-btn btn-secondary"
|
||||
@click="handleDelete(task)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-info">
|
||||
@ -308,37 +331,15 @@
|
||||
<ClockCircleOutlined />
|
||||
{{ formatTime(task.createTime) }}
|
||||
</span>
|
||||
<div class="card-actions" @click.stop>
|
||||
<a-tooltip v-if="task.status === 'completed'" title="预览">
|
||||
<div class="action-btn" @click="handlePreview(task)">
|
||||
<EyeOutlined />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="['failed', 'timeout'].includes(task.status)"
|
||||
title="重试"
|
||||
>
|
||||
<div
|
||||
class="action-btn"
|
||||
:class="{ disabled: task.retryCount >= 3 }"
|
||||
@click="handleRetry(task)"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="删除">
|
||||
<div class="action-btn danger" @click="handleDelete(task)">
|
||||
<DeleteOutlined />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<span class="card-type">
|
||||
{{ task.inputType === "text" ? "文生3D" : "图生3D" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -358,6 +359,10 @@ import {
|
||||
ArrowRightOutlined,
|
||||
ArrowLeftOutlined,
|
||||
ClockCircleOutlined,
|
||||
BulbOutlined,
|
||||
FolderOutlined,
|
||||
SyncOutlined,
|
||||
EditOutlined,
|
||||
} from "@ant-design/icons-vue"
|
||||
import {
|
||||
createAI3DTask,
|
||||
@ -873,7 +878,6 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid rgba($primary, 0.1);
|
||||
flex-shrink: 0;
|
||||
|
||||
.back-btn {
|
||||
@ -946,7 +950,6 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
|
||||
.panel-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid rgba($primary, 0.1);
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.ant-segmented) {
|
||||
@ -999,6 +1002,7 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
|
||||
.label-icon {
|
||||
font-size: 16px;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1211,7 +1215,6 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
|
||||
.panel-footer {
|
||||
padding: 24px;
|
||||
border-top: 1px solid rgba($primary, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -1302,66 +1305,46 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
height: 100vh;
|
||||
background: rgba($surface, 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.intro-section {
|
||||
padding: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.intro-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
background: rgba($primary, 0.15);
|
||||
border: 1px solid rgba($primary, 0.3);
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: $primary-light;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: $accent;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: $text;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
background: $gradient-primary;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$primary 0%,
|
||||
$primary-light 40%,
|
||||
#8b5cf6 70%,
|
||||
#a855f7 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.intro-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.intro-desc {
|
||||
font-size: 16px;
|
||||
color: $text-muted;
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.6;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.intro-features {
|
||||
@ -1389,7 +1372,8 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.gradient-1 {
|
||||
@ -1578,53 +1562,47 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
flex-shrink: 0;
|
||||
width: 240px;
|
||||
background: $surface;
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
// border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
.card-preview {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
opacity: 0.3;
|
||||
.card-preview .preview-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-actions-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: $primary;
|
||||
border-radius: 10px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.card-preview {
|
||||
height: 160px;
|
||||
background: rgba($surface-light, 0.8);
|
||||
// border-radius: 8px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba($surface-light, 0.9) 0%,
|
||||
rgba($primary, 0.05) 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
.history-card:hover & .preview-image {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.preview-loading,
|
||||
@ -1646,7 +1624,11 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
.failed-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, rgba($error, 0.15) 0%, rgba($error, 0.25) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba($error, 0.15) 0%,
|
||||
rgba($error, 0.25) 100%
|
||||
);
|
||||
border: 2px solid rgba($error, 0.3);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
@ -1659,8 +1641,65 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// 悬停时显示的操作按钮遮罩层
|
||||
.card-actions-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.6) 0%,
|
||||
rgba(0, 0, 0, 0.2) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: 16px;
|
||||
gap: 10px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 10;
|
||||
|
||||
.overlay-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
|
||||
&.btn-primary {
|
||||
background: $primary;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: $primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: $text;
|
||||
|
||||
&:hover {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-error {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba($error, 0.3);
|
||||
}
|
||||
@ -1740,7 +1779,8 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
|
||||
.card-info {
|
||||
padding: 16px;
|
||||
background: rgba(9, 88, 217, 0.15);
|
||||
background: $surface;
|
||||
border-top: 1px solid rgba($primary, 0.06);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
@ -1751,6 +1791,7 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
@ -1767,50 +1808,20 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba($primary, 0.1);
|
||||
border: 1px solid rgba($primary, 0.2);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
.card-type {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $primary-light;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary-light, 0.2);
|
||||
border-color: $primary-light;
|
||||
color: $primary-light;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
&:hover {
|
||||
background: rgba($accent, 0.2);
|
||||
border-color: $accent;
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary, 0.1);
|
||||
border-color: rgba($primary, 0.2);
|
||||
color: $primary-light;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba($primary, 0.1) 0%,
|
||||
rgba($primary-light, 0.15) 100%
|
||||
);
|
||||
border: 1px solid rgba($primary, 0.2);
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@ -1849,6 +1860,13 @@ $gradient-secondary: linear-gradient(135deg, $accent 0%, $primary 100%);
|
||||
|
||||
.history-card {
|
||||
width: 180px;
|
||||
|
||||
.card-actions-overlay {
|
||||
.overlay-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user