mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
es lint fix
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user