mirror of
https://github.com/unilei/aipan-netdisk-search.git
synced 2024-11-25 16:32:42 +08:00
- 新增alist源聚合播放
- 修改部分ui - 修改部分seo
This commit is contained in:
parent
dab30640d4
commit
5d4619b2be
@ -8,7 +8,8 @@
|
||||
👉 [爱盼-网盘资源搜索](https://www.aipan.me)
|
||||
|
||||
### 🔥更新日志
|
||||
|
||||
- tv播放
|
||||
- 新增Alist源聚合播放
|
||||
- 新增批量删除功能
|
||||
- 新增博客功能 (分支:[feat-admin-panel](https://github.com/unilei/aipan-netdisk-search/tree/feat-add-admin-panel))
|
||||
- 新增批量上传数据 [csv示例](/assets//readme//demo/demo-multi.csv) [xlsx 示例](/assets/readme/demo/demo-multi.xls)
|
||||
@ -97,6 +98,8 @@ yarn dev
|
||||
|
||||
### 4. 在浏览器打开 [http://localhost:3001](http://localhost:3001)
|
||||
|
||||
![success_deploy.jpg](/assets/readme/screen-6.png)
|
||||
![success_deploy.jpg](/assets/readme/screen-5.png)
|
||||
![success_deploy.jpg](/assets/readme/screen-1.png)
|
||||
![success_deploy.jpg](/assets/readme/screen-2.png)
|
||||
![success_deploy.jpg](/assets/readme/screen-3.png)
|
||||
|
BIN
assets/readme/screen-5.png
Normal file
BIN
assets/readme/screen-5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 770 KiB |
BIN
assets/readme/screen-6.png
Normal file
BIN
assets/readme/screen-6.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="py-[20px] dark:bg-slate-800">
|
||||
<p class="text-[12px] text-center text-gray-400">
|
||||
爱盼-网盘资源搜索
|
||||
爱盼 - 资源随心,娱乐无限
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -5,25 +5,23 @@ export default defineNuxtConfig({
|
||||
// head
|
||||
pageTransition: { name: 'page', mode: 'out-in' },
|
||||
head: {
|
||||
title: '爱盼-网盘资源搜索',
|
||||
title: '爱盼:资源随心,娱乐无限',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: '爱盼-网盘资源搜索, 一个开源免费的网盘资源搜索程序,仅供学习使用,不支持商业用途。'
|
||||
},
|
||||
{ name: 'keywords', content: '爱盼, 开源, 免费资源搜索, 网盘搜索, 音乐下载, TVBox数据接口, 电视直播, 博客发布, 影视资源, 教学工具, 非商业用途' },
|
||||
{ hid: 'description', name: 'description', content: '爱盼是一个开源免费的资源搜索平台,提供网盘资源搜索、音乐下载、TV直播、TVBox接口地址以及博客发布等多项功能,打造丰富的影视音聚合体验,供学习与探索使用,不支持商业用途。' },
|
||||
{ name: 'format-detection', content: 'telephone=no' },
|
||||
{ property: 'og:title', content: '爱盼:资源随心,音乐下载与影视聚合平台' },
|
||||
{ property: 'og:description', content: '爱盼是一个开源免费的资源搜索平台,提供网盘、音乐、影视等多种资源,一站式服务,供学习使用。' },
|
||||
{ property: 'og:image', content: 'https://aipan.me/logo.png' },
|
||||
{ property: 'og:url', content: 'https://aipan.me' },
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: '爱盼:资源随心,音乐下载与影视聚合平台' },
|
||||
{ name: 'twitter:description', content: '免费开源的资源搜索平台,涵盖音乐、网盘、影视等内容,学习探索好去处!' },
|
||||
{ name: 'twitter:image', content: 'https://aipan.me/logo.png' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{
|
||||
name: 'referrer',
|
||||
content: 'no-referrer'
|
||||
},
|
||||
{
|
||||
name: 'referrer',
|
||||
content: 'always'
|
||||
},
|
||||
{
|
||||
name: 'referrer',
|
||||
content: 'strict-origin-when-cross-origin'
|
||||
}
|
||||
{ name: 'referrer', content: 'no-referrer' },
|
||||
{ name: 'referrer', content: 'always' },
|
||||
{ name: 'referrer', content: 'strict-origin-when-cross-origin' }
|
||||
],
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
|
||||
script: [
|
||||
@ -137,9 +135,9 @@ export default defineNuxtConfig({
|
||||
]
|
||||
},
|
||||
site: {
|
||||
name: '爱盼-网盘资源搜索',
|
||||
name: '爱盼 - 资源随心,娱乐无限',
|
||||
url: 'https://www.aipan.me',
|
||||
description: '爱盼-网盘资源搜索, 一个开源免费的网盘资源搜索程序,仅供学习使用,不支持商业用途。'
|
||||
description: '爱盼 - 资源随心,娱乐无限, 一个开源免费的网盘资源搜索程序,仅供学习使用,不支持商业用途。'
|
||||
},
|
||||
compatibilityDate: '2024-09-12'
|
||||
})
|
165
pages/admin/alist.vue
Normal file
165
pages/admin/alist.vue
Normal file
@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
})
|
||||
|
||||
const alistDialogShow = ref(false)
|
||||
const handleAddAlist = () => {
|
||||
alistDialogShow.value = true
|
||||
|
||||
}
|
||||
const form = reactive({
|
||||
name: '',
|
||||
link: '',
|
||||
})
|
||||
const formRef = ref()
|
||||
|
||||
const handleSubmitAdd = () => {
|
||||
formRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
if (form.id) {
|
||||
$fetch(`/api/admin/alist/${form.id}`, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: form.id,
|
||||
name: form.name,
|
||||
link: form.link
|
||||
},
|
||||
headers: {
|
||||
"authorization": "Bearer " + useCookie('token').value
|
||||
}
|
||||
})
|
||||
} else {
|
||||
$fetch('/api/admin/alist/post', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: form.name,
|
||||
link: form.link
|
||||
},
|
||||
headers: {
|
||||
"authorization": "Bearer " + useCookie('token').value
|
||||
}
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
getAlists()
|
||||
}, 3000);
|
||||
|
||||
alistDialogShow.value = false
|
||||
})
|
||||
}
|
||||
const alistsData = ref([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const totalCount = ref(0)
|
||||
const getAlists = async () => {
|
||||
const res = await $fetch('/api/admin/alist/get', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value
|
||||
},
|
||||
headers: {
|
||||
"authorization": "Bearer " + useCookie('token').value
|
||||
}
|
||||
})
|
||||
// console.log(res)
|
||||
alistsData.value = res.alists;
|
||||
totalCount.value = res.totalCount;
|
||||
}
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getAlists()
|
||||
}
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getAlists()
|
||||
}
|
||||
const handleEditClouddrive = (row) => {
|
||||
// console.log(row)
|
||||
|
||||
form.id = row.id
|
||||
form.name = row.name
|
||||
form.link = row.link
|
||||
alistDialogShow.value = true
|
||||
}
|
||||
|
||||
const handleDeleteAlist = (row) => {
|
||||
// console.log(row)
|
||||
$fetch(`/api/admin/alist/${row.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
"authorization": "Bearer " + useCookie('token').value
|
||||
}
|
||||
})
|
||||
getAlists()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getAlists()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="p-10 max-w-[1240px] mx-auto ">
|
||||
<h1 class="text-xl text-bold space-x-2">
|
||||
<nuxt-link to="/admin/dashboard">后台管理面板</nuxt-link>
|
||||
<span>/</span>
|
||||
<nuxt-link to="/admin/alist">Alist源管理</nuxt-link>
|
||||
</h1>
|
||||
<div class="h-[1px] bg-slate-300 mt-6"></div>
|
||||
<div class="mt-6 grid grid-cols-4 gap-4">
|
||||
<el-button type="primary" @click="handleAddAlist()">添加数据</el-button>
|
||||
</div>
|
||||
<client-only>
|
||||
<div class="mt-6">
|
||||
<el-table ref="multipleTableRef" :data="alistsData">
|
||||
<el-table-column prop="name" label="名字"></el-table-column>
|
||||
<el-table-column prop="link" label="源链接"></el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary"
|
||||
@click="handleEditClouddrive(scope.row, scope.$index)">编辑</el-button>
|
||||
<el-button type="danger"
|
||||
@click="handleDeleteAlist(scope.row, scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt-6 flex items-center justify-center">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||
:page-sizes="[100, 200, 300, 400]" background
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="totalCount"
|
||||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</div>
|
||||
</client-only>
|
||||
</div>
|
||||
<el-dialog v-model="alistDialogShow" :title="form.id ? '编辑资源' : '添加资源'">
|
||||
<main>
|
||||
<el-form ref="formRef" :model="form" label-width="auto">
|
||||
<el-form-item label="名字" prop="name" :rules="{
|
||||
required: true,
|
||||
message: '名字不能为空',
|
||||
trigger: 'blur'
|
||||
}">
|
||||
<el-input v-model="form.name" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="源链接" prop="link"
|
||||
:rules="{ required: true, message: '链接不能为空', trigger: 'blur' }">
|
||||
<el-input v-model="form.link" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</main>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="alistDialogShow = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitAdd()"> 确认 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
@ -14,6 +14,9 @@ definePageMeta({
|
||||
<NuxtLink class="bg-slate-500 p-3 text-white rounded-lg hover:bg-black" to="/admin/blog">
|
||||
博客管理
|
||||
</NuxtLink>
|
||||
<NuxtLink class="bg-slate-500 p-3 text-white rounded-lg hover:bg-black" to="/admin/alist">
|
||||
Alist源管理
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -48,7 +48,7 @@ onMounted(async () => {
|
||||
<div class="bg-[#ffffff] dark:bg-gray-800 min-h-screen py-[60px]">
|
||||
<div class="flex flex-row items-center justify-center gap-3 mt-[80px]">
|
||||
<img class="w-[40px] h-[40px] sm:w-[60px] sm:h-[60px]" src="@/assets/my-logo.png" alt="logo">
|
||||
<h1 class="text-[18px] sm:text-[22px] font-bold dark:text-white ">爱盼-网盘资源搜索</h1>
|
||||
<h1 class="text-[18px] sm:text-[22px] font-bold dark:text-white ">爱盼 - 资源随心,娱乐无限</h1>
|
||||
</div>
|
||||
<div class="max-w-[1240px] mx-auto mt-[20px]">
|
||||
<div class="w-[80%] md:w-[700px] mx-auto flex flex-row items-center gap-2 relative">
|
||||
|
@ -1,10 +1,35 @@
|
||||
<script setup>
|
||||
useHead({
|
||||
title: '爱盼 - 电视直播与 Alist 数据源聚合播放',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, shrink-to-fit=no' },
|
||||
{ name: 'keywords', content: '爱盼, 电视直播, Alist 数据源, 聚合播放, 在线电视' },
|
||||
{ hid: 'description', name: 'description', content: '爱盼提供最新的电视直播和 Alist 数据源聚合播放,轻松享受精彩内容!' },
|
||||
{ name: 'author', content: '爱盼团队' },
|
||||
{ name: 'robots', content: 'index, follow' },
|
||||
{ name: 'format-detection', content: 'telephone=no' },
|
||||
{ property: 'og:title', content: '爱盼 - 电视直播与 Alist 数据源聚合播放' },
|
||||
{ property: 'og:description', content: '爱盼提供最新的电视直播和 Alist 数据源聚合播放,轻松享受精彩内容!' },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: "https://aipan.me/tv" }, // 动态获取当前页面的 URL
|
||||
{ property: 'og:image', content: '/logo.png' }, // 替换为适当的缩略图链接
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: '爱盼 - 电视直播与 Alist 数据源聚合播放' },
|
||||
{ name: 'twitter:description', content: '爱盼提供最新的电视直播和 Alist 数据源聚合播放,轻松享受精彩内容!' },
|
||||
{ name: 'twitter:image', content: '/logo.png' } // 替换为适当的 Twitter 卡片图像链接
|
||||
]
|
||||
});
|
||||
import Hls from "hls.js";
|
||||
import videojs from "video.js";
|
||||
import "video.js/dist/video-js.css";
|
||||
import bgImage from '~/assets/tv-bg-1.jpg';
|
||||
import { sourcesAipan } from "~/assets/vod/tv";
|
||||
import { useTvStore } from "~/stores/tv";
|
||||
definePageMeta({
|
||||
layout: 'custom',
|
||||
});
|
||||
const tvStore = useTvStore();
|
||||
const sourceIndex = ref(0);
|
||||
const tvSources = ref([]);
|
||||
const videoPlayer = ref(null);
|
||||
@ -14,11 +39,20 @@ const videoPlayStatus = ref(false);
|
||||
const videoLoading = ref(false);
|
||||
const videoMuted = ref(false);
|
||||
|
||||
let player = null;
|
||||
let hls = null; // 缓存 HLS 实例
|
||||
let currentEffectIndex = 0;
|
||||
const tvPassword = ref("");
|
||||
const tvPasswordInputShow = ref(false);
|
||||
|
||||
const alistData = ref([])
|
||||
const alistPath = ref([''])
|
||||
const currentIsDir = ref(true) // 当前是否是文件夹 默认是文件夹
|
||||
const alistUrl = ref("")
|
||||
const alistSettingData = ref([])
|
||||
const alistSettingShow = ref(false)
|
||||
const alistCurrentPlayIndex = ref(0)
|
||||
|
||||
// 判断是否为 m3u8 格式
|
||||
const isM3u8 = (url) => /\.m3u8(\?.*)?$/.test(url);
|
||||
// 获取视频源
|
||||
const getTvSources = async () => {
|
||||
try {
|
||||
@ -32,92 +66,100 @@ const getTvSources = async () => {
|
||||
console.error('Error fetching TV sources:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载 HLS 视频
|
||||
const loadHLS = (url) => {
|
||||
// 判断是否是 m3u8 格式
|
||||
if (url.endsWith('.m3u8')) {
|
||||
if (!hls && Hls.isSupported()) {
|
||||
// 显示加载动画
|
||||
const showLoadingSpinner = () => {
|
||||
videoLoading.value = true;
|
||||
};
|
||||
|
||||
// 隐藏加载动画
|
||||
const hideLoadingSpinner = () => {
|
||||
videoLoading.value = false;
|
||||
videoPlayStatus.value = true;
|
||||
};
|
||||
|
||||
// 判断播放类型
|
||||
let type = '';
|
||||
if (url.includes('.mp4')) {
|
||||
type = 'video/mp4';
|
||||
} else if (url.includes('.mkv')) {
|
||||
type = 'video/webm';
|
||||
} else if (url.includes('.ts')) {
|
||||
type = 'video/mp2t';
|
||||
} else if (isM3u8(url)) {
|
||||
type = 'application/x-mpegURL'; // HLS/M3U8 类型
|
||||
}
|
||||
|
||||
if (!player) {
|
||||
// 设置播放器选项
|
||||
const options = {
|
||||
liveui: true,
|
||||
html5: {
|
||||
hls: {
|
||||
enableLowInitialPlaylist: true,
|
||||
smoothQualityChange: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 创建播放器
|
||||
player = videojs(videoPlayer.value, options);
|
||||
player.on('ended', () => {
|
||||
player.currentTime(0);
|
||||
player.play();
|
||||
});
|
||||
|
||||
player.on("waiting", showLoadingSpinner);
|
||||
player.on("playing", hideLoadingSpinner);
|
||||
player.on("error", hideLoadingSpinner);
|
||||
}
|
||||
|
||||
if (type === 'application/x-mpegURL' && Hls.isSupported()) {
|
||||
// HLS 播放
|
||||
if (!hls) {
|
||||
hls = new Hls();
|
||||
hls.attachMedia(videoPlayer.value);
|
||||
hls.on(Hls.Events.MANIFEST_LOADING, showLoadingSpinner);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, hideLoadingSpinner);
|
||||
hls.on(Hls.Events.ERROR, hideLoadingSpinner);
|
||||
}
|
||||
if (Hls.isSupported()) {
|
||||
hls.loadSource(url);
|
||||
videoPlayer.value.play();
|
||||
videoPlayer.value.muted = videoMuted.value;
|
||||
videoPlayStatus.value = true;
|
||||
videoLoading.value = false;
|
||||
modalShow.value = false;
|
||||
} else if (videoPlayer.value.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
// 对于 Safari 或其他原生支持 HLS 的浏览器
|
||||
videoPlayer.value.src = url;
|
||||
videoPlayer.value.play();
|
||||
videoPlayer.value.muted = videoMuted.value;
|
||||
videoPlayStatus.value = true;
|
||||
videoLoading.value = false;
|
||||
modalShow.value = false;
|
||||
}
|
||||
hls.loadSource(url);
|
||||
} else {
|
||||
// 如果不是 m3u8,直接将 URL 赋值给 videoPlayer 的 src
|
||||
// 非 HLS 类型播放
|
||||
if (hls) {
|
||||
hls.destroy();
|
||||
hls = null; // 确保 HLS 实例不再被使用
|
||||
hls = null;
|
||||
}
|
||||
videoPlayer.value.src = url;
|
||||
videoPlayer.value.load(); // 加载新视频
|
||||
videoPlayer.value.play();
|
||||
videoPlayer.value.muted = videoMuted.value;
|
||||
showLoadingSpinner();
|
||||
player.src({ type, src: url });
|
||||
}
|
||||
|
||||
player.play();
|
||||
player.on("loadeddata", hideLoadingSpinner);
|
||||
player.on("loadedmetadata", hideLoadingSpinner);
|
||||
};
|
||||
|
||||
const handleSwithcSource = async (url) => {
|
||||
if (channelCategory.value === 3) {
|
||||
modalShow.value = true
|
||||
} else {
|
||||
videoLoading.value = true;
|
||||
videoSrc.value = url;
|
||||
loadHLS(url);
|
||||
videoPlayStatus.value = true;
|
||||
videoLoading.value = false;
|
||||
modalShow.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 视频切换处理
|
||||
const handleSwithcSource = (url) => {
|
||||
videoLoading.value = true;
|
||||
|
||||
// 在切换视频源之前,停止当前视频播放,并清除旧的src
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.pause();
|
||||
videoPlayer.value.removeAttribute('src'); // 清空旧视频源
|
||||
videoPlayer.value.load(); // 重置 <video> 标签
|
||||
}
|
||||
|
||||
videoSrc.value = url;
|
||||
loadHLS(url);
|
||||
};
|
||||
|
||||
const handleInputPassword = () => {
|
||||
if (tvPassword.value === 'aipan.me') {
|
||||
alert('密码正确')
|
||||
tvPasswordInputShow.value = false
|
||||
} else {
|
||||
alert('密码错误')
|
||||
}
|
||||
}
|
||||
const handleSwithcSourceAipan = (url) => {
|
||||
if (tvPassword.value) {
|
||||
if (tvPassword.value === 'aipan.me') {
|
||||
handleSwithcSource(url)
|
||||
} else {
|
||||
alert('请输入密码', '提示')
|
||||
tvPasswordInputShow.value = true
|
||||
}
|
||||
} else {
|
||||
alert('请输入密码', '提示')
|
||||
tvPasswordInputShow.value = true
|
||||
|
||||
}
|
||||
}
|
||||
// 视频播放和暂停
|
||||
const handleSwitchVideoStatus = () => {
|
||||
if (videoPlayer.value.paused) {
|
||||
videoPlayer.value.play();
|
||||
videoPlayStatus.value = true;
|
||||
} else {
|
||||
videoPlayer.value.pause();
|
||||
videoPlayStatus.value = false;
|
||||
if (player) {
|
||||
if (videoPlayer.value.paused) {
|
||||
videoPlayer.value.play();
|
||||
videoPlayStatus.value = true;
|
||||
} else {
|
||||
videoPlayer.value.pause();
|
||||
videoPlayStatus.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -130,6 +172,7 @@ const videoEffects = [
|
||||
];
|
||||
|
||||
const handleSwitchVideoTheme = () => {
|
||||
if (!player) return
|
||||
videoPlayer.value.classList.remove(videoEffects[currentEffectIndex]);
|
||||
currentEffectIndex = (currentEffectIndex + 1) % videoEffects.length;
|
||||
videoPlayer.value.classList.add(videoEffects[currentEffectIndex]);
|
||||
@ -144,25 +187,13 @@ const handleResetTheme = () => {
|
||||
|
||||
// 全屏功能
|
||||
const handleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
if (videoPlayer.value.requestFullscreen) {
|
||||
videoPlayer.value.requestFullscreen();
|
||||
} else if (videoPlayer.value.mozRequestFullScreen) {
|
||||
videoPlayer.value.mozRequestFullScreen();
|
||||
} else if (videoPlayer.value.webkitRequestFullscreen) {
|
||||
videoPlayer.value.webkitRequestFullscreen();
|
||||
} else if (videoPlayer.value.msRequestFullscreen) {
|
||||
videoPlayer.value.msRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
|
||||
if (player) {
|
||||
if (player.isFullscreen()) {
|
||||
player.exitFullscreen();
|
||||
} else {
|
||||
player.requestFullscreen();
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -176,20 +207,197 @@ const handlePlaying = () => {
|
||||
videoLoading.value = false;
|
||||
};
|
||||
const handleMute = () => {
|
||||
if (videoPlayer.value.muted) {
|
||||
videoPlayer.value.muted = false;
|
||||
videoMuted.value = false
|
||||
} else {
|
||||
videoPlayer.value.muted = true;
|
||||
videoMuted.value = true
|
||||
if (player) {
|
||||
videoMuted.value = !videoMuted.value;
|
||||
player.muted(videoMuted.value);
|
||||
}
|
||||
}
|
||||
|
||||
const channelCategory = ref(1)
|
||||
const channelCategoryData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "常用",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "电视直播",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Alist",
|
||||
}
|
||||
]
|
||||
|
||||
const handleSwithcChannelCategory = (id) => {
|
||||
tvStore.setTvCategory(id)
|
||||
channelCategory.value = id
|
||||
if (id === 3) {
|
||||
alistSettingShow.value = false
|
||||
currentIsDir.value = true
|
||||
alistPath.value = [""]
|
||||
tvStore.setAlistPath([""])
|
||||
let params = {
|
||||
page: 1,
|
||||
password: '',
|
||||
path: alistPath.value.join('/'),
|
||||
per_page: 0,
|
||||
refresh: false
|
||||
}
|
||||
getFsList(params)
|
||||
}
|
||||
}
|
||||
const getFsList = async (params) => {
|
||||
if (!alistUrl.value) return
|
||||
let res = await $fetch(`${alistUrl.value}/api/fs/list`, {
|
||||
method: 'POST',
|
||||
body: params
|
||||
})
|
||||
// console.log(res)
|
||||
if (res.code === 200) {
|
||||
alistData.value = res.data
|
||||
tvStore.setAlistData(res.data)
|
||||
} else {
|
||||
alistData.value.pop()
|
||||
tvStore.setAlistData(alistData.value)
|
||||
}
|
||||
}
|
||||
const getFsGet = async (params) => {
|
||||
if (!alistUrl.value) return
|
||||
let res = await $fetch(`${alistUrl.value}/api/fs/get`, {
|
||||
method: 'POST',
|
||||
body: params
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
let sign = res.data.sign;
|
||||
let alistPathTemp = []
|
||||
|
||||
alistPath.value.forEach((item, index) => {
|
||||
alistPathTemp[index] = encodeURIComponent(item)
|
||||
})
|
||||
let temp_url = alistPathTemp.join('/') + '?sign=' + sign
|
||||
videoSrc.value = `${alistUrl.value}/d${temp_url}`
|
||||
loadHLS(`${alistUrl.value}/d${temp_url}`);
|
||||
} else {
|
||||
alistData.value.pop()
|
||||
tvStore.setAlistData(alistData.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickAlist = (item, index) => {
|
||||
|
||||
if (item.is_dir) {
|
||||
console.log('this is is dir')
|
||||
// 如果是文件夹
|
||||
if (currentIsDir.value) {
|
||||
alistPath.value.push(item.name)
|
||||
} else {
|
||||
alistPath.value.pop()
|
||||
alistPath.value.push(item.name)
|
||||
}
|
||||
currentIsDir.value = true
|
||||
// 在这里进行深拷贝
|
||||
const currentPath = [...alistPath.value]; // 或使用 JSON.parse(JSON.stringify(alistPath.value)) 进行深拷贝
|
||||
tvStore.setAlistPath(currentPath)
|
||||
tvStore.setAlistCurrentPlayIndex(0)
|
||||
getFsList({
|
||||
page: 1,
|
||||
password: "",
|
||||
path: alistPath.value.join('/'),
|
||||
per_page: 0,
|
||||
refresh: false
|
||||
})
|
||||
} else {
|
||||
// 如果是文件
|
||||
if (currentIsDir.value) {
|
||||
alistPath.value.push(item.name)
|
||||
} else {
|
||||
alistPath.value.pop()
|
||||
alistPath.value.push(item.name)
|
||||
}
|
||||
currentIsDir.value = false
|
||||
tvStore.setAlistCurrentPlayIndex(index)
|
||||
alistCurrentPlayIndex.value = index
|
||||
getFsGet({
|
||||
path: alistPath.value.join('/'),
|
||||
password: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleBackAlist = () => {
|
||||
alistPath.value.pop()
|
||||
currentIsDir.value = true
|
||||
// console.log(alistPath.value)
|
||||
// tvStore.setAlistPath(alistPath.value)
|
||||
getFsList({
|
||||
page: 1,
|
||||
password: "",
|
||||
path: alistPath.value.join('/'),
|
||||
per_page: 0,
|
||||
refresh: false
|
||||
})
|
||||
}
|
||||
|
||||
const getAlists = async () => {
|
||||
try {
|
||||
let res = await $fetch("/api/alist/get", {
|
||||
method: "GET",
|
||||
})
|
||||
// console.log(res)
|
||||
alistSettingData.value = res.alists;
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
const handleAlistSetting = async () => {
|
||||
alistSettingShow.value = true
|
||||
tvStore.setAlistSettingShow(true)
|
||||
await getAlists()
|
||||
}
|
||||
|
||||
const handleClickAlistUrl = (item) => {
|
||||
alistUrl.value = item.link;
|
||||
tvStore.setAlistUrl(item.link)
|
||||
}
|
||||
|
||||
// 页面挂载和销毁
|
||||
onMounted(() => {
|
||||
// 获取视频源
|
||||
getTvSources();
|
||||
tvPassword.value = localStorage.getItem('tvPassword') || '';
|
||||
videoPlayer.value.addEventListener('waiting', handleWaiting);
|
||||
videoPlayer.value.addEventListener('playing', handlePlaying);
|
||||
getAlists();
|
||||
|
||||
// 从store中获取数据
|
||||
if (tvStore.tvCategory) {
|
||||
channelCategory.value = tvStore.tvCategory
|
||||
}
|
||||
if (tvStore.alistUrl) {
|
||||
alistUrl.value = tvStore.alistUrl
|
||||
}
|
||||
if (tvStore.alistSettingShow) {
|
||||
alistSettingShow.value = tvStore.alistSettingShow
|
||||
}
|
||||
if (tvStore.alistData) {
|
||||
alistData.value = tvStore.alistData
|
||||
}
|
||||
if (tvStore.alistPath) {
|
||||
alistPath.value = tvStore.alistPath
|
||||
}
|
||||
if (tvStore.alistCurrentPlayIndex) {
|
||||
alistCurrentPlayIndex.value = tvStore.alistCurrentPlayIndex
|
||||
}
|
||||
|
||||
if (channelCategory.value === 3) {
|
||||
|
||||
let params = {
|
||||
page: 1,
|
||||
password: '',
|
||||
path: alistPath.value.join('/'),
|
||||
per_page: 0,
|
||||
refresh: false
|
||||
}
|
||||
getFsList(params)
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@ -197,24 +405,24 @@ onBeforeUnmount(() => {
|
||||
hls.destroy();
|
||||
hls = null; // 确保不再引用该实例
|
||||
}
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.removeEventListener('waiting', handleWaiting);
|
||||
videoPlayer.value.removeEventListener('playing', handlePlaying);
|
||||
videoPlayer.value = null;
|
||||
if (player) {
|
||||
player.dispose();
|
||||
player = null;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dark:bg-slate-800 min-h-screen bg-no-repeat bg-cover bg-center relative"
|
||||
<div class="custom-bg dark:bg-slate-800 min-h-screen bg-no-repeat bg-cover bg-center relative"
|
||||
:style="{ 'background-image': `url(${bgImage})` }">
|
||||
<div class="absolute top-0 left-0 right-0 bottom-0 bg-black/20 backdrop-blur-sm"></div>
|
||||
<div class="fixed bottom-10 left-10 right-10 top-10 rounded-xl max-w-screen-lg mx-auto">
|
||||
<div class="fixed bottom-10 left-10 right-10 top-10 rounded-xl max-w-screen-lg mx-auto flex items-center ">
|
||||
<div class="w-full rounded-t-xl dark:bg-slate-700">
|
||||
<div
|
||||
<div id="aipan-video-container"
|
||||
class="relative w-full h-full bg-black rounded-t-md overflow-hidden px-6 pt-6 flex items-center justify-center aspect-video">
|
||||
<video ref="videoPlayer" id="video"
|
||||
class="w-full h-full relative shadow-md border border-gray-900 rounded-md"></video>
|
||||
<video ref="videoPlayer" id="aipan-video"
|
||||
class="video-js w-full h-full relative shadow-md border border-gray-900 rounded-md"></video>
|
||||
<div v-if="!videoPlayStatus"
|
||||
class="absolute top-6 left-6 right-6 bottom-0 video-mask shadow-xl rounded-md flex items-center justify-center">
|
||||
<button class="bg-red-500 text-white px-2 py-1 rounded-md text-xs hover:text-md"
|
||||
@ -266,52 +474,97 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="modalShow"
|
||||
class="fixed bottom-0 left-0 right-0 w-full h-full bg-black/50 flex flex-col items-center justify-center">
|
||||
<div class="bg-white p-10 rounded-xl dark:bg-black dark:text-white">
|
||||
<div class="flex flex-row items-center justify-center gap-2">
|
||||
<input class="border border-gray-300 px-4 py-2 rounded-md w-2/3" type="text" v-model="videoSrc"
|
||||
placeholder="请输入视频链接">
|
||||
<button class="bg-red-500 text-white px-2 py-2 rounded-md text-xs hover:text-md" type="button"
|
||||
@click="handleSwithcSource(videoSrc)">切换视频</button>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap items-center justify-center max-w-screen-sm mx-auto gap-4 mt-5">
|
||||
<div class="text-sm font-semibold border border-gray-300 text-slate-600 dark:text-white dark:bg-slate-700 rounded-full p-2 cursor-pointer hover:bg-black hover:text-white transition duration-300"
|
||||
:class="{ 'bg-black text-white': item.url === videoSrc }" v-for=" (item, index) in tvSources"
|
||||
:key="index" @click="handleSwithcSource(item.url)">
|
||||
{{ item.name }}
|
||||
class="fixed bottom-0 top-0 left-0 p-5 w-full md:w-[520px] h-full bg-black/80 overflow-y-scroll">
|
||||
<div class="flex flex-row items-center justify-center gap-2">
|
||||
<input class="border border-gray-300 px-4 py-2 rounded-md w-2/3" type="text" v-model="videoSrc"
|
||||
placeholder="请输入视频链接">
|
||||
<button class="bg-red-500 text-white px-2 py-2 rounded-md text-xs hover:text-md" type="button"
|
||||
@click="handleSwithcSource(videoSrc)">切换视频</button>
|
||||
</div>
|
||||
<div class="mt-5 flex flex-row gap-2">
|
||||
<div class="w-10 space-y-2">
|
||||
<div>
|
||||
<button class="bg-red-500 text-white px-2 py-1 rounded-md text-xs hover:text-md" type="button"
|
||||
@click="modalShow = false">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
<li class="text-sm font-semibold text-gray-600 border border-gray-700 p-2 rounded-md cursor-pointer hover:bg-gray-700 hover:text-white transition duration-300"
|
||||
:class="{ 'bg-gray-700 text-white': channelCategory === category.id }"
|
||||
style="writing-mode: vertical-rl;" v-for="(category, index) in channelCategoryData"
|
||||
:key="index" @click="handleSwithcChannelCategory(category.id)">
|
||||
|
||||
<div v-if="tvPasswordInputShow" class="flex flex-row items-center justify-center gap-2 mt-10">
|
||||
<input class="border border-gray-300 px-4 py-2 rounded-md w-2/3" type="text" v-model="tvPassword"
|
||||
placeholder="请输入密码">
|
||||
<button class="bg-red-500 text-white px-2 py-2 rounded-md text-xs hover:text-md" type="button"
|
||||
@click="handleInputPassword">输入密码</button>
|
||||
{{ category.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap items-center justify-center max-w-screen-lg mx-auto gap-4 mt-4">
|
||||
<div class="text-sm font-semibold border border-gray-300 text-slate-600 dark:text-white dark:bg-slate-700 rounded-full p-2 cursor-pointer hover:bg-black hover:text-white transition duration-300"
|
||||
:class="{ 'bg-black text-white': sourceIndex === index }" v-for=" (item, index) in sourcesAipan"
|
||||
:key="index" @click="sourceIndex = index">
|
||||
{{ item.label }}
|
||||
<div class="w-full">
|
||||
<div v-if="channelCategory === 1" class="space-y-2">
|
||||
<div class="w-full text-sm font-semibold border border-gray-800 text-slate-600 dark:text-white dark:bg-slate-700 rounded-full p-2 cursor-pointer hover:bg-black hover:text-white transition duration-300"
|
||||
:class="{ 'bg-black text-white': item.url === videoSrc }"
|
||||
v-for=" (item, index) in tvSources" :key="index" @click="handleSwithcSource(item.url)">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row flex-wrap items-center justify-center max-w-screen-sm mx-auto gap-4 mt-5">
|
||||
<div class="text-sm font-semibold border border-gray-300 text-slate-600 dark:text-white dark:bg-slate-700 rounded-full p-2 cursor-pointer hover:bg-black hover:text-white transition duration-300"
|
||||
:class="{ 'bg-black text-white': item.url === videoSrc }"
|
||||
v-for=" (item, index) in sourcesAipan[sourceIndex]['sources']" :key="index"
|
||||
@click="handleSwithcSourceAipan(item.url)">
|
||||
{{ item.name }}
|
||||
<div v-if="channelCategory === 2">
|
||||
<div class="flex flex-row flex-wrap items-center justify-center max-w-screen-lg mx-auto gap-4">
|
||||
<div class="text-sm font-semibold border border-gray-700 text-slate-600 dark:text-white dark:bg-slate-700 rounded-md p-2 cursor-pointer hover:bg-black hover:text-white transition duration-300"
|
||||
:class="{ 'bg-black text-white': sourceIndex === index }"
|
||||
v-for=" (item, index) in sourcesAipan" :key="index" @click="sourceIndex = index">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" space-y-2 mt-5">
|
||||
<div class="text-sm font-semibold border border-gray-700 text-slate-600 dark:text-white dark:bg-slate-700 rounded-full p-2 cursor-pointer hover:bg-black hover:text-white transition duration-300"
|
||||
:class="{ 'bg-black text-white': item.url === videoSrc }"
|
||||
v-for=" (item, index) in sourcesAipan[sourceIndex]['sources']" :key="index"
|
||||
@click="handleSwithcSource(item.url)">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2" v-if="channelCategory === 3">
|
||||
<div class="space-x-2">
|
||||
<button class="border-gray-800 text-white px-2 py-1 rounded-md text-xs hover:text-md"
|
||||
type="button" @click="handleBackAlist">
|
||||
返回上级
|
||||
</button>
|
||||
<button class="border border-gray-800 text-white px-2 py-1 rounded-md text-xs hover:text-md"
|
||||
type="button" @click="() => {
|
||||
handleSwithcChannelCategory(3)
|
||||
alistSettingShow = false
|
||||
tvStore.setAlistSettingShow(false)
|
||||
}">
|
||||
主页
|
||||
</button>
|
||||
<button class="border border-gray-800 text-white px-2 py-1 rounded-md text-xs hover:text-md"
|
||||
:class="alistSettingShow ? 'bg-red-500' : ''" type="button" @click="handleAlistSetting">
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-2" v-if="alistSettingShow">
|
||||
<div class="text-gray-600 p-2 text-xs border border-gray-700 rounded-md cursor-pointer transition duration-300 break-words"
|
||||
:class="alistUrl === item.link ? 'bg-gray-400 text-gray-900 ' : 'hover:bg-gray-200'"
|
||||
v-for=" (item, index) in alistSettingData" :key="index"
|
||||
@click="handleClickAlistUrl(item)">
|
||||
{{ item.name }} {{ item.link }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2" v-else>
|
||||
<div class="text-gray-600 p-2 text-xs border border-gray-700 rounded-md cursor-pointer transition duration-300 break-words"
|
||||
:class="item.is_dir ? 'bg-yellow-400 text-gray-900 hover:bg-yellow-600' : alistCurrentPlayIndex === index ? 'bg-gray-400 text-gray-900 ' : 'hover:bg-gray-200'"
|
||||
v-for=" (item, index) in alistData?.content" :key="index"
|
||||
@click="handleClickAlist(item, index)">
|
||||
{{ item.name }} - {{ item.is_dir ? '文件夹' : '文件' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center justify-center gap-2 mt-5">
|
||||
<button class="bg-red-500 text-white px-2 py-1 rounded-md text-xs hover:text-md" type="button"
|
||||
@click="modalShow = false">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.video-mask {
|
||||
|
@ -9,8 +9,12 @@ useHead({
|
||||
{ name: 'format-detection', content: 'telephone=no' }
|
||||
]
|
||||
})
|
||||
const getData = async () => {
|
||||
const res = await $fetch('/api/tvbox')
|
||||
tvbox.value = res.list || [];
|
||||
}
|
||||
onMounted(() => {
|
||||
|
||||
getData()
|
||||
})
|
||||
const { data: tvbox } = await useAsyncData('tvbox', async () => {
|
||||
const res = await $fetch('/api/tvbox')
|
||||
|
14
prisma/migrations/20241027063215_add/migration.sql
Normal file
14
prisma/migrations/20241027063215_add/migration.sql
Normal file
@ -0,0 +1,14 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Alist" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"link" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"creatorId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Alist_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Alist" ADD CONSTRAINT "Alist_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -28,6 +28,7 @@ model User {
|
||||
resources Resource[] // 一个用户可以有多个资源
|
||||
resourceTypes ResourceType[] // 一个用户可以创建多种资源类型
|
||||
Post Post[]
|
||||
Alist Alist[]
|
||||
}
|
||||
|
||||
model ResourceType {
|
||||
@ -84,3 +85,14 @@ model PostToCategory {
|
||||
|
||||
@@id([postId, categoryId]) // 组合主键,确保唯一性
|
||||
}
|
||||
|
||||
model Alist {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
link String // alist 源链接
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
creatorId Int
|
||||
|
||||
creator User @relation(fields: [creatorId], references: [id])
|
||||
}
|
||||
|
16
server/api/admin/alist/[id].delete.ts
Normal file
16
server/api/admin/alist/[id].delete.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import prisma from "~/lib/prisma";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { id } = getRouterParams(event)
|
||||
const userId = event.context.user.userId
|
||||
const alist = await prisma.alist.delete({
|
||||
where: {
|
||||
id: Number(id),
|
||||
}
|
||||
})
|
||||
return {
|
||||
code: 200,
|
||||
msg: 'success',
|
||||
data: []
|
||||
}
|
||||
})
|
14
server/api/admin/alist/[id].get.ts
Normal file
14
server/api/admin/alist/[id].get.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import prisma from "~/lib/prisma";
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { id } = getRouterParams(event)
|
||||
const alist = await prisma.alist.findUnique({
|
||||
where: {
|
||||
id: Number(id),
|
||||
}
|
||||
})
|
||||
return {
|
||||
code: 200,
|
||||
msg: 'success',
|
||||
data: alist
|
||||
}
|
||||
})
|
31
server/api/admin/alist/[id].put.ts
Normal file
31
server/api/admin/alist/[id].put.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import prisma from "~/lib/prisma";
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { id } = getRouterParams(event);
|
||||
const userId = event.context.user.userId;
|
||||
const { name, link } = await readBody(event);
|
||||
const alist = await prisma.alist.findUnique({
|
||||
where: {
|
||||
id: Number(id),
|
||||
}
|
||||
})
|
||||
if (!alist) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Alist not found' });
|
||||
}
|
||||
if (alist.creatorId !== userId) {
|
||||
throw createError({ statusCode: 403, statusMessage: 'Forbidden' }); // 仅允许资源的创建者更新资源
|
||||
}
|
||||
|
||||
const updatedAlist = await prisma.alist.update({
|
||||
where: { id: Number(id) },
|
||||
data: {
|
||||
name,
|
||||
link
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
msg: 'success',
|
||||
data: updatedAlist
|
||||
};
|
||||
})
|
31
server/api/admin/alist/get.ts
Normal file
31
server/api/admin/alist/get.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import prisma from '~/lib/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = await getQuery(event);
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const skip = (page - 1) * pageSize;
|
||||
const take = pageSize;
|
||||
// 获取总记录数
|
||||
const totalCount = await prisma.alist.count();
|
||||
const alists = await prisma.alist.findMany({
|
||||
skip,
|
||||
take,
|
||||
include: {
|
||||
creator: {
|
||||
select: { username: true }, // 包含创建者的用户名
|
||||
},
|
||||
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalCount,
|
||||
page,
|
||||
pageSize,
|
||||
alists,
|
||||
};
|
||||
});
|
21
server/api/admin/alist/post.ts
Normal file
21
server/api/admin/alist/post.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import prisma from "~/lib/prisma";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { name, link } = await readBody(event)
|
||||
|
||||
const userId = event.context.user.userId;
|
||||
const alist = await prisma.alist.create({
|
||||
data: {
|
||||
name,
|
||||
link,
|
||||
creatorId: userId
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
msg: 'success',
|
||||
data: alist
|
||||
}
|
||||
|
||||
})
|
31
server/api/alist/get.ts
Normal file
31
server/api/alist/get.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import prisma from '~/lib/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = await getQuery(event);
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const skip = (page - 1) * pageSize;
|
||||
const take = pageSize;
|
||||
// 获取总记录数
|
||||
const totalCount = await prisma.alist.count();
|
||||
const alists = await prisma.alist.findMany({
|
||||
skip,
|
||||
take,
|
||||
include: {
|
||||
creator: {
|
||||
select: { username: true }, // 包含创建者的用户名
|
||||
},
|
||||
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalCount,
|
||||
page,
|
||||
pageSize,
|
||||
alists,
|
||||
};
|
||||
});
|
40
stores/tv.ts
Normal file
40
stores/tv.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { defineStore } from "pinia";
|
||||
export const useTvStore = defineStore('tv', {
|
||||
state() {
|
||||
return {
|
||||
tvCategory: 1,
|
||||
alistUrl: '',
|
||||
alistData: {},
|
||||
alistPath: [],
|
||||
alistSettingShow: false,
|
||||
alistPlayingShow: false,
|
||||
alistCurrentPlayIndex: 0
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setTvCategory(id: number) {
|
||||
this.tvCategory = id
|
||||
},
|
||||
setAlistUrl(url: string) {
|
||||
this.alistUrl = url
|
||||
},
|
||||
setAlistSettingShow(show: boolean) {
|
||||
this.alistSettingShow = show
|
||||
},
|
||||
setAlistPlayingShow(show: boolean) {
|
||||
this.alistPlayingShow = show
|
||||
},
|
||||
setAlistPath(path: []) {
|
||||
this.alistPath = path
|
||||
},
|
||||
setAlistData(data: {}) {
|
||||
this.alistData = data
|
||||
},
|
||||
setAlistCurrentPlayIndex(index: number) {
|
||||
this.alistCurrentPlayIndex = index
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
storage: persistedState.localStorage
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue
Block a user