七十二家房客-观看平台v1.2
This commit is contained in:
parent
a20c6ad819
commit
6978074af3
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.pw-user-data
|
.pw-user-data
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|||||||
36
README.md
36
README.md
@ -18,29 +18,35 @@
|
|||||||
- 前端分页加载,避免一次加载过多图片
|
- 前端分页加载,避免一次加载过多图片
|
||||||
|
|
||||||
---
|
---
|
||||||
获取的空数据
|
# 运行&安装
|
||||||
```json
|
## 一、后端
|
||||||
|
安装后端依赖
|
||||||
{
|
```shell
|
||||||
"updatedAt": "2026-03-10T18:00:00Z",
|
npm i
|
||||||
"name": "七十二家房客",
|
|
||||||
"items": []
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# 安装
|
开启后端服务
|
||||||
|
```shell
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
如需更新数据:
|
||||||
|
```shell
|
||||||
|
node capture.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、前端
|
||||||
## 1 安装前端依赖
|
## 1 安装前端依赖
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
npm i
|
||||||
|
```
|
||||||
|
|
||||||
|
运行前端服务:
|
||||||
|
```shell
|
||||||
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
安装播放器依赖:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install axios hls.js
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"year": 2024,
|
"year": 2024,
|
||||||
"updatedAt": "2026-03-10T20:58:50.560Z",
|
"updatedAt": "2026-03-10T21:35:04.482Z",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": "7f012566d7827b5046de9f92a4d7e159",
|
"id": "7f012566d7827b5046de9f92a4d7e159",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"updatedAt": "2026-03-10T21:02:47.178Z",
|
"updatedAt": "2026-03-10T21:34:22.356Z",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": "b2b4f80b846360647d13e297c029be8d",
|
"id": "b2b4f80b846360647d13e297c029be8d",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"year": 2026,
|
"year": 2026,
|
||||||
"updatedAt": "2026-03-10T21:02:47.172Z",
|
"updatedAt": "2026-03-10T21:33:31.594Z",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": "5abbc88ea530b5db6438a4e584e80281",
|
"id": "5abbc88ea530b5db6438a4e584e80281",
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"running": true,
|
"running": false,
|
||||||
"lastMessage": "已采集第 1 页",
|
"lastMessage": "自动采集完成",
|
||||||
"updatedAt": "2026-03-10T21:11:58.820Z",
|
"updatedAt": "2026-03-10T21:35:26.989Z"
|
||||||
"currentPage": "1",
|
|
||||||
"pageItems": 0,
|
|
||||||
"addedCount": 0
|
|
||||||
}
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
express cors axios
|
|
||||||
@ -13,25 +13,79 @@ app.use(express.json())
|
|||||||
|
|
||||||
ensureDataFiles()
|
ensureDataFiles()
|
||||||
|
|
||||||
app.get('/api/qishier/all', (_req, res) => {
|
let memoryCache = {
|
||||||
try {
|
updatedAt: null,
|
||||||
const cache = readAllYearsData()
|
name: '七十二家房客',
|
||||||
|
coverUrl: '',
|
||||||
|
displayType: 0,
|
||||||
|
years: [],
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
function toSimpleItem(item) {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
coverUrl: item.coverUrl,
|
||||||
|
releasedAt: item.releasedAt,
|
||||||
|
timeLength: item.timeLength,
|
||||||
|
videoUrl: item.videoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshMemoryCache() {
|
||||||
|
const cache = readAllYearsData()
|
||||||
|
memoryCache = {
|
||||||
updatedAt: cache.updatedAt,
|
updatedAt: cache.updatedAt,
|
||||||
name: cache.name || '七十二家房客',
|
name: cache.name || '七十二家房客',
|
||||||
coverUrl: cache.coverUrl || '',
|
coverUrl: cache.coverUrl || '',
|
||||||
displayType: cache.displayType || 0,
|
displayType: cache.displayType || 0,
|
||||||
total: cache.items?.length || 0,
|
|
||||||
years: cache.years || [],
|
years: cache.years || [],
|
||||||
list: cache.items || []
|
items: (cache.items || []).map(toSimpleItem)
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
message: '读取缓存失败',
|
|
||||||
error: error.message
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[server] 内存缓存已刷新:',
|
||||||
|
'total=', memoryCache.items.length,
|
||||||
|
'years=', memoryCache.years.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMemoryCache()
|
||||||
|
|
||||||
|
app.get('/api/qishier/all', (_req, res) => {
|
||||||
|
res.json({
|
||||||
|
updatedAt: memoryCache.updatedAt,
|
||||||
|
name: memoryCache.name,
|
||||||
|
coverUrl: memoryCache.coverUrl,
|
||||||
|
displayType: memoryCache.displayType,
|
||||||
|
total: memoryCache.items.length,
|
||||||
|
years: memoryCache.years,
|
||||||
|
list: memoryCache.items
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/qishier/list', (req, res) => {
|
||||||
|
const page = Math.max(1, Number(req.query.page || 1))
|
||||||
|
const pageSize = Math.max(1, Math.min(100, Number(req.query.pageSize || 50)))
|
||||||
|
|
||||||
|
const total = memoryCache.items.length
|
||||||
|
const start = (page - 1) * pageSize
|
||||||
|
const end = start + pageSize
|
||||||
|
const list = memoryCache.items.slice(start, end)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
updatedAt: memoryCache.updatedAt,
|
||||||
|
name: memoryCache.name,
|
||||||
|
coverUrl: memoryCache.coverUrl,
|
||||||
|
displayType: memoryCache.displayType,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
hasMore: end < total,
|
||||||
|
years: memoryCache.years,
|
||||||
|
list
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/api/qishier/year/:year', (req, res) => {
|
app.get('/api/qishier/year/:year', (req, res) => {
|
||||||
@ -46,7 +100,7 @@ app.get('/api/qishier/year/:year', (req, res) => {
|
|||||||
displayType: 0,
|
displayType: 0,
|
||||||
total: data.items?.length || 0,
|
total: data.items?.length || 0,
|
||||||
years: [year],
|
years: [year],
|
||||||
list: data.items || []
|
list: (data.items || []).map(toSimpleItem)
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@ -56,6 +110,22 @@ app.get('/api/qishier/year/:year', (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.post('/api/qishier/refresh', (_req, res) => {
|
||||||
|
try {
|
||||||
|
refreshMemoryCache()
|
||||||
|
res.json({
|
||||||
|
message: '缓存已刷新',
|
||||||
|
total: memoryCache.items.length,
|
||||||
|
updatedAt: memoryCache.updatedAt
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
message: '刷新缓存失败',
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.get('/api/qishier/status', (_req, res) => {
|
app.get('/api/qishier/status', (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const status = readStatus()
|
const status = readStatus()
|
||||||
@ -68,6 +138,6 @@ app.get('/api/qishier/status', (_req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(3000, () => {
|
app.listen(23822, () => {
|
||||||
console.log('Node 服务已启动: http://localhost:3000')
|
console.log('Node 服务已启动: http://127.0.0.1:23822')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"hls.js": "^1.6.15",
|
"hls.js": "^1.6.15",
|
||||||
|
"plyr": "^3.8.4",
|
||||||
"vue": "^3.5.29"
|
"vue": "^3.5.29"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -7,28 +7,33 @@ export interface EpisodeData {
|
|||||||
releasedAt: number
|
releasedAt: number
|
||||||
timeLength: number
|
timeLength: number
|
||||||
videoUrl: string
|
videoUrl: string
|
||||||
raw?: Record<string, unknown>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QishierCacheResponse {
|
export interface QishierListResponse {
|
||||||
updatedAt: string | null
|
updatedAt: string | null
|
||||||
name: string
|
name: string
|
||||||
coverUrl: string
|
coverUrl: string
|
||||||
displayType: number
|
displayType: number
|
||||||
total: number
|
total: number
|
||||||
beginScoreMap: Record<string, number>
|
years: number[]
|
||||||
pages: Record<string, { currentPage: number; beginScore: number; count: number; capturedAt: string }>
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
hasMore: boolean
|
||||||
list: EpisodeData[]
|
list: EpisodeData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllQishierList() {
|
export function getQishierList(page = 1, pageSize = 50) {
|
||||||
return axios.get<QishierCacheResponse>('/api/qishier/all', {
|
return axios.get<QishierListResponse>('/api/qishier/list', {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
},
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getQishierStatus() {
|
export function refreshQishierCache() {
|
||||||
return axios.get('/api/qishier/status', {
|
return axios.post('/api/qishier/refresh', {}, {
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/types/plyr.d.ts
vendored
Normal file
21
src/types/plyr.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
declare module 'plyr' {
|
||||||
|
export interface PlyrOptions {
|
||||||
|
controls?: any[]
|
||||||
|
settings?: any[]
|
||||||
|
speed?: {
|
||||||
|
selected?: number
|
||||||
|
options?: number[]
|
||||||
|
}
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Plyr {
|
||||||
|
constructor(target: string | HTMLElement, options?: PlyrOptions)
|
||||||
|
destroy(): void
|
||||||
|
play(): Promise<void>
|
||||||
|
pause(): void
|
||||||
|
on(event: string, callback: (...args: any[]) => void): void
|
||||||
|
off(event: string, callback: (...args: any[]) => void): void
|
||||||
|
source: any
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,24 +3,21 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div>
|
<div>
|
||||||
<h1>{{ columnInfo.name || '七十二家房客播放器' }}</h1>
|
<h1>{{ columnInfo.name || '七十二家房客播放器' }}</h1>
|
||||||
<p>共 {{ allEpisodeList.length }} 条,当前显示 {{ visibleEpisodeList.length }} 条</p>
|
<p>共 {{ total }} 条,当前显示 {{ episodeList.length }} 条</p>
|
||||||
<p class="sub">最后更新:{{ updatedAtText }}</p>
|
<p class="sub">最后更新:{{ updatedAtText }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="action-btn" @click="fetchList">刷新数据</button>
|
<button class="action-btn" @click="handleRefresh">刷新数据</button>
|
||||||
|
<button class="action-btn source-btn" @click="openSource">
|
||||||
|
数据来源:荔枝网
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="player-panel">
|
<div class="player-panel">
|
||||||
<video
|
<video ref="videoRef" class="video" playsinline webkit-playsinline></video>
|
||||||
ref="videoRef"
|
|
||||||
class="video"
|
|
||||||
controls
|
|
||||||
playsinline
|
|
||||||
webkit-playsinline
|
|
||||||
></video>
|
|
||||||
|
|
||||||
<div v-if="currentEpisode" class="info">
|
<div v-if="currentEpisode" class="info">
|
||||||
<h2>{{ currentEpisode.title }}</h2>
|
<h2>{{ currentEpisode.title }}</h2>
|
||||||
@ -37,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="list-panel">
|
<div class="list-panel">
|
||||||
<div
|
<div
|
||||||
v-for="item in visibleEpisodeList"
|
v-for="item in episodeList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="episode-item"
|
class="episode-item"
|
||||||
:class="{ active: currentEpisode?.id === item.id }"
|
:class="{ active: currentEpisode?.id === item.id }"
|
||||||
@ -50,9 +47,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="load-more-wrap" v-if="visibleEpisodeList.length < allEpisodeList.length">
|
<div class="load-more-wrap" v-if="hasMore">
|
||||||
<button class="load-more-btn" @click="loadMoreEpisodes">
|
<button class="load-more-btn" :disabled="loadingMore" @click="loadMoreEpisodes">
|
||||||
加载更多
|
{{ loadingMore ? '加载中...' : '加载更多' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -63,30 +60,41 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||||
import Hls from 'hls.js'
|
import Hls from 'hls.js'
|
||||||
import { getAllQishierList, type EpisodeData } from '@/api/qishier'
|
import Plyr from 'plyr'
|
||||||
|
import 'plyr/dist/plyr.css'
|
||||||
|
import { getQishierList, refreshQishierCache, type EpisodeData } from '@/api/qishier'
|
||||||
|
|
||||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||||
const hlsRef = ref<Hls | null>(null)
|
|
||||||
|
|
||||||
const columnInfo = reactive({
|
const columnInfo = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
coverUrl: ''
|
coverUrl: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const allEpisodeList = ref<EpisodeData[]>([])
|
const episodeList = ref<EpisodeData[]>([])
|
||||||
const visibleEpisodeList = ref<EpisodeData[]>([])
|
|
||||||
const currentEpisode = ref<EpisodeData | null>(null)
|
const currentEpisode = ref<EpisodeData | null>(null)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
const updatedAt = ref<string | null>(null)
|
const updatedAt = ref<string | null>(null)
|
||||||
|
|
||||||
const pageSize = 20
|
const total = ref(0)
|
||||||
const currentVisibleCount = ref(pageSize)
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const hasMore = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
|
||||||
|
let hlsInstance: Hls | null = null
|
||||||
|
let plyrInstance: Plyr | null = null
|
||||||
|
|
||||||
const updatedAtText = computed(() => {
|
const updatedAtText = computed(() => {
|
||||||
if (!updatedAt.value) return '暂无缓存'
|
if (!updatedAt.value) return '暂无缓存'
|
||||||
return new Date(updatedAt.value).toLocaleString('zh-CN')
|
return new Date(updatedAt.value).toLocaleString('zh-CN')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function openSource() {
|
||||||
|
window.open('https://www1.gdtv.cn/', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
function setCookie(name: string, value: string, days = 30) {
|
function setCookie(name: string, value: string, days = 30) {
|
||||||
const expires = new Date()
|
const expires = new Date()
|
||||||
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)
|
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)
|
||||||
@ -105,24 +113,54 @@ function getCookie(name: string): string | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVisibleList() {
|
function initPlyr() {
|
||||||
visibleEpisodeList.value = allEpisodeList.value.slice(0, currentVisibleCount.value)
|
if (!videoRef.value) return
|
||||||
|
|
||||||
|
if (plyrInstance) {
|
||||||
|
plyrInstance.destroy()
|
||||||
|
plyrInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMoreEpisodes() {
|
plyrInstance = new Plyr(videoRef.value, {
|
||||||
currentVisibleCount.value += pageSize
|
controls: [
|
||||||
updateVisibleList()
|
'play-large',
|
||||||
|
'restart',
|
||||||
|
'rewind',
|
||||||
|
'play',
|
||||||
|
'fast-forward',
|
||||||
|
'progress',
|
||||||
|
'current-time',
|
||||||
|
'duration',
|
||||||
|
'mute',
|
||||||
|
'volume',
|
||||||
|
'settings',
|
||||||
|
'pip',
|
||||||
|
'airplay',
|
||||||
|
'fullscreen'
|
||||||
|
],
|
||||||
|
settings: ['speed'],
|
||||||
|
speed: {
|
||||||
|
selected: 1,
|
||||||
|
options: [0.5, 0.75, 1, 1.25, 1.5, 2]
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroyHls(): void {
|
function destroyPlayer() {
|
||||||
if (hlsRef.value) {
|
if (hlsInstance) {
|
||||||
hlsRef.value.destroy()
|
hlsInstance.destroy()
|
||||||
hlsRef.value = null
|
hlsInstance = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plyrInstance) {
|
||||||
|
plyrInstance.destroy()
|
||||||
|
plyrInstance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoRef.value) {
|
if (videoRef.value) {
|
||||||
videoRef.value.ontimeupdate = null
|
videoRef.value.ontimeupdate = null
|
||||||
videoRef.value.onloadedmetadata = null
|
videoRef.value.onloadedmetadata = null
|
||||||
|
videoRef.value.src = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,51 +173,82 @@ function bindProgressSave(video: HTMLVideoElement, item: EpisodeData) {
|
|||||||
function restoreProgress(video: HTMLVideoElement, item: EpisodeData) {
|
function restoreProgress(video: HTMLVideoElement, item: EpisodeData) {
|
||||||
const savedTime = getCookie(`qishier_progress_${item.id}`)
|
const savedTime = getCookie(`qishier_progress_${item.id}`)
|
||||||
if (savedTime && !Number.isNaN(Number(savedTime))) {
|
if (savedTime && !Number.isNaN(Number(savedTime))) {
|
||||||
video.currentTime = Number(savedTime)
|
const time = Number(savedTime)
|
||||||
|
if (time >= 0) {
|
||||||
|
video.currentTime = time
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchList(): Promise<void> {
|
async function fetchFirstPage(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
loading.value = true
|
||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
const res = await getAllQishierList()
|
|
||||||
const data = res.data
|
|
||||||
|
|
||||||
console.log('前端拿到缓存:', {
|
const res = await getQishierList(1, pageSize.value)
|
||||||
updatedAt: data.updatedAt,
|
const data = res.data
|
||||||
total: data.total,
|
|
||||||
pages: Object.keys(data.pages || {}).length,
|
|
||||||
listLength: Array.isArray(data.list) ? data.list.length : 0
|
|
||||||
})
|
|
||||||
|
|
||||||
columnInfo.name = data.name || '七十二家房客'
|
columnInfo.name = data.name || '七十二家房客'
|
||||||
columnInfo.coverUrl = data.coverUrl || ''
|
columnInfo.coverUrl = data.coverUrl || ''
|
||||||
updatedAt.value = data.updatedAt || null
|
updatedAt.value = data.updatedAt || null
|
||||||
|
total.value = data.total || 0
|
||||||
|
currentPage.value = data.page || 1
|
||||||
|
hasMore.value = !!data.hasMore
|
||||||
|
episodeList.value = Array.isArray(data.list) ? data.list : []
|
||||||
|
|
||||||
allEpisodeList.value = Array.isArray(data.list) ? data.list : []
|
if (episodeList.value.length > 0) {
|
||||||
currentVisibleCount.value = pageSize
|
|
||||||
updateVisibleList()
|
|
||||||
|
|
||||||
if (allEpisodeList.value.length > 0) {
|
|
||||||
const savedEpisodeId = getCookie('qishier_current_episode_id')
|
const savedEpisodeId = getCookie('qishier_current_episode_id')
|
||||||
const savedEpisode = allEpisodeList.value.find((item) => item.id === savedEpisodeId)
|
const savedEpisode = episodeList.value.find((item) => item.id === savedEpisodeId)
|
||||||
|
const firstEpisode = episodeList.value[0]
|
||||||
|
|
||||||
if (savedEpisode) {
|
if (savedEpisode) {
|
||||||
const index = allEpisodeList.value.findIndex((item) => item.id === savedEpisode.id)
|
|
||||||
if (index >= 0 && index + 1 > currentVisibleCount.value) {
|
|
||||||
currentVisibleCount.value = Math.ceil((index + 1) / pageSize) * pageSize
|
|
||||||
updateVisibleList()
|
|
||||||
}
|
|
||||||
await playEpisode(savedEpisode)
|
await playEpisode(savedEpisode)
|
||||||
} else {
|
} else if (firstEpisode) {
|
||||||
await playEpisode(allEpisodeList.value[0])
|
await playEpisode(firstEpisode)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMsg.value = '缓存里还没有节目数据,请先运行采集脚本'
|
errorMsg.value = '暂无节目数据'
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('fetchList error:', error)
|
console.error('fetchFirstPage error:', error)
|
||||||
errorMsg.value = error?.message || '读取缓存失败'
|
errorMsg.value = error?.message || '读取数据失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreEpisodes(): Promise<void> {
|
||||||
|
if (!hasMore.value || loadingMore.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadingMore.value = true
|
||||||
|
const nextPage = currentPage.value + 1
|
||||||
|
const res = await getQishierList(nextPage, pageSize.value)
|
||||||
|
const data = res.data
|
||||||
|
|
||||||
|
const oldIds = new Set(episodeList.value.map((item) => item.id))
|
||||||
|
const newItems = (data.list || []).filter((item) => !oldIds.has(item.id))
|
||||||
|
|
||||||
|
episodeList.value.push(...newItems)
|
||||||
|
currentPage.value = data.page || nextPage
|
||||||
|
hasMore.value = !!data.hasMore
|
||||||
|
total.value = data.total || total.value
|
||||||
|
updatedAt.value = data.updatedAt || updatedAt.value
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('loadMoreEpisodes error:', error)
|
||||||
|
errorMsg.value = error?.message || '加载更多失败'
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRefresh(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await refreshQishierCache()
|
||||||
|
await fetchFirstPage()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('handleRefresh error:', error)
|
||||||
|
errorMsg.value = error?.message || '刷新失败'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +262,8 @@ async function playEpisode(item: EpisodeData): Promise<void> {
|
|||||||
const video = videoRef.value
|
const video = videoRef.value
|
||||||
if (!video) return
|
if (!video) return
|
||||||
|
|
||||||
destroyHls()
|
destroyPlayer()
|
||||||
|
initPlyr()
|
||||||
|
|
||||||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
video.src = item.videoUrl
|
video.src = item.videoUrl
|
||||||
@ -201,29 +271,27 @@ async function playEpisode(item: EpisodeData): Promise<void> {
|
|||||||
|
|
||||||
video.onloadedmetadata = () => {
|
video.onloadedmetadata = () => {
|
||||||
restoreProgress(video, item)
|
restoreProgress(video, item)
|
||||||
}
|
|
||||||
|
|
||||||
bindProgressSave(video, item)
|
bindProgressSave(video, item)
|
||||||
void video.play().catch(() => {})
|
void video.play().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
const hls = new Hls()
|
hlsInstance = new Hls()
|
||||||
hls.loadSource(item.videoUrl)
|
hlsInstance.loadSource(item.videoUrl)
|
||||||
hls.attachMedia(video)
|
hlsInstance.attachMedia(video)
|
||||||
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
restoreProgress(video, item)
|
restoreProgress(video, item)
|
||||||
bindProgressSave(video, item)
|
bindProgressSave(video, item)
|
||||||
void video.play().catch(() => {})
|
void video.play().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
hls.on(Hls.Events.ERROR, () => {
|
hlsInstance.on(Hls.Events.ERROR, () => {
|
||||||
errorMsg.value = '视频播放失败'
|
errorMsg.value = '视频播放失败'
|
||||||
})
|
})
|
||||||
|
|
||||||
hlsRef.value = hls
|
|
||||||
} else {
|
} else {
|
||||||
errorMsg.value = '当前浏览器不支持 m3u8 播放'
|
errorMsg.value = '当前浏览器不支持 m3u8 播放'
|
||||||
}
|
}
|
||||||
@ -246,11 +314,11 @@ function formatDuration(seconds: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void fetchList()
|
void fetchFirstPage()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
destroyHls()
|
destroyPlayer()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -286,6 +354,7 @@ onBeforeUnmount(() => {
|
|||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
@ -295,6 +364,7 @@ onBeforeUnmount(() => {
|
|||||||
background: #333;
|
background: #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn:hover {
|
.action-btn:hover {
|
||||||
@ -409,6 +479,15 @@ onBeforeUnmount(() => {
|
|||||||
background: #444;
|
background: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-more-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.plyr) {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.layout {
|
.layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@ -2,22 +2,55 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => ({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 23811,
|
||||||
host: true,
|
host: true,
|
||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://127.0.0.1:23822/',
|
||||||
|
//target: 'http://8.134.120.132:23822/',
|
||||||
|
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
|
||||||
})
|
// 打包配置
|
||||||
|
build: {
|
||||||
|
// https://vite.dev/config/build-options.html
|
||||||
|
sourcemap: command === 'build' ? false : 'inline',
|
||||||
|
target: 'es2018',
|
||||||
|
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
|
||||||
|
chunkSizeWarningLimit: 2000,
|
||||||
|
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// JS 拆分
|
||||||
|
chunkFileNames: 'static/js/[name]-[hash].js',
|
||||||
|
entryFileNames: 'static/js/[name]-[hash].js',
|
||||||
|
|
||||||
|
// 资源文件
|
||||||
|
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
|
||||||
|
|
||||||
|
// 手动拆包
|
||||||
|
manualChunks: {
|
||||||
|
vue: ['vue'],
|
||||||
|
hls: ['hls.js']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user