321 lines
9.1 KiB
Vue
321 lines
9.1 KiB
Vue
<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>
|