Merge branch 'main' into develop-20240417-用户配置

This commit is contained in:
jxxghp
2024-04-23 10:00:29 +08:00
committed by GitHub
41 changed files with 549 additions and 479 deletions

1
components.d.ts vendored
View File

@@ -10,6 +10,7 @@ declare module 'vue' {
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.8.2",
"version": "1.8.2-2",
"private": true,
"bin": "dist/service.js",
"scripts": {

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
// 定义输入参数
const props = defineProps({
progress: Number,
text: String
})
</script>
<template>
<div
class="w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!props.text"
size="48"
indeterminate
color="primary"
/>
<VProgressCircular
v-if="props.progress"
class="mb-3"
color="primary"
:model-value="props.progress"
size="64"
/>
<span>{{ props.text }}</span>
</div>
</template>

View File

@@ -2,13 +2,8 @@
import { useToast } from 'vue-toast-notification'
import { useTheme } from 'vuetify'
import api from '@/api'
import store from './store'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 提示框
const $toast = useToast()

View File

@@ -88,6 +88,9 @@ export interface Subscribe {
// 保存目录
save_path: string
// 时间
date: string
}
// 历史记录

View File

@@ -1,9 +1,8 @@
<script lang="ts" setup>
import type { Axios } from 'axios'
import axios from 'axios'
import List from './filebrowser/List.vue'
import Toolbar from './filebrowser/Toolbar.vue'
import FileList from './filebrowser/FileList.vue'
import FileToolbar from './filebrowser/FileToolbar.vue'
import type { EndPoints } from '@/api/types'
// 输入参数
@@ -100,7 +99,7 @@ onMounted(() => {
<template>
<VCard class="mx-auto" :loading="loading > 0 || !path">
<div v-if="path">
<Toolbar
<FileToolbar
:path="path"
:storages="storagesArray"
:storage="activeStorage"
@@ -111,7 +110,7 @@ onMounted(() => {
@foldercreated="refreshPending = true"
@sortchanged="sortChanged"
/>
<List
<FileList
:path="path"
:storage="activeStorage"
:icons="fileIcons"

View File

@@ -25,7 +25,7 @@ function goPlay() {
// 计算图片地址
const getImgUrl = computed(() => {
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(image).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
})
</script>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { kFormatter } from '@core/utils/formatters'
interface Props {
title: string
color?: string
icon: string
stats: number
change: number
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
size="44"
rounded
:color="props.color"
variant="tonal"
class="me-4"
>
<VIcon
:icon="props.icon"
size="30"
/>
</VAvatar>
<div>
<span class="text-caption">{{ props.title }}</span>
<div class="d-flex align-center flex-wrap">
<span class="text-h6 font-weight-semibold">{{ kFormatter(props.stats) }}</span>
<div
v-if="props.change"
:class="`${isPositive ? 'text-success' : 'text-error'} mt-1`"
>
<VIcon :icon="isPositive ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
<span class="text-caption font-weight-semibold">{{ Math.abs(props.change) }}%</span>
</div>
</div>
</div>
</VCardText>
</VCard>
</template>

View File

@@ -1,56 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
color?: string
icon: string
stats: string
change: number
subtitle: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard>
<VCardText class="d-flex align-center">
<VAvatar
v-if="props.icon"
size="38"
:color="props.color"
>
<VIcon
:icon="props.icon"
size="24"
/>
</VAvatar>
<VSpacer />
<MoreBtn class="me-n3 mt-n1" />
</VCardText>
<VCardText>
<h6 class="text-sm font-weight-semibold mb-2">
{{ props.title }}
</h6>
<div
v-if="props.change"
class="d-flex align-center mb-2"
>
<span class="font-weight-semibold text-h5 me-2">{{ props.stats }}</span>
<span
:class="isPositive ? 'text-success' : 'text-error'"
class="text-caption"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<span class="text-caption">{{ props.subtitle }}</span>
</VCardText>
</VCard>
</template>

View File

@@ -1,65 +0,0 @@
<script setup lang="ts">
interface Props {
title: string
subtitle: string
stats: string
change: number
image: string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
color: 'primary',
})
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
</script>
<template>
<VCard class="overflow-visible">
<div class="d-flex position-relative">
<VCardText>
<h6 class="text-base font-weight-semibold mb-4">
{{ props.title }}
</h6>
<div class="d-flex align-center flex-wrap mb-4">
<h5 class="text-h5 font-weight-semibold me-2">
{{ props.stats }}
</h5>
<span
class="text-caption"
:class="isPositive ? 'text-success' : 'text-error'"
>
{{ isPositive ? `+${props.change}` : props.change }}%
</span>
</div>
<VChip
v-if="props.subtitle"
size="small"
:color="props.color"
>
{{ props.subtitle }}
</VChip>
</VCardText>
<VSpacer />
<div class="illustrator-img">
<VImg
v-if="props.image"
:src="props.image"
:width="110"
/>
</div>
</div>
</VCard>
</template>
<style lang="scss">
.illustrator-img {
position: absolute;
inset-block-end: 0;
inset-inline-end: 5%;
}
</style>

View File

@@ -56,7 +56,7 @@ function getImgUrl(url: string) {
if (!url)
return getDefaultImage()
else
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(url).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
}
// 根据多张图片生成媒体库封面
@@ -68,7 +68,7 @@ async function drawImages(imageList: string[]) {
// 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++)
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(IMAGES[i]).replace(/%2F/g, '/')}`
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
// canvas
const canvas = canvasRef.value

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { PropType, Ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
@@ -157,7 +157,7 @@ async function addSubscribe(season = 0) {
// 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) {
const show_edit_dialog = await querySubscribeRules()
const show_edit_dialog = await queryDefaultSubscribeConfig()
if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
@@ -313,11 +313,16 @@ async function getMediaSeasons() {
}
// 查询订阅弹窗规则
async function querySubscribeRules() {
async function queryDefaultSubscribeConfig() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
let subscribe_config_url = ''
if (props.media?.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value)
return result.data.value.show_edit_dialog
}
@@ -364,14 +369,16 @@ function getExistText(season: number) {
}
// 打开详情页
function goMediaDetail() {
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
type: props.media?.type,
},
})
function goMediaDetail(isHovering = false) {
if (isHovering) {
router.push({
path: '/media',
query: {
mediaid: getMediaId(),
type: props.media?.type,
},
})
}
}
// 开始搜索
@@ -400,7 +407,7 @@ const getImgUrl: Ref<string> = computed(() => {
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
// 如果地址中包含douban则使用中转代理
if (url.includes('doubanio.com'))
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`
return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
return url
})
@@ -440,7 +447,7 @@ function getYear(airDate: string) {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goMediaDetail"
@click.stop="goMediaDetail(hover.isHovering)"
>
<VImg
aspect-ratio="2/3"
@@ -592,7 +599,7 @@ function getYear(airDate: string) {
</VCard>
</VBottomSheet>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="subscribeId"

View File

@@ -91,7 +91,7 @@ const iconPath: Ref<string> = computed(() => {
return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(props.plugin?.plugin_icon).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}`
})

View File

@@ -223,7 +223,7 @@ const iconPath: Ref<string> = computed(() => {
return noImage
// 如果是网络图片则使用代理后返回
if (props.plugin?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(props.plugin?.plugin_icon).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
return `./plugin_icon/${props.plugin?.plugin_icon}`
})

View File

@@ -31,12 +31,12 @@ const getImgUrl = computed(() => {
if (imageLoadError.value)
return noImage
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0/${encodeURIComponent(image).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
})
// 跳转播放
function goPlay() {
if (props.media?.link)
function goPlay(isHovering = false) {
if (props.media?.link && isHovering)
window.open(props.media?.link, '_blank')
}
</script>
@@ -53,7 +53,7 @@ function goPlay() {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goPlay"
@click.stop="goPlay(hover.isHovering)"
>
<VImg
aspect-ratio="2/3"

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
import SiteTorrentTable from '../table/SiteTorrentTable.vue'
import { requiredValidator } from '@/@validators'
import api from '@/api'
@@ -166,13 +166,19 @@ const statColor = computed(() => {
}
else if (siteStats.value?.lst_state == 0){
if (!siteStats.value?.seconds)
return 'success'
return 'secondary'
if (siteStats.value?.seconds >= 5)
return 'warning'
return 'success'
}
})
// 监听resourceDialog如果为false则重新查询站点使用统计
watch(resourceDialog, (value) => {
if (!value)
getSiteStats()
})
// 装载时查询站点图标
onMounted(() => {
getSiteIcon()
@@ -360,7 +366,7 @@ onMounted(() => {
</VCardActions>
</VCard>
</VDialog>
<SiteAddEditForm
<SiteAddEditDialog
v-if="siteEditDialog"
v-model="siteEditDialog"
:siteid="cardProps.site?.id"

View File

@@ -1,6 +1,6 @@
<script lang='ts' setup>
import { useToast } from 'vue-toast-notification'
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { calculateTimeDifference } from '@/@core/utils'
import { formatSeason } from '@/@core/utils/formatters'
import api from '@/api'
@@ -284,7 +284,7 @@ const dropdownItems = ref([
/>
</VCard>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="props.media?.id"

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
title: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('close')
}
</script>
<template>
<VDialog
width="40rem"
scrollable
max-height="85vh"
>
<VCard
:title="props.title"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="handleImport"
>
导入
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
import TmdbSelector from '../misc/TmdbSelector.vue'
import store from '@/store'
import api from '@/api'
import { numberValidator } from '@/@validators'
@@ -306,7 +306,7 @@ async function transfer() {
scrollable
max-height="85vh"
>
<TmdbSelectorCard
<TmdbSelector
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>

View File

@@ -43,6 +43,8 @@ const subscribeForm = ref<Subscribe>({
username: '',
current_priority: 0,
save_path: '',
date: '',
show_edit_dialog: false
})
//
@@ -270,7 +272,7 @@ onMounted(() => {
max-width="60rem"
>
<VCard
:title="`${props.default ? `设置${props.type}默认订阅规则` : `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`}`"
:title="`${props.default ? `${props.type}默认订阅规则` : `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`}`"
class="rounded-t"
>
<VCardText class="pt-2">
@@ -412,6 +414,16 @@ onMounted(() => {
hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持"
/>
</VCol>
<VCol v-if="props.default"
cols="12"
md="4"
>
<VSwitch
v-model="subscribeForm.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后将在添加订阅后弹出编辑订阅的对话框,方便用户编辑订阅规则"
/>
</VCol>
</VRow>
</VForm>
</VCardText>

View File

@@ -0,0 +1,239 @@
<script lang="ts" setup>
import api from '@/api';
import { Subscribe } from '@/api/types';
import { formatDateDifference } from '@core/utils/formatters'
// 输入参数
const props = defineProps({
type: String,
})
// 定义触发的自定义事件
const emit = defineEmits(['close', 'save'])
// 订阅历史列表
const historyList = ref<Subscribe[]>([])
// 当前加载数据
const currData = ref<Subscribe[]>([])
// 当前页
const currentPage = ref(1)
// 每页数量
const pageSize = ref(30)
// 是否加载中
const loading = ref(false)
// 是否加载完成
const isRefreshed = ref(false)
// 进度框
const progressDialog = ref(false)
// 进度文字
const progressText = ref('正在重新订阅...')
// 调用API查询列表
async function loadHistory({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 调用API查询列表
try {
// 设置加载中
loading.value = true
currData.value = await api.get(`subscribe/history/${props.type}`, {
params: {
page: currentPage.value,
count: pageSize.value,
},
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
historyList.value = [...historyList.value, ...currData.value]
// 页码+1
currentPage.value++
// 返回加载成功
done('ok')
}
// 取消加载中
loading.value = false
} catch (e) {
console.error(e)
// 返回加载失败
done('error')
}
}
// 重新订阅
async function reSubscribe(item: Subscribe) {
if (item.type === '电影')
progressText.value = `正在重新订阅 ${item.name} ...`
else
progressText.value = `正在重新订阅 ${item.name}${item.season} 季 ...`
progressDialog.value = true
try {
const result: {[key: string]: any} = await api.post('subscribe', item)
if (result.success){
emit('save')
}
} catch (e) {
console.error(e)
}
progressDialog.value = false
}
// 删除记录
async function deleteHistory(item: Subscribe) {
try {
const result: {[key: string]: any} = await api.delete(`subscribe/history/${item.id}`)
if (result.success){
historyList.value = historyList.value.filter((i) => i.id !== item.id)
}
} catch (e) {
console.error(e)
}
}
// 弹出菜单
const dropdownItems = ref([
{
title: '重新订阅',
value: 1,
color: '',
props: {
prependIcon: 'mdi-redo',
click: reSubscribe,
},
},
{
title: '删除',
value: 2,
color: 'error',
props: {
prependIcon: 'mdi-delete',
click: deleteHistory,
},
}
])
</script>
<template>
<VDialog
scrollable
max-width="50rem"
max-height="90vh"
>
<VCard
class="mx-auto"
width="100%"
>
<VCardItem class="pb-0">
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
</VCardItem>
<DialogCloseBtn @click="() => { emit('close') }" />
<VList
lines="two"
>
<VInfiniteScroll
mode="intersect"
side="end"
:items="historyList"
class="overflow-hidden"
@load="loadHistory"
>
<template #loading>
<LoadingBanner />
</template>
<template #empty />
<template v-for="(item, i) in historyList" :key="i">
<VListItem>
<template #prepend>
<VImg
height="75"
width="50"
:src="item.poster"
aspect-ratio="2/3"
class="object-cover rounded shadow ring-gray-500 me-3"
cover
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
</div>
</template>
</VImg>
</template>
<VListItemTitle v-if="item.type == '电视剧'">
{{ item.name }} <span class="text-sm"> {{ item.season }} </span>
</VListItemTitle>
<VListItemTitle v-else>
{{ item.name }}
</VListItemTitle>
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
v-for="(menu, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="menu.color"
@click="menu.props.click(item)"
>
<template #prepend>
<VIcon :icon="menu.props.prependIcon" />
</template>
<VListItemTitle v-text="menu.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VListItem>
</template>
</VInfiniteScroll>
</VList>
</VCard>
<!-- 进度框 -->
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<VCard
color="primary"
>
<VCardText class="text-center">
{{ progressText }}
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</VCardText>
</VCard>
</VDialog>
</VDialog>
</template>

View File

@@ -4,7 +4,7 @@ import type { PropType } from 'vue'
import { useConfirm } from 'vuetify-use-dialog'
import axios from 'axios'
import { useToast } from 'vue-toast-notification'
import ReorganizeForm from '../form/ReorganizeForm.vue'
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
import { formatBytes } from '@core/utils/formatters'
import type { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
@@ -535,7 +535,7 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 文件整理弹窗 -->
<ReorganizeForm
<ReorganizeDialog
v-if="transferPopper"
v-model="transferPopper"
:path="currentItem?.path"

View File

@@ -1,39 +0,0 @@
<script lang="ts" setup>
// 输入参数
const props = defineProps({
title: String,
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'close'])
// 代码
const codeString = ref('')
// 导入
function handleImport() {
emit('update:modelValue', codeString.value)
emit('close')
}
</script>
<template>
<VCard
:title="props.title"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="tonal"
@click="handleImport"
>
导入
</VBtn>
</VCardActions>
</VCard>
</template>

View File

@@ -18,7 +18,12 @@ import 'vue-toast-notification/dist/theme-bootstrap.css'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar';
import 'vue3-perfect-scrollbar/style.css';
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 加载字体
loadFonts()
// 创建Vue实例

View File

@@ -120,11 +120,12 @@ onMounted(() => {
</script>
<template>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" />
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
<span>{{ progressText }}</span>
</div>
<LoadingBanner
v-if="!isRefreshed"
class="mt-12"
:text="progressText"
:progress="progressValue"
/>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
:error-title="errorTitle"

View File

@@ -52,70 +52,63 @@ async function fetchData({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错
if (!hasScroll()) {
// 加载多次
while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: getParams(),
})
// 取消加载中
loading.value = false
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('ok')
done('empty')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
}
}
else {
// 加载一次
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: getParams(),
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
}
// 取消加载中
loading.value = false
// 返回加载成功
done('ok')
}
catch (error) {
console.error(error)
// 返回加载失败
done('error')
}
@@ -123,16 +116,10 @@ async function fetchData({ done }: { done: any }) {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<VInfiniteScroll
mode="intersect"
side="end"
@@ -141,6 +128,7 @@ async function fetchData({ done }: { done: any }) {
@load="fetchData"
>
<template #loading />
<template #empty />
<div
v-if="dataList.length > 0"
class="grid gap-4 grid-media-card mx-3"

View File

@@ -8,7 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
// 输入参数
const mediaProps = defineProps({
@@ -46,11 +46,6 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号
const subscribeId = ref<number>()
// 订阅规则
const subscribeRules = ref({
show_edit_dialog: false,
})
// 获得mediaid
function getMediaId() {
return mediaDetail.value?.tmdb_id
@@ -230,9 +225,12 @@ async function addSubscribe(season = 0) {
)
// 显示编辑弹窗
if (result.success && subscribeRules.value.show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
if (result.success) {
const show_edit_dialog = await queryDefaultSubscribeConfig()
if (show_edit_dialog) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
}
catch (error) {
@@ -290,20 +288,6 @@ async function removeSubscribe(season: number) {
doneNProgress()
}
// 查询订阅弹窗规则
async function querySubscribeRules() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
subscribeRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 订阅按钮响应
function handleSubscribe(season = 0) {
if (isSubscribed.value)
@@ -450,23 +434,35 @@ async function handlePlay() {
}
}
async function queryDefaultSubscribeConfig() {
try {
let subscribe_config_url = ''
if (mediaProps.type === '电影')
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
else
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
const result: { [key: string]: any } = await api.get(subscribe_config_url)
if (result.data?.value)
return result.data.value.show_edit_dialog
}
catch (error) {
console.log(error)
}
return false
}
onBeforeMount(() => {
getMediaDetail()
querySubscribeRules()
})
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
@@ -638,16 +634,10 @@ onBeforeMount(() => {
</VExpansionPanelTitle>
<VExpansionPanelText>
<template #default>
<div
<LoadingBanner
v-if="!seasonEpisodesInfo[season.season_number || 0]"
class="mt-3 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-3"
/>
<div class="flex flex-col justify-center divide-y divide-gray-700">
<div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4">
<div class="flex-1">
@@ -849,7 +839,7 @@ onBeforeMount(() => {
error-description="未识别到媒体信息"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"

View File

@@ -42,74 +42,67 @@ async function fetchData({ done }: { done: any }) {
// 如果正在加载中,直接返回
if (loading.value) {
done('ok')
return
}
// 设置加载中
loading.value = true
// 加载到满屏或者加载出错
if (!hasScroll()) {
// 加载多次
while (!hasScroll()) {
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: {
page: page.value,
},
})
// 取消加载中
loading.value = false
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
}
}
else {
// 加载一次
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
params: {
page: page.value,
},
})
// 标计为已请求完成
isRefreshed.value = true
if (currData.value.length === 0) {
// 如果没有数据,跳出
done('empty')
} else {
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 返回加载成功
done('ok')
return
}
// 合并数据
dataList.value = [...dataList.value, ...currData.value]
// 页码+1
page.value++
// 取消加载中
loading.value = false
}
// 取消加载中
loading.value = false
// 返回加载成功
done('ok')
}
catch (error) {
console.error(error)
// 返回加载失败
done('error')
}
@@ -117,16 +110,10 @@ async function fetchData({ done }: { done: any }) {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<VInfiniteScroll
mode="intersect"
side="end"
@@ -135,6 +122,7 @@ async function fetchData({ done }: { done: any }) {
@load="fetchData"
>
<template #loading />
<template #empty />
<div
v-if="dataList.length > 0 && props.type === 'tmdb'"
class="grid gap-4 grid-media-card mx-3"

View File

@@ -48,16 +48,10 @@ onBeforeMount(() => {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<div v-if="personDetail.id" class="max-w-8xl mx-auto px-4">
<div class="relative z-10 mt-4 mb-8 flex flex-col items-center lg:flex-row ">
<VAvatar

View File

@@ -117,7 +117,7 @@ function pluginIcon(item: Plugin) {
return noImage
// 如果是网络图片则使用代理后返回
if (item?.plugin_icon?.startsWith('http'))
return `${import.meta.env.VITE_API_BASE_URL}system/img/1/${encodeURIComponent(item?.plugin_icon).replace(/%2F/g, '/')}`
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(item?.plugin_icon)}`
return `./plugin_icon/${item?.plugin_icon}`
}
@@ -211,17 +211,10 @@ onBeforeMount(() => {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<div
v-if="dataList.length > 0"
class="grid gap-4 grid-plugin-card"
@@ -285,17 +278,10 @@ onBeforeMount(() => {
</VToolbar>
</div>
<VCardText>
<div
<LoadingBanner
v-if="!isAppMarketLoaded"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isAppMarketLoaded"
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
<PluginAppCard
v-for="data in sortedUninstalledList"

View File

@@ -67,17 +67,10 @@ onUnmounted(() => {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<PullRefresh
v-model="loading"
@refresh="onRefresh"

View File

@@ -3,7 +3,7 @@ import { ref, unref } from 'vue'
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
import ReorganizeDialog from '@/components/dialog/ReorganizeDialog.vue'
// 提示框
const $toast = useToast()
@@ -482,7 +482,7 @@ onMounted(fetchData)
</VCard>
</VBottomSheet>
<!-- 文件整理弹窗 -->
<ReorganizeForm
<ReorganizeDialog
v-if="redoDialog"
v-model="redoDialog"
:logids="redoIds"

View File

@@ -4,7 +4,7 @@ import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types'
import { copyToClipboard } from '@/@core/utils/navigator'
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型
interface FilterCard {
@@ -420,7 +420,7 @@ onMounted(() => {
width="60rem"
scrollable
>
<ImportCodeForm
<ImportCodeDialog
v-model="importCodeString"
title="导入优先级规则"
@close="importCodeDialog = false"

View File

@@ -4,7 +4,7 @@ import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types'
import { copyToClipboard } from '@/@core/utils/navigator'
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
// 规则卡片类型
interface FilterCard {
@@ -41,8 +41,7 @@ const defaultFilterRules = ref({
exclude: '',
movie_size: '',
tv_size: '',
min_seeders: 0,
show_edit_dialog: false,
min_seeders: 0
})
// 订阅模式选择项
@@ -622,13 +621,6 @@ onMounted(() => {
hint="小于该值的资源将被过滤掉0表示不过滤"
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="defaultFilterRules.show_edit_dialog"
label="订阅时编辑更多规则"
hint="开启后,添加订阅时将自动弹出订阅编辑框,要设置更多订阅选项"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
@@ -648,7 +640,7 @@ onMounted(() => {
width="60rem"
scrollable
>
<ImportCodeForm
<ImportCodeDialog
v-model="importCodeString"
title="导入优先级规则"
@close="importCodeDialog = false"

View File

@@ -3,7 +3,7 @@ import api from '@/api'
import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import { useDefer } from '@/@core/utils/dom'
// 数据列表
@@ -35,17 +35,10 @@ onBeforeMount(fetchData)
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<div
v-if="dataList.length > 0"
class="grid gap-3 grid-site-card"
@@ -80,7 +73,7 @@ onBeforeMount(fetchData)
@click="siteAddDialog = true"
/>
<!-- 新增站点弹窗 -->
<SiteAddEditForm
<SiteAddEditDialog
v-if="siteAddDialog"
v-model="siteAddDialog"
oper="add"

View File

@@ -155,7 +155,7 @@ onMounted(() => {
</VCard>
</div>
<div class="md:hidden">
<VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`">
<VTooltip :text="`${arg.event.title} ${arg.event.extendedProps.subtitle}`">
<template #activator="{ props }">
<VImg
height="60"
@@ -384,8 +384,8 @@ onMounted(() => {
}
.v-application .fc .fc-daygrid-day-number {
padding-block: 0rem;
padding-inline: 0rem;
padding-block: 0;
padding-inline: 0;
}
.v-application .fc .fc-list-event-dot {
@@ -435,7 +435,7 @@ onMounted(() => {
margin-inline-end: 0.25rem;
}
@media (max-width: 1264px) {
@media (width <= 1264px) {
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-drawerToggler-button {
display: block !important;
}
@@ -481,10 +481,10 @@ onMounted(() => {
}
.v-application .fc .fc-button-primary {
background-color: transparent;
border: none;
outline: none;
background-color: transparent;
color: var(--v-theme-on-surface);
outline: none;
}
.v-application .fc .fc-button-primary:hover {
@@ -492,7 +492,7 @@ onMounted(() => {
color: rgb(var(--v-theme-primary));
}
@media (max-width: 776px) {
@media (width <= 776px) {
.fc-daygrid-event-harness {
display: flex;
align-items: center;

View File

@@ -4,7 +4,8 @@ import api from '@/api'
import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import store from '@/store'
// 输入参数
@@ -21,6 +22,9 @@ const dataList = ref<Subscribe[]>([])
// 弹窗
const subscribeEditDialog = ref(false)
// 历史记录弹窗
const historyDialog = ref(false)
// 获取订阅列表数据
async function fetchData() {
try {
@@ -58,17 +62,10 @@ const filteredDataList = computed(() => {
</script>
<template>
<div
<LoadingBanner
v-if="!isRefreshed"
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
>
<VProgressCircular
v-if="!isRefreshed"
size="48"
indeterminate
color="primary"
/>
</div>
class="mt-12"
/>
<PullRefresh
v-model="loading"
@refresh="onRefresh"
@@ -102,8 +99,19 @@ const filteredDataList = computed(() => {
appear
@click="subscribeEditDialog = true"
/>
<VFab
icon="mdi-history"
color="info"
location="bottom end"
class="mb-2"
size="x-large"
fixed
app
appear
@click="historyDialog = true"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:default="true"
@@ -111,6 +119,14 @@ const filteredDataList = computed(() => {
@save="subscribeEditDialog = false"
@close="subscribeEditDialog = false"
/>
<!-- 历史记录弹窗 -->
<SubscribeHistoryDialog
v-if="historyDialog"
v-model="historyDialog"
:type="props.type"
@close="historyDialog = false"
@save="() => {historyDialog = false; fetchData()}"
/>
</template>
<style lang="scss">

View File

@@ -55,38 +55,39 @@ async function loadMessages({ done }: { done: any }) {
done('ok')
return
}
// 设置加载中
loading.value = true
try {
// 设置加载中
loading.value = true
currData.value = await api.get('message/web', {
params: {
page: page.value,
size: 20,
},
})
// 已加载过
isLoaded.value = true
if (currData.value.length > 0) {
// 取最后一条时间为存量消息最新时间
lastTime.value = currData.value[currData.value.length - 1].reg_time ?? ''
// 合并数据
messages.value = [...currData.value, ...messages.value]
// 加载完成
done('ok')
if (page.value === 1) {
// 滚动到底部
emit('scroll')
// 监听SSE消息
startSSEMessager()
}
// 页码+1
page.value++
// 完成
done('ok')
}
else {
done('ok')
// 监听SSE消息
startSSEMessager()
// 没有新数据
done('empty')
}
// 取消加载中
loading.value = false
isLoaded.value = true
// 监听SSE消息
startSSEMessager()
}
catch (error) {
console.error(error)
@@ -110,20 +111,18 @@ onBeforeUnmount(() => {
<template>
<VInfiniteScroll
mode="intersect"
:mode="!isLoaded ? 'intersect' : 'manual'"
side="start"
:items="messages"
class="overflow-hidden"
@load="loadMessages"
load-more-text="加载更多 ..."
>
<template #loading>
<VProgressCircular
v-if="loading"
indeterminate
size="48"
class="mb-5"
color="primary"
/>
<LoadingBanner />
</template>
<template #empty>
没有更多数据
</template>
<div>
<VRow