2025-11-07 17:06:09 +08:00

905 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.

<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const route = useRoute()
// 响应式数据
const currentTab = ref('ranking') // 'ranking' 或 'news'
const rankingData = ref([])
const loading = ref(false)
const selectedDate = ref('')
const currentPage = ref(1)
const totalPages = ref(1)
const updateTime = ref('') // 添加更新时间字段
const showDatePicker = ref(false) // 控制日期选择器显示
const dateOptions = ref([]) // 日期选项列表
const selectedCategory = ref('all') // 当前选中的分类
// 初始化日期为今天
const initDate = () => {
const today = new Date()
selectedDate.value = today.toISOString().split('T')[0]
generateDateOptions()
}
// 生成日期选项今天和往前7天
const generateDateOptions = () => {
const options = []
const today = new Date()
for (let i = 0; i < 8; i++) {
const date = new Date(today)
date.setDate(today.getDate() - i)
const value = date.toISOString().split('T')[0]
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const weekday = weekdays[date.getDay()]
let label = ''
if (i === 0) {
label = '今天'
} else if (i === 1) {
label = '昨天'
} else {
label = `${i}天前`
}
const display = `${date.getMonth() + 1}${date.getDate()}${weekday}`
options.push({
value,
label,
display
})
}
dateOptions.value = options
}
// 获取排行榜数据
const fetchRankingData = async () => {
loading.value = true
try {
// 构建API参数
const params = {
page: currentPage.value,
limit: 100,
sort: 'growth',
start_date: selectedDate.value,
end_date: selectedDate.value
}
// 如果选择了特定分类,添加分类参数
if (selectedCategory.value !== 'all') {
params.classification_type = selectedCategory.value
}
const response = await axios.get('http://159.75.150.210:8443/api/rank/videos', {
params: params
})
if (response.data.success) {
rankingData.value = response.data.data
totalPages.value = response.data.pagination.pages
// 获取后端返回的更新时间
updateTime.value = response.data.update_time || ''
console.log(`获取${selectedCategory.value === 'all' ? '全部' : selectedCategory.value}分类数据成功,共${response.data.data.length}`)
} else {
console.error('获取数据失败:', response.data.message)
rankingData.value = []
}
} catch (error) {
console.error('API调用失败:', error)
rankingData.value = []
} finally {
loading.value = false
}
}
// 获取当前时间
const getCurrentTime = () => {
const now = new Date()
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
}
const getRankClass = (rank) => {
if (rank === 1) return 'rank-first'
if (rank === 2) return 'rank-second'
if (rank === 3) return 'rank-third'
return 'rank-normal'
}
// 格式化播放量
const formatPlayCount = (count) => {
if (!count) return '0'
if (count >= 100000000) {
return (count / 100000000).toFixed(1) + '亿'
} else if (count >= 10000) {
return (count / 10000).toFixed(1) + '万'
}
return count.toString()
}
// 格式化增长数据
const formatGrowth = (item) => {
const timelineData = item.timeline_data || {}
const change = timelineData.play_vv_change || 0
const changeRate = timelineData.play_vv_change_rate || 0
if (change > 0) {
return `${formatPlayCount(change)}`
}
return '暂无数据'
}
// 切换标签页
const switchTab = (tab) => {
currentTab.value = tab
if (tab === 'ranking') {
fetchRankingData()
}
}
// 获取图片源地址
const getImageSrc = (item) => {
// 优先使用 cover_image_url
if (item.cover_image_url) {
return item.cover_image_url
}
// 如果有备用链接,使用第一个
if (item.cover_backup_urls && item.cover_backup_urls.length > 0) {
return item.cover_backup_urls[0]
}
// 最后使用占位符
return '/placeholder-poster.svg'
}
// 处理图片加载错误
const handleImageError = (event, item) => {
const img = event.target
console.log('图片加载失败:', img.src, '视频:', item.title)
// 如果当前显示的是主链接,尝试备用链接
if (img.src === item.cover_image_url && item.cover_backup_urls && item.cover_backup_urls.length > 0) {
console.log('尝试备用链接:', item.cover_backup_urls[0])
// 尝试第一个备用链接
img.src = item.cover_backup_urls[0]
return
}
// 如果当前显示的是第一个备用链接,尝试其他备用链接
if (item.cover_backup_urls && item.cover_backup_urls.length > 1) {
const currentIndex = item.cover_backup_urls.indexOf(img.src)
if (currentIndex >= 0 && currentIndex < item.cover_backup_urls.length - 1) {
console.log('尝试下一个备用链接:', item.cover_backup_urls[currentIndex + 1])
img.src = item.cover_backup_urls[currentIndex + 1]
return
}
}
// 所有链接都失败,使用占位符
console.log('使用占位符图片')
img.src = '/placeholder-poster.svg'
}
// 日期改变处理
const onDateChange = () => {
currentPage.value = 1
fetchRankingData()
}
// 格式化显示日期
const formatDisplayDate = (dateStr) => {
if (!dateStr) return '选择日期'
const date = new Date(dateStr)
const today = new Date()
const diffTime = today.getTime() - date.getTime()
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
}
// 切换日期选择器显示状态
const toggleDatePicker = () => {
showDatePicker.value = !showDatePicker.value
}
// 关闭日期选择器
const closeDatePicker = () => {
showDatePicker.value = false
}
// 选择日期
const selectDate = (dateValue) => {
selectedDate.value = dateValue
showDatePicker.value = false
onDateChange()
}
// 切换分类
const switchCategory = (category) => {
selectedCategory.value = category
currentPage.value = 1 // 重置页码
fetchRankingData() // 重新获取数据
console.log(`切换到分类: ${category}`)
}
// 格式化日期显示(用于日榜标题)
const formatDateTitle = (dateStr) => {
if (!dateStr) return '日榜 2025年10月19日/周日'
const date = new Date(dateStr)
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const weekday = weekdays[date.getDay()]
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return `日榜 ${year}${month}${day}日/${weekday}`
}
// 获取排名徽章样式类
const getRankBadgeClass = (rank) => {
if (rank === 1) return 'rank-gold'
if (rank === 2) return 'rank-silver'
if (rank === 3) return 'rank-bronze'
return 'rank-normal'
}
// 跳转到后台管理
// const goToAdmin = () => {
// router.push('/admin')
// }
// 页面加载时初始化
onMounted(() => {
initDate()
fetchRankingData()
})
</script>
<template>
<div class="app">
<!-- 路由视图 -->
<router-view v-if="route.path !== '/'" />
<!-- 主容器 - 仅在首页显示 -->
<div v-if="route.path === '/'" class="main-container">
<!-- 顶部标题区域 -->
<div class="header-section">
<div class="title-wrapper">
<h1 class="main-title">AI棒榜</h1>
</div>
</div>
<!-- 横幅区域 -->
<!-- <div class="banner-section">
<div class="banner-content">
<p class="banner-subtitle">微短剧爆火</p>
<p class="banner-title">中国"血统"的ReelShort征服美国</p>
</div>
</div> -->
<!-- 装饰分隔线 -->
<!-- <div class="divider-dots"></div> -->
<!-- 日期显示区域 -->
<div class="date-section">
<p class="date-title">{{ formatDateTitle(selectedDate) }}</p>
<div class="date-dropdown-icon" @click="toggleDatePicker"></div>
</div>
<!-- 分类标签区域 -->
<div class="category-section">
<div
class="category-tab"
:class="{ active: selectedCategory === 'all' }"
@click="switchCategory('all')"
>
<span>全部</span>
</div>
<div
class="category-tab"
:class="{ active: selectedCategory === 'novel' }"
@click="switchCategory('novel')"
>
<span>小说</span>
</div>
<div
class="category-tab"
:class="{ active: selectedCategory === 'anime' }"
@click="switchCategory('anime')"
>
<span>动漫</span>
</div>
<div
class="category-tab"
:class="{ active: selectedCategory === 'drama' }"
@click="switchCategory('drama')"
>
<span>短剧</span>
</div>
</div>
<!-- 排行榜内容区域 -->
<div class="ranking-content">
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<div class="loading-spinner"></div>
<p>加载中...</p>
</div>
<!-- 排行榜列表 -->
<div v-else class="ranking-list">
<div
v-for="(item, index) in rankingData"
:key="item._id || index"
class="ranking-item"
>
<!-- 排名标识 -->
<div class="rank-badge" :class="getRankBadgeClass(index + 1)">
<span class="rank-number">{{ index + 1 }}</span>
</div>
<!-- 海报图片 -->
<div class="poster-container">
<img
:src="getImageSrc(item)"
:alt="item.title || item.mix_name"
@error="handleImageError($event, item)"
class="poster-image"
/>
</div>
<!-- 内容信息区域 -->
<div class="content-area">
<!-- 剧名 -->
<h3 class="drama-title">{{ item.title || item.mix_name || '奶团' }}</h3>
<!-- 详细信息 -->
<div class="drama-details">
<div class="detail-icons">
<img src="./images/剧场名icon.svg" alt="剧场名" class="detail-icon" />
<img src="./images/承制icon.svg" alt="承制" class="detail-icon" />
<img src="./images/版权icon.svg" alt="版权" class="detail-icon" />
</div>
<div class="detail-text">
<p>剧场名{{ item.series_author || '' }}</p>
<p>承制{{ item.Manufacturing_Field || '' }}</p>
<p>版权{{ item.Copyright_field || '' }}</p>
</div>
</div>
<!-- 数据统计 -->
<div class="stats-row">
<div class="stat-item">
<img src="./images/播放icon.svg" alt="播放" class="stat-icon" />
<span class="stat-value">{{ formatPlayCount(item.play_vv) || '9999W' }}</span>
</div>
<div class="stat-item">
<img src="./images/点赞icon.svg" alt="点赞" class="stat-icon" />
<span class="stat-value">{{ item.total_likes_formatted || '0' }}</span>
</div>
</div>
</div>
<!-- 增长数据 -->
<div class="growth-section">
<img src="./images/热度icon.svg" alt="热度" class="growth-icon" />
<span class="growth-value">{{ formatGrowth(item) || '300W' }}</span>
</div>
</div>
<!-- 空状态 -->
<div v-if="rankingData.length === 0" class="empty-state">
<p>暂无排行榜数据</p>
</div>
</div>
</div>
<!-- 日期选择弹窗 -->
<div v-if="showDatePicker" class="date-picker-overlay" @click="closeDatePicker">
<div class="date-picker-popup" @click.stop>
<div class="date-picker-header">
<h3>选择日期</h3>
<button class="close-btn" @click="closeDatePicker">×</button>
</div>
<div class="date-list">
<div
v-for="date in dateOptions"
:key="date.value"
class="date-option"
:class="{ active: selectedDate === date.value }"
@click="selectDate(date.value)"
>
<span class="date-label">{{ date.label }}</span>
<span class="date-value">{{ date.display }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 全局样式 */
.app {
min-height: 100vh;
background: #ebedf2;
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica, sans-serif;
}
/* 主容器 */
.main-container {
max-width: 428px;
margin: 0 auto;
background: #ebedf2;
min-height: 100vh;
position: relative;
}
/* 顶部标题区域 */
.header-section {
padding-top: 20px;
text-align: center;
}
.title-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
position: relative;
}
.main-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin: 0;
font-family: Alatsi, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica, sans-serif;
}
/* 横幅区域 */
.banner-section {
margin: 20px 16px;
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
border-radius: 12px;
padding: 20px;
position: relative;
overflow: hidden;
}
.banner-content {
position: relative;
z-index: 2;
}
.banner-subtitle {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
margin: 0 0 8px 0;
}
.banner-title {
font-size: 18px;
font-weight: bold;
color: white;
margin: 0;
line-height: 1.4;
}
/* 装饰分隔线 */
.divider-dots {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0;
gap: 8px;
}
.divider-dots::before,
.divider-dots::after {
content: '';
width: 4px;
height: 4px;
background: #4a90e2;
border-radius: 50%;
}
/* 日期显示区域 */
.date-section {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
gap: 8px;
}
.date-title {
font-size: 16px;
color: #333;
margin: 0;
font-weight: 500;
}
.date-dropdown-icon {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #4a90e2;
cursor: pointer;
}
/* 分类标签区域 */
.category-section {
display: flex;
justify-content: center;
gap: 12px;
margin: 20px 16px;
}
.category-tab {
padding: 8px 20px;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
}
.category-tab:not(.active) {
background: #d1d5db;
color: #6b7280;
}
.category-tab.active {
background: #4a90e2;
color: white;
}
/* 排行榜内容区域 */
.ranking-content {
padding: 0 16px;
}
/* 加载状态 */
.loading {
text-align: center;
padding: 40px 20px;
color: #666;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #4a90e2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 排行榜列表 */
.ranking-list {
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
padding: 0 16px;
}
.ranking-item {
min-height: 120px;
padding: 12px 0 ;
display: flex;
align-items: flex-start;
gap: 12px;
position: relative;
border-bottom: 1px solid #E1E3E5;
}
/* 排名徽章 */
.rank-badge {
position: absolute;
top: 11px;
left: -1px;
width: 24px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
color: white;
z-index: 10;
}
.rank-badge.rank-gold {
background: linear-gradient(135deg, #ffd700, #ffed4e);
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.4);
}
.rank-badge.rank-silver {
background: linear-gradient(135deg, #c0c0c0, #e8e8e8);
box-shadow: 0 2px 8px rgba(192, 192, 192, 0.4);
color: #666;
}
.rank-badge.rank-bronze {
background: linear-gradient(135deg, #cd7f32, #daa520);
box-shadow: 0 2px 8px rgba(205, 127, 50, 0.4);
}
.rank-badge.rank-normal {
background: #6b7280;
box-shadow: 0 2px 8px rgba(107, 114, 128, 0.3);
}
/* 海报容器 */
.poster-container {
width: 84px;
height: 112px;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.poster-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 内容区域 */
.content-area {
flex: 1;
min-width: 0;
}
.drama-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin: 0 0 8px 0;
line-height: 1.3;
}
/* 详细信息 */
.drama-details {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 12px;
}
.detail-icons {
display: flex;
flex-direction: column;
gap: 2px;
}
.detail-icon {
margin: 1px 0;
width: 14px;
height: 14px;
flex-shrink: 0;
}
.detail-text {
flex: 1;
}
.detail-text p {
font-size: 12px;
color: #6b7280;
margin: 0 0 2px 0;
line-height: 1.3;
}
/* 数据统计行 */
.stats-row {
display: flex;
align-items: center;
gap: 16px;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.stat-icon {
width: 16px;
height: 16px;
border-radius: 2px;
}
.stat-value {
font-size: 12px;
color: #374151;
font-weight: 500;
}
/* 增长数据 */
.growth-section {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
min-width: 60px;
}
.growth-icon {
width: 16px;
height: 16px;
background: white;
border-radius: 2px;
}
.growth-value {
color: #ef4444;
font-weight: bold;
font-size: 12px;
}
.comment-summary p {
font-size: 12px;
color: #6b7280;
margin: 0;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6b7280;
font-size: 16px;
}
/* 日期选择弹窗 */
.date-picker-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.date-picker-popup {
background: white;
border-radius: 20px;
padding: 0;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.date-picker-header {
background: #4a90e2;
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.date-picker-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.3s ease;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.date-list {
max-height: 400px;
overflow-y: auto;
}
.date-option {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.3s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.date-option:hover {
background: #f8f9fa;
}
.date-option.active {
background: #4a90e2;
color: white;
}
.date-option:last-child {
border-bottom: none;
}
.date-label {
font-weight: 500;
}
.date-value {
color: #666;
font-size: 14px;
}
.date-option.active .date-value {
color: rgba(255, 255, 255, 0.8);
}
/* 响应式设计 */
@media (max-width: 480px) {
.main-container {
max-width: 100%;
}
.banner-section {
margin: 20px 12px;
padding: 16px;
}
.ranking-content {
padding: 0 12px;
}
.ranking-item {
padding: 12px;
gap: 10px;
}
.poster-container {
width: 50px;
height: 70px;
}
}
</style>