72QishierPlayer/src/views/QishierPlayer.vue
2026-03-12 13:10:11 +08:00

321 lines
9.1 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="container-fluid qishier-page py-3">
<div class="row g-3">
<div class="col-12">
<div class="top-card card border-0 shadow-sm">
<div class="card-body d-flex flex-wrap justify-content-between align-items-center gap-3">
<div>
<h2 class="mb-2 page-title">{{ columnInfo.name || '七十二家房客播放器' }}</h2>
<div class="page-subtitle">
{{ allEpisodeList.length }} 最后更新{{ updatedAtText }}
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-dark btn-sm px-3" @click="handleRefresh">刷新数据</button>
<button class="btn btn-warning btn-sm px-3" @click="openSource">数据来源荔枝网</button>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="toolbar-card card border-0 shadow-sm">
<div class="card-body d-flex flex-wrap gap-2 align-items-center">
<button class="btn btn-outline-dark btn-sm" @click="playPrevEpisode" :disabled="!hasPrevEpisode">
上一集
</button>
<button class="btn btn-outline-dark btn-sm" @click="playNextEpisode" :disabled="!hasNextEpisode">
下一集
</button>
<div class="form-check form-switch ms-2">
<input id="autoNext" v-model="autoNext" class="form-check-input" type="checkbox" />
<label class="form-check-label" for="autoNext">自动下一集</label>
</div>
<select v-model="playMode" class="form-select form-select-sm mode-select">
<option value="asc">顺序播放</option>
<option value="desc">倒序播放</option>
</select>
</div>
</div>
</div>
<div class="col-xl-8 col-lg-7">
<div class="content-card card border-0 shadow-sm">
<div class="card-body">
<QishierVideoPlayer
:episode="currentEpisode"
:auto-next="autoNext"
@error="handlePlayerError"
@ended-next="handleEndedNext"
/>
<div v-if="currentEpisode" class="episode-info mt-3">
<div class="episode-badge mb-2">当前播放</div>
<h4 class="mb-2 episode-title">{{ currentEpisode.title }}</h4>
<div class="episode-meta">
发布时间{{ formatDate(currentEpisode.releasedAt) }}
<span class="mx-2"></span>
时长{{ formatDuration(currentEpisode.timeLength) }}
</div>
</div>
<div v-if="errorMsg" class="alert alert-danger mt-3 mb-0">
{{ errorMsg }}
</div>
<QishierShowcase
v-if="showcaseEpisodes.length > 0"
:episodes="showcaseEpisodes"
@select="playEpisode"
/>
<QishierProjectLinks />
</div>
</div>
</div>
<div class="col-xl-4 col-lg-5">
<QishierEpisodeList
:episodes="displayEpisodeList"
:current-episode-id="currentEpisode?.id || ''"
:years="years"
:selected-year="selectedYear"
@select="playEpisode"
@updateYear="handleYearChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { getAllQishierList, refreshQishierCache, type EpisodeData } from '@/api/qishier'
import { getCurrentEpisodeId, saveCurrentEpisodeId } from '@/utils/playHistory'
import QishierVideoPlayer from '@/components/qishier/QishierVideoPlayer.vue'
import QishierEpisodeList from '@/components/qishier/QishierEpisodeList.vue'
import QishierShowcase from '@/components/qishier/QishierShowcase.vue'
import QishierProjectLinks from '@/components/qishier/QishierProjectLinks.vue'
const columnInfo = reactive({
name: '',
coverUrl: ''
})
const allEpisodeList = ref<EpisodeData[]>([])
const currentEpisode = ref<EpisodeData | null>(null)
const errorMsg = ref('')
const updatedAt = ref<string | null>(null)
const years = ref<number[]>([])
const selectedYear = ref('all')
const autoNext = ref(true)
const playMode = ref<'asc' | 'desc'>('asc')
const updatedAtText = computed(() => {
if (!updatedAt.value) return '暂无缓存'
return new Date(updatedAt.value).toLocaleString('zh-CN')
})
const displayEpisodeList = computed(() => {
let list = [...allEpisodeList.value]
if (selectedYear.value !== 'all') {
const year = Number(selectedYear.value)
list = list.filter(item => new Date(item.releasedAt).getFullYear() === year)
}
if (playMode.value === 'desc') {
list.reverse()
}
return list
})
const showcaseEpisodes = computed(() => {
const list = displayEpisodeList.value.length > 0 ? displayEpisodeList.value : allEpisodeList.value
return list.slice(0, 8)
})
const currentDisplayIndex = computed(() => {
if (!currentEpisode.value) return -1
return displayEpisodeList.value.findIndex(item => item.id === currentEpisode.value?.id)
})
const hasPrevEpisode = computed(() => currentDisplayIndex.value > 0)
const hasNextEpisode = computed(() => {
return currentDisplayIndex.value >= 0 && currentDisplayIndex.value < displayEpisodeList.value.length - 1
})
function openSource() {
window.open('https://www1.gdtv.cn/', '_blank')
}
function handlePlayerError(message: string) {
errorMsg.value = message
}
function handleYearChange(year: string) {
selectedYear.value = year
}
function getEpisodeYear(item: EpisodeData) {
return String(new Date(item.releasedAt).getFullYear())
}
async function fetchAllEpisodes() {
try {
errorMsg.value = ''
const res = await getAllQishierList()
const data = res.data
columnInfo.name = data.name || '七十二家房客'
updatedAt.value = data.updatedAt || null
years.value = Array.isArray(data.years) ? [...data.years].sort((a, b) => b - a) : []
allEpisodeList.value = Array.isArray(data.list) ? data.list : []
if (allEpisodeList.value.length === 0) {
errorMsg.value = '暂无节目数据'
return
}
const savedEpisodeId = getCurrentEpisodeId()
const savedEpisode = allEpisodeList.value.find(item => item.id === savedEpisodeId)
if (savedEpisode) {
selectedYear.value = getEpisodeYear(savedEpisode)
currentEpisode.value = savedEpisode
return
}
const firstEpisode = allEpisodeList.value[0]
if (firstEpisode) {
selectedYear.value = getEpisodeYear(firstEpisode)
currentEpisode.value = firstEpisode
}
} catch (error: any) {
console.error('fetchAllEpisodes error:', error)
errorMsg.value = error?.message || '读取数据失败'
}
}
async function handleRefresh() {
try {
await refreshQishierCache()
await fetchAllEpisodes()
} catch (error: any) {
console.error('handleRefresh error:', error)
errorMsg.value = error?.message || '刷新失败'
}
}
async function playEpisode(item: EpisodeData) {
currentEpisode.value = item
selectedYear.value = getEpisodeYear(item)
errorMsg.value = ''
saveCurrentEpisodeId(item.id)
}
async function playPrevEpisode() {
if (!hasPrevEpisode.value) return
const prev = displayEpisodeList.value[currentDisplayIndex.value - 1]
if (prev) {
await playEpisode(prev)
}
}
async function playNextEpisode() {
if (!hasNextEpisode.value) return
const next = displayEpisodeList.value[currentDisplayIndex.value + 1]
if (next) {
await playEpisode(next)
}
}
async function handleEndedNext() {
if (autoNext.value) {
await playNextEpisode()
}
}
function formatDate(timestamp: number): string {
if (!timestamp) return '--'
const d = new Date(timestamp)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
function formatDuration(seconds: number): string {
if (!seconds && seconds !== 0) return '--'
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}
onMounted(() => {
void fetchAllEpisodes()
})
</script>
<style scoped>
.qishier-page {
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(255, 193, 7, 0.12), transparent 28%),
radial-gradient(circle at top right, rgba(13, 110, 253, 0.1), transparent 24%),
linear-gradient(180deg, #f8f9fa 0%, #eef1f5 100%);
}
.top-card,
.toolbar-card,
.content-card {
border-radius: 18px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8px);
}
.page-title {
font-weight: 700;
color: #212529;
}
.page-subtitle {
color: #6c757d;
font-size: 14px;
}
.mode-select {
width: 140px;
}
.episode-info {
padding: 2px 2px 6px;
}
.episode-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
background: rgba(255, 193, 7, 0.18);
color: #9a6a00;
font-size: 12px;
font-weight: 600;
}
.episode-title {
color: #212529;
font-weight: 700;
}
.episode-meta {
color: #6c757d;
font-size: 14px;
}
</style>