Compare commits

...

7 Commits

Author SHA1 Message Date
云上贵猪
28e5fb434b 七十二家房客-观看平台v163
- 数据更新
2026-03-25 09:39:11 +08:00
云上贵猪
a2ec441735 七十二家房客-观看平台v163
- 数据更新
2026-03-25 09:38:49 +08:00
云上贵猪
2e24ba9f28 Merge remote-tracking branch 'origin/main' 2026-03-14 02:13:22 +08:00
云上贵猪
7a93661e24 七十二家房客-观看平台v163
新增导航拦地址,方便分享
2026-03-14 02:13:18 +08:00
云上贵猪
50343891f4 七十二家房客-观看平台v163
新增导航拦地址,方便分享
2026-03-14 02:10:15 +08:00
云上贵猪
320c7e3b9e 七十二家房客-观看平台v162 2026-03-12 14:12:49 +08:00
云上贵猪
f3a5c1e9c9 七十二家房客-观看平台v161 2026-03-12 13:32:12 +08:00
8 changed files with 213 additions and 63 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
.DS_Store .DS_Store
.pw-user-data .pw-user-data
*.zip
node_modules/ node_modules/
dist/ dist/
npm-debug.log* npm-debug.log*

BIN
backServer/data.zip Normal file

Binary file not shown.

View File

@ -1,6 +1,6 @@
{ {
"year": 2024, "year": 2024,
"updatedAt": "2026-03-12T04:57:57.972Z", "updatedAt": "2026-03-25T01:35:17.796Z",
"items": [ "items": [
{ {
"id": "7f012566d7827b5046de9f92a4d7e159", "id": "7f012566d7827b5046de9f92a4d7e159",

View File

@ -1,6 +1,6 @@
{ {
"year": 2025, "year": 2025,
"updatedAt": "2026-03-12T04:57:16.089Z", "updatedAt": "2026-03-25T01:34:35.851Z",
"items": [ "items": [
{ {
"id": "b2b4f80b846360647d13e297c029be8d", "id": "b2b4f80b846360647d13e297c029be8d",

View File

@ -1,6 +1,6 @@
{ {
"year": 2026, "year": 2026,
"updatedAt": "2026-03-12T04:56:24.101Z", "updatedAt": "2026-03-25T01:33:45.340Z",
"items": [ "items": [
{ {
"id": "5abbc88ea530b5db6438a4e584e80281", "id": "5abbc88ea530b5db6438a4e584e80281",

View File

@ -1,5 +1,5 @@
{ {
"running": false, "running": false,
"lastMessage": "自动采集完成", "lastMessage": "自动采集完成",
"updatedAt": "2026-03-12T05:01:28.586Z" "updatedAt": "2026-03-25T01:35:31.093Z"
} }

View File

@ -3,7 +3,7 @@
<div class="section-head d-flex justify-content-between align-items-center mb-3"> <div class="section-head d-flex justify-content-between align-items-center mb-3">
<div> <div>
<h4 class="section-title mb-1">推荐展示</h4> <h4 class="section-title mb-1">推荐展示</h4>
<div class="section-subtitle">基于当前节目数据生成展示内容</div> <!--<div class="section-subtitle">基于当前节目数据生成展示内容</div>-->
</div> </div>
<button <button
class="btn btn-sm btn-outline-secondary d-none d-lg-inline-flex" class="btn btn-sm btn-outline-secondary d-none d-lg-inline-flex"

View File

@ -1,48 +1,94 @@
<template> <template>
<div class="container-fluid qishier-page py-3"> <div class="container-fluid qishier-page py-3">
<div class="row g-3"> <div class="row g-3">
<!-- 顶部标题区 -->
<div class="col-12"> <div class="col-12">
<div class="top-card card border-0 shadow-sm"> <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 class="card-body d-flex flex-wrap justify-content-between align-items-start gap-3">
<div> <div class="page-main-info">
<h2 class="mb-2 page-title">{{ columnInfo.name || '七十二家房客播放器' }}</h2> <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"> <div class="page-subtitle">
{{ allEpisodeList.length }} 最后更新{{ updatedAtText }} {{ allEpisodeList.length }}
<span class="mx-2"></span>
最后更新{{ updatedAtText }}
</div> </div>
</div> </div>
<div class="d-flex flex-wrap gap-2"> <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-dark btn-sm px-3" @click="handleRefresh">
<button class="btn btn-warning btn-sm px-3" @click="openSource">数据来源荔枝网</button> 刷新数据
</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> </div>
</div> </div>
<!-- 控制区 -->
<div class="col-12"> <div class="col-12">
<div class="toolbar-card card border-0 shadow-sm"> <div class="control-card card border-0 shadow-sm">
<div class="card-body d-flex flex-wrap gap-2 align-items-center"> <div class="card-body d-flex flex-wrap justify-content-between align-items-center gap-3">
<button class="btn btn-outline-dark btn-sm" @click="playPrevEpisode" :disabled="!hasPrevEpisode"> <div class="d-flex flex-wrap gap-2 align-items-center">
上一集 <button class="btn btn-outline-dark btn-sm" @click="playPrevEpisode" :disabled="!hasPrevEpisode">
</button> 上一集
</button>
<button class="btn btn-outline-dark btn-sm" @click="playNextEpisode" :disabled="!hasNextEpisode"> <button class="btn btn-outline-dark btn-sm" @click="playNextEpisode" :disabled="!hasNextEpisode">
下一集 下一集
</button> </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" /> <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>
<select v-model="playMode" class="form-select form-select-sm mode-select"> <div class="d-flex flex-wrap gap-2 align-items-center">
<option value="asc">顺序播放</option> <select v-model="playMode" class="form-select form-select-sm mode-select">
<option value="desc">倒序播放</option> <option value="asc">顺序播放</option>
</select> <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>
</div> </div>
<!-- 左侧播放器 -->
<div class="col-xl-8 col-lg-7"> <div class="col-xl-8 col-lg-7">
<div class="content-card card border-0 shadow-sm"> <div class="content-card card border-0 shadow-sm">
<div class="card-body"> <div class="card-body">
@ -53,16 +99,6 @@
@ended-next="handleEndedNext" @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"> <div v-if="errorMsg" class="alert alert-danger mt-3 mb-0">
{{ errorMsg }} {{ errorMsg }}
</div> </div>
@ -72,20 +108,22 @@
:episodes="showcaseEpisodes" :episodes="showcaseEpisodes"
@select="playEpisode" @select="playEpisode"
/> />
<QishierProjectLinks />
</div> </div>
</div> </div>
</div> </div>
<!-- 右侧列表 -->
<div class="col-xl-4 col-lg-5"> <div class="col-xl-4 col-lg-5">
<QishierEpisodeList <QishierEpisodeList
:episodes="displayEpisodeList" :episodes="displayEpisodeList"
:current-episode-id="currentEpisode?.id || ''" :current-episode-id="currentEpisode?.id || ''"
:years="years" :years="years"
:months="months"
:selected-year="selectedYear" :selected-year="selectedYear"
:selected-month="selectedMonth"
@select="playEpisode" @select="playEpisode"
@updateYear="handleYearChange" @updateYear="handleYearChange"
@updateMonth="handleMonthChange"
/> />
</div> </div>
</div> </div>
@ -93,13 +131,12 @@
</template> </template>
<script setup lang="ts"> <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 { getAllQishierList, refreshQishierCache, type EpisodeData } from '@/api/qishier'
import { getCurrentEpisodeId, saveCurrentEpisodeId } from '@/utils/playHistory' import { getCurrentEpisodeId, saveCurrentEpisodeId } from '@/utils/playHistory'
import QishierVideoPlayer from '@/components/qishier/QishierVideoPlayer.vue' import QishierVideoPlayer from '@/components/qishier/QishierVideoPlayer.vue'
import QishierEpisodeList from '@/components/qishier/QishierEpisodeList.vue' import QishierEpisodeList from '@/components/qishier/QishierEpisodeList.vue'
import QishierShowcase from '@/components/qishier/QishierShowcase.vue' import QishierShowcase from '@/components/qishier/QishierShowcase.vue'
import QishierProjectLinks from '@/components/qishier/QishierProjectLinks.vue'
const columnInfo = reactive({ const columnInfo = reactive({
name: '', name: '',
@ -112,6 +149,8 @@ const errorMsg = ref('')
const updatedAt = ref<string | null>(null) const updatedAt = ref<string | null>(null)
const years = ref<number[]>([]) const years = ref<number[]>([])
const selectedYear = ref('all') const selectedYear = ref('all')
const months = ref<number[]>([])
const selectedMonth = ref('all')
const autoNext = ref(true) const autoNext = ref(true)
const playMode = ref<'asc' | 'desc'>('asc') const playMode = ref<'asc' | 'desc'>('asc')
@ -129,6 +168,11 @@ const displayEpisodeList = computed(() => {
list = list.filter(item => new Date(item.releasedAt).getFullYear() === year) 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') { if (playMode.value === 'desc') {
list.reverse() list.reverse()
} }
@ -155,6 +199,28 @@ function openSource() {
window.open('https://www1.gdtv.cn/', '_blank') 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) { function handlePlayerError(message: string) {
errorMsg.value = message errorMsg.value = message
} }
@ -163,10 +229,38 @@ function handleYearChange(year: string) {
selectedYear.value = year selectedYear.value = year
} }
function handleMonthChange(month: string) {
selectedMonth.value = month
}
function getEpisodeYear(item: EpisodeData) { function getEpisodeYear(item: EpisodeData) {
return String(new Date(item.releasedAt).getFullYear()) 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() { async function fetchAllEpisodes() {
try { try {
errorMsg.value = '' errorMsg.value = ''
@ -183,19 +277,41 @@ async function fetchAllEpisodes() {
return 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 savedEpisodeId = getCurrentEpisodeId()
const savedEpisode = allEpisodeList.value.find(item => item.id === savedEpisodeId) const savedEpisode = allEpisodeList.value.find(item => item.id === savedEpisodeId)
if (savedEpisode) { if (savedEpisode) {
selectedYear.value = getEpisodeYear(savedEpisode) selectedYear.value = getEpisodeYear(savedEpisode)
rebuildMonthsByYear()
selectedMonth.value = getEpisodeMonth(savedEpisode)
currentEpisode.value = savedEpisode currentEpisode.value = savedEpisode
updateUrlByEpisodeId(savedEpisode.id)
return return
} }
const firstEpisode = allEpisodeList.value[0] const firstEpisode = allEpisodeList.value[0]
if (firstEpisode) { if (firstEpisode) {
selectedYear.value = getEpisodeYear(firstEpisode) selectedYear.value = getEpisodeYear(firstEpisode)
rebuildMonthsByYear()
selectedMonth.value = getEpisodeMonth(firstEpisode)
currentEpisode.value = firstEpisode currentEpisode.value = firstEpisode
saveCurrentEpisodeId(firstEpisode.id)
updateUrlByEpisodeId(firstEpisode.id)
} }
} catch (error: any) { } catch (error: any) {
console.error('fetchAllEpisodes error:', error) console.error('fetchAllEpisodes error:', error)
@ -216,8 +332,11 @@ async function handleRefresh() {
async function playEpisode(item: EpisodeData) { async function playEpisode(item: EpisodeData) {
currentEpisode.value = item currentEpisode.value = item
selectedYear.value = getEpisodeYear(item) selectedYear.value = getEpisodeYear(item)
rebuildMonthsByYear()
selectedMonth.value = getEpisodeMonth(item)
errorMsg.value = '' errorMsg.value = ''
saveCurrentEpisodeId(item.id) saveCurrentEpisodeId(item.id)
updateUrlByEpisodeId(item.id)
} }
async function playPrevEpisode() { async function playPrevEpisode() {
@ -258,6 +377,10 @@ function formatDuration(seconds: number): string {
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
} }
watch(selectedYear, () => {
rebuildMonthsByYear()
})
onMounted(() => { onMounted(() => {
void fetchAllEpisodes() void fetchAllEpisodes()
}) })
@ -267,54 +390,81 @@ onMounted(() => {
.qishier-page { .qishier-page {
min-height: 100vh; min-height: 100vh;
background: background:
radial-gradient(circle at top left, rgba(255, 193, 7, 0.12), transparent 28%), radial-gradient(circle at top left, rgba(99, 102, 241, 0.10), transparent 28%),
radial-gradient(circle at top right, rgba(13, 110, 253, 0.1), transparent 24%), radial-gradient(circle at top right, rgba(16, 185, 129, 0.08), transparent 24%),
linear-gradient(180deg, #f8f9fa 0%, #eef1f5 100%); linear-gradient(180deg, #f7f8fb 0%, #eef2f7 100%);
} }
.top-card, .top-card,
.toolbar-card, .control-card,
.content-card { .content-card {
border-radius: 18px; border-radius: 18px;
background: rgba(255, 255, 255, 0.92); background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(8px); backdrop-filter: blur(10px);
}
.page-main-info {
min-width: 0;
} }
.page-title { .page-title {
font-weight: 700; font-weight: 800;
color: #212529; color: #1f2937;
letter-spacing: -0.02em;
line-height: 1.35;
} }
.page-subtitle { .page-subtitle {
color: #6c757d; color: #6b7280;
font-size: 14px; font-size: 14px;
} }
.header-actions {
flex-shrink: 0;
}
.mode-select { .mode-select {
width: 140px; width: 132px;
} }
.episode-info { .filter-select {
padding: 2px 2px 6px; width: 120px;
} }
.episode-badge { .episode-badges {
display: inline-block; display: flex;
padding: 4px 10px; flex-wrap: wrap;
gap: 8px;
}
.info-badge,
.meta-pill {
display: inline-flex;
align-items: center;
padding: 5px 10px;
border-radius: 999px; border-radius: 999px;
background: rgba(255, 193, 7, 0.18);
color: #9a6a00;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
} }
.episode-title { .info-badge.playing {
color: #212529; background: rgba(59, 130, 246, 0.12);
font-weight: 700; color: #2563eb;
}
.meta-pill {
background: rgba(15, 23, 42, 0.06);
color: #475569;
} }
.episode-meta { .episode-meta {
color: #6c757d; color: #6b7280;
font-size: 14px; font-size: 14px;
} }
@media (max-width: 991px) {
.header-actions {
width: 100%;
}
}
</style> </style>