244 lines
5.9 KiB
Vue
244 lines
5.9 KiB
Vue
<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>
|