es lint fix

This commit is contained in:
thofx
2023-07-22 16:09:07 +08:00
parent 48749b29fe
commit 018778488a
80 changed files with 3994 additions and 2981 deletions
+3 -3
View File
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { useTheme } from 'vuetify'
import misc404 from '@images/pages/404.png'
import miscMaskDark from '@images/pages/misc-mask-dark.png'
import miscMaskLight from '@images/pages/misc-mask-light.png'
import tree from '@images/pages/tree.png'
import { useTheme } from 'vuetify'
const props = defineProps<Props>()
const vuetifyTheme = useTheme()
@@ -16,8 +18,6 @@ interface Props {
errorTitle?: string
errorDescription?: string
}
const props = defineProps<Props>()
</script>
<template>
+63 -40
View File
@@ -1,69 +1,74 @@
<script lang="ts" setup>
import api from "@/api";
import { DownloadingInfo } from "@/api/types";
import api from '@/api'
import type { DownloadingInfo } from '@/api/types'
// 输入参数
const props = defineProps({
info: Object as PropType<DownloadingInfo>,
});
})
// 是否显示卡片
const cardState = ref(true);
const cardState = ref(true)
// 进度条
const getPercentage = () => {
return props.info?.progress ?? 0;
};
function getPercentage() {
return props.info?.progress ?? 0
}
// 速度
const getSpeedText = () => {
return `${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s`;
};
function getSpeedText() {
return `${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s`
}
// 下载状态
const isDownloading = ref(props.info?.state === "downloading" ? true : false);
const isDownloading = ref(props.info?.state === 'downloading')
// 图片是否加载完成
const imageLoaded = ref(false);
const imageLoaded = ref(false)
// 图片加载完成响应
const imageLoadHandler = () => {
imageLoaded.value = true;
};
function imageLoadHandler() {
imageLoaded.value = true
}
// 计算文本类
const getTextClass = () => {
return imageLoaded.value ? "text-white" : "";
};
function getTextClass() {
return imageLoaded.value ? 'text-white' : ''
}
// 下载状态控制
const toggleDownload = async () => {
let operation = isDownloading.value ? "stop" : "start";
async function toggleDownload() {
const operation = isDownloading.value ? 'stop' : 'start'
try {
const result: { [key: string]: any } = await api.put(
`download/${props.info?.hash}/${operation}`
);
if (result.success) {
isDownloading.value = !isDownloading.value;
}
} catch (error) {
console.error(error);
`download/${props.info?.hash}/${operation}`,
)
if (result.success)
isDownloading.value = !isDownloading.value
}
};
catch (error) {
console.error(error)
}
}
// 删除下截
const deleteDownload = async () => {
async function deleteDownload() {
try {
await api.delete(`download/${props.info?.hash}`);
cardState.value = false;
} catch (error) {
console.error(error);
await api.delete(`download/${props.info?.hash}`)
cardState.value = false
}
};
catch (error) {
console.error(error)
}
}
</script>
<template>
<VCard :key="props.info?.hash" v-if="cardState">
<VCard
v-if="cardState"
:key="props.info?.hash"
>
<template #image>
<VImg
:src="props.info?.media.image"
@@ -74,18 +79,32 @@ const deleteDownload = async () => {
/>
</template>
<VCardTitle class="break-words whitespace-normal" :class="getTextClass()">
<VCardTitle
class="break-words whitespace-normal"
:class="getTextClass()"
>
{{ props.info?.media.title || props.info?.name }}
{{ props.info?.season_episode }}
</VCardTitle>
<VCardSubtitle class="break-words whitespace-normal" :class="getTextClass()">
<VCardSubtitle
class="break-words whitespace-normal"
:class="getTextClass()"
>
{{ props.info?.title }}
</VCardSubtitle>
<VCardText class="text-subtitle-1 pt-3 pb-1" :class="getTextClass()"> {{ getSpeedText() }} </VCardText>
<VCardText
class="text-subtitle-1 pt-3 pb-1"
:class="getTextClass()"
>
{{ getSpeedText() }}
</VCardText>
<VCardText v-if="getPercentage() > 0" :class="getTextClass()">
<VCardText
v-if="getPercentage() > 0"
:class="getTextClass()"
>
<VProgressLinear :model-value="getPercentage()" />
</VCardText>
@@ -93,7 +112,11 @@ const deleteDownload = async () => {
<VBtn @click="toggleDownload">
<span class="ms-2">{{ isDownloading ? "暂停" : "开始" }}</span>
</VBtn>
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
<VBtn
color="error"
icon="mdi-trash-can-outline"
@click="deleteDownload"
/>
</VCardActions>
</VCard>
</template>
+44 -40
View File
@@ -1,73 +1,77 @@
<script lang="ts" setup>
// 定义触发的自定义事件
const emit = defineEmits(["close", "changed"]);
// 输入参数
const props = defineProps({
pri: String,
rules: Array as PropType<string[]>,
width: String,
height: String,
});
})
// 定义触发的自定义事件
const emit = defineEmits(['close', 'changed'])
// 按钮点击
const onClose = () => {
emit("close");
};
function onClose() {
emit('close')
}
// 选项变化
const filtersChanged = (value: string[]) => {
emit("changed", props.pri, value);
};
function filtersChanged(value: string[]) {
emit('changed', props.pri, value)
}
// 过滤规则下拉框
const selectFilterOptions = ref<{ [key: string]: string }[]>([
{ title: "特效字幕", value: " SPECSUB " },
{ title: "中文字幕", value: " CNSUB " },
{ title: "分辨率: 4K", value: " 4K " },
{ title: "分辨率: 1080P", value: " 1080P " },
{ title: "分辨率: 720P", value: " 720P " },
{ title: "排除: 720P", value: " !720P " },
{ title: "质量: 蓝光原盘", value: " BLU " },
{ title: "排除: 蓝光原盘", value: " !BLU ", color: "error" },
{ title: "质量: REMUX", value: " REMUX " },
{ title: "排除: REMUX", value: " !REMUX ", color: "error" },
{ title: "质量: WEB-DL", value: " WEBDL " },
{ title: "排除: WEB-DL", value: " !WEBDL ", color: "error" },
{ title: "编码: H265", value: " H265 " },
{ title: "排除: H265", value: " !H265 ", color: "error" },
{ title: "编码: H264", value: " H264 " },
{ title: "排除: H264", value: " !H264 ", color: "error" },
{ title: "效果: 杜比视界", value: " DOLBY " },
{ title: "排除: 杜比视界", value: " !DOLBY ", color: "error" },
{ title: "效果: HDR", value: " HDR " },
{ title: "排除: HDR", value: " !HDR ", color: "error" },
{ title: "国语配音", value: " CNVOI ", color: "error" },
{ title: "排除: 国语配音", value: " !CNVOI ", color: "error" },
{ title: "促销: 免费", value: " FREE " },
]);
{ title: '特效字幕', value: ' SPECSUB ' },
{ title: '中文字幕', value: ' CNSUB ' },
{ title: '分辨率: 4K', value: ' 4K ' },
{ title: '分辨率: 1080P', value: ' 1080P ' },
{ title: '分辨率: 720P', value: ' 720P ' },
{ title: '排除: 720P', value: ' !720P ' },
{ title: '质量: 蓝光原盘', value: ' BLU ' },
{ title: '排除: 蓝光原盘', value: ' !BLU ', color: 'error' },
{ title: '质量: REMUX', value: ' REMUX ' },
{ title: '排除: REMUX', value: ' !REMUX ', color: 'error' },
{ title: '质量: WEB-DL', value: ' WEBDL ' },
{ title: '排除: WEB-DL', value: ' !WEBDL ', color: 'error' },
{ title: '编码: H265', value: ' H265 ' },
{ title: '排除: H265', value: ' !H265 ', color: 'error' },
{ title: '编码: H264', value: ' H264 ' },
{ title: '排除: H264', value: ' !H264 ', color: 'error' },
{ title: '效果: 杜比视界', value: ' DOLBY ' },
{ title: '排除: 杜比视界', value: ' !DOLBY ', color: 'error' },
{ title: '效果: HDR', value: ' HDR ' },
{ title: '排除: HDR', value: ' !HDR ', color: 'error' },
{ title: '国语配音', value: ' CNVOI ', color: 'error' },
{ title: '排除: 国语配音', value: ' !CNVOI ', color: 'error' },
{ title: '促销: 免费', value: ' FREE ' },
])
// 已选择的过滤规则
const selectedFilters = ref<string[]>(props.rules ?? []);
const selectedFilters = ref<string[]>(props.rules ?? [])
</script>
<template>
<VCard variant="tonal" :width="props.width" :height="props.height">
<VCard
variant="tonal"
:width="props.width"
:height="props.height"
>
<DialogCloseBtn @click="onClose" />
<VCardItem>
<VCardTitle>优先级 {{ props.pri }}</VCardTitle>
<VRow>
<VCol>
<VSelect
variant="underlined"
:key="props.pri"
v-model="selectedFilters"
variant="underlined"
:items="selectFilterOptions"
@update:modelValue="filtersChanged"
chips
label=""
multiple
>
</VSelect>
@update:modelValue="filtersChanged"
/>
</VCol>
</VRow>
</VCardItem>
+288 -257
View File
@@ -1,209 +1,225 @@
<script lang="ts" setup>
import { formatSeason } from "@/@core/utils/formatters";
import api from "@/api";
import { doneNProgress, startNProgress } from "@/api/nprogress";
import { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from "@/api/types";
import router from "@/router";
import { useToast } from "vue-toast-notification";
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from '@/api/types'
import router from '@/router'
// 输入参数
const props = defineProps({
media: Object as PropType<MediaInfo>,
width: String,
height: String,
});
})
// 提示框
const $toast = useToast();
const $toast = useToast()
// 图片加载状态
const isImageLoaded = ref(false);
const isImageLoaded = ref(false)
// 图片加载失败
const imageLoadError = ref(false);
const imageLoadError = ref(false)
// TMDB识别标志
const tmdbFlag = ref(true);
const tmdbFlag = ref(true)
// 当前订阅状态
const isSubscribed = ref(false);
const isSubscribed = ref(false)
// 本地存在状态
const isExists = ref(false);
const isExists = ref(false)
// 各季缺失状态:0-已存在 1-部分缺失 2-全部缺失
const seasonsNotExisted = ref<{ [key: number]: number }>({});
const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 订阅季弹窗
const subscribeSeasonDialog = ref(false);
const subscribeSeasonDialog = ref(false)
// 季详情
const seasonInfos = ref<TmdbSeason[]>([]);
const seasonInfos = ref<TmdbSeason[]>([])
// 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([])
// 订阅弹窗选择的多季
const subscribeSeasons = () => {
subscribeSeasonDialog.value = false;
function subscribeSeasons() {
subscribeSeasonDialog.value = false
seasonsSelected.value.forEach((season) => {
addSubscribe(season.season_number);
});
};
addSubscribe(season.season_number)
})
}
// 角标颜色
const getChipColor = (type: string) => {
if (type === "电影") {
return "border-blue-500 bg-blue-600";
} else if (type === "电视剧") {
return " bg-indigo-500 border-indigo-600";
} else {
return "border-purple-600 bg-purple-600";
}
};
function getChipColor(type: string) {
if (type === '电影')
return 'border-blue-500 bg-blue-600'
else if (type === '电视剧')
return ' bg-indigo-500 border-indigo-600'
else
return 'border-purple-600 bg-purple-600'
}
// 添加订阅处理
const handleAddSubscribe = async () => {
if (props.media?.type == "电视剧" && props.media?.tmdb_id) {
// eslint-disable-next-line sonarjs/cognitive-complexity
async function handleAddSubscribe() {
if (props.media?.type === '电视剧' && props.media?.tmdb_id) {
// TMDB电视剧
// 查询TMDB所有季信息
await getMediaSeasons();
await getMediaSeasons()
if (!seasonInfos.value) {
$toast.error(`${props.media?.title} 查询剧集信息失败!`);
return;
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
return
}
// 检查各季的缺失状态
await checkSeasonsNotExists();
if (!tmdbFlag.value) {
return;
}
if (seasonInfos.value.length == 1) {
await checkSeasonsNotExists()
if (!tmdbFlag.value)
return
if (seasonInfos.value.length === 1) {
// 只有1季
if (!seasonsNotExisted.value[1]) {
// 已存在
$toast.warning(`${props.media?.title} 媒体库中已存在!`);
} else {
// 添加订阅
addSubscribe();
$toast.warning(`${props.media?.title} 媒体库中已存在!`)
}
} else {
else {
// 添加订阅
addSubscribe()
}
}
else {
// 弹出季选择列表,支持多选
subscribeSeasonDialog.value = true;
}
} else if (props.media?.type == "电视剧") {
// 豆瓣电视剧,只会有一季
let season = props.media?.season || 1;
// 检查缺失情况
await checkSeasonsNotExists();
if (!tmdbFlag.value) {
return;
}
if (!seasonsNotExisted.value[season]) {
// 已存在
$toast.warning(`${props.media?.title} 媒体库中已存在!`);
} else {
// 添加订阅
addSubscribe(season);
}
} else {
// 电影
let exists = await checkMovieExists();
if (exists) {
$toast.warning(`${props.media?.title} 媒体库中已存在!`);
} else {
addSubscribe();
subscribeSeasonDialog.value = true
}
}
};
else if (props.media?.type === '电视剧') {
// 豆瓣电视剧,只会有一季
const season = props.media?.season || 1
// 检查缺失情况
await checkSeasonsNotExists()
if (!tmdbFlag.value)
return
if (!seasonsNotExisted.value[season]) {
// 已存在
$toast.warning(`${props.media?.title} 媒体库中已存在!`)
}
else {
// 添加订阅
addSubscribe(season)
}
}
else {
// 电影
const exists = await checkMovieExists()
if (exists)
$toast.warning(`${props.media?.title} 媒体库中已存在!`)
else
addSubscribe()
}
}
// 调用API添加订阅,电视剧的话需要指定季
const addSubscribe = async (season: number = 0) => {
async function addSubscribe(season = 0) {
// 开始处理
startNProgress();
startNProgress()
try {
const result: { [key: string]: any } = await api.post("subscribe", {
const result: { [key: string]: any } = await api.post('subscribe', {
name: props.media?.title,
type: props.media?.type,
year: props.media?.year,
tmdbid: props.media?.tmdb_id,
doubanid: props.media?.douban_id,
season: season,
});
season,
})
// 订阅状态
if (result.success) {
// 订阅成功
isSubscribed.value = true;
isSubscribed.value = true
}
// 提示
showSubscribeAddToast(
result.success,
props.media?.title ?? "",
props.media?.title ?? '',
season,
result.message
);
} catch (error) {
console.error(error);
result.message,
)
}
doneNProgress();
};
catch (error) {
console.error(error)
}
doneNProgress()
}
// 弹出添加订阅提示
const showSubscribeAddToast = (
result: boolean,
function showSubscribeAddToast(result: boolean,
title: string,
season: number,
message: string
) => {
if (season) {
title = `${title} ${formatSeason(season.toString())}`;
}
if (result) {
$toast.success(`${title} 添加订阅成功!`);
} else {
$toast.error(`${title} 添加订阅失败:${message}`);
}
};
message: string) {
if (season)
title = `${title} ${formatSeason(season.toString())}`
if (result)
$toast.success(`${title} 添加订阅成功!`)
else
$toast.error(`${title} 添加订阅失败:${message}`)
}
// 调用API取消订阅
const removeSubscribe = async () => {
async function removeSubscribe() {
// 开始处理
startNProgress();
startNProgress()
try {
let mediaid = props.media?.tmdb_id
const mediaid = props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`;
: `douban:${props.media?.douban_id}`
const result: { [key: string]: any } = await api.delete(
`subscribe/media/${mediaid}`,
{
params: {
season: props.media?.season,
},
}
);
},
)
if (result.success) {
isSubscribed.value = false;
$toast.success(`${props.media?.title} 已取消订阅!`);
} else {
$toast.error(`${props.media?.title} 取消订阅失败:${result.message}`);
isSubscribed.value = false
$toast.success(`${props.media?.title} 已取消订阅!`)
}
else {
$toast.error(`${props.media?.title} 取消订阅失败:${result.message}`)
}
} catch (error) {
console.error(error);
}
doneNProgress();
};
catch (error) {
console.error(error)
}
doneNProgress()
}
// 查询当前媒体是否已订阅
const handleCheckSubscribe = async () => {
async function handleCheckSubscribe() {
try {
const result = await checkSubscribe(props.media?.season);
if (result) {
isSubscribed.value = true;
}
} catch (error) {
console.error(error);
const result = await checkSubscribe(props.media?.season)
if (result)
isSubscribed.value = true
}
};
catch (error) {
console.error(error)
}
}
// 查询当前媒体是否已存在
const handleCheckExists = async () => {
async function handleCheckExists() {
try {
const result: {[key:string]: any} = await api.get(`media/exists`, {
const result: { [key: string]: any } = await api.get('media/exists', {
params: {
tmdbid: props.media?.tmdb_id,
title: props.media?.title,
@@ -211,143 +227,145 @@ const handleCheckExists = async () => {
season: props.media?.season,
mtype: props.media?.type,
},
});
if (result.success) {
isExists.value = true;
}
} catch (error) {
console.error(error);
})
if (result.success)
isExists.value = true
}
};
catch (error) {
console.error(error)
}
}
// 调用API检查是否已订阅,电视剧需要指定季
const checkSubscribe = async (season: number = 0) => {
async function checkSubscribe(season = 0) {
try {
let mediaid = props.media?.tmdb_id
const mediaid = props.media?.tmdb_id
? `tmdb:${props.media?.tmdb_id}`
: `douban:${props.media?.douban_id}`;
: `douban:${props.media?.douban_id}`
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
params: {
season: season,
season,
},
});
return result.id || null;
} catch (error) {
console.error(error);
})
return result.id || null
}
return null;
};
catch (error) {
console.error(error)
}
return null
}
// 检查所有季的缺失状态
const checkSeasonsNotExists = async () => {
async function checkSeasonsNotExists() {
// 开始处理
startNProgress();
startNProgress()
try {
const result: NotExistMediaInfo[] = await api.post(`download/notexists`, props.media);
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
if (result) {
result.forEach((item) => {
// 0-已存在 1-部分缺失 2-全部缺失
let state = 0;
if (item.episodes.length == 0) {
state = 2;
} else if (item.episodes.length < item.total_episodes) {
state = 1;
}
seasonsNotExisted.value[item.season] = state;
});
let state = 0
if (item.episodes.length === 0)
state = 2
else if (item.episodes.length < item.total_episodes)
state = 1
seasonsNotExisted.value[item.season] = state
})
}
} catch (error) {
$toast.error(`${props.media?.title}无法识别TMDB媒体信息!`);
tmdbFlag.value = false;
}
catch (error) {
$toast.error(`${props.media?.title}无法识别TMDB媒体信息!`)
tmdbFlag.value = false
}
// 处理完成
doneNProgress();
};
doneNProgress()
}
// 检查电影是否存在
const checkMovieExists = async () => {
async function checkMovieExists() {
try {
const result: NotExistMediaInfo[] = await api.post(`download/notexists`, props.media);
if (!result || result.length === 0) {
// 没有缺失的就是存在
return true;
}
return false;
} catch (error) {
console.error(error);
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
return !result || result.length === 0
}
};
catch (error) {
console.error(error)
}
}
// 查询TMDB的所有季信息
const getMediaSeasons = async () => {
async function getMediaSeasons() {
try {
seasonInfos.value = await api.get(`tmdb/${props.media?.tmdb_id}/seasons`);
} catch (error) {
console.error(error);
seasonInfos.value = await api.get(`tmdb/${props.media?.tmdb_id}/seasons`)
}
};
catch (error) {
console.error(error)
}
}
// 爱心订阅按钮响应
const handleSubscribe = () => {
if (isSubscribed.value) {
removeSubscribe();
} else {
handleAddSubscribe();
}
};
function handleSubscribe() {
if (isSubscribed.value)
removeSubscribe()
else
handleAddSubscribe()
}
// 拼装详情页链接
const getDetailLink = () => {
let link = "";
if (props.media?.douban_id) {
link = `https://movie.douban.com/subject/${props.media?.douban_id}/`;
} else if (props.media?.type === "电影") {
link = `https://www.themoviedb.org/movie/${props.media?.tmdb_id}`;
} else if (props.media?.type === "电视剧") {
link = `https://www.themoviedb.org/tv/${props.media?.tmdb_id}`;
}
return link;
};
function getDetailLink() {
let link = ''
if (props.media?.douban_id)
link = `https://movie.douban.com/subject/${props.media?.douban_id}/`
else if (props.media?.type === '电影')
link = `https://www.themoviedb.org/movie/${props.media?.tmdb_id}`
else if (props.media?.type === '电视剧')
link = `https://www.themoviedb.org/tv/${props.media?.tmdb_id}`
return link
}
// 计算存在状态的颜色
const getExistColor = (season: number) => {
let state = seasonsNotExisted.value[season];
if (!state) {
return "success";
}
if (state === 1) {
return "warning";
} else if (state === 2) {
return "error";
} else {
return "success";
}
};
function getExistColor(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return 'success'
if (state === 1)
return 'warning'
else if (state === 2)
return 'error'
else
return 'success'
}
// 计算存在状态的文本
const getExistText = (season: number) => {
let state = seasonsNotExisted.value[season];
if (!state) {
return "已存在";
}
if (state === 1) {
return "部分缺失";
} else if (state === 2) {
return "缺失";
} else {
return "已存在";
}
};
function getExistText(season: number) {
const state = seasonsNotExisted.value[season]
if (!state)
return '已存在'
if (state === 1)
return '部分缺失'
else if (state === 2)
return '缺失'
else
return '已存在'
}
// 打开详情页
const openDetailWindow = () => {
window.open(getDetailLink(), "_blank");
};
function openDetailWindow() {
window.open(getDetailLink(), '_blank')
}
// 开始搜索
const handleSearch = () => {
function handleSearch() {
router.push({
path: "/resource",
path: '/resource',
query: {
keyword: `${
props.media?.tmdb_id
@@ -356,34 +374,31 @@ const handleSearch = () => {
}`,
type: props.media?.type,
},
});
};
})
}
// 装载时检查是否已订阅
onBeforeMount(() => {
handleCheckSubscribe();
handleCheckExists();
});
handleCheckSubscribe()
handleCheckExists()
})
// 订阅季表头
const seasonsHeaders = [
{ title: "季", key: "title", sortable: false },
{ title: "集数", key: "episodes", sortable: false },
{ title: "评分", key: "vote", sortable: false },
{ title: "状态", key: "status", sortable: false },
];
// 选中的订阅季
const seasonsSelected = ref<TmdbSeason[]>([]);
{ title: '季', key: 'title', sortable: false },
{ title: '集数', key: 'episodes', sortable: false },
{ title: '评分', key: 'vote', sortable: false },
{ title: '状态', key: 'status', sortable: false },
]
// 计算图片地址
const getImgUrl = (url: string) => {
function getImgUrl(url: string) {
// 如果地址中包含douban则使用中转代理
if (url.includes("doubanio.com")) {
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`;
}
return url;
};
if (url.includes('doubanio.com'))
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`
return url
}
</script>
<template>
@@ -404,13 +419,13 @@ const getImgUrl = (url: string) => {
:src="getImgUrl(props.media?.poster_path || '')"
class="object-cover aspect-w-2 aspect-h-3"
:class="hover.isHovering ? 'on-hover' : ''"
cover
@load="isImageLoaded = true"
@error="imageLoadError = true"
cover
>
<template #placeholder>
<div class="relative animate-pulse bg-gray-300 w-full">
<div class="w-full h-full"></div>
<div class="w-full h-full" />
</div>
</template>
<!-- 类型角标 -->
@@ -423,12 +438,12 @@ const getImgUrl = (url: string) => {
{{ props.media?.type }}
</VChip>
<!-- 本地存在标识 -->
<ExistIcon v-if="isExists"/>
<ExistIcon v-if="isExists" />
<!-- 评分角标 -->
<VChip
v-if="props.media?.vote_average && !isExists"
variant="elevated"
size="small"
v-if="props.media?.vote_average && !isExists"
:class="getChipColor('rating')"
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
>
@@ -436,21 +451,23 @@ const getImgUrl = (url: string) => {
</VChip>
<!-- 详情 -->
<VCardText
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
v-show="hover.isHovering || imageLoadError"
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
@click.stop="openDetailWindow"
>
<span class="font-bold">{{ props.media?.year }}</span>
<h1
class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ..."
>
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
{{ props.media?.title }}
</h1>
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
{{ props.media?.overview }}
</p>
<div class="flex align-center justify-between">
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
<IconBtn
icon="mdi-magnify"
color="white"
@click.stop="handleSearch"
/>
<IconBtn
icon="mdi-heart"
:color="isSubscribed ? 'error' : 'white'"
@@ -484,39 +501,53 @@ const getImgUrl = (url: string) => {
height="auto"
>
<template #item.title="{ item }">
<span class="d-block whitespace-nowrap"
> {{ item.raw.season_number }}
</span></template
>
<span class="d-block whitespace-nowrap"> {{ item.raw.season_number }}
</span>
</template>
<template #item.episodes="{ item }">
<VChip variant="outlined" size="small">{{ item.raw.episode_count }}</VChip>
<VChip
variant="outlined"
size="small"
>
{{ item.raw.episode_count }}
</VChip>
</template>
<template #item.vote="{ item }">
{{ item.raw.vote_average }}
</template>
<template #item.status="{ item }">
<VChip
:color="getExistColor(item.raw.season_number)"
v-if="seasonsNotExisted"
:color="getExistColor(item.raw.season_number)"
flat
size="small"
>{{ getExistText(item.raw.season_number) }}</VChip
>
{{ getExistText(item.raw.season_number) }}
</VChip>
</template>
<template #no-data> 没有数据 </template>
<template #bottom></template>
<template #no-data>
没有数据
</template>
<template #bottom />
</VDataTable>
</VCardText>
<VCardActions>
<VBtn @click="subscribeSeasonDialog = false"> 取消 </VBtn>
<VBtn @click="subscribeSeasonDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="subscribeSeasons" @keydown.enter="subscribeSeasons"> 确定 </VBtn>
<VBtn
@click="subscribeSeasons"
@keydown.enter="subscribeSeasons"
>
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>
<style type="scss">
<style lang="scss">
.on-hover img {
@apply brightness-50;
}
+46 -25
View File
@@ -1,47 +1,63 @@
<script lang="ts" setup>
import api from "@/api";
import { Plugin } from "@/api/types";
import { useToast } from "vue-toast-notification";
// 定义触发的自定义事件
const emit = defineEmits(["install"]);
// 提示框
const $toast = useToast();
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Plugin } from '@/api/types'
// 输入参数
const props = defineProps({
plugin: Object as PropType<Plugin>,
width: String,
height: String,
});
})
// 定义触发的自定义事件
const emit = defineEmits(['install'])
// 提示框
const $toast = useToast()
// 安装插件
const installPlugin = async () => {
async function installPlugin() {
try {
const result: { [key: string]: any } = await api.get(
`plugin/install/${props.plugin?.id}`
);
`plugin/install/${props.plugin?.id}`,
)
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`);
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
// 通知父组件刷新
emit("install");
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`);
emit('install')
}
else {
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`)
}
} catch (error) {
console.error(error);
}
};
catch (error) {
console.error(error)
}
}
</script>
<template>
<VCard :width="props.width" :height="props.height" @click="installPlugin">
<VCard
:width="props.width"
:height="props.height"
@click="installPlugin"
>
<div
class="relative pa-4 text-center card-cover-blurred"
:style="{ background: `${props.plugin?.plugin_color}` }"
>
<VAvatar size="128" class="shadow">
<VImg :src="`/plugin/${props.plugin?.plugin_icon}`" aspect-ratio="4/3" cover />
<VAvatar
size="128"
class="shadow"
>
<VImg
:src="`/plugin/${props.plugin?.plugin_icon}`"
aspect-ratio="4/3"
cover
/>
</VAvatar>
</div>
<VCardTitle>{{ props.plugin?.plugin_name }}</VCardTitle>
@@ -50,13 +66,18 @@ const installPlugin = async () => {
{{ props.plugin?.plugin_desc }}
</VCardText>
<VCardText>
作者<a :href="props.plugin?.author_url" target="_blank" @click.stop>
作者<a
:href="props.plugin?.author_url"
target="_blank"
@click.stop
>
{{ props.plugin?.plugin_author }}
</a>
</VCardText>
</VCard>
</template>
<style type="scss" scoped>
<style lang="scss" scoped>
.card-cover-blurred::before {
position: absolute;
/* stylelint-disable-next-line property-no-vendor-prefix */
+48 -33
View File
@@ -1,62 +1,66 @@
<script lang="ts" setup>
import api from "@/api";
import { Plugin } from "@/api/types";
import { useToast } from "vue-toast-notification";
// 定义触发的自定义事件
const emit = defineEmits(["remove"]);
// 提示框
const $toast = useToast();
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Plugin } from '@/api/types'
// 输入参数
const props = defineProps({
plugin: Object as PropType<Plugin>,
width: String,
height: String,
});
})
// 定义触发的自定义事件
const emit = defineEmits(['remove'])
// 提示框
const $toast = useToast()
// 本身是否可见
const isVisible = ref(true);
const isVisible = ref(true)
// 卸载插件
const uninstallPlugin = async () => {
async function uninstallPlugin() {
try {
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`);
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
if (result.success) {
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`);
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
// 通知父组件刷新
emit("remove");
} else {
$toast.error(`插件 ${props.plugin?.plugin_name} 卸载失败:${result.message}}`);
emit('remove')
}
else {
$toast.error(`插件 ${props.plugin?.plugin_name} 卸载失败:${result.message}}`)
}
} catch (error) {
console.error(error);
}
};
catch (error) {
console.error(error)
}
}
// 显示插件详情
const showPluginInfo = () => {};
function showPluginInfo() {}
// 弹出菜单
const dropdownItems = ref([
{
title: "卸载",
title: '卸载',
value: 1,
props: {
prependIcon: "mdi-trash-can-outline",
color: "error",
prependIcon: 'mdi-trash-can-outline',
color: 'error',
click: uninstallPlugin,
},
},
]);
])
</script>
<template>
<VCard
v-if="isVisible"
:width="props.width"
:height="props.height"
@click="showPluginInfo"
v-if="isVisible"
>
<div
class="relative pa-4 text-center card-cover-blurred"
@@ -65,26 +69,36 @@ const dropdownItems = ref([
<div class="me-n3 absolute top-0 right-3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="item.props.color"
:key="i"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon"></VIcon>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title"></VListItemTitle>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
<VAvatar size="128" class="shadow">
<VImg :src="`/plugin/${props.plugin?.plugin_icon}`" aspect-ratio="4/3" cover />
<VAvatar
size="128"
class="shadow"
>
<VImg
:src="`/plugin/${props.plugin?.plugin_icon}`"
aspect-ratio="4/3"
cover
/>
</VAvatar>
</div>
<VCardItem class="py-2">
@@ -95,7 +109,8 @@ const dropdownItems = ref([
</VCardText>
</VCard>
</template>
<style type="scss" scoped>
<style lang="scss" scoped>
.card-cover-blurred::before {
position: absolute;
/* stylelint-disable-next-line property-no-vendor-prefix */
+243 -123
View File
@@ -1,183 +1,207 @@
<script lang="ts" setup>
import { numberValidator, requiredValidator } from "@/@validators";
import api from "@/api";
import { Site } from "@/api/types";
import ExistIcon from "@core/components/ExistIcon.vue";
import { useToast } from "vue-toast-notification";
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
import type { Site } from '@/api/types'
import ExistIcon from '@core/components/ExistIcon.vue'
// 输入参数
const props = defineProps({
site: Object as PropType<Site>,
width: String,
height: String,
});
})
// 密码输入
const isPasswordVisible = ref(false);
const isPasswordVisible = ref(false)
// 图标
const siteIcon = ref<string>("");
const siteIcon = ref<string>('')
// 提示框
const $toast = useToast();
const $toast = useToast()
// 测试按钮文字
const testButtonText = ref("测试");
const testButtonText = ref('测试')
// 测试按钮可用性
const testButtonDisable = ref(false);
const testButtonDisable = ref(false)
// 更新按钮文字
const updateButtonText = ref("更新");
const updateButtonText = ref('更新')
// 更新按钮可用性
const updateButtonDisable = ref(false);
const updateButtonDisable = ref(false)
// 更新站点Cookie UA弹窗
const siteCookieDialog = ref(false);
const siteCookieDialog = ref(false)
// 站点编辑弹窗
const siteInfoDialog = ref(false);
const siteInfoDialog = ref(false)
// 用户名密码表单
const userPwForm = ref({
username: "",
password: "",
});
username: '',
password: '',
})
// 查询站点图标
const getSiteIcon = async () => {
async function getSiteIcon() {
try {
siteIcon.value = (await api.get("site/icon/" + props.site?.id)).data.icon;
} catch (error) {
console.error(error);
siteIcon.value = (await api.get(`site/icon/${props.site?.id}`)).data.icon
}
};
catch (error) {
console.error(error)
}
}
// 测试站点连通性
const testSite = async () => {
async function testSite() {
try {
testButtonText.value = "测试中 ...";
testButtonDisable.value = true;
const result: { [key: string]: any } = await api.get("site/test/" + props.site?.id);
if (result.success) {
$toast.success(`${props.site?.name} 连通性测试成功,可正常使用!`);
} else {
$toast.error(`${props.site?.name} 连通性测试失败:${result.message}`);
}
testButtonText.value = "测试";
testButtonDisable.value = false;
} catch (error) {
console.error(error);
testButtonText.value = '测试中 ...'
testButtonDisable.value = true
const result: { [key: string]: any } = await api.get(`site/test/${props.site?.id}`)
if (result.success)
$toast.success(`${props.site?.name} 连通性测试成功,可正常使用!`)
else
$toast.error(`${props.site?.name} 连通性测试失败:${result.message}`)
testButtonText.value = '测试'
testButtonDisable.value = false
}
};
catch (error) {
console.error(error)
}
}
// 打开更新站点Cookie UA弹窗
const handleSiteUpdate = async () => {
siteCookieDialog.value = true;
};
async function handleSiteUpdate() {
siteCookieDialog.value = true
}
// 打开站点编辑弹窗
const handleSiteInfo = async () => {
siteInfoDialog.value = true;
};
async function handleSiteInfo() {
siteInfoDialog.value = true
}
// 调用API,更新站点Cookie UA
const updateSiteCookie = async () => {
async function updateSiteCookie() {
try {
if (!userPwForm.value.username || !userPwForm.value.password) {
return;
}
if (!userPwForm.value.username || !userPwForm.value.password)
return
// 更新按钮状态
siteCookieDialog.value = false;
updateButtonText.value = "更新中 ...";
updateButtonDisable.value = true;
siteCookieDialog.value = false
updateButtonText.value = '更新中 ...'
updateButtonDisable.value = true
const result: { [key: string]: any } = await api.get(
"site/cookie/" + props.site?.id,
`site/cookie/${props.site?.id}`,
{
params: {
username: userPwForm.value.username,
password: userPwForm.value.password,
},
}
);
if (result.success) {
$toast.success(`${props.site?.name} 更新Cookie & UA 成功!`);
} else {
$toast.error(`${props.site?.name} 更新失败:${result.message}`);
}
updateButtonText.value = "更新";
updateButtonDisable.value = false;
} catch (error) {
console.error(error);
}
};
},
)
// 调用API更新站点信息
const updateSiteInfo = async () => {
try {
// 更新按钮状态
siteInfoDialog.value = false;
if (result.success)
$toast.success(`${props.site?.name} 更新Cookie & UA 成功!`)
else
$toast.error(`${props.site?.name} 更新失败:${result.message}`)
const result: { [key: string]: any } = await api.put("site", siteForm);
if (result.success) {
$toast.success(`${props.site?.name} 更新成功!`);
} else {
$toast.error(`${props.site?.name} 更新失败:${result.message}`);
}
} catch (error) {
$toast.error(`${props.site?.name} 更新失败!`);
console.error(error);
updateButtonText.value = '更新'
updateButtonDisable.value = false
}
};
catch (error) {
console.error(error)
}
}
// 站点编辑表单数据
const siteForm = reactive({
// ID
id: props.site?.id,
// 站点名称
name: props.site?.name,
// 站点主域名Key
domain: props.site?.domain,
// 站点地址
url: props.site?.url,
// 站点优先级
pri: props.site?.pri,
// RSS地址
rss: props.site?.rss,
// Cookie
cookie: props.site?.cookie,
// User-Agent
ua: props.site?.ua,
// 是否使用代理
proxy: props.site?.proxy ? true : false,
proxy: !!props.site?.proxy,
// 过滤规则
filter: props.site?.filter,
// 是否演染
render: props.site?.render ? true : false,
render: !!props.site?.render,
// 是否公开站点
public: props.site?.public,
// 备注
note: props.site?.note,
// 流控单位周期
limit_interval: props.site?.limit_interval,
// 流控次数
limit_count: props.site?.limit_count,
// 流控间隔
limit_seconds: props.site?.limit_seconds,
// 是否启用
is_active: props.site?.is_active,
});
})
// 调用API更新站点信息
async function updateSiteInfo() {
try {
// 更新按钮状态
siteInfoDialog.value = false
const result: { [key: string]: any } = await api.put('site', siteForm)
if (result.success)
$toast.success(`${props.site?.name} 更新成功!`)
else
$toast.error(`${props.site?.name} 更新失败:${result.message}`)
}
catch (error) {
$toast.error(`${props.site?.name} 更新失败!`)
console.error(error)
}
}
// 状态下拉项
const statusItems = [
{ title: "启用", value: true },
{ title: "停用", value: false },
];
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 装载时查询站点图标
onMounted(() => {
getSiteIcon();
});
getSiteIcon()
})
</script>
<template>
@@ -189,37 +213,70 @@ onMounted(() => {
@click="siteInfoDialog = true"
>
<template #image>
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
<VAvatar
class="absolute right-2 bottom-2 rounded"
variant="flat"
rounded="0"
>
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VCardItem>
<VCardTitle class="font-bold">{{ props.site?.name }}</VCardTitle>
<VCardTitle class="font-bold">
{{ props.site?.name }}
</VCardTitle>
<VCardSubtitle>{{ props.site?.url }}</VCardSubtitle>
</VCardItem>
<ExistIcon v-if="siteForm.is_active" />
<VCardText class="py-2">
<VTooltip text="浏览器仿真" v-if="siteForm.render">
<VTooltip
v-if="siteForm.render"
text="浏览器仿真"
>
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-apple-safari"
/>
</template>
</VTooltip>
<VTooltip text="代理" v-if="siteForm.proxy">
<VTooltip
v-if="siteForm.proxy"
text="代理"
>
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-network-outline"
/>
</template>
</VTooltip>
<VTooltip text="流控" v-if="siteForm.limit_interval">
<VTooltip
v-if="siteForm.limit_interval"
text="流控"
>
<template #activator="{ props }">
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
<VIcon
color="primary"
class="me-2"
v-bind="props"
icon="mdi-speedometer"
/>
</template>
</VTooltip>
<VTooltip text="过滤" v-if="siteForm.filter">
<VTooltip
v-if="siteForm.filter"
text="过滤"
>
<template #activator="{ props }">
<VIcon
color="primary"
@@ -233,44 +290,56 @@ onMounted(() => {
<VCardActions>
<VBtn
@click.stop="handleSiteUpdate"
:disabled="updateButtonDisable"
v-if="!props.site?.public"
:disabled="updateButtonDisable"
@click.stop="handleSiteUpdate"
>
<template #prepend>
<VIcon icon="mdi-refresh"></VIcon>
<VIcon icon="mdi-refresh" />
</template>
{{ updateButtonText }}
</VBtn>
<VBtn @click.stop="handleSiteInfo">
<template #prepend>
<VIcon icon="mdi-square-edit-outline"></VIcon>
<VIcon icon="mdi-square-edit-outline" />
</template>
编辑
</VBtn>
<VBtn @click.stop="testSite" :disabled="testButtonDisable">
<VBtn
:disabled="testButtonDisable"
@click.stop="testSite"
>
<template #prepend>
<VIcon icon="mdi-network-outline"></VIcon>
<VIcon icon="mdi-network-outline" />
</template>
{{ testButtonText }}
</VBtn>
</VCardActions>
</VCard>
<!-- 更新站点Cookie & UA弹窗 -->
<VDialog v-model="siteCookieDialog" max-width="600">
<VDialog
v-model="siteCookieDialog"
max-width="600"
>
<!-- Dialog Content -->
<VCard title="更新站点Cookie & UA">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userPwForm.username"
label="用户名"
:rules="[requiredValidator]"
/>
</VCol>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<VTextField
v-model="userPwForm.password"
label="密码"
@@ -278,8 +347,8 @@ onMounted(() => {
:append-inner-icon="
isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'
"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
:rules="[requiredValidator]"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@keydown.enter="updateSiteCookie"
/>
</VCol>
@@ -289,25 +358,38 @@ onMounted(() => {
<VCardActions>
<VSpacer />
<VBtn @click="updateSiteCookie"> 开始更新 </VBtn>
<VBtn @click="updateSiteCookie">
开始更新
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 站点编辑弹窗 -->
<VDialog v-model="siteInfoDialog" max-width="1000" persistent scrollable>
<VDialog
v-model="siteInfoDialog"
max-width="1000"
persistent
scrollable
>
<!-- Dialog Content -->
<VCard :title="`编辑站点 - ${props.site?.name}`">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol cols="12" md="3">
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
@@ -315,34 +397,56 @@ onMounted(() => {
:rules="[requiredValidator]"
/>
</VCol>
<VCol cols="12" md="3">
<VSelect v-model="siteForm.is_active" :items="statusItems" label="状态" />
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextarea v-model="siteForm.cookie" label="站点Cookie" />
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField v-model="siteForm.ua" label="站点User-Agent" />
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="4">
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol cols="12" md="4">
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol cols="12" md="4">
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
@@ -351,20 +455,36 @@ onMounted(() => {
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch v-model="siteForm.proxy" label="代理" />
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch v-model="siteForm.render" label="仿真" />
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="siteInfoDialog = false"> 取消 </VBtn>
<VBtn @click="siteInfoDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="updateSiteInfo"> 确定 </VBtn>
<VBtn @click="updateSiteInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
+214 -147
View File
@@ -1,209 +1,225 @@
<script lang="ts" setup>
import { calculateTimeDifference } from "@/@core/utils";
import { formatSeason } from "@/@core/utils/formatters";
import { numberValidator } from "@/@validators";
import api from "@/api";
import { Site, Subscribe } from "@/api/types";
import { useToast } from "vue-toast-notification";
import { useToast } from 'vue-toast-notification'
import { calculateTimeDifference } from '@/@core/utils'
import { formatSeason } from '@/@core/utils/formatters'
import { numberValidator } from '@/@validators'
import api from '@/api'
import type { Site, Subscribe } from '@/api/types'
// 输入参数
const props = defineProps({
media: Object as PropType<Subscribe>,
});
})
// 提示框
const $toast = useToast();
const $toast = useToast()
// 是否显示卡片
const cardState = ref(true);
const cardState = ref(true)
// 图片是否加载完成
const imageLoaded = ref(false);
const imageLoaded = ref(false)
// 订阅弹窗
const subscribeInfoDialog = ref(false);
const subscribeInfoDialog = ref(false)
// 站点数据列表
const siteList = ref<Site[]>([]);
const siteList = ref<Site[]>([])
// 站点选择下载框
const selectSitesOptions = ref<{ [key: number]: string }[]>([]);
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
// 订阅编辑表单
const subscribeForm = reactive({
id: props.media?.id,
// 搜索关键字
keyword: props.media?.keyword,
// 过滤规则
filter: props.media?.filter,
// 包含
include: props.media?.include,
// 排除
exclude: props.media?.exclude,
// 总集数
total_episode: props.media?.total_episode,
// 开始集数
start_episode: props.media?.start_episode,
// 订阅站点
sites: props.media?.sites,
})
// 上一次更新时间
const lastUpdateText = ref(
`${
props.media?.last_update
? `${calculateTimeDifference(props.media?.last_update || "")}`
: ""
}`
);
? `${calculateTimeDifference(props.media?.last_update || '')}`
: ''
}`,
)
// 图片加载完成响应
const imageLoadHandler = () => {
imageLoaded.value = true;
};
function imageLoadHandler() {
imageLoaded.value = true
}
// 根据 type 返回不同的图标
const getIcon = () => {
if (props.media?.type === "电影") {
return "mdi-movie";
} else if (props.media?.type === "电视剧") {
return "mdi-television-classic";
} else {
return "mdi-help-circle";
}
};
function getIcon() {
if (props.media?.type === '电影')
return 'mdi-movie'
else if (props.media?.type === '电视剧')
return 'mdi-television-classic'
else
return 'mdi-help-circle'
}
// 计算百分比
const getPercentage = () => {
if (props.media?.total_episode === 0) {
return 0;
}
function getPercentage() {
if (props.media?.total_episode === 0)
return 0
return Math.round(
(((props.media?.total_episode || 0) - (props.media?.lack_episode || 0)) /
(props.media?.total_episode || 1)) *
100
);
};
(((props.media?.total_episode || 0) - (props.media?.lack_episode || 0))
/ (props.media?.total_episode || 1))
* 100,
)
}
// 计算文本颜色
const getTextColor = () => {
return imageLoaded.value ? "white" : "";
};
function getTextColor() {
return imageLoaded.value ? 'white' : ''
}
// 计算文本类
const getTextClass = () => {
return imageLoaded.value ? "text-white" : "";
};
function getTextClass() {
return imageLoaded.value ? 'text-white' : ''
}
// 删除订阅
const removeSubscribe = async () => {
async function removeSubscribe() {
try {
const result: { [key: string]: any } = await api.delete(
`subscribe/${props.media?.id}`
);
if (result.success) {
cardState.value = false;
}
} catch (e) {
console.log(e);
`subscribe/${props.media?.id}`,
)
if (result.success)
cardState.value = false
}
};
catch (e) {
console.log(e)
}
}
// 搜索订阅
const searchSubscribe = async () => {
async function searchSubscribe() {
try {
const result: { [key: string]: any } = await api.get(
`subscribe/search/${props.media?.id}`
);
`subscribe/search/${props.media?.id}`,
)
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 提交搜索请求成功!`);
}
} catch (e) {
console.log(e);
if (result.success)
$toast.success(`${props.media?.name} 提交搜索请求成功!`)
}
};
catch (e) {
console.log(e)
}
}
// 调用API修改订阅
const updateSubscribeInfo = async () => {
subscribeInfoDialog.value = false;
async function updateSubscribeInfo() {
subscribeInfoDialog.value = false
try {
const result: { [key: string]: any } = await api.put(`subscribe`, subscribeForm);
const result: { [key: string]: any } = await api.put('subscribe', subscribeForm)
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 更新成功!`);
} else {
$toast.error(`${props.media?.name} 更新失败:${result.message}`);
}
} catch (e) {
console.log(e);
if (result.success)
$toast.success(`${props.media?.name} 更新成功!`)
else
$toast.error(`${props.media?.name} 更新失败:${result.message}`)
}
};
catch (e) {
console.log(e)
}
}
// 获取站点列表数据
const loadSites = async () => {
async function loadSites() {
try {
const data: Site[] = await api.get("site");
const data: Site[] = await api.get('site')
// 过滤站点,只有启用的站点才显示
siteList.value = data.filter((item) => item.is_active);
} catch (error) {
console.error(error);
siteList.value = data.filter(item => item.is_active)
}
};
catch (error) {
console.error(error)
}
}
// 获取站点列表选择框数据
const getSiteList = async () => {
async function getSiteList() {
// 加载订阅站点列表
if (!siteList.value.length) {
await loadSites();
}
if (!siteList.value.length)
await loadSites()
const maps = siteList.value.map((item) => {
return {
title: item.name,
value: item.id,
};
});
selectSitesOptions.value = maps.flat();
};
}
})
selectSitesOptions.value = maps.flat()
}
// 编辑订阅响应
const editSubscribeDialog = async () => {
await getSiteList();
subscribeInfoDialog.value = true;
};
async function editSubscribeDialog() {
await getSiteList()
subscribeInfoDialog.value = true
}
// 弹出菜单
const dropdownItems = ref([
{
title: "编辑",
title: '编辑',
value: 1,
props: {
prependIcon: "mdi-file-edit-outline",
prependIcon: 'mdi-file-edit-outline',
click: editSubscribeDialog,
},
},
{
title: "搜索",
title: '搜索',
value: 2,
props: {
prependIcon: "mdi-magnify",
prependIcon: 'mdi-magnify',
click: searchSubscribe,
},
},
{
title: "取消订阅",
title: '取消订阅',
value: 3,
props: {
prependIcon: "mdi-trash-can-outline",
color: "error",
prependIcon: 'mdi-trash-can-outline',
color: 'error',
click: removeSubscribe,
},
},
]);
// 订阅编辑表单
const subscribeForm = reactive({
id: props.media?.id,
// 搜索关键字
keyword: props.media?.keyword,
// 过滤规则
filter: props.media?.filter,
// 包含
include: props.media?.include,
// 排除
exclude: props.media?.exclude,
// 总集数
total_episode: props.media?.total_episode,
// 开始集数
start_episode: props.media?.start_episode,
// 订阅站点
sites: props.media?.sites,
});
])
</script>
<template>
<VCard :key="props.media?.id" v-if="cardState" @click="editSubscribeDialog">
<VCard
v-if="cardState"
:key="props.media?.id"
@click="editSubscribeDialog"
>
<template #image>
<VImg
:src="props.media?.backdrop || props.media?.poster"
@@ -215,7 +231,11 @@ const subscribeForm = reactive({
</template>
<VCardItem>
<template #prepend>
<VIcon size="1.9rem" :color="getTextColor()" :icon="getIcon()" />
<VIcon
size="1.9rem"
:color="getTextColor()"
:icon="getIcon()"
/>
</template>
<VCardTitle :class="getTextClass()">
{{ props.media?.name }}
@@ -224,20 +244,26 @@ const subscribeForm = reactive({
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" :color="getTextColor()" />
<VMenu activator="parent" close-on-content-click>
<VIcon
icon="mdi-dots-vertical"
:color="getTextColor()"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="item.props.color"
:key="i"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon"></VIcon>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title"></VListItemTitle>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
@@ -247,51 +273,63 @@ const subscribeForm = reactive({
</VCardItem>
<VCardText>
<p class="clamp-text mb-0" :class="getTextClass()">
<p
class="clamp-text mb-0"
:class="getTextClass()"
>
{{ props.media?.description }}
</p>
</VCardText>
<VCardText class="d-flex justify-space-between align-center flex-wrap">
<div class="d-flex align-center">
<IconBtn icon="mdi-star" :color="getTextColor()" class="me-1" />
<span class="text-subtitle-2 me-4" :class="getTextClass()">{{
<IconBtn
icon="mdi-star"
:color="getTextColor()"
class="me-1"
/>
<span
class="text-subtitle-2 me-4"
:class="getTextClass()"
>{{
props.media?.vote
}}</span>
<IconBtn
v-if="props.media?.total_episode"
v-bind="props"
icon="mdi-progress-clock"
:color="getTextColor()"
class="me-1"
v-if="props.media?.total_episode"
/>
<span
v-if="props.media?.season"
class="text-subtitle-2 me-4"
:class="getTextClass()"
v-if="props.media?.season"
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}</span
>
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}</span>
<IconBtn
v-if="props.media?.username"
icon="mdi-account"
:color="getTextColor()"
class="me-1"
v-if="props.media?.username"
/>
<span
v-if="props.media?.username"
class="text-subtitle-2 me-4"
:class="getTextClass()"
v-if="props.media?.username"
>
{{ props.media?.username }}
</span>
</div>
</VCardText>
<VCardText
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
v-if="lastUpdateText"
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
>
<VIcon icon="mdi-download" class="me-1" /> {{ lastUpdateText }}
<VIcon
icon="mdi-download"
class="me-1"
/> {{ lastUpdateText }}
</VCardText>
<VProgressLinear
v-if="getPercentage() > 0"
@@ -301,23 +339,42 @@ const subscribeForm = reactive({
/>
</VCard>
<!-- 订阅编辑弹窗 -->
<VDialog v-model="subscribeInfoDialog" max-width="1000" persistent scrollable>
<VDialog
v-model="subscribeInfoDialog"
max-width="1000"
persistent
scrollable
>
<!-- Dialog Content -->
<VCard :title="`编辑订阅 - ${props.media?.name}`">
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="6">
<VTextField v-model="subscribeForm.keyword" label="搜索关键词" />
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.keyword"
label="搜索关键词"
/>
</VCol>
<VCol cols="12" md="3" v-if="props.media?.type == '电视剧'">
<VCol
v-if="props.media?.type === '电视剧'"
cols="12"
md="3"
>
<VTextField
v-model="subscribeForm.total_episode"
label="总集数"
:rules="[numberValidator]"
/>
</VCol>
<VCol cols="12" md="3" v-if="props.media?.type == '电视剧'">
<VCol
v-if="props.media?.type === '电视剧'"
cols="12"
md="3"
>
<VTextField
v-model="subscribeForm.start_episode"
label="开始集数"
@@ -326,13 +383,19 @@ const subscribeForm = reactive({
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.include"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VCol
cols="12"
md="6"
>
<VTextField
v-model="subscribeForm.exclude"
label="排除(关键字、正则式)"
@@ -347,16 +410,20 @@ const subscribeForm = reactive({
chips
label="订阅站点"
multiple
></VSelect>
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn @click="subscribeInfoDialog = false"> 取消 </VBtn>
<VBtn @click="subscribeInfoDialog = false">
取消
</VBtn>
<VSpacer />
<VBtn @click="updateSubscribeInfo"> 确定 </VBtn>
<VBtn @click="updateSubscribeInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
+122 -93
View File
@@ -1,10 +1,11 @@
<script lang="ts" setup>
import { formatFileSize } from "@/@core/utils/formatters";
import api from "@/api";
import { doneNProgress, startNProgress } from "@/api/nprogress";
import { Context } from "@/api/types";
import { useToast } from "vue-toast-notification";
import { useConfirm } from "vuetify-use-dialog";
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { Context } from '@/api/types'
// 输入参数
const props = defineProps({
@@ -12,135 +13,152 @@ const props = defineProps({
more: Array as PropType<Context[]>,
width: String,
height: String,
});
})
// 提示框
const $toast = useToast();
const $toast = useToast()
// 确认框
const createConfirm = useConfirm();
const createConfirm = useConfirm()
// 更多来源界面
const showMoreTorrents = ref(false);
const showMoreTorrents = ref(false)
// 种子信息
const torrent = ref(props.torrent?.torrent_info);
const torrent = ref(props.torrent?.torrent_info)
// 媒体信息
const media = ref(props.torrent?.media_info);
const media = ref(props.torrent?.media_info)
// 识别元数据
const meta = ref(props.torrent?.meta_info);
const meta = ref(props.torrent?.meta_info)
// 站点图标
const siteIcon = ref("");
const siteIcon = ref('')
// 查询站点图标
const getSiteIcon = async () => {
async function getSiteIcon() {
try {
siteIcon.value = (await api.get("site/icon/" + torrent?.value?.site)).data.icon;
} catch (error) {
console.error(error);
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
}
};
catch (error) {
console.error(error)
}
}
// 询问并添加下载
const handleAddDownload = async (
_site: any = undefined,
async function handleAddDownload(_site: any = undefined,
_media: any = undefined,
_torrent: any = undefined
) => {
_torrent: any = undefined) {
if (!_media || !_torrent || !_site) {
_site = torrent.value?.site_name;
_media = media.value;
_torrent = torrent.value;
_site = torrent.value?.site_name
_media = media.value
_torrent = torrent.value
}
const isConfirmed = await createConfirm({
title: "确认",
title: '确认',
content: `是否确认下载【${_site}${_torrent?.title} ?`,
confirmationText: "确认",
cancellationText: "取消",
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: 600,
},
});
})
if (!isConfirmed) return;
if (!isConfirmed)
return
addDownload(_media, _torrent);
};
addDownload(_media, _torrent)
}
// 添加下载
const addDownload = async (_media: any, _torrent: any) => {
startNProgress();
async function addDownload(_media: any, _torrent: any) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post("download", {
const result: { [key: string]: any } = await api.post('download', {
media_in: _media,
torrent_in: _torrent,
});
})
if (result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`);
} else {
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
}
else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`);
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
}
} catch (error) {
console.error(error);
}
doneNProgress();
};
catch (error) {
console.error(error)
}
doneNProgress()
}
// 打开种子详情页面
const openTorrentDetail = () => {
window.open(torrent.value?.page_url, "_blank");
};
function openTorrentDetail() {
window.open(torrent.value?.page_url, '_blank')
}
// 下载种子文件
const downloadTorrentFile = async () => {
window.open(torrent.value?.enclosure, "_blank");
};
async function downloadTorrentFile() {
window.open(torrent.value?.enclosure, '_blank')
}
// 促销Chip类
const getVolumeFactorClass = (downloadVolume: number, uploadVolume: number) => {
if (downloadVolume === 0) {
return "text-white bg-lime-500";
} else if (downloadVolume < 1) {
return "text-white bg-green-500";
} else if (uploadVolume != 1) {
return "text-white bg-sky-500";
} else {
return "text-white bg-gray-500";
}
};
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0)
return 'text-white bg-lime-500'
else if (downloadVolume < 1)
return 'text-white bg-green-500'
else if (uploadVolume !== 1)
return 'text-white bg-sky-500'
else
return 'text-white bg-gray-500'
}
// 装载时查询站点图标
onMounted(() => {
getSiteIcon();
});
getSiteIcon()
})
// 弹出菜单
const dropdownItems = ref([
{
title: "查看详情",
title: '查看详情',
value: 1,
props: {
prependIcon: "mdi-information",
prependIcon: 'mdi-information',
click: openTorrentDetail,
},
},
{
title: "下载种子",
title: '下载种子',
value: 2,
props: {
prependIcon: "mdi-download",
prependIcon: 'mdi-download',
click: downloadTorrentFile,
},
},
]);
])
</script>
<template>
<VCard :width="props.width" :height="props.height" @click="handleAddDownload">
<template #image v-if="!showMoreTorrents">
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
<VCard
:width="props.width"
:height="props.height"
@click="handleAddDownload"
>
<template
v-if="!showMoreTorrents"
#image
>
<VAvatar
class="absolute right-2 bottom-2 rounded"
variant="flat"
rounded="0"
>
<VImg :src="siteIcon" />
</VAvatar>
</template>
@@ -153,19 +171,25 @@ const dropdownItems = ref([
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" color="white" />
<VMenu activator="parent" close-on-content-click>
<VIcon
icon="mdi-dots-vertical"
color="white"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
variant="plain"
:key="i"
variant="plain"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon"></VIcon>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title"></VListItemTitle>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
@@ -177,23 +201,28 @@ const dropdownItems = ref([
{{ torrent?.title }}
</VCardText>
<VCardText>{{ torrent?.description }}</VCardText>
<VCardItem class="pb-3 pt-0 pe-12" v-if="torrent?.labels">
<VCardItem
v-if="torrent?.labels"
class="pb-3 pt-0 pe-12"
>
<VChip
v-for="(label, index) in torrent?.labels"
:key="index"
variant="elevated"
size="small"
v-for="label in torrent?.labels"
color="primary"
class="me-1 mb-1"
>{{ label }}</VChip
>
{{ label }}
</VChip>
<VChip
v-if="meta?.edition"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
{{ meta?.edition }}</VChip
>
{{ meta?.edition }}
</VChip>
<VChip
v-if="meta?.resource_pix"
variant="elevated"
@@ -240,26 +269,27 @@ const dropdownItems = ref([
</VCardItem>
<VCardActions>
<VBtn
@click.stop="showMoreTorrents = !showMoreTorrents"
v-if="props.more && props.more.length > 0"
@click.stop="showMoreTorrents = !showMoreTorrents"
>
<template #append>
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'"></VIcon>
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
</template>
更多来源
</VBtn>
</VCardActions>
<VExpandTransition>
<div v-show="showMoreTorrents">
<VDivider></VDivider>
<VDivider />
<VChipGroup class="p-3">
<VChip
v-for="item in props.more"
v-for="(item, index) in props.more"
:key="index"
@click.stop="
handleAddDownload(
item.torrent_info?.site_name,
item.media_info,
item.torrent_info
item.torrent_info,
)
"
>
@@ -269,17 +299,16 @@ const dropdownItems = ref([
:content="`↑${item.torrent_info?.seeders}`"
inline
size="small"
></VBadge>
/>
<VBadge
v-if="
item.torrent_info?.downloadvolumefactor !== 1
|| item.torrent_info?.uploadvolumefactor !== 1
"
:content="item.torrent_info?.volume_factor"
inline
size="small"
v-if="
item.torrent_info?.downloadvolumefactor !== 1 ||
item.torrent_info?.uploadvolumefactor !== 1
"
>
</VBadge>
/>
</template>
{{ item.torrent_info.site_name }}
</VChip>