72QishierPlayer/src/views/QishierPlayer.vue
云上贵猪 7a93661e24 七十二家房客-观看平台v163
新增导航拦地址,方便分享
2026-03-14 02:13:18 +08:00

471 lines
13 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-start gap-3">
<div class="page-main-info">
<template v-if="currentEpisode">
<div class="episode-badges mb-2">
<span class="info-badge playing">当前播放</span>
<span class="meta-pill">发布时间{{ formatDate(currentEpisode.releasedAt) }}</span>
<span class="meta-pill">时长{{ formatDuration(currentEpisode.timeLength) }}</span>
</div>
<h2 class="mb-2 page-title">{{ currentEpisode.title }}</h2>
</template>
<template v-else>
<h2 class="mb-2 page-title">{{ columnInfo.name || '七十二家房客播放器' }}</h2>
<div class="episode-meta mb-2">正在加载节目数据...</div>
</template>
<div class="page-subtitle">
{{ allEpisodeList.length }}
<span class="mx-2"></span>
最后更新{{ updatedAtText }}
</div>
</div>
<div class="header-actions d-flex flex-wrap gap-2">
<button class="btn btn-dark btn-sm px-3" @click="handleRefresh">
刷新数据
</button>
<button class="btn btn-outline-dark btn-sm px-3" @click="openSource">
数据来源
</button>
<button class="btn btn-outline-primary btn-sm px-3" @click="openGitRepo">
Git仓库v163
</button>
</div>
</div>
</div>
</div>
<!-- 控制区 -->
<div class="col-12">
<div class="control-card card border-0 shadow-sm">
<div class="card-body d-flex flex-wrap justify-content-between align-items-center gap-3">
<div class="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-1">
<input id="autoNext" v-model="autoNext" class="form-check-input" type="checkbox" />
<label class="form-check-label small" for="autoNext">自动下一集</label>
</div>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<select v-model="playMode" class="form-select form-select-sm mode-select">
<option value="asc">顺序播放</option>
<option value="desc">倒序播放</option>
</select>
<select v-model="selectedYear" class="form-select form-select-sm filter-select">
<option value="all">全部年份</option>
<option v-for="year in years" :key="year" :value="String(year)">
{{ year }}
</option>
</select>
<select v-model="selectedMonth" class="form-select form-select-sm filter-select">
<option value="all">全部月份</option>
<option v-for="month in months" :key="month" :value="String(month)">
{{ month }}
</option>
</select>
</div>
</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="errorMsg" class="alert alert-danger mt-3 mb-0">
{{ errorMsg }}
</div>
<QishierShowcase
v-if="showcaseEpisodes.length > 0"
:episodes="showcaseEpisodes"
@select="playEpisode"
/>
</div>
</div>
</div>
<!-- 右侧列表 -->
<div class="col-xl-4 col-lg-5">
<QishierEpisodeList
:episodes="displayEpisodeList"
:current-episode-id="currentEpisode?.id || ''"
:years="years"
:months="months"
:selected-year="selectedYear"
:selected-month="selectedMonth"
@select="playEpisode"
@updateYear="handleYearChange"
@updateMonth="handleMonthChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } 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'
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 months = ref<number[]>([])
const selectedMonth = 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 (selectedMonth.value !== 'all') {
const month = Number(selectedMonth.value)
list = list.filter(item => new Date(item.releasedAt).getMonth() + 1 === month)
}
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 openGitRepo() {
window.open('https://git.a-hxin.cn/ahxin/72QishierPlayer.git', '_blank')
}
function getEpisodeIdFromUrl() {
const path = window.location.pathname
const match = path.match(/^\/episode\/([^/]+)$/)
if (match?.[1]) {
return decodeURIComponent(match[1])
}
const queryId = new URLSearchParams(window.location.search).get('episode')
return queryId || ''
}
function updateUrlByEpisodeId(id: string) {
const nextUrl = `/episode/${encodeURIComponent(id)}`
if (window.location.pathname !== nextUrl) {
window.history.replaceState({}, '', nextUrl)
}
}
function handlePlayerError(message: string) {
errorMsg.value = message
}
function handleYearChange(year: string) {
selectedYear.value = year
}
function handleMonthChange(month: string) {
selectedMonth.value = month
}
function getEpisodeYear(item: EpisodeData) {
return String(new Date(item.releasedAt).getFullYear())
}
function getEpisodeMonth(item: EpisodeData) {
return String(new Date(item.releasedAt).getMonth() + 1)
}
function rebuildMonthsByYear() {
let source = [...allEpisodeList.value]
if (selectedYear.value !== 'all') {
const year = Number(selectedYear.value)
source = source.filter(item => new Date(item.releasedAt).getFullYear() === year)
}
months.value = [
...new Set(source.map(item => new Date(item.releasedAt).getMonth() + 1))
].sort((a, b) => a - b)
if (selectedMonth.value !== 'all') {
const currentMonth = Number(selectedMonth.value)
if (!months.value.includes(currentMonth)) {
selectedMonth.value = 'all'
}
}
}
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
}
rebuildMonthsByYear()
const urlEpisodeId = getEpisodeIdFromUrl()
const urlEpisode = allEpisodeList.value.find(item => item.id === urlEpisodeId)
if (urlEpisode) {
selectedYear.value = getEpisodeYear(urlEpisode)
rebuildMonthsByYear()
selectedMonth.value = getEpisodeMonth(urlEpisode)
currentEpisode.value = urlEpisode
saveCurrentEpisodeId(urlEpisode.id)
updateUrlByEpisodeId(urlEpisode.id)
return
}
const savedEpisodeId = getCurrentEpisodeId()
const savedEpisode = allEpisodeList.value.find(item => item.id === savedEpisodeId)
if (savedEpisode) {
selectedYear.value = getEpisodeYear(savedEpisode)
rebuildMonthsByYear()
selectedMonth.value = getEpisodeMonth(savedEpisode)
currentEpisode.value = savedEpisode
updateUrlByEpisodeId(savedEpisode.id)
return
}
const firstEpisode = allEpisodeList.value[0]
if (firstEpisode) {
selectedYear.value = getEpisodeYear(firstEpisode)
rebuildMonthsByYear()
selectedMonth.value = getEpisodeMonth(firstEpisode)
currentEpisode.value = firstEpisode
saveCurrentEpisodeId(firstEpisode.id)
updateUrlByEpisodeId(firstEpisode.id)
}
} 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)
rebuildMonthsByYear()
selectedMonth.value = getEpisodeMonth(item)
errorMsg.value = ''
saveCurrentEpisodeId(item.id)
updateUrlByEpisodeId(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')}`
}
watch(selectedYear, () => {
rebuildMonthsByYear()
})
onMounted(() => {
void fetchAllEpisodes()
})
</script>
<style scoped>
.qishier-page {
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(99, 102, 241, 0.10), transparent 28%),
radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 24%),
linear-gradient(180deg, #f7f8fb 0%, #eef2f7 100%);
}
.top-card,
.control-card,
.content-card {
border-radius: 18px;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(10px);
}
.page-main-info {
min-width: 0;
}
.page-title {
font-weight: 800;
color: #1f2937;
letter-spacing: -0.02em;
line-height: 1.35;
}
.page-subtitle {
color: #6b7280;
font-size: 14px;
}
.header-actions {
flex-shrink: 0;
}
.mode-select {
width: 132px;
}
.filter-select {
width: 120px;
}
.episode-badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.info-badge,
.meta-pill {
display: inline-flex;
align-items: center;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.info-badge.playing {
background: rgba(59, 130, 246, 0.12);
color: #2563eb;
}
.meta-pill {
background: rgba(15, 23, 42, 0.06);
color: #475569;
}
.episode-meta {
color: #6b7280;
font-size: 14px;
}
@media (max-width: 991px) {
.header-actions {
width: 100%;
}
}
</style>