mirror of
https://github.com/App1ePine/free-video.git
synced 2025-12-11 10:49:38 +00:00
release(project): publish project
This commit is contained in:
commit
ac70cca4a2
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Local
|
||||
.DS_Store
|
||||
*.local
|
||||
*.log*
|
||||
|
||||
# Dist
|
||||
node_modules
|
||||
dist/
|
||||
|
||||
# Profile
|
||||
.rspack-profile-*/
|
||||
|
||||
# IDE
|
||||
.vscode/*
|
||||
.vscode/
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
bun.lock
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Lock files
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
60
.prettierrc.json
Normal file
60
.prettierrc.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"proseWrap": "always",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"bracketSameLine": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"jsxSingleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.{json,json5}",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.{yaml,yml}",
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"printWidth": 120,
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"proseWrap": "always",
|
||||
"printWidth": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.sql",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"keywordCase": "upper",
|
||||
"identifierCase": "lower",
|
||||
"linesBetweenQueries": 1,
|
||||
"useTabs": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
## 免责声明
|
||||
|
||||
本项目仅供学习和技术研究使用,不得用于商业目的。使用本工具时,请遵守以下规定:
|
||||
|
||||
1. 本工具仅提供技术演示,不提供任何视频内容本身,不提供任何视频接口
|
||||
2. 使用者应当遵守当地法律法规,不得利用本工具从事任何违法活动
|
||||
3. 本工具不存储、复制或分发任何受版权保护的内容
|
||||
4. 对于使用本工具可能引发的法律问题,使用者须自行承担全部责任
|
||||
5. 开发者保留对本项目进行修改、更新或终止的权利,且不承担任何责任
|
||||
6. 使用本工具即表示您已阅读并同意本免责声明的全部内容
|
||||
|
||||
请尊重知识产权,支持正版内容。若权利人认为本项目侵犯了您的权益,请联系开发者进行处理。
|
||||
15
eslint.config.mjs
Normal file
15
eslint.config.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
import js from '@eslint/js';
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||
},
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
{ languageOptions: { globals: globals.browser } },
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
]);
|
||||
12
free-video-backend/.gitignore
vendored
Normal file
12
free-video-backend/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# deps
|
||||
node_modules/
|
||||
|
||||
# ide
|
||||
.vscode/
|
||||
.idea/
|
||||
.zed/
|
||||
|
||||
# lock file
|
||||
bun.lock
|
||||
package-lock.json
|
||||
|
||||
60
free-video-backend/.prettierrc.json
Normal file
60
free-video-backend/.prettierrc.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"proseWrap": "always",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"bracketSameLine": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"jsxSingleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.{json,json5}",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.{yaml,yml}",
|
||||
"options": {
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"printWidth": 120,
|
||||
"htmlWhitespaceSensitivity": "ignore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"proseWrap": "always",
|
||||
"printWidth": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.sql",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"keywordCase": "upper",
|
||||
"identifierCase": "lower",
|
||||
"linesBetweenQueries": 1,
|
||||
"useTabs": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
15
free-video-backend/package.json
Normal file
15
free-video-backend/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "free_backend",
|
||||
"scripts": {
|
||||
"dev": "bun run --hot src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"hono": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"eslint": "^9.29.0",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
72
free-video-backend/src/index.ts
Normal file
72
free-video-backend/src/index.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors'
|
||||
import axios from 'axios'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use('*', cors())
|
||||
|
||||
app.get('/api/searchvideo', async (c) => {
|
||||
const url_prefix = 'https://api.so.360kan.com/index?force_v=1&kw='
|
||||
const keyword = c.req.query('keyword') || ''
|
||||
const url = url_prefix + keyword
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
|
||||
},
|
||||
})
|
||||
const rows = response.data.data.longData.rows || []
|
||||
if (rows.length === 0) {
|
||||
return c.json({ error: 'No results found' }, 404)
|
||||
}
|
||||
const promises = rows.map(async (item: any) => {
|
||||
let videoLinks = []
|
||||
if (!item.seriesPlaylinks || item.seriesPlaylinks.length === 0) {
|
||||
const videoResponse = await axios.get(
|
||||
`https://api.so.360kan.com/episodesv2?v_ap=1&s=[{"cat_id":"${item.cat_id}","ent_id":"${item.en_id}","site":"${item.vipSite[0]}"}]`,
|
||||
)
|
||||
if (videoResponse.data.data.length === 0) {
|
||||
return
|
||||
}
|
||||
videoLinks = videoResponse.data.data[0].seriesHTML.seriesPlaylinks.map((video: any) => {
|
||||
return video.url.split('?')[0]
|
||||
})
|
||||
} else {
|
||||
videoLinks = item.seriesPlaylinks.map((video: any) => {
|
||||
if (typeof video.url === 'string') {
|
||||
return video.url.split('?')[0]
|
||||
}
|
||||
return video
|
||||
})
|
||||
}
|
||||
const videoItem = {
|
||||
video_id: item.id,
|
||||
video_id_en: item.en_id,
|
||||
cat_id: item.cat_id,
|
||||
cat_name: item.cat_name,
|
||||
cover: item.cover,
|
||||
title: item.titleTxt,
|
||||
year: item.year,
|
||||
description: item.description,
|
||||
areas: item.area,
|
||||
tags: item.tag,
|
||||
actList: item.actList,
|
||||
dirList: item.dirList,
|
||||
isVip: item.vip,
|
||||
vipSites: item.vipSite,
|
||||
videoLinks: videoLinks,
|
||||
episodeCount: videoLinks.length,
|
||||
}
|
||||
return videoItem
|
||||
})
|
||||
const res = (await Promise.all(promises)).filter(Boolean)
|
||||
return c.json(res)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return c.json({ error: 'Failed to fetch data' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
||||
11
free-video-backend/tsconfig.json
Normal file
11
free-video-backend/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "free-video-app",
|
||||
"version": "1.0.0",
|
||||
"description": "视频解析工具,支持多种视频网站",
|
||||
"main": "electron/main.js",
|
||||
"author": "Jianlong Deng",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rsbuild build",
|
||||
"dev": "rsbuild dev --open",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"element-plus": "^2.10.2",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.16",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@rsbuild/core": "^1.3.22",
|
||||
"@rsbuild/plugin-vue": "^1.0.7",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-plugin-vue": "^10.1.0",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
17
rsbuild.config.mjs
Normal file
17
rsbuild.config.mjs
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineConfig } from '@rsbuild/core'
|
||||
import { pluginVue } from '@rsbuild/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [pluginVue()],
|
||||
server: {
|
||||
port: 8080,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
7
src/App.vue
Normal file
7
src/App.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style></style>
|
||||
16
src/index.js
Normal file
16
src/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './styles/global.css'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
import router from './router/index.js'
|
||||
app.use(ElementPlus, { zIndex: 1000 })
|
||||
app.use(router)
|
||||
|
||||
app.mount('#root')
|
||||
36
src/router/index.js
Normal file
36
src/router/index.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import VideoSearch from '../views/VideoSearch.vue'
|
||||
import VideoPlayer from '../views/VideoPlayer.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: VideoSearch,
|
||||
meta: {
|
||||
title: '免费视频解析 - 支持全网视频解析,免费观看VIP视频',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/video_player',
|
||||
name: 'VideoPlayer',
|
||||
component: VideoPlayer,
|
||||
meta: {
|
||||
title: '免费视频解析 - 视频播放器',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const title = to.meta.title || '免费视频解析'
|
||||
document.title = title
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
82
src/store/index.js
Normal file
82
src/store/index.js
Normal file
@ -0,0 +1,82 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useStore = defineStore('main', {
|
||||
state: () => {
|
||||
return {
|
||||
prefix_urls: [''],
|
||||
search_results: [],
|
||||
current_video: {
|
||||
title: '', // 视频标题
|
||||
episodeIdx: 0, // 当前集数索引
|
||||
episodeUrl: '', // 当前播放地址
|
||||
totalEpisodes: 0, // 总集数
|
||||
coverUrl: '', // 封面
|
||||
description: '', // 描述
|
||||
videoLinks: [], // 所有剧集的链接
|
||||
},
|
||||
supportedPlatforms: ['优酷', '爱奇艺', '腾讯视频'],
|
||||
supportedFormats: ['MP4', 'M3U8', 'FLV', '...'],
|
||||
apiConfig: {
|
||||
timeout: 10000, // 请求超时时间(毫秒)
|
||||
retryTimes: 3, // 重试次数
|
||||
},
|
||||
appConfig: {
|
||||
title: '免费视频解析',
|
||||
description: '支持全网视频解析,免费观看VIP视频',
|
||||
version: '1.0.0',
|
||||
site_begin_time: new Date('2025-06-20 18:00:00'),
|
||||
},
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// 更新搜索结果
|
||||
updateSearchResults(results) {
|
||||
this.search_results = results
|
||||
},
|
||||
|
||||
// 更新当前视频信息 - 综合方法,处理不同情况
|
||||
updateCurrentVideo(videoData, episodeIdx = 0, episodeUrl = '') {
|
||||
// 处理直接传入URL字符串的情况
|
||||
if (typeof videoData === 'string') {
|
||||
this.current_video = {
|
||||
title: '外部链接',
|
||||
episodeIdx: 0,
|
||||
episodeUrl: videoData,
|
||||
totalEpisodes: 1,
|
||||
coverUrl: '',
|
||||
description: '',
|
||||
videoLinks: [videoData],
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 处理传入视频对象的情况
|
||||
const videoLinks = videoData.videoLinks || []
|
||||
|
||||
this.current_video = {
|
||||
title: videoData.title || '未知标题',
|
||||
episodeIdx: episodeIdx,
|
||||
episodeUrl: episodeUrl || videoLinks[episodeIdx] || '',
|
||||
totalEpisodes: videoLinks.length || 0,
|
||||
coverUrl: videoData.cover || '',
|
||||
description: videoData.description || '',
|
||||
videoLinks: videoLinks,
|
||||
}
|
||||
},
|
||||
|
||||
// 仅更新当前播放URL
|
||||
updateEpisodeUrl(url) {
|
||||
if (!url) return
|
||||
this.current_video.episodeUrl = url
|
||||
},
|
||||
|
||||
// 更新当前播放集数
|
||||
updateEpisodeIdx(idx) {
|
||||
if (idx < 0 || !this.current_video.videoLinks || idx >= this.current_video.videoLinks.length)
|
||||
return
|
||||
|
||||
this.current_video.episodeIdx = idx
|
||||
this.current_video.episodeUrl = this.current_video.videoLinks[idx]
|
||||
},
|
||||
},
|
||||
})
|
||||
20
src/styles/global.css
Normal file
20
src/styles/global.css
Normal file
@ -0,0 +1,20 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
426
src/views/VideoPlayer.vue
Normal file
426
src/views/VideoPlayer.vue
Normal file
@ -0,0 +1,426 @@
|
||||
<template>
|
||||
<div class="video-player">
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="player-container">
|
||||
<iframe
|
||||
v-if="currentPlayUrl"
|
||||
:src="currentPlayUrl"
|
||||
class="video-frame"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
@load="onFrameLoad"
|
||||
@error="onFrameError"
|
||||
></iframe>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="loading-container">
|
||||
<el-icon class="loading-icon is-loading" size="48">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<p>正在加载视频...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="hasError && !isLoading" class="error-container">
|
||||
<el-icon size="48"><VideoCamera /></el-icon>
|
||||
<p>视频加载失败,请尝试切换解析接口</p>
|
||||
<el-button type="primary" @click="showDrawer = true"> 切换接口 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回首页按钮 -->
|
||||
<el-button class="back-btn" type="primary" :icon="ArrowLeft" @click="goBack">
|
||||
返回首页
|
||||
</el-button>
|
||||
|
||||
<!-- 悬浮切换接口按钮 -->
|
||||
<el-button class="switch-btn" type="success" :icon="Setting" circle @click="showDrawer = true">
|
||||
</el-button>
|
||||
|
||||
<!-- 切换接口抽屉 -->
|
||||
<el-drawer v-model="showDrawer" title="选项" direction="rtl" size="400px">
|
||||
<div class="drawer-content">
|
||||
<div class="current-info">
|
||||
<h4>当前视频地址:</h4>
|
||||
<p class="url-text">{{ videoUrl }}</p>
|
||||
</div>
|
||||
|
||||
<div class="interface-list">
|
||||
<h4>可用解析接口:</h4>
|
||||
<div
|
||||
v-for="(prefix, index) in prefixes"
|
||||
:key="prefix"
|
||||
class="interface-item"
|
||||
:class="{ active: currentPrefixIndex === index }"
|
||||
@click="switchInterface(index)"
|
||||
>
|
||||
<div class="interface-info">
|
||||
<span class="interface-name">接口 {{ index + 1 }}</span>
|
||||
<!-- <span class="interface-url">{{ prefix }}</span> -->
|
||||
</div>
|
||||
<el-icon v-if="currentPrefixIndex === index" class="active-icon">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 集数选择(如果有多集) -->
|
||||
<div v-if="hasMultipleEpisodes" class="episodes-selector">
|
||||
<h4>选集:</h4>
|
||||
<div class="episodes-grid">
|
||||
<el-button
|
||||
v-for="index in totalEpisodes"
|
||||
:key="index - 1"
|
||||
size="small"
|
||||
:type="currentEpisodeIdx === index - 1 ? 'primary' : ''"
|
||||
@click="switchEpisode(index - 1)"
|
||||
class="episode-btn"
|
||||
>
|
||||
{{ index }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-actions">
|
||||
<el-button @click="showDrawer = false" type="danger"> 关闭 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft, Setting, VideoCamera, Check, Loading } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useStore } from '../store/index'
|
||||
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const videoUrl = ref('')
|
||||
const prefixes = ref([])
|
||||
const currentPrefixIndex = ref(0)
|
||||
const showDrawer = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
|
||||
// 视频信息
|
||||
const videoTitle = computed(() => {
|
||||
if (store.current_video && store.current_video.title) {
|
||||
if (hasMultipleEpisodes.value) {
|
||||
return `${store.current_video.title} - 第 ${store.current_video.episodeIdx + 1} 集`
|
||||
}
|
||||
return store.current_video.title
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const currentEpisodeIdx = computed(() => store.current_video.episodeIdx || 0)
|
||||
const totalEpisodes = computed(() => store.current_video.totalEpisodes || 0)
|
||||
const hasMultipleEpisodes = computed(() => totalEpisodes.value > 1)
|
||||
|
||||
// 计算当前播放URL
|
||||
const currentPlayUrl = computed(() => {
|
||||
if (!videoUrl.value || !prefixes.value.length) return ''
|
||||
const prefix = prefixes.value[currentPrefixIndex.value]
|
||||
return prefix + encodeURIComponent(videoUrl.value)
|
||||
})
|
||||
|
||||
// 更新页面标题
|
||||
const updatePageTitle = () => {
|
||||
let title = '视频播放器'
|
||||
if (videoTitle.value) {
|
||||
title = `${videoTitle.value} - 免费视频解析`
|
||||
}
|
||||
document.title = title
|
||||
}
|
||||
|
||||
// 监听视频信息变化更新页面标题
|
||||
watch(videoTitle, () => {
|
||||
updatePageTitle()
|
||||
})
|
||||
|
||||
// 监听 store 中的 current_video_url 变化
|
||||
watch(
|
||||
() => store.current_video.episodeUrl,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
videoUrl.value = newValue
|
||||
loadCurrentInterface()
|
||||
updatePageTitle()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化解析接口列表
|
||||
prefixes.value = store.prefix_urls || []
|
||||
|
||||
// 获取当前视频URL
|
||||
if (store.current_video && store.current_video.episodeUrl) {
|
||||
videoUrl.value = store.current_video.episodeUrl
|
||||
} else {
|
||||
// 如果store中没有,尝试从路由获取
|
||||
videoUrl.value = route.query.url || ''
|
||||
// 如果获取到了URL但没有视频信息,则更新
|
||||
if (videoUrl.value && (!store.current_video || !store.current_video.episodeUrl)) {
|
||||
store.updateEpisodeUrl(videoUrl.value)
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoUrl.value) {
|
||||
ElMessage.error('视频地址错误')
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
if (prefixes.value.length === 0) {
|
||||
ElMessage.error('没有可用的解析接口')
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
// 更新页面标题
|
||||
updatePageTitle()
|
||||
|
||||
// 开始加载第一个接口
|
||||
loadCurrentInterface()
|
||||
})
|
||||
|
||||
// 监听接口切换
|
||||
watch(currentPrefixIndex, () => {
|
||||
loadCurrentInterface()
|
||||
})
|
||||
|
||||
// 加载当前接口
|
||||
const loadCurrentInterface = () => {
|
||||
isLoading.value = true
|
||||
hasError.value = false
|
||||
}
|
||||
|
||||
// iframe加载完成
|
||||
const onFrameLoad = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = false
|
||||
ElMessage.success(`接口 ${currentPrefixIndex.value + 1} 加载成功`)
|
||||
}
|
||||
|
||||
// iframe加载错误
|
||||
const onFrameError = () => {
|
||||
isLoading.value = false
|
||||
hasError.value = true
|
||||
ElMessage.error(`接口 ${currentPrefixIndex.value + 1} 加载失败`)
|
||||
}
|
||||
|
||||
// 切换接口
|
||||
const switchInterface = (index) => {
|
||||
if (index === currentPrefixIndex.value) return
|
||||
|
||||
currentPrefixIndex.value = index
|
||||
showDrawer.value = false
|
||||
ElMessage.info(`正在切换到接口 ${index + 1}`)
|
||||
}
|
||||
|
||||
// 切换集数
|
||||
const switchEpisode = (index) => {
|
||||
if (index === currentEpisodeIdx.value) return
|
||||
|
||||
store.updateEpisodeIdx(index)
|
||||
ElMessage.success(`正在切换到第${index + 1}集`)
|
||||
showDrawer.value = false
|
||||
}
|
||||
|
||||
// 返回首页
|
||||
const goBack = () => {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-player {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.player-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.error-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-container p,
|
||||
.error-container p {
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
right: 30px;
|
||||
z-index: 1000;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.current-info {
|
||||
margin-bottom: 30px;
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.current-info h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
margin: 0;
|
||||
word-break: break-all;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.interface-list h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.interface-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.interface-item:hover {
|
||||
border-color: #409eff;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.interface-item.active {
|
||||
border-color: #67c23a;
|
||||
background: #f0f9f0;
|
||||
}
|
||||
|
||||
.interface-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.interface-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.interface-url {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.active-icon {
|
||||
color: #67c23a;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.back-btn {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.episodes-selector {
|
||||
margin-top: 30px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.episodes-selector h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.episodes-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.episodes-grid :deep(.el-button) {
|
||||
flex: 0 0 48px;
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
639
src/views/VideoSearch.vue
Normal file
639
src/views/VideoSearch.vue
Normal file
@ -0,0 +1,639 @@
|
||||
<template>
|
||||
<div class="video-search">
|
||||
<div class="main-content">
|
||||
<!-- 时间显示 -->
|
||||
<div class="time-display">
|
||||
<div class="current-time">{{ currentTime }}</div>
|
||||
<div class="current-date">{{ currentDate }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="videoUrl"
|
||||
placeholder="请输入视频地址或者视频标题"
|
||||
class="search-input"
|
||||
size="large"
|
||||
clearable
|
||||
:loading="isSearching"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
:icon="Search"
|
||||
:loading="isSearching"
|
||||
style="color: white"
|
||||
>
|
||||
{{ isSearching ? '搜索中' : '搜索' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果区域 -->
|
||||
<div v-if="searchResults.length > 0" class="search-results">
|
||||
<h3 class="results-title">搜索结果 ({{ searchResults.length }})</h3>
|
||||
|
||||
<el-card
|
||||
v-for="item in searchResults"
|
||||
:key="item.video_id"
|
||||
class="video-card"
|
||||
shadow="hover"
|
||||
>
|
||||
<div class="card-content">
|
||||
<!-- 左侧信息 -->
|
||||
<div class="video-info">
|
||||
<div class="video-poster">
|
||||
<img :src="item.cover" :alt="item.title" />
|
||||
<div v-if="item.isVip" class="vip-badge">VIP</div>
|
||||
</div>
|
||||
<div class="video-details">
|
||||
<h4 class="video-title">{{ item.title }}</h4>
|
||||
<div class="video-meta">
|
||||
<span class="year">{{ item.year }}</span>
|
||||
<span class="category">{{ item.cat_name }}</span>
|
||||
<span class="area" v-if="item.areas && item.areas.length">{{
|
||||
item.areas.join('/')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="video-cast" v-if="item.actList && item.actList.length">
|
||||
<span class="cast-label">演员:</span>
|
||||
{{ item.actList.slice(0, 3).join('、')
|
||||
}}{{ item.actList.length > 3 ? '...' : '' }}
|
||||
</div>
|
||||
<div class="video-desc">{{ truncateText(item.description, 80) }}</div>
|
||||
<div class="video-tags">
|
||||
<el-tag v-for="tag in item.tags.slice(0, 4)" :key="tag" size="small">{{
|
||||
tag
|
||||
}}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧集数 -->
|
||||
<div class="episodes-container">
|
||||
<h5 class="episodes-title">共{{ item.episodeCount }}集</h5>
|
||||
<div class="episodes-grid">
|
||||
<el-button
|
||||
v-for="(link, index) in item.videoLinks.slice(0, 24)"
|
||||
:key="index"
|
||||
size="small"
|
||||
@click="playEpisode(item, link, index)"
|
||||
:type="index === 0 ? 'primary' : ''"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.videoLinks.length > 24"
|
||||
size="small"
|
||||
@click="showAllEpisodes(item)"
|
||||
>
|
||||
更多...
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 无搜索结果时的提示 -->
|
||||
<el-empty
|
||||
v-if="hasSearched && searchResults.length === 0"
|
||||
description="没有找到相关视频"
|
||||
></el-empty>
|
||||
|
||||
<!-- 解析支持 -->
|
||||
<el-card v-if="!searchResults.length" class="support-card" shadow="hover">
|
||||
<div class="support-text">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
解析支持:{{ supportedPlatforms }}、{{ supportedFormats }}
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 官方网站 -->
|
||||
<div v-if="!searchResults.length" class="links">
|
||||
<span class="link-text">官方网站:</span>
|
||||
<el-link type="primary" href="https://www.iqiyi.com" target="_blank">爱奇艺</el-link>
|
||||
<el-link type="primary" href="https://v.qq.com" target="_blank">腾讯视频</el-link>
|
||||
<el-link type="primary" href="https://www.youku.com" target="_blank">优酷</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 运行时间统计 -->
|
||||
<el-card v-if="!searchResults.length" class="runtime-card" shadow="never">
|
||||
<div class="runtime-text">
|
||||
<el-icon><Timer /></el-icon>
|
||||
已运行 {{ runtimeStats }}
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 全部剧集对话框 -->
|
||||
<el-dialog
|
||||
v-model="episodesDialogVisible"
|
||||
:title="`${currentVideo?.title || ''} - 全部剧集`"
|
||||
width="80%"
|
||||
>
|
||||
<div class="all-episodes">
|
||||
<el-button
|
||||
v-for="(link, index) in currentVideo?.videoLinks"
|
||||
:key="index"
|
||||
size="default"
|
||||
@click="playEpisode(currentVideo, link, index)"
|
||||
>
|
||||
第{{ index + 1 }}集
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search, VideoPlay, Timer } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useStore } from '../store/index'
|
||||
import axios from 'axios'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const currentTime = ref('')
|
||||
const currentDate = ref('')
|
||||
const videoUrl = ref('')
|
||||
const runtimeStats = ref('')
|
||||
const isSearching = ref(false)
|
||||
const hasSearched = ref(false)
|
||||
const searchResults = ref([])
|
||||
const episodesDialogVisible = ref(false)
|
||||
const currentVideo = ref(null)
|
||||
const supportedPlatforms = store.supportedPlatforms.join('、')
|
||||
const supportedFormats = store.supportedFormats.join('、')
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (date) => {
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const weekday = weekdays[date.getDay()]
|
||||
return `${year}-${month}-${day} 星期${weekday}`
|
||||
}
|
||||
|
||||
// 计算运行时间
|
||||
const calculateRuntime = () => {
|
||||
const beginDate = store.appConfig.site_begin_time
|
||||
const now = Date.now()
|
||||
const diffTime = Math.abs(now - beginDate)
|
||||
// 计算年、月、日、小时、分钟、秒
|
||||
const days = Math.floor(diffTime / (24 * 60 * 60 * 1000))
|
||||
const years = Math.floor(days / 365)
|
||||
const remainingDays = days % 365
|
||||
|
||||
const hours = Math.floor((diffTime % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000))
|
||||
const minutes = Math.floor((diffTime % (60 * 60 * 1000)) / (60 * 1000))
|
||||
const seconds = Math.floor((diffTime % (60 * 1000)) / 1000)
|
||||
|
||||
return `${years} 年 ${remainingDays} 天 ${hours} 小时 ${minutes} 分钟 ${seconds} 秒`
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
const now = new Date()
|
||||
currentTime.value = formatTime(now)
|
||||
currentDate.value = formatDate(now)
|
||||
runtimeStats.value = calculateRuntime()
|
||||
}
|
||||
|
||||
// 截断文本
|
||||
const truncateText = (text, maxLength) => {
|
||||
if (!text) return ''
|
||||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = async () => {
|
||||
if (!videoUrl.value.trim()) {
|
||||
ElMessage({
|
||||
message: '请输入视频链接或搜索关键词',
|
||||
type: 'warning',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// URL验证
|
||||
const urlPattern = /^https?:\/\/.+/
|
||||
if (urlPattern.test(videoUrl.value)) {
|
||||
store.updateCurrentVideo(videoUrl.value)
|
||||
router.push('/video_player')
|
||||
} else {
|
||||
// 执行搜索功能
|
||||
try {
|
||||
isSearching.value = true
|
||||
hasSearched.value = true
|
||||
|
||||
// 调用搜索API
|
||||
const response = await axios.get(
|
||||
`/api/searchvideo?keyword=${encodeURIComponent(videoUrl.value)}`,
|
||||
)
|
||||
searchResults.value = response.data || []
|
||||
|
||||
// 更新到store中
|
||||
store.updateSearchResults(searchResults.value)
|
||||
|
||||
if (searchResults.value.length === 0) {
|
||||
ElMessage.info('未找到相关视频')
|
||||
} else {
|
||||
ElMessage.success(`找到 ${searchResults.value.length} 个相关视频`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
ElMessage.error('搜索失败,请稍后重试')
|
||||
searchResults.value = []
|
||||
} finally {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 播放指定集数
|
||||
const playEpisode = (video, videoLink, episodeIndex) => {
|
||||
if (!video || !videoLink) return
|
||||
|
||||
// 更新当前视频信息到store,包含所有必要信息
|
||||
const videoData = {
|
||||
title: video.title,
|
||||
cover: video.cover,
|
||||
description: video.description,
|
||||
videoLinks: video.videoLinks,
|
||||
}
|
||||
store.updateCurrentVideo(videoData, episodeIndex, videoLink)
|
||||
|
||||
// 提示用户
|
||||
ElMessage.success(`正在为您解析: ${video.title} 第${episodeIndex + 1}集`)
|
||||
|
||||
// 跳转到播放页面
|
||||
router.push('/video_player')
|
||||
|
||||
// 关闭对话框
|
||||
episodesDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 显示全部剧集
|
||||
const showAllEpisodes = (video) => {
|
||||
currentVideo.value = video
|
||||
episodesDialogVisible.value = true
|
||||
}
|
||||
|
||||
let timer = null
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timer = setInterval(updateTime, 1000)
|
||||
|
||||
// 如果store中有搜索结果,则显示
|
||||
if (store.search_results && store.search_results.length > 0) {
|
||||
searchResults.value = store.search_results
|
||||
hasSearched.value = true
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-search {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: white;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
font-size: 4rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.current-date {
|
||||
font-size: 1rem;
|
||||
opacity: 0.8;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.search-input :deep(.el-input__wrapper) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.search-input :deep(.el-input__inner) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-input :deep(.el-input__inner::placeholder) {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.search-input :deep(.el-input-group__append) {
|
||||
background: rgba(64, 158, 255, 0.8);
|
||||
border-color: rgba(64, 158, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 搜索结果样式 */
|
||||
.search-results {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
color: white;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 5px;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
|
||||
.video-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.video-card :deep(.el-card__body) {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
display: flex;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.video-poster {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 200px;
|
||||
margin-right: 15px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.video-poster img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.vip-badge {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background-color: #f56c6c;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.video-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
font-size: 18px;
|
||||
margin: 0 0 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.video-meta span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.video-cast {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cast-label {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.video-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.video-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.episodes-container {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.episodes-title {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
margin: 0 0 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.episodes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.all-episodes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.support-card {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.support-card :deep(.el-card__body) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.support-text {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.runtime-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.runtime-card :deep(.el-card__body) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.runtime-text {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.current-time {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
max-width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.episodes-container {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.links {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Element Plus 深色主题适配 */
|
||||
:deep(.el-link) {
|
||||
color: #87ceeb;
|
||||
}
|
||||
|
||||
:deep(.el-link:hover) {
|
||||
color: #b3d9f2;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
background: rgba(30, 60, 114, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-dialog__title) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__headerbtn .el-dialog__close) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
background: rgba(64, 158, 255, 0.2);
|
||||
border-color: rgba(64, 158, 255, 0.3);
|
||||
color: #87ceeb;
|
||||
}
|
||||
|
||||
:deep(.el-empty__description) {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user