Files
ggl/src/pages/HomePage.vue
2025-09-23 07:35:11 +00:00

886 lines
19 KiB
Vue
Executable File
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="home-page">
<!-- 页面头部 -->
<Header />
<!-- 主要内容区域 -->
<main class="main-content">
<!-- 悬浮搜索按钮 -->
<div class="floating-search-btn" @click="toggleSearchModal">
<el-icon :size="24">
<Search />
</el-icon>
</div>
<!-- 搜索模态框 -->
<el-dialog
v-model="showSearchModal"
title="搜索视频"
width="80%"
:show-close="true"
center
class="search-modal"
>
<div class="modal-search-container">
<SearchBox
v-model="searchQuery"
:show-filters="true"
:show-hot-keywords="true"
@search="handleSearchFromModal"
class="modal-search"
/>
</div>
</el-dialog>
<!-- 分类导航 -->
<section class="categories-section">
</section>
<!-- 视频列表区域 -->
<section class="videos-section">
<div class="videos-container">
<!-- 筛选和排序 -->
<div class="videos-header">
<div class="header-left">
<h3 class="section-title">
<el-icon><VideoCamera /></el-icon>
{{ currentSectionTitle }}
</h3>
<span class="videos-count"> {{ totalVideos }} 个视频</span>
</div>
<div class="header-right">
<el-select
v-model="sortBy"
@change="handleSortChange"
size="small"
class="sort-select"
>
<el-option label="最新发布" value="latest" />
<el-option label="最多播放" value="popular" />
<el-option label="最多点赞" value="likes" />
<el-option label="时长排序" value="duration" />
</el-select>
<el-button-group class="view-mode-group">
<el-button
:type="viewMode === 'grid' ? 'primary' : 'default'"
@click="viewMode = 'grid'"
size="small"
>
<el-icon><Grid /></el-icon>
</el-button>
<el-button
:type="viewMode === 'list' ? 'primary' : 'default'"
@click="viewMode = 'list'"
size="small"
>
<el-icon><List /></el-icon>
</el-button>
</el-button-group>
</div>
</div>
<!-- 加载状态 -->
<Loading v-if="loading" type="goguryeo" />
<!-- 视频网格/列表 -->
<div
v-else-if="videos.length > 0"
class="videos-grid"
:class="{
'grid-mode': viewMode === 'grid',
'list-mode': viewMode === 'list'
}"
>
<VideoCard
v-for="video in videos"
:key="video.id"
:video="video"
:view-mode="viewMode"
@click="handleVideoClick(video)"
@like="handleVideoLike"
@share="handleVideoShare"
class="video-item"
/>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<el-empty
:image-size="200"
description="暂无视频内容"
>
<template #image>
<el-icon :size="80" color="var(--text-color-placeholder)">
<VideoCamera />
</el-icon>
</template>
<el-button type="primary" @click="handleRefresh">
刷新页面
</el-button>
</el-empty>
</div>
<!-- 加载更多指示器 -->
<div v-if="hasMore && !loading" class="load-more-indicator">
<div class="load-more-text">滚动加载更多...</div>
</div>
<!-- 加载中指示器 -->
<div v-if="loadingMore" class="loading-more">
<div class="loading-spinner"></div>
<span>加载更多中...</span>
</div>
<!-- 没有更多数据提示 -->
<div v-if="!hasMore && videos.length > 0" class="no-more-data">
<span>已加载全部内容</span>
</div>
</div>
</section>
<!-- 统计信息 -->
</main>
<!-- 页面底部 -->
<Footer />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useVideoStore } from '@/stores/video'
import { videoApi, statsApi, categoryApi } from '@/api'
import type { Video } from '@/stores/video'
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import Loading from '@/components/Loading.vue'
import VideoCard from '@/components/VideoCard.vue'
import Pagination from '@/components/Pagination.vue'
import SearchBox from '@/components/SearchBox.vue'
import {
VideoPlay,
Grid,
VideoCamera,
List,
View,
User,
Clock,
OfficeBuilding as Monument,
Picture,
Document,
Star,
Search
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 路由和状态管理
const router = useRouter()
const videoStore = useVideoStore()
// 响应式数据
const loading = ref(false)
const loadingMore = ref(false)
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('latest')
const viewMode = ref<'grid' | 'list'>('grid')
const currentPage = ref(1)
const pageSize = ref(20)
const totalVideos = ref(0)
const hasMore = ref(true)
const showSearchModal = ref(false)
// 搜索相关方法
const toggleSearchModal = () => {
showSearchModal.value = !showSearchModal.value
}
const handleSearchFromModal = (query: string, filters?: any) => {
searchQuery.value = query
if (filters) {
selectedCategory.value = filters.category || 'all'
sortBy.value = filters.sortBy || 'latest'
}
// 关闭模态框
showSearchModal.value = false
// 重新加载数据
loadVideos(false)
}
// 分类数据
const categories = ref([
{ id: 'all', name: '全部', icon: 'Grid', count: 0 },
{ id: 'history', name: '历史文化', icon: 'Monument', count: 0 },
{ id: 'architecture', name: '建筑艺术', icon: 'Picture', count: 0 },
{ id: 'archaeology', name: '考古发现', icon: 'Document', count: 0 },
{ id: 'documentary', name: '纪录片', icon: 'VideoCamera', count: 0 },
{ id: 'featured', name: '精选推荐', icon: 'Star', count: 0 }
])
// 统计数据
const stats = ref({
totalVideos: 0,
totalViews: 0,
totalUsers: 0,
totalDuration: 0,
todayViews: 0
})
// 计算属性
const videos = computed(() => videoStore.videos)
const currentSectionTitle = computed(() => {
const category = categories.value.find(c => c.id === selectedCategory.value)
return category ? category.name : '全部视频'
})
// 生命周期
onMounted(() => {
loadInitialData()
// 添加滚动事件监听
window.addEventListener('scroll', handleScroll)
})
// 组件卸载时移除滚动监听
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
// 监听器
watch([selectedCategory, sortBy], () => {
loadVideos(false) // 重新加载数据
})
// 方法
const loadInitialData = async () => {
await Promise.all([
loadVideos(),
loadStats(),
loadCategories()
])
}
const loadVideos = async (append = false) => {
try {
if (append) {
loadingMore.value = true
} else {
loading.value = true
currentPage.value = 1
hasMore.value = true
}
const params = {
page: currentPage.value,
pageSize: pageSize.value,
category: selectedCategory.value === 'all' ? undefined : selectedCategory.value,
sortBy: sortBy.value,
search: searchQuery.value || undefined
}
const response = await videoApi.getVideos(params)
if (response.code === 200) {
const newVideos = response.data.list
totalVideos.value = response.data.total
if (append) {
// 追加模式:将新数据添加到现有数据后面
const currentVideos = videoStore.videos
videoStore.setVideos([...currentVideos, ...newVideos])
} else {
// 替换模式:替换所有数据
videoStore.setVideos(newVideos)
}
// 检查是否还有更多数据
hasMore.value = videoStore.videos.length < totalVideos.value
}
} catch (error) {
console.error('Failed to load videos:', error)
ElMessage.warning('视频加载失败,请检查网络连接')
// 网络错误时清空视频列表,避免显示过期数据
if (!append) {
videoStore.setVideos([])
totalVideos.value = 0
}
} finally {
loading.value = false
loadingMore.value = false
}
}
const loadStats = async () => {
try {
const response = await statsApi.getOverallStats()
if (response.code === 200) {
stats.value = {
...response.data,
totalDuration: (response.data as any).totalDuration || 0
}
}
} catch (error) {
console.error('Failed to load stats:', error)
// 使用模拟数据作为后备
stats.value = {
totalVideos: 156,
totalViews: 89234,
totalUsers: 1247,
totalDuration: 45678,
todayViews: 0
}
}
}
const loadCategories = async () => {
try {
const response = await categoryApi.getCategories()
if (response.code === 200 && response.data) {
// 更新分类数据保留原有的静态分类并添加API返回的分类
const apiCategories = response.data
// 更新现有分类的计数
categories.value.forEach(category => {
if (category.id !== 'all') {
const apiCategory = apiCategories.find((c: any) => c.name === category.name)
if (apiCategory) {
category.count = apiCategory.videoCount || 0
}
}
})
// 计算全部分类的总数
const totalCount = apiCategories.reduce((sum: number, cat: any) => sum + (cat.videoCount || 0), 0)
const allCategory = categories.value.find(c => c.id === 'all')
if (allCategory) {
allCategory.count = totalCount
}
}
} catch (error) {
console.error('Failed to load categories:', error)
// 分类加载失败时不显示错误消息,保持静默
}
}
const handleSearch = (query: string, filters?: any) => {
searchQuery.value = query
if (filters) {
selectedCategory.value = filters.category || 'all'
sortBy.value = filters.sortBy || 'latest'
}
// 重新加载数据
loadVideos(false)
}
const handleCategoryClick = (category: any) => {
selectedCategory.value = category.id
}
const handleSortChange = () => {
// 排序变化会被watch监听自动重新加载数据
}
const handleVideoClick = (video: any) => {
router.push(`/video/${video.id}`)
}
const handleVideoLike = async (video: Video) => {
try {
const response = await videoApi.likeVideo(video.id)
if (response.data) {
// 更新本地状态
videoStore.updateVideo(video.id, {
likes: response.data.likes
})
}
} catch (error) {
console.error('Failed to like video:', error)
ElMessage.error('操作失败')
}
}
const handleVideoShare = (video: any) => {
const shareUrl = `${window.location.origin}/video/${video.id}`
if (navigator.share) {
navigator.share({
title: video.title,
text: video.description,
url: shareUrl
})
} else {
// 复制到剪贴板
navigator.clipboard.writeText(shareUrl).then(() => {
ElMessage.success('链接已复制到剪贴板')
})
}
}
// 加载更多数据
const loadMoreVideos = async () => {
if (loadingMore.value || !hasMore.value) return
currentPage.value += 1
await loadVideos(true)
}
// 滚动监听
const handleScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
// 当滚动到距离底部200px时开始加载更多
if (scrollTop + windowHeight >= documentHeight - 200) {
loadMoreVideos()
}
}
const handleRefresh = () => {
loadVideos(false)
}
const formatNumber = (num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
return num.toString()
}
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}小时${minutes}分钟`
}
return `${minutes}分钟`
}
</script>
<style scoped>
.home-page {
min-height: 100vh;
background: var(--bg-color-page);
}
.main-content {
padding-top: 0;
}
/* 悬浮搜索按钮 */
.floating-search-btn {
position: fixed;
bottom: 60px;
right: 30px;
width: 60px;
height: 60px;
background: var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
z-index: 1000;
color: white;
}
.floating-search-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
background: var(--primary-color-dark-2);
}
.floating-search-btn:active {
transform: translateY(0);
}
/* 搜索模态框 */
.search-modal {
border-radius: 12px;
}
.modal-search-container {
padding: 20px 0;
text-align: center;
}
.modal-search {
max-width: 600px;
margin: 0 auto;
}
/* 分类导航 */
.categories-section {
padding: 0 20px;
margin-bottom: 40px;
}
.categories-container {
max-width: 1200px;
margin: 0 auto;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 24px;
font-weight: 600;
color: var(--text-color-primary);
margin-bottom: 24px;
}
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
}
.category-card {
background: white;
border-radius: 12px;
padding: 24px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid var(--border-color-light);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.category-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border-color: var(--primary-color);
}
.category-card.active {
border-color: var(--primary-color);
background: var(--primary-color-light-9);
}
.category-icon {
color: var(--primary-color);
margin-bottom: 12px;
}
.category-name {
font-size: 16px;
font-weight: 600;
color: var(--text-color-primary);
margin-bottom: 8px;
}
.category-count {
font-size: 14px;
color: var(--text-color-secondary);
}
/* 视频列表区域 */
.videos-section {
padding: 0 20px;
margin-bottom: 40px;
}
.videos-container {
max-width: 1200px;
margin: 0 auto;
}
.videos-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.videos-count {
color: var(--text-color-secondary);
font-size: 14px;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.sort-select {
width: 120px;
}
.view-mode-group {
border-radius: 6px;
}
/* 视频网格 - 瀑布流布局 */
.videos-grid.grid-mode {
column-count: auto;
column-width: 300px;
column-gap: 1rem;
column-fill: balance;
}
.videos-grid.list-mode {
display: flex;
flex-direction: column;
gap: 16px;
}
.video-item {
transition: all 0.3s ease;
break-inside: avoid;
page-break-inside: avoid;
margin-bottom: 0.75rem;
display: inline-block;
width: 100%;
}
.video-item:hover {
transform: translateY(-2px);
}
/* 无限滚动相关样式 */
.load-more-indicator {
text-align: center;
padding: 20px;
color: var(--text-color-secondary);
}
.load-more-text {
font-size: 14px;
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 20px;
color: var(--text-color-secondary);
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border-color-light);
border-top: 2px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.no-more-data {
text-align: center;
padding: 20px;
color: var(--text-color-placeholder);
font-size: 14px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态 */
.empty-state {
padding: 60px 20px;
text-align: center;
}
/* 统计信息 */
.stats-section {
padding: 40px 20px;
background: linear-gradient(135deg, var(--primary-color-light-9) 0%, white 100%);
margin-bottom: 40px;
}
.stats-container {
max-width: 1200px;
margin: 0 auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 24px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.stat-icon {
color: var(--primary-color);
background: var(--primary-color-light-9);
padding: 12px;
border-radius: 8px;
}
.stat-number {
font-size: 28px;
font-weight: 700;
color: var(--text-color-primary);
line-height: 1;
}
.stat-label {
font-size: 14px;
color: var(--text-color-secondary);
margin-top: 4px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.search-title {
font-size: 24px;
}
.categories-grid {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
}
.category-card {
padding: 16px;
}
.videos-header {
flex-direction: column;
align-items: flex-start;
}
.videos-grid.grid-mode {
column-count: auto;
column-width: 280px;
column-gap: 0.8rem;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
}
/* 悬浮搜索按钮 */
.floating-search-btn {
position: fixed;
bottom: 80px;
right: 30px;
width: 56px;
height: 56px;
background: var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
z-index: 1000;
color: white;
}
.floating-search-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
background: var(--primary-color-dark-2);
}
.floating-search-btn:active {
transform: translateY(0);
}
/* 搜索模态框样式 */
.search-modal {
border-radius: 12px;
}
.search-modal .el-dialog__header {
padding: 20px 24px 0;
}
.search-modal .el-dialog__body {
padding: 20px 24px 30px;
}
.modal-search-container {
width: 100%;
}
.modal-search {
width: 100%;
}
@media (max-width: 480px) {
.main-content {
padding-top: 70px;
}
.search-title {
font-size: 20px;
}
.categories-grid {
grid-template-columns: repeat(2, 1fr);
}
.videos-grid.grid-mode {
column-count: 1;
column-width: auto;
}
.stats-grid {
grid-template-columns: 1fr;
}
.stat-card {
padding: 16px;
}
.floating-search-btn {
bottom: 100px;
right: 20px;
width: 48px;
height: 48px;
}
.search-modal {
width: 95% !important;
}
}
</style>