@ -1,27 +1,55 @@
< 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 >
< 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 } } 条 | 最后更新 : { { updatedAtText } }
共 { { allEpisodeList . length } } 条
< span class = "mx-2" > | < / span >
最后更新 : { { 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 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 = "toolbar-card card border-0 shadow-sm" >
< div class = "card-body d-flex flex-wrap gap-2 align-items-center" >
< 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 >
@ -30,19 +58,37 @@
下一集
< / button >
< div class = "form-check form-switch ms- 2 ">
< 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" for = "autoNext" > 自动下一集 < / label >
< 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" >
@ -53,16 +99,6 @@
@ 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 >
@ -72,20 +108,22 @@
: 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"
: months = "months"
: selected - year = "selectedYear"
: selected - month = "selectedMonth"
@ select = "playEpisode"
@ updateYear = "handleYearChange"
@ updateMonth = "handleMonthChange"
/ >
< / div >
< / div >
@ -93,13 +131,12 @@
< / template >
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
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'
import QishierProjectLinks from '@/components/qishier/QishierProjectLinks.vue'
const columnInfo = reactive ( {
name : '' ,
@ -112,6 +149,8 @@ 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' )
@ -129,6 +168,11 @@ const displayEpisodeList = computed(() => {
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 ( )
}
@ -155,6 +199,28 @@ 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
}
@ -163,10 +229,38 @@ 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 = ''
@ -183,19 +277,41 @@ async function fetchAllEpisodes() {
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 )
@ -216,8 +332,11 @@ async function handleRefresh() {
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 ( ) {
@ -258,6 +377,10 @@ function formatDuration(seconds: number): string {
return ` ${ String ( m ) . padStart ( 2 , '0' ) } : ${ String ( s ) . padStart ( 2 , '0' ) } `
}
watch ( selectedYear , ( ) => {
rebuildMonthsByYear ( )
} )
onMounted ( ( ) => {
void fetchAllEpisodes ( )
} )
@ -267,54 +390,81 @@ onMounted(() => {
. qishier - page {
min - height : 100 vh ;
background :
radial - gradient ( circle at top left , rgba ( 255, 193 , 7 , 0.12 ) , transparent 28 % ) ,
radial - gradient ( circle at top right , rgba ( 1 3, 110 , 253 , 0.1 ) , transparent 24 % ) ,
linear - gradient ( 180 deg , # f 8f9fa 0 % , # eef1f5 100 % ) ;
radial - gradient ( circle at top left , rgba ( 99, 102 , 241 , 0.10 ) , transparent 28 % ) ,
radial - gradient ( circle at top right , rgba ( 1 6, 185 , 129 , 0.08 ) , transparent 24 % ) ,
linear - gradient ( 180 deg , # f 7f8fb 0 % , # eef2f7 100 % ) ;
}
. top - card ,
. toolbar - card ,
. control - card ,
. content - card {
border - radius : 18 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop - filter : blur ( 8 px ) ;
background : rgba ( 255 , 255 , 255 , 0.96 ) ;
backdrop - filter : blur ( 10 px ) ;
}
. page - main - info {
min - width : 0 ;
}
. page - title {
font - weight : 700 ;
color : # 212529 ;
font - weight : 800 ;
color : # 1 f2937 ;
letter - spacing : - 0.02 em ;
line - height : 1.35 ;
}
. page - subtitle {
color : # 6 c757d ;
color : # 6 b7280 ;
font - size : 14 px ;
}
. header - actions {
flex - shrink : 0 ;
}
. mode - select {
width : 140 px ;
width : 1 32 px ;
}
. episode- info {
padding: 2 px 2 px 6 px ;
. filter- select {
width: 120 px ;
}
. episode - badge {
display : inline - block ;
padding : 4 px 10 px ;
. episode - badges {
display : flex ;
flex - wrap : wrap ;
gap : 8 px ;
}
. info - badge ,
. meta - pill {
display : inline - flex ;
align - items : center ;
padding : 5 px 10 px ;
border - radius : 999 px ;
background : rgba ( 255 , 193 , 7 , 0.18 ) ;
color : # 9 a6a00 ;
font - size : 12 px ;
font - weight : 600 ;
}
. episode - title {
color : # 212529 ;
font - weight : 700 ;
. info - badge . playing {
background : rgba ( 59 , 130 , 246 , 0.12 ) ;
color : # 2563 eb ;
}
. meta - pill {
background : rgba ( 15 , 23 , 42 , 0.06 ) ;
color : # 475569 ;
}
. episode - meta {
color : # 6 c757d ;
color : # 6 b7280 ;
font - size : 14 px ;
}
@ media ( max - width : 991 px ) {
. header - actions {
width : 100 % ;
}
}
< / style >