File size: 6,269 Bytes
c4d0ee2 b877067 c4d0ee2 e04fdc1 c4d0ee2 821f77b c4d0ee2 ee4e4ea 36fd5c6 ee4e4ea 36fd5c6 ee4e4ea c4d0ee2 a9b6650 c4d0ee2 a9b6650 821f77b c4d0ee2 821f77b c4d0ee2 a9b6650 821f77b c4d0ee2 821f77b c4d0ee2 d5bab4d c4d0ee2 d5bab4d c4d0ee2 db6c1c9 c4d0ee2 db6c1c9 c4d0ee2 db6c1c9 c4d0ee2 db6c1c9 c4d0ee2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | <template>
<div class="page-container">
<div class="page-header">
<h1 class="page-title">🏆 播放排行</h1>
<p class="page-subtitle">YouTube & TikTok 播放量最高的视频排名,含收入估算明细</p>
</div>
<div class="filter-bar">
<select v-model="platform">
<option value="">全部平台</option>
<option value="youtube">YouTube</option>
<option value="tiktok">TikTok</option>
</select>
<select v-model="limit">
<option value="20">Top 20</option>
<option value="50">Top 50</option>
<option value="100">Top 100</option>
</select>
<button class="btn btn-primary" @click="loadData" :disabled="loading">🔄 刷新数据</button>
</div>
<div class="card">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="videos.length === 0" class="empty-state">
<div class="empty-icon">🏆</div>
<p>暂无数据,请先到数据采集页面抓取</p>
</div>
<div v-else class="table-wrapper topviewed-table-wrapper">
<table class="video-table">
<thead>
<tr>
<th>排名</th>
<th>平台</th>
<th>视频</th>
<th>播放量</th>
<th>点赞</th>
<th>发布时间</th>
<th>收入明细</th>
</tr>
</thead>
<tbody>
<tr v-for="v in videos" :key="v.id">
<td><span class="rank-number data-value">{{ v.rank }}</span></td>
<td>
<span class="platform-badge" :class="v.platform">{{ v.platform === 'youtube' ? 'YT' : 'TT' }}</span>
</td>
<td>
<div class="video-info">
<img :src="v.thumbnail_url" class="video-thumb" @error="handleImgError" />
<div class="video-info-content">
<a :href="v.video_url" target="_blank" class="video-title" :title="v.title">{{ v.title }}</a>
<div class="video-channel">{{ v.channel_title }}</div>
</div>
</div>
</td>
<td class="data-value">{{ formatNumber(v.view_count) }}</td>
<td class="data-value">{{ formatNumber(v.like_count) }}</td>
<td class="data-value">{{ formatTimeAgo(v.published_at) }}</td>
<td class="revenue-cell">
<span class="revenue-main">${{ formatRevenue(v.estimated_revenue_mid) }}</span>
<span class="revenue-range">${{ formatRevenue(v.estimated_revenue_low) }} ~ ${{ formatRevenue(v.estimated_revenue_high) }}</span>
<span class="revenue-detail">广告 ${{ formatRevenue(v.ad_revenue) }} · 分成 ${{ formatRevenue(v.creator_share_amount) }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { videosApi } from '../api'
export default {
name: 'TopViewed',
data() {
return {
videos: [],
platform: '',
limit: '50',
loading: false,
}
},
mounted() {
console.log('[TopViewed] Component mounted, calling loadData...')
this.loadData()
},
watch: {
platform(newVal, oldVal) {
if (oldVal !== undefined) {
this.loadData()
}
},
limit(newVal, oldVal) {
if (oldVal !== undefined) {
this.loadData()
}
},
},
methods: {
async loadData() {
console.log('[TopViewed] loadData called, platform:', this.platform, 'limit:', this.limit)
this.loading = true
try {
const params = { limit: parseInt(this.limit) }
if (this.platform) params.platform = this.platform
console.log('[TopViewed] API params:', params)
console.log('[TopViewed] Calling API...')
const res = await videosApi.getTopViewed(params)
console.log('[TopViewed] API response status:', res.status)
console.log('[TopViewed] API response data:', res.data)
console.log('[TopViewed] API response length:', res.data?.length)
this.videos = res.data || []
console.log('[TopViewed] videos set to:', this.videos.length, 'items')
} catch (e) {
console.error('[TopViewed] 加载失败:', e)
console.error('[TopViewed] Error details:', e.message, e.stack)
} finally {
this.loading = false
console.log('[TopViewed] Loading complete, videos:', this.videos.length)
}
},
formatNumber(num) {
if (!num) return '0'
if (num >= 1e9) return (num / 1e9).toFixed(1) + 'B'
if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M'
if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K'
return num.toString()
},
formatRevenue(num) {
if (!num) return '0'
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M'
if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K'
return num.toFixed(2)
},
formatTimeAgo(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
if (diffMs < 0) return '刚刚'
const diffSeconds = Math.floor(diffMs / 1000)
const diffMinutes = Math.floor(diffSeconds / 60)
const diffHours = Math.floor(diffMinutes / 60)
if (diffMinutes < 1) return '刚刚'
if (diffHours < 1) return diffMinutes + '分钟前'
if (diffHours < 24) return diffHours + '小时前'
const diffDays = Math.floor(diffHours / 24)
if (diffDays < 30) return diffDays + '天前'
const diffMonths = Math.floor(diffDays / 30)
if (diffMonths < 12) return diffMonths + '个月前'
return Math.floor(diffMonths / 12) + '年前'
},
handleImgError(e) {
e.target.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="96" height="54"><rect fill="%23f1f5f9" width="96" height="54" rx="8"/></svg>'
},
},
}
</script>
<style scoped>
.revenue-cell {
white-space: nowrap;
text-align: right;
}
.revenue-main {
color: #059669;
font-weight: 700;
font-size: 13px;
}
.revenue-range {
display: none;
}
.revenue-detail {
display: none;
}
</style>
|