diff --git a/backServer/data/2024.json b/backServer/data/2024.json index 712d4ea..590cb42 100644 --- a/backServer/data/2024.json +++ b/backServer/data/2024.json @@ -1,6 +1,6 @@ { "year": 2024, - "updatedAt": "2026-03-10T21:35:04.482Z", + "updatedAt": "2026-03-12T04:57:57.972Z", "items": [ { "id": "7f012566d7827b5046de9f92a4d7e159", diff --git a/backServer/data/2025.json b/backServer/data/2025.json index ced23aa..41dc608 100644 --- a/backServer/data/2025.json +++ b/backServer/data/2025.json @@ -1,6 +1,6 @@ { "year": 2025, - "updatedAt": "2026-03-10T21:34:22.356Z", + "updatedAt": "2026-03-12T04:57:16.089Z", "items": [ { "id": "b2b4f80b846360647d13e297c029be8d", diff --git a/backServer/data/2026.json b/backServer/data/2026.json index 4fd3175..2ff1132 100644 --- a/backServer/data/2026.json +++ b/backServer/data/2026.json @@ -1,6 +1,6 @@ { "year": 2026, - "updatedAt": "2026-03-10T21:33:31.594Z", + "updatedAt": "2026-03-12T04:56:24.101Z", "items": [ { "id": "5abbc88ea530b5db6438a4e584e80281", diff --git a/backServer/data/capture-status.json b/backServer/data/capture-status.json index 19943a4..da75efa 100644 --- a/backServer/data/capture-status.json +++ b/backServer/data/capture-status.json @@ -1,5 +1,5 @@ { "running": false, "lastMessage": "自动采集完成", - "updatedAt": "2026-03-10T21:35:26.989Z" + "updatedAt": "2026-03-12T05:01:28.586Z" } \ No newline at end of file diff --git a/dist.zip b/dist.zip deleted file mode 100644 index 05cd0c4..0000000 Binary files a/dist.zip and /dev/null differ diff --git a/package.json b/package.json index 80024bd..0033afa 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "artplayer": "^5.3.0", "axios": "^1.13.6", + "bootstrap": "^5.3.8", "cors": "^2.8.6", "express": "^5.2.1", "hls.js": "^1.6.15", diff --git a/src/api/qishier.ts b/src/api/qishier.ts index 8ec8667..7f49123 100644 --- a/src/api/qishier.ts +++ b/src/api/qishier.ts @@ -9,25 +9,18 @@ export interface EpisodeData { videoUrl: string } -export interface QishierListResponse { +export interface QishierAllResponse { updatedAt: string | null name: string coverUrl: string displayType: number total: number years: number[] - page: number - pageSize: number - hasMore: boolean list: EpisodeData[] } -export function getQishierList(page = 1, pageSize = 50) { - return axios.get('/api/qishier/list', { - params: { - page, - pageSize - }, +export function getAllQishierList() { + return axios.get('/api/qishier/all', { timeout: 15000 }) } diff --git a/src/components/qishier/QishierEpisodeList.vue b/src/components/qishier/QishierEpisodeList.vue index f21987f..e6dffbd 100644 --- a/src/components/qishier/QishierEpisodeList.vue +++ b/src/components/qishier/QishierEpisodeList.vue @@ -1,45 +1,115 @@ diff --git a/src/components/qishier/QishierProjectLinks.vue b/src/components/qishier/QishierProjectLinks.vue new file mode 100644 index 0000000..9149028 --- /dev/null +++ b/src/components/qishier/QishierProjectLinks.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/src/components/qishier/QishierShowcase.vue b/src/components/qishier/QishierShowcase.vue new file mode 100644 index 0000000..bb3da97 --- /dev/null +++ b/src/components/qishier/QishierShowcase.vue @@ -0,0 +1,362 @@ + + + + + diff --git a/src/components/qishier/QishierVideoPlayer.vue b/src/components/qishier/QishierVideoPlayer.vue index d0bc5b9..47b0365 100644 --- a/src/components/qishier/QishierVideoPlayer.vue +++ b/src/components/qishier/QishierVideoPlayer.vue @@ -11,6 +11,8 @@ import Hls from 'hls.js' import type { EpisodeData } from '@/api/qishier' import { getEpisodeProgress, saveEpisodeProgress } from '@/utils/playHistory' + + type ArtWithHls = Artplayer & { hls?: Hls } @@ -27,8 +29,18 @@ const emit = defineEmits<{ const playerRef = ref(null) let art: ArtWithHls | null = null +let playRetryTimer: number | null = null + +function clearPlayRetryTimer() { + if (playRetryTimer !== null) { + window.clearTimeout(playRetryTimer) + playRetryTimer = null + } +} function destroyPlayer() { + clearPlayRetryTimer() + if (art?.hls) { art.hls.destroy() art.hls = undefined @@ -44,6 +56,16 @@ function destroyPlayer() { } } +function safeAutoPlay() { + clearPlayRetryTimer() + + playRetryTimer = window.setTimeout(() => { + void art?.video?.play().catch(() => { + // 某些浏览器自动播放会被拦截,这里静默处理 + }) + }, 180) +} + function buildPlayer(episode: EpisodeData) { if (!playerRef.value) return @@ -58,23 +80,56 @@ function buildPlayer(episode: EpisodeData) { setting: true, playbackRate: true, hotkey: true, - - autoHide: 2000, - mutex: true, theme: '#f5c542', moreVideoAttr: { - playsInline: true + playsInline: true, + preload: 'auto' } as any, customType: { m3u8(video: HTMLVideoElement, url: string, artInstance: Artplayer) { const artplayer = artInstance as ArtWithHls if (Hls.isSupported()) { - const hls = new Hls() + const hls = new Hls({ + enableWorker: true, + lowLatencyMode: false, + backBufferLength: 30, + maxBufferLength: 30, + maxMaxBufferLength: 60, + maxBufferHole: 1, + highBufferWatchdogPeriod: 2, + nudgeOffset: 0.1, + nudgeMaxRetry: 5 + }) + artplayer.hls = hls hls.loadSource(url) hls.attachMedia(video) + + hls.on(Hls.Events.ERROR, (_event, data) => { + console.error('HLS Error:', data) + + if (!data.fatal) { + return + } + + if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { + console.warn('HLS NETWORK_ERROR,尝试恢复') + hls.startLoad() + emit('error', '网络异常,正在尝试恢复') + return + } + + if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { + console.warn('HLS MEDIA_ERROR,尝试恢复') + hls.recoverMediaError() + emit('error', '播放异常,正在尝试恢复') + return + } + + emit('error', '视频播放失败') + }) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url } else { @@ -86,7 +141,7 @@ function buildPlayer(episode: EpisodeData) { art.type = 'm3u8' - const restore = () => { + const restoreProgress = () => { const progress = getEpisodeProgress(episode.id) if (progress > 0 && art?.video) { try { @@ -95,9 +150,21 @@ function buildPlayer(episode: EpisodeData) { // ignore } } + + safeAutoPlay() } - art.on('video:loadedmetadata', restore) + art.on('video:loadedmetadata', restoreProgress) + + art.on('video:canplay', () => { + // 某些设备 loadedmetadata 后还没准备好,canplay 时再兜底一次 + safeAutoPlay() + }) + + art.on('video:waiting', () => { + // 这里不报错,只作为缓冲状态 + console.warn('视频缓冲中...') + }) art.on('video:timeupdate', () => { if (art?.video) { @@ -135,7 +202,7 @@ watch( watch( () => props.autoNext, () => { - // 让 ended 事件读取最新 autoNext 值 + // 保持响应式依赖 } ) @@ -152,6 +219,8 @@ onBeforeUnmount(() => { .player-wrap { width: 100%; background: #000; + border-radius: 12px; + overflow: hidden; } .artplayer-container { @@ -164,4 +233,8 @@ onBeforeUnmount(() => { width: 100%; height: 100%; } + +:deep(.art-video-player) { + background: #000; +} diff --git a/src/main.ts b/src/main.ts index fe5bae3..b2f9976 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,8 @@ -import { createApp } from 'vue' -import App from './App.vue' import './style.css' +import { createApp } from 'vue' +import App from './App.vue' + +import 'bootstrap/dist/css/bootstrap.min.css' + createApp(App).mount('#app') diff --git a/src/views/QishierPlayer.vue b/src/views/QishierPlayer.vue index b5372d2..0513394 100644 --- a/src/views/QishierPlayer.vue +++ b/src/views/QishierPlayer.vue @@ -1,97 +1,117 @@