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

244 lines
5.9 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="episode-card card border-0 shadow-sm h-100">
<div class="card-header bg-transparent border-0 pt-3 pb-2 px-3">
<div class="d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="fw-bold text-dark">剧集列表</div>
<select v-model="innerYear" class="form-select form-select-sm year-select">
<option value="all">全部年份</option>
<option v-for="year in years" :key="year" :value="String(year)">
{{ year }}
</option>
</select>
</div>
</div>
<div ref="listPanelRef" class="card-body pt-2 episode-panel">
<button
v-for="item in pagedEpisodes"
:key="item.id"
:data-episode-id="item.id"
type="button"
class="episode-row btn w-100 text-start mb-2"
:class="currentEpisodeId === item.id ? 'btn-warning current-row' : 'btn-light'"
@click="handleSelect(item, $event)"
>
<div class="episode-date mb-1">{{ formatDate(item.releasedAt) }}</div>
<div class="episode-title">{{ item.title }}</div>
</button>
<div v-if="pagedEpisodes.length === 0" class="empty-tip text-center py-4">
当前筛选没有数据
</div>
</div>
<div class="card-footer bg-transparent border-0 px-3 pb-3 pt-1">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="small text-secondary">
{{ filteredEpisodes.length }} 当前第 {{ currentPage }} / {{ totalPages || 1 }}
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" :disabled="currentPage <= 1" @click="prevPage">
上一页
</button>
<button class="btn btn-outline-secondary" :disabled="currentPage >= totalPages" @click="nextPage">
下一页
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import type { EpisodeData } from '@/api/qishier'
const props = defineProps<{
episodes: EpisodeData[]
currentEpisodeId: string
years: number[]
selectedYear: string
}>()
const emit = defineEmits<{
select: [episode: EpisodeData]
updateYear: [year: string]
}>()
const listPanelRef = ref<HTMLElement | null>(null)
const innerYear = ref(props.selectedYear || 'all')
const pageSize = 20
const currentPage = ref(1)
watch(
() => props.selectedYear,
(val) => {
const nextVal = val || 'all'
if (innerYear.value !== nextVal) {
innerYear.value = nextVal
}
}
)
watch(innerYear, (val) => {
if (val === props.selectedYear) return
currentPage.value = 1
emit('updateYear', val)
})
const filteredEpisodes = computed(() => {
if (innerYear.value === 'all') return props.episodes
const year = Number(innerYear.value)
return props.episodes.filter(item => new Date(item.releasedAt).getFullYear() === year)
})
const totalPages = computed(() => {
return Math.ceil(filteredEpisodes.value.length / pageSize)
})
const pagedEpisodes = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filteredEpisodes.value.slice(start, start + pageSize)
})
function prevPage() {
if (currentPage.value > 1) currentPage.value--
}
function nextPage() {
if (currentPage.value < totalPages.value) currentPage.value++
}
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 isMobileDevice() {
return window.innerWidth < 992
}
function handleSelect(item: EpisodeData, event: MouseEvent) {
;(event.currentTarget as HTMLButtonElement | null)?.blur()
emit('select', item)
}
async function locateCurrentEpisode() {
if (!props.currentEpisodeId) return
const fullList = filteredEpisodes.value
const idx = fullList.findIndex(item => item.id === props.currentEpisodeId)
if (idx < 0) return
const targetPage = Math.floor(idx / pageSize) + 1
if (currentPage.value !== targetPage) {
currentPage.value = targetPage
await nextTick()
}
const panel = listPanelRef.value
if (!panel) return
const activeEl = panel.querySelector(
`[data-episode-id="${props.currentEpisodeId}"]`
) as HTMLElement | null
if (!activeEl) return
// 手机端不自动滚整个页面
if (isMobileDevice()) {
const panelTop = panel.scrollTop
const itemTop = activeEl.offsetTop
const itemHeight = activeEl.offsetHeight
const panelHeight = panel.clientHeight
panel.scrollTo({
top: itemTop - panelHeight / 2 + itemHeight / 2,
behavior: 'smooth'
})
return
}
activeEl.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
})
}
watch(
() => props.currentEpisodeId,
async () => {
await locateCurrentEpisode()
},
{ immediate: true }
)
watch(
() => props.episodes.length,
async () => {
await locateCurrentEpisode()
}
)
watch(
() => props.selectedYear,
async () => {
await locateCurrentEpisode()
}
)
</script>
<style scoped>
.episode-card {
border-radius: 18px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8px);
}
.episode-panel {
max-height: 72vh;
overflow-y: auto;
}
.episode-row {
border-radius: 14px;
border: 1px solid rgba(0, 0, 0, 0.06);
padding: 12px 14px;
transition: all 0.2s ease;
}
.episode-row:hover {
transform: translateY(-1px);
}
.current-row {
box-shadow: 0 8px 20px rgba(255, 193, 7, 0.22);
}
.episode-date {
font-size: 12px;
color: #6c757d;
}
.episode-title {
line-height: 1.45;
font-size: 14px;
font-weight: 600;
white-space: normal;
}
.year-select {
width: 120px;
}
.empty-tip {
color: #6c757d;
}
</style>