Files
album-view/src/views/index/index.vue
2025-12-12 11:34:31 +08:00

760 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="album-index">
<div class="container">
<!-- 头部区域:标题 + 视图切换 -->
<div class="header-section">
<h1 class="main-title">影像集</h1>
<!-- 视图切换按钮组 -->
<div class="view-mode-group glass-panel">
<a-tooltip title="网格视图" color="#e63946">
<button
class="group-btn"
:class="{ active: viewMode === 'grid' }"
@click="changeViewMode('grid')"
>
<i class="fas fa-th-large"></i>
</button>
</a-tooltip>
<a-tooltip title="列表视图" color="#e63946">
<button
class="group-btn"
:class="{ active: viewMode === 'list' }"
@click="changeViewMode('list')"
>
<i class="fas fa-list-ul"></i>
</button>
</a-tooltip>
<a-tooltip title="时光轴" color="#e63946">
<button
class="group-btn"
:class="{ active: viewMode === 'timeline' }"
@click="changeViewMode('timeline')"
>
<i class="fas fa-stream"></i>
</button>
</a-tooltip>
<a-tooltip title="卡片堆叠" color="#e63946">
<button
class="group-btn"
:class="{ active: viewMode === 'stack' }"
@click="changeViewMode('stack')"
>
<i class="fas fa-layer-group"></i>
</button>
</a-tooltip>
<a-tooltip title="3D 封面流" color="#e63946">
<button
class="group-btn"
:class="{ active: viewMode === 'coverflow' }"
@click="changeViewMode('coverflow')"
>
<i class="fas fa-box-open"></i>
</button>
</a-tooltip>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载相册...</p>
</div>
<!-- 空状态 -->
<div v-else-if="albums.length === 0" class="empty-state">
<i class="fas fa-folder-open empty-icon"></i>
<p>暂无相册,去创建一个吧</p>
</div>
<!-- 主内容区:根据模式切换 -->
<div v-else class="content-wrapper">
<!-- 模式 A: 网格视图 (Grid) -->
<transition name="fade-up" mode="out-in">
<div v-if="viewMode === 'grid'" class="album-grid">
<div
v-for="album in albums"
:key="album.id"
class="album-card"
@click="goToAlbum(album.id)"
>
<div class="album-cover">
<img
:src="getImgUrl(album.url)"
:alt="album.title"
loading="lazy"
class="cover-image"
/>
<div class="card-overlay">
<div class="card-content">
<h3 class="card-title">{{ album.title }}</h3>
<div class="card-meta">
<span class="date">{{ formatDate(album.created_at) }}</span>
<span class="count">{{ album.photo_count }} 张</span>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
<!-- 模式 B: 列表视图 (List) -->
<transition name="fade-up" mode="out-in">
<div v-if="viewMode === 'list'" class="album-list">
<div
v-for="album in albums"
:key="album.id"
class="list-item glass-panel"
@click="goToAlbum(album.id)"
>
<div class="list-cover">
<img :src="getImgUrl(album.url)" loading="lazy" />
</div>
<div class="list-info">
<div class="info-header">
<h3 class="list-title">{{ album.title }}</h3>
<span class="list-count badge">{{ album.photo_count }} Photos</span>
</div>
<p class="list-desc">{{ album.description || '暂无描述...' }}</p>
<div class="list-footer">
<span class="list-date"><i class="far fa-calendar-alt"></i> {{ formatDate(album.created_at) }}</span>
<button class="enter-btn">进入相册 <i class="fas fa-arrow-right"></i></button>
</div>
</div>
</div>
</div>
</transition>
<!-- 模式 C: 时光轴视图 (Timeline) -->
<transition name="fade-up" mode="out-in">
<div v-if="viewMode === 'timeline'" class="album-timeline">
<div class="timeline-line"></div>
<div
v-for="(album, index) in albums"
:key="album.id"
class="timeline-item"
:class="index % 2 === 0 ? 'left' : 'right'"
@click="goToAlbum(album.id)"
>
<div class="timeline-dot"></div>
<div class="timeline-date">{{ formatDate(album.created_at) }}</div>
<div class="timeline-content glass-panel">
<div class="timeline-cover">
<img :src="getImgUrl(album.url)" loading="lazy" />
</div>
<div class="timeline-info">
<h3>{{ album.title }}</h3>
<span class="count">{{ album.photo_count }} 张照片</span>
</div>
</div>
</div>
</div>
</transition>
<!-- 模式 D: 卡片堆叠视图 (Card Stack) -->
<transition name="fade-scale" mode="out-in">
<div v-if="viewMode === 'stack'" class="album-stack">
<swiper
:effect="'cards'"
:grabCursor="true"
:modules="swiperModules"
class="stack-swiper"
>
<swiper-slide v-for="album in albums" :key="album.id" @click="goToAlbum(album.id)">
<div class="stack-card glass-panel">
<div class="stack-cover">
<img :src="getImgUrl(album.url)" />
</div>
<div class="stack-info">
<h3>{{ album.title }}</h3>
<div class="stack-meta">
<span>{{ formatDate(album.created_at) }}</span>
<span>{{ album.photo_count }} 张</span>
</div>
</div>
</div>
</swiper-slide>
</swiper>
<div class="hint-text">
<i class="fas fa-layer-group"></i> 左右滑动切换卡片
</div>
</div>
</transition>
<!-- 模式 E: 3D 封面流 (Cover Flow) -->
<transition name="fade-scale" mode="out-in">
<div v-if="viewMode === 'coverflow'" class="album-coverflow">
<swiper
:effect="'coverflow'"
:grabCursor="true"
:centeredSlides="true"
:slidesPerView="'auto'"
:coverflowEffect="{
rotate: 50,
stretch: 0,
depth: 100,
modifier: 1,
slideShadows: true,
}"
:pagination="{ clickable: true }"
:modules="swiperModules"
class="coverflow-swiper"
>
<swiper-slide v-for="album in albums" :key="album.id" @click="goToAlbum(album.id)">
<div class="slide-inner">
<img :src="getImgUrl(album.url)" />
<div class="slide-info glass-panel">
<h3>{{ album.title }}</h3>
<p>{{ album.photo_count }} 张照片</p>
</div>
</div>
</swiper-slide>
</swiper>
<div class="hint-text">
<i class="fas fa-hand-pointer"></i> 左右滑动选择 · 点击进入
</div>
</div>
</transition>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { albumListApi } from "@/api/request/api.js"
// Swiper
import { Swiper, SwiperSlide } from 'swiper/vue';
import { EffectCoverflow, EffectCards, Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/effect-coverflow';
import 'swiper/css/effect-cards';
import 'swiper/css/pagination';
const router = useRouter()
const albums = ref([])
const loading = ref(true)
const viewMode = ref('grid') // 'grid' | 'list' | 'timeline' | 'stack' | 'coverflow'
const swiperModules = [EffectCoverflow, EffectCards, Pagination]
// const getImgUrl = (path) => path ? `http://i.nailaoyun.cn${path}` : ''
const getImgUrl = (path) => path ? `/t-oss${path}` : ''
const getList = async () => {
loading.value = true
try {
const res = await albumListApi()
albums.value = res || []
} catch (error) {
console.error('获取相册列表失败:', error)
albums.value = []
} finally {
loading.value = false
}
}
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
}
const goToAlbum = (albumId) => {
router.push(`/detail/${albumId}`)
}
// 切换模式并持久化
const changeViewMode = (mode) => {
viewMode.value = mode
localStorage.setItem('album_view_mode', mode)
}
onMounted(() => {
// 读取缓存的视图模式
const savedMode = localStorage.getItem('album_view_mode')
if (savedMode) {
viewMode.value = savedMode
}
getList()
})
</script>
<style scoped>
.album-index {
font-family: 'Montserrat', sans-serif;
min-height: 100vh;
position: relative;
z-index: 2;
padding-bottom: 4rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* 头部布局 */
.header-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 3rem;
position: relative;
}
.main-title {
color: var(--light-color);
font-weight: 300;
letter-spacing: 4px;
font-size: 2.5rem;
text-transform: uppercase;
margin-bottom: 1.5rem;
text-shadow: 0 0 20px rgba(230, 57, 70, 0.4);
}
/* 视图切换按钮组 */
.view-mode-group {
display: flex;
background: rgba(255, 255, 255, 0.05);
border-radius: 30px;
padding: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
/* 移动端支持横向滑动 */
overflow-x: auto;
max-width: 100%;
}
.group-btn {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.5);
width: 44px;
height: 36px;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
flex-shrink: 0; /* 防止挤压 */
}
.group-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.1); }
.group-btn.active { background: #e63946; color: white; box-shadow: 0 2px 10px rgba(230, 57, 70, 0.4); }
/* --- 模式 A: 网格视图 --- */
.album-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.album-card {
position: relative;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
background: #1a1a1a;
border: 1px solid rgba(255,255,255,0.05);
}
.album-card:hover {
transform: translateY(-8px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4);
border-color: #e63946;
}
.album-cover {
aspect-ratio: 4/3;
position: relative;
overflow: hidden;
}
.cover-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.album-card:hover .cover-image { transform: scale(1.1); }
.card-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.2) 50%, transparent 100%);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 1.5rem;
opacity: 0.9;
}
.card-title {
color: #fff;
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
.card-meta {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: rgba(255,255,255,0.7);
}
/* --- 模式 B: 列表视图 --- */
.album-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 1000px;
margin: 0 auto;
}
.list-item {
display: flex;
background: rgba(20, 20, 20, 0.6);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
height: 180px;
}
.list-item:hover {
background: rgba(30, 30, 30, 0.8);
border-color: #e63946;
transform: translateX(10px);
}
.list-cover {
width: 280px;
height: 100%;
flex-shrink: 0;
}
.list-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.list-info {
flex: 1;
padding: 1.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.list-title {
color: #fff;
font-size: 1.4rem;
margin: 0;
}
.badge {
background: rgba(230, 57, 70, 0.2);
color: #e63946;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.list-desc {
color: rgba(255,255,255,0.6);
font-size: 0.95rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0.5rem 0;
}
.list-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.list-date {
color: rgba(255,255,255,0.4);
font-size: 0.85rem;
}
.enter-btn {
background: transparent;
border: 1px solid rgba(255,255,255,0.2);
color: #fff;
padding: 6px 16px;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
font-size: 0.85rem;
}
.list-item:hover .enter-btn {
background: #e63946;
border-color: #e63946;
}
/* --- 模式 C: 时光轴 (Timeline) --- */
.album-timeline {
position: relative;
max-width: 1000px;
margin: 0 auto;
padding: 2rem 0;
}
/* 垂直线 */
.timeline-line {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 2px;
background: rgba(255,255,255,0.1);
transform: translateX(-50%);
}
.timeline-item {
position: relative;
margin-bottom: 4rem;
width: 50%;
padding: 0 40px;
box-sizing: border-box;
cursor: pointer;
transition: transform 0.3s ease;
}
.timeline-item:hover { transform: translateY(-5px); }
.timeline-item.left { left: 0; text-align: right; }
.timeline-item.right { left: 50%; text-align: left; }
/* 节点 */
.timeline-dot {
position: absolute;
top: 20px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #e63946;
border: 4px solid #1a1a1a;
box-shadow: 0 0 0 2px rgba(230, 57, 70, 0.5);
z-index: 2;
}
.timeline-item.left .timeline-dot { right: -8px; }
.timeline-item.right .timeline-dot { left: -8px; }
/* 日期 */
.timeline-date {
position: absolute;
top: 18px;
color: #e63946;
font-weight: 600;
font-size: 0.9rem;
letter-spacing: 1px;
}
.timeline-item.left .timeline-date { right: -120px; text-align: left; width: 100px; }
.timeline-item.right .timeline-date { left: -120px; text-align: right; width: 100px; }
/* 内容卡片 */
.timeline-content {
background: rgba(30,30,30,0.6);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: border-color 0.3s;
}
.timeline-item:hover .timeline-content { border-color: #e63946; }
.timeline-cover {
width: 100%;
height: 180px;
}
.timeline-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.timeline-info {
padding: 1rem;
}
.timeline-info h3 { color: #fff; margin: 0 0 5px 0; font-size: 1.1rem; }
.timeline-info .count { color: rgba(255,255,255,0.5); font-size: 0.8rem; }
/* --- 模式 D: 卡片堆叠 (Stack) --- */
.album-stack {
display: flex;
justify-content: center;
align-items: center;
min-height: 500px;
flex-direction: column;
}
.stack-swiper {
width: 320px;
height: 460px;
}
.stack-card {
width: 100%;
height: 100%;
border-radius: 16px;
background: #222;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
.stack-cover {
height: 320px;
overflow: hidden;
}
.stack-cover img { width: 100%; height: 100%; object-fit: cover; }
.stack-info {
padding: 1.5rem;
background: linear-gradient(to bottom, #2a2a2a, #222);
height: calc(100% - 320px);
display: flex;
flex-direction: column;
justify-content: center;
}
.stack-info h3 { color: #fff; font-size: 1.4rem; margin-bottom: 0.5rem; }
.stack-meta { color: #e63946; font-size: 0.9rem; display: flex; justify-content: space-between; }
/* --- 模式 E: Cover Flow --- */
.album-coverflow {
padding: 3rem 0;
text-align: center;
}
.coverflow-swiper {
width: 100%;
padding-top: 50px;
padding-bottom: 50px;
}
.coverflow-swiper .swiper-slide {
background-position: center;
background-size: cover;
width: 300px;
height: 400px;
/* 倒影效果 (CSS Reflect) */
-webkit-box-reflect: below 1px linear-gradient(transparent, transparent 70%, rgba(0,0,0,0.3));
}
.slide-inner {
width: 100%;
height: 100%;
position: relative;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
cursor: pointer;
}
.slide-inner img {
width: 100%;
height: 100%;
object-fit: cover;
}
.slide-info {
position: absolute;
bottom: 0; left: 0; right: 0;
padding: 1rem;
background: rgba(0,0,0,0.7);
color: #fff;
transform: translateY(100%);
transition: transform 0.3s;
}
.swiper-slide-active .slide-info {
transform: translateY(0);
}
.hint-text {
margin-top: 2rem;
color: rgba(255,255,255,0.5);
font-size: 0.9rem;
}
.coverflow-hint {
margin-top: 2rem;
color: rgba(255,255,255,0.5);
font-size: 0.9rem;
}
/* Loading */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
color: var(--light-color);
}
.loading-spinner {
width: 40px; height: 40px;
border: 3px solid rgba(230, 57, 70, 0.3); border-top-color: #e63946;
border-radius: 50%;
animation: spin 1s infinite linear;
margin-bottom: 1rem;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
/* Transitions */
.fade-up-enter-active, .fade-up-leave-active { transition: all 0.4s ease; }
.fade-up-enter-from, .fade-up-leave-to { opacity: 0; transform: translateY(20px); }
.fade-scale-enter-active, .fade-scale-leave-active { transition: all 0.4s ease; }
.fade-scale-enter-from, .fade-scale-leave-to { opacity: 0; transform: scale(0.9); }
/* Mobile Responsive */
@media (max-width: 768px) {
.header-section { margin-bottom: 2rem; }
.main-title { font-size: 1.8rem; margin-bottom: 1rem; }
.list-item { height: auto; flex-direction: column; }
.list-cover { width: 100%; height: 200px; }
.list-item:hover { transform: translateY(-5px); }
.coverflow-swiper .swiper-slide { width: 240px; height: 320px; }
/* 时光轴移动端适配 */
.timeline-line { left: 20px; }
.timeline-item { width: 100%; padding-left: 50px; padding-right: 0; margin-bottom: 2rem; }
.timeline-item.left, .timeline-item.right { left: 0; text-align: left; }
.timeline-dot { left: 12px !important; right: auto !important; }
.timeline-date { position: relative; top: 0; left: 0 !important; right: auto !important; text-align: left !important; margin-bottom: 0.5rem; display: block; width: auto; }
}
</style>