mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-25 01:59:35 +08:00
feat:订阅和插件支持手动排序
This commit is contained in:
@@ -115,7 +115,7 @@ function changeTheme(theme: string) {
|
|||||||
// 保存主题到服务端
|
// 保存主题到服务端
|
||||||
try {
|
try {
|
||||||
api.post('/user/config/Layout', {
|
api.post('/user/config/Layout', {
|
||||||
theme: nextTheme
|
theme: nextTheme,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存主题到服务端失败')
|
console.error('保存主题到服务端失败')
|
||||||
@@ -176,7 +176,7 @@ async function saveCustomCSS() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) $toast.success('自定义CSS保存成功!')
|
if (result.success) $toast.success('自定义CSS保存成功,请刷新页面生效!')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存自定义 CSS 到服务端失败')
|
console.error('保存自定义 CSS 到服务端失败')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,80 +149,82 @@ const dropdownItems = ref([
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
|
<div>
|
||||||
<div class="me-n3 absolute bottom-0 right-3">
|
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col h-full">
|
||||||
<IconBtn>
|
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
|
||||||
<VMenu activator="parent" close-on-content-click>
|
|
||||||
<VList>
|
|
||||||
<VListItem
|
|
||||||
v-for="(item, i) in dropdownItems"
|
|
||||||
v-show="item.show"
|
|
||||||
:key="i"
|
|
||||||
variant="plain"
|
|
||||||
@click="item.props.click"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle v-text="item.title" />
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
|
||||||
:style="{ background: `${backgroundColor}` }"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-cover bg-center"
|
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
:style="{ background: `${backgroundColor}` }"
|
||||||
></div>
|
>
|
||||||
<div class="relative flex-1 min-w-0">
|
<div
|
||||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
{{ props.plugin?.plugin_name }}
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||||
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
></div>
|
||||||
</VCardTitle>
|
<div class="relative flex-1 min-w-0">
|
||||||
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
|
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
{{ props.plugin?.plugin_desc }}
|
{{ props.plugin?.plugin_name }}
|
||||||
</VCardText>
|
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
|
||||||
|
{{ props.plugin?.plugin_desc }}
|
||||||
|
</VCardText>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex-shrink-0 self-center">
|
||||||
|
<VAvatar size="64">
|
||||||
|
<VImg
|
||||||
|
ref="imageRef"
|
||||||
|
:src="iconPath"
|
||||||
|
aspect-ratio="4/3"
|
||||||
|
cover
|
||||||
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex-shrink-0 self-center">
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
<VAvatar size="64">
|
<span>
|
||||||
<VImg
|
<VIcon icon="mdi-github" class="me-1" />
|
||||||
ref="imageRef"
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
:src="iconPath"
|
{{ props.plugin?.plugin_author }}
|
||||||
aspect-ratio="4/3"
|
</a>
|
||||||
cover
|
</span>
|
||||||
:class="{ shadow: isImageLoaded }"
|
<span v-if="props.count" class="ms-3">
|
||||||
@load="imageLoaded"
|
<VIcon icon="mdi-download" />
|
||||||
@error="imageLoadError = true"
|
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||||
/>
|
</span>
|
||||||
</VAvatar>
|
<div class="me-n3 absolute bottom-1 right-3">
|
||||||
</div>
|
<IconBtn>
|
||||||
</div>
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<span>
|
<VList>
|
||||||
<VIcon icon="mdi-github" class="me-1" />
|
<VListItem
|
||||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
v-for="(item, i) in dropdownItems"
|
||||||
{{ props.plugin?.plugin_author }}
|
v-show="item.show"
|
||||||
</a>
|
:key="i"
|
||||||
</span>
|
variant="plain"
|
||||||
<span v-if="props.count" class="ms-3">
|
@click="item.props.click"
|
||||||
<VIcon icon="mdi-download" />
|
>
|
||||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
<template #prepend>
|
||||||
</span>
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
</VCardText>
|
</template>
|
||||||
</VCard>
|
<VListItemTitle v-text="item.title" />
|
||||||
<!-- 安装插件进度框 -->
|
</VListItem>
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
</VList>
|
||||||
<!-- 更新日志 -->
|
</VMenu>
|
||||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
</IconBtn>
|
||||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
</div>
|
||||||
<DialogCloseBtn @click="releaseDialog = false" />
|
</VCardText>
|
||||||
<VDivider />
|
|
||||||
<VersionHistory :history="props.plugin?.history" />
|
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
<!-- 安装插件进度框 -->
|
||||||
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
|
<!-- 更新日志 -->
|
||||||
|
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||||
|
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||||
|
<DialogCloseBtn @click="releaseDialog = false" />
|
||||||
|
<VDivider />
|
||||||
|
<VersionHistory :history="props.plugin?.history" />
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -390,130 +390,146 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 插件卡片 -->
|
<div>
|
||||||
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
|
<!-- 插件卡片 -->
|
||||||
<div class="me-n3 absolute bottom-0 right-3">
|
<VHover>
|
||||||
<IconBtn>
|
<template #default="hover">
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VCard
|
||||||
<VMenu activator="parent" close-on-content-click>
|
v-if="isVisible"
|
||||||
<VList>
|
v-bind="hover.props"
|
||||||
<VListItem
|
:width="props.width"
|
||||||
v-for="(item, i) in dropdownItems"
|
:height="props.height"
|
||||||
v-show="item.show"
|
@click="openPluginDetail"
|
||||||
:key="i"
|
class="flex flex-col h-full"
|
||||||
variant="plain"
|
>
|
||||||
:base-color="item.props.color"
|
<div
|
||||||
@click="item.props.click"
|
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||||
>
|
:style="{ background: `${backgroundColor}` }"
|
||||||
<template #prepend>
|
>
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
<div
|
||||||
</template>
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
<VListItemTitle v-text="item.title" />
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||||
</VListItem>
|
/>
|
||||||
</VList>
|
<div class="relative flex-1 min-w-0">
|
||||||
</VMenu>
|
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
</IconBtn>
|
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||||
</div>
|
{{ props.plugin?.plugin_name }}
|
||||||
<div
|
<span class="text-sm mt-1 text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
</VCardTitle>
|
||||||
:style="{ background: `${backgroundColor}` }"
|
<VCardText class="px-2 py-1 text-white text-shadow line-clamp-3">
|
||||||
>
|
{{ props.plugin?.plugin_desc }}
|
||||||
<div
|
</VCardText>
|
||||||
class="absolute inset-0 bg-cover bg-center"
|
</div>
|
||||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
<div class="relative flex-shrink-0 self-center">
|
||||||
/>
|
<VAvatar size="64">
|
||||||
<div class="relative flex-1 min-w-0">
|
<VImg
|
||||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
ref="imageRef"
|
||||||
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
:src="iconPath"
|
||||||
{{ props.plugin?.plugin_name }}
|
aspect-ratio="4/3"
|
||||||
<span class="text-sm mt-1 text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
cover
|
||||||
</VCardTitle>
|
:class="{ shadow: isImageLoaded }"
|
||||||
<VCardText class="px-2 py-1 text-white text-shadow line-clamp-3">
|
@load="imageLoaded"
|
||||||
{{ props.plugin?.plugin_desc }}
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
|
<span class="author-info">
|
||||||
|
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
||||||
|
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
||||||
|
</VImg>
|
||||||
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
|
{{ props.plugin?.plugin_author }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span v-if="props.count" class="ms-3">
|
||||||
|
<VIcon icon="mdi-download" />
|
||||||
|
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||||
|
</span>
|
||||||
|
<div class="me-n3 absolute bottom-1 right-3">
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
|
<VMenu activator="parent" close-on-content-click>
|
||||||
|
<VList>
|
||||||
|
<VListItem
|
||||||
|
v-for="(item, i) in dropdownItems"
|
||||||
|
v-show="item.show"
|
||||||
|
:key="i"
|
||||||
|
variant="plain"
|
||||||
|
:base-color="item.props.color"
|
||||||
|
@click="item.props.click"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle v-text="item.title" />
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
<div v-if="hover.isHovering" class="me-n3 absolute top-0 right-5">
|
||||||
|
<VIcon class="cursor-move text-white">mdi-drag</VIcon>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||||
|
<VIcon icon="mdi-new-box" class="text-white" />
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
|
||||||
|
<!-- 插件配置页面 -->
|
||||||
|
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
||||||
|
<DialogCloseBtn v-model="pluginConfigDialog" />
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</div>
|
<VCardActions class="pt-3">
|
||||||
<div class="relative flex-shrink-0 self-center">
|
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
||||||
<VAvatar size="64">
|
查看数据
|
||||||
<VImg
|
</VBtn>
|
||||||
ref="imageRef"
|
<VSpacer />
|
||||||
:src="iconPath"
|
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
||||||
aspect-ratio="4/3"
|
</VCardActions>
|
||||||
cover
|
</VCard>
|
||||||
:class="{ shadow: isImageLoaded }"
|
</VDialog>
|
||||||
@load="imageLoaded"
|
|
||||||
@error="imageLoadError = true"
|
|
||||||
/>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
|
||||||
<span class="author-info">
|
|
||||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
|
||||||
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
|
||||||
</VImg>
|
|
||||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
|
||||||
{{ props.plugin?.plugin_author }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-if="props.count" class="ms-3">
|
|
||||||
<VIcon icon="mdi-download" />
|
|
||||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
|
||||||
</span>
|
|
||||||
</VCardText>
|
|
||||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
|
||||||
<VIcon icon="mdi-new-box" class="text-white" />
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
|
|
||||||
<!-- 插件配置页面 -->
|
<!-- 插件数据页面 -->
|
||||||
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||||
<DialogCloseBtn v-model="pluginConfigDialog" />
|
<DialogCloseBtn v-model="pluginInfoDialog" />
|
||||||
<VDivider />
|
<VCardText class="min-h-40">
|
||||||
<VCardText>
|
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
||||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
|
</VCardText>
|
||||||
</VCardText>
|
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||||
<VCardActions class="pt-3">
|
</VCard>
|
||||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
</VDialog>
|
||||||
查看数据
|
|
||||||
</VBtn>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
|
|
||||||
<!-- 插件数据页面 -->
|
<!-- 进度框 -->
|
||||||
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
|
||||||
<DialogCloseBtn v-model="pluginInfoDialog" />
|
|
||||||
<VCardText class="min-h-40">
|
|
||||||
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
|
||||||
</VCardText>
|
|
||||||
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
|
|
||||||
<!-- 进度框 -->
|
<!-- 更新日志 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||||
|
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||||
<!-- 更新日志 -->
|
<DialogCloseBtn @click="releaseDialog = false" />
|
||||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
<VDivider />
|
||||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
<VersionHistory :history="props.plugin?.history" />
|
||||||
<DialogCloseBtn @click="releaseDialog = false" />
|
<VDivider />
|
||||||
<VDivider />
|
<VCardText>
|
||||||
<VersionHistory :history="props.plugin?.history" />
|
<VBtn @click="updatePlugin" block>
|
||||||
<VDivider />
|
<template #prepend>
|
||||||
<VCardText>
|
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||||
<VBtn @click="updatePlugin" block>
|
</template>
|
||||||
<template #prepend>
|
更新到最新版本
|
||||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
</VBtn>
|
||||||
</template>
|
</VCardText>
|
||||||
更新到最新版本
|
</VCard>
|
||||||
</VBtn>
|
</VDialog>
|
||||||
</VCardText>
|
</div>
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -271,134 +271,136 @@ function onSubscribeEditRemove() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VHover>
|
<div>
|
||||||
<template #default="hover">
|
<VHover>
|
||||||
<VCard
|
<template #default="hover">
|
||||||
v-bind="hover.props"
|
<VCard
|
||||||
:key="props.media?.id"
|
v-bind="hover.props"
|
||||||
class="flex flex-col rounded-lg"
|
:key="props.media?.id"
|
||||||
:class="{
|
class="flex flex-col rounded-lg h-full"
|
||||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
:class="{
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||||
'opacity-70': subscribeState === 'S',
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
}"
|
'opacity-70': subscribeState === 'S',
|
||||||
min-height="170"
|
}"
|
||||||
@click="editSubscribeDialog"
|
min-height="170"
|
||||||
>
|
@click="editSubscribeDialog"
|
||||||
<div class="me-n3 absolute top-1 right-2">
|
>
|
||||||
<IconBtn>
|
<div class="me-n3 absolute top-1 right-2">
|
||||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
<IconBtn>
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||||
<VList>
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
<VList>
|
||||||
<VListItem
|
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||||
v-if="item.show !== false"
|
<VListItem
|
||||||
variant="plain"
|
v-if="item.show !== false"
|
||||||
:base-color="item.props.color"
|
variant="plain"
|
||||||
@click="item.props.click"
|
:base-color="item.props.color"
|
||||||
>
|
@click="item.props.click"
|
||||||
<template #prepend>
|
>
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
<template #prepend>
|
||||||
</template>
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
<VListItemTitle v-text="item.title" />
|
</template>
|
||||||
</VListItem>
|
<VListItemTitle v-text="item.title" />
|
||||||
</template>
|
</VListItem>
|
||||||
</VList>
|
</template>
|
||||||
</VMenu>
|
</VList>
|
||||||
</IconBtn>
|
</VMenu>
|
||||||
</div>
|
</IconBtn>
|
||||||
<template #image>
|
|
||||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
|
||||||
</VImg>
|
|
||||||
<div
|
|
||||||
v-if="subscribeState === 'P'"
|
|
||||||
class="absolute inset-0 bg-yellow-900 opacity-40 pointer-events-none"
|
|
||||||
></div>
|
|
||||||
</template>
|
|
||||||
<div>
|
|
||||||
<VCardText class="flex items-center">
|
|
||||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
|
||||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VImg>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
|
||||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
|
||||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
|
||||||
{{ props.media?.name }}
|
|
||||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
|
||||||
<div class="flex align-center">
|
|
||||||
<IconBtn
|
|
||||||
v-if="props.media?.total_episode"
|
|
||||||
v-bind="props"
|
|
||||||
icon="mdi-progress-download"
|
|
||||||
color="white"
|
|
||||||
class="me-1"
|
|
||||||
/>
|
|
||||||
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
|
||||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
|
||||||
{{ props.media?.total_episode }}
|
|
||||||
</div>
|
|
||||||
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
|
||||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
|
||||||
{{ props.media?.username }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText 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 }}
|
|
||||||
</VCardText>
|
|
||||||
<div class="w-full absolute bottom-0">
|
|
||||||
<VProgressLinear
|
|
||||||
v-if="getPercentage() > 0"
|
|
||||||
:model-value="getPercentage()"
|
|
||||||
bg-color="success"
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<template #image>
|
||||||
</VCard>
|
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||||
</template>
|
<template #placeholder>
|
||||||
</VHover>
|
<div class="w-full h-full">
|
||||||
<!-- 订阅编辑弹窗 -->
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
<SubscribeEditDialog
|
</div>
|
||||||
v-if="subscribeEditDialog"
|
</template>
|
||||||
v-model="subscribeEditDialog"
|
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||||
:subid="props.media?.id"
|
</VImg>
|
||||||
@remove="onSubscribeEditRemove"
|
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
||||||
@save="onSubscribeEditSave"
|
</template>
|
||||||
@close="subscribeEditDialog = false"
|
<div>
|
||||||
/>
|
<VCardText class="flex items-center">
|
||||||
|
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||||
|
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VImg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||||
|
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||||
|
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||||
|
{{ props.media?.name }}
|
||||||
|
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||||
|
<div class="flex align-center">
|
||||||
|
<IconBtn
|
||||||
|
v-if="props.media?.total_episode"
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-progress-download"
|
||||||
|
color="white"
|
||||||
|
class="me-1"
|
||||||
|
/>
|
||||||
|
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
||||||
|
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||||
|
{{ props.media?.total_episode }}
|
||||||
|
</div>
|
||||||
|
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
||||||
|
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
||||||
|
{{ props.media?.username }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText 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 }}
|
||||||
|
</VCardText>
|
||||||
|
<div class="w-full absolute bottom-0">
|
||||||
|
<VProgressLinear
|
||||||
|
v-if="getPercentage() > 0"
|
||||||
|
:model-value="getPercentage()"
|
||||||
|
bg-color="success"
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="hover.isHovering" class="me-n3 absolute top-1 right-10">
|
||||||
|
<IconBtn><VIcon class="cursor-move text-white">mdi-drag</VIcon></IconBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
<!-- 订阅编辑弹窗 -->
|
||||||
|
<SubscribeEditDialog
|
||||||
|
v-if="subscribeEditDialog"
|
||||||
|
v-model="subscribeEditDialog"
|
||||||
|
:subid="props.media?.id"
|
||||||
|
@remove="onSubscribeEditRemove"
|
||||||
|
@save="onSubscribeEditSave"
|
||||||
|
@close="subscribeEditDialog = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 订阅文件信息弹窗 -->
|
<!-- 订阅文件信息弹窗 -->
|
||||||
<SubscribeFilesDialog
|
<SubscribeFilesDialog
|
||||||
v-if="subscribeFilesDialog"
|
v-if="subscribeFilesDialog"
|
||||||
v-model="subscribeFilesDialog"
|
v-model="subscribeFilesDialog"
|
||||||
:subid="props.media?.id"
|
:subid="props.media?.id"
|
||||||
@close="subscribeFilesDialog = false"
|
@close="subscribeFilesDialog = false"
|
||||||
/>
|
/>
|
||||||
<!-- 分享订阅弹窗 -->
|
<!-- 分享订阅弹窗 -->
|
||||||
<SubscribeShareDialog
|
<SubscribeShareDialog
|
||||||
v-if="subscribeShareDialog"
|
v-if="subscribeShareDialog"
|
||||||
v-model="subscribeShareDialog"
|
v-model="subscribeShareDialog"
|
||||||
:sub="props.media"
|
:sub="props.media"
|
||||||
@close="subscribeShareDialog = false"
|
@close="subscribeShareDialog = false"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.subscribe-card-background {
|
.subscribe-card-background {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Plugin } from '@/api/types'
|
import type { Plugin } from '@/api/types'
|
||||||
@@ -28,6 +29,9 @@ const pluginId = ref(route.query.id)
|
|||||||
// 当前排序字段
|
// 当前排序字段
|
||||||
const activeSort = ref(null)
|
const activeSort = ref(null)
|
||||||
|
|
||||||
|
// 插件顺序配置
|
||||||
|
const orderConfig = ref<{ id: string }[]>([])
|
||||||
|
|
||||||
// 排序选项
|
// 排序选项
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ title: '热门', value: 'count' },
|
{ title: '热门', value: 'count' },
|
||||||
@@ -104,6 +108,49 @@ const labelFilterOptions = ref<string[]>([])
|
|||||||
// 插件库过滤项
|
// 插件库过滤项
|
||||||
const repoFilterOptions = ref<string[]>([])
|
const repoFilterOptions = ref<string[]>([])
|
||||||
|
|
||||||
|
// 加载插件顺序
|
||||||
|
async function loadPluginOrderConfig() {
|
||||||
|
// 顺序配置
|
||||||
|
const local_order = localStorage.getItem('MP_PLUGIN_ORDER')
|
||||||
|
if (local_order) {
|
||||||
|
orderConfig.value = JSON.parse(local_order)
|
||||||
|
} else {
|
||||||
|
const response2 = await api.get('/user/config/PluginOrder')
|
||||||
|
if (response2 && response2.data && response2.data.value) {
|
||||||
|
orderConfig.value = response2.data.value
|
||||||
|
localStorage.setItem('MP_PLUGIN_ORDER', JSON.stringify(orderConfig.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 排序
|
||||||
|
if (orderConfig.value) {
|
||||||
|
sortPluginOrder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按order的顺序对插件进行排序
|
||||||
|
function sortPluginOrder() {
|
||||||
|
dataList.value.sort((a, b) => {
|
||||||
|
const aIndex = orderConfig.value.findIndex((item: { id: string }) => item.id === a.id)
|
||||||
|
const bIndex = orderConfig.value.findIndex((item: { id: string }) => item.id === b.id)
|
||||||
|
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存顺序设置
|
||||||
|
async function savePluginOrder() {
|
||||||
|
// 顺序配置
|
||||||
|
const orderObj = dataList.value.map(item => ({ id: item.id }))
|
||||||
|
const orderString = JSON.stringify(orderObj)
|
||||||
|
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
|
||||||
|
|
||||||
|
// 保存到服务端
|
||||||
|
try {
|
||||||
|
await api.post('/user/config/PluginOrder', orderObj)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化过滤选项
|
// 初始化过滤选项
|
||||||
function initOptions(item: Plugin) {
|
function initOptions(item: Plugin) {
|
||||||
const optionValue = (options: Array<string>, value: string | undefined) => {
|
const optionValue = (options: Array<string>, value: string | undefined) => {
|
||||||
@@ -329,8 +376,9 @@ function handleRepoUrl(url: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载时获取数据
|
// 加载时获取数据
|
||||||
onBeforeMount(async () => {
|
onMounted(async () => {
|
||||||
await refreshData()
|
await refreshData()
|
||||||
|
await loadPluginOrderConfig()
|
||||||
getPluginStatistics()
|
getPluginStatistics()
|
||||||
if (activeTab.value != 'market' && pluginId.value) {
|
if (activeTab.value != 'market' && pluginId.value) {
|
||||||
// 找到这个插件
|
// 找到这个插件
|
||||||
@@ -356,18 +404,26 @@ onBeforeMount(async () => {
|
|||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<div>
|
<div>
|
||||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-plugin-card">
|
<draggable
|
||||||
<template v-for="(data, index) in dataList" :key="`${data.id}_v${data.plugin_version}`">
|
v-if="dataList.length > 0"
|
||||||
|
v-model="dataList"
|
||||||
|
@end="savePluginOrder"
|
||||||
|
handle=".cursor-move"
|
||||||
|
item-key="id"
|
||||||
|
tag="div"
|
||||||
|
:component-data="{ class: 'grid gap-4 grid-plugin-card' }"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
<PluginCard
|
<PluginCard
|
||||||
:count="PluginStatistics[data.id || '0']"
|
:count="PluginStatistics[element.id || '0']"
|
||||||
:plugin="data"
|
:plugin="element"
|
||||||
:action="pluginActions[data.id || '0']"
|
:action="pluginActions[element.id || '0']"
|
||||||
@remove="refreshData"
|
@remove="refreshData"
|
||||||
@save="refreshData"
|
@save="refreshData"
|
||||||
@action-done="pluginActions[data.id || '0'] = false"
|
@action-done="pluginActions[element.id || '0'] = false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</draggable>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
v-if="dataList.length === 0 && isRefreshed"
|
v-if="dataList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
|
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Subscribe } from '@/api/types'
|
import type { Subscribe } from '@/api/types'
|
||||||
@@ -31,6 +32,52 @@ const subscribeEditDialog = ref(false)
|
|||||||
// 历史记录弹窗
|
// 历史记录弹窗
|
||||||
const historyDialog = ref(false)
|
const historyDialog = ref(false)
|
||||||
|
|
||||||
|
// 订阅顺序配置
|
||||||
|
const orderConfig = ref<{ id: number }[]>([])
|
||||||
|
|
||||||
|
// 加载顺序
|
||||||
|
async function loadSubscribeOrderConfig() {
|
||||||
|
// 顺序配置
|
||||||
|
const local_order = localStorage.getItem('MP_SUBSCRIBE_ORDER')
|
||||||
|
if (local_order) {
|
||||||
|
orderConfig.value = JSON.parse(local_order)
|
||||||
|
} else {
|
||||||
|
const response2 = await api.get('/user/config/SubscribeOrder')
|
||||||
|
if (response2 && response2.data && response2.data.value) {
|
||||||
|
orderConfig.value = response2.data.value
|
||||||
|
localStorage.setItem('MP_SUBSCRIBE_ORDER', JSON.stringify(orderConfig.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 排序
|
||||||
|
if (orderConfig.value) {
|
||||||
|
sortSubscribeOrder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按order的顺序排序
|
||||||
|
function sortSubscribeOrder() {
|
||||||
|
displayList.value.sort((a, b) => {
|
||||||
|
const aIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === a.id)
|
||||||
|
const bIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === b.id)
|
||||||
|
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存顺序设置
|
||||||
|
async function saveSubscribeOrder() {
|
||||||
|
// 顺序配置
|
||||||
|
const orderObj = displayList.value.map(item => ({ id: item.id }))
|
||||||
|
const orderString = JSON.stringify(orderObj)
|
||||||
|
localStorage.setItem('MP_SUBSCRIBE_ORDER', orderString)
|
||||||
|
|
||||||
|
// 保存到服务端
|
||||||
|
try {
|
||||||
|
await api.post('/user/config/SubscribeOrder', orderObj)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
@@ -49,20 +96,28 @@ const loading = ref(false)
|
|||||||
// 下拉刷新
|
// 下拉刷新
|
||||||
async function onRefresh({ done }: { done: any }) {
|
async function onRefresh({ done }: { done: any }) {
|
||||||
await fetchData()
|
await fetchData()
|
||||||
|
await loadSubscribeOrderConfig()
|
||||||
done('ok')
|
done('ok')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
// 显示的订阅列表
|
||||||
const filteredDataList = computed(() => {
|
const displayList = ref<Subscribe[]>([])
|
||||||
|
|
||||||
|
// 监听dataList变化,同步更新displayList
|
||||||
|
watch(dataList, () => {
|
||||||
// 从Vuex Store中获取用户信息
|
// 从Vuex Store中获取用户信息
|
||||||
const superUser = store.state.auth.superUser
|
const superUser = store.state.auth.superUser
|
||||||
const userName = store.state.auth.userName
|
const userName = store.state.auth.userName
|
||||||
if (superUser) return dataList.value.filter(data => data.type === props.type)
|
if (superUser) displayList.value = dataList.value.filter(data => data.type === props.type)
|
||||||
else return dataList.value.filter(data => data.type === props.type && data.username === userName)
|
else displayList.value = dataList.value.filter(data => data.type === props.type && data.username === userName)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||||
|
const filteredDataList = computed(() => {})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchData()
|
await fetchData()
|
||||||
|
await loadSubscribeOrderConfig()
|
||||||
if (props.subid) {
|
if (props.subid) {
|
||||||
// 找到这个订阅
|
// 找到这个订阅
|
||||||
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
|
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
|
||||||
@@ -75,7 +130,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
if (!loading.value) {
|
if (!loading.value) {
|
||||||
fetchData()
|
await fetchData()
|
||||||
|
await loadSubscribeOrderConfig()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -83,17 +139,21 @@ onActivated(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||||
<VPullToRefresh v-model="loading" @load="onRefresh">
|
<VPullToRefresh v-model="loading" @load="onRefresh">
|
||||||
<div v-if="filteredDataList.length > 0" class="mx-3 grid gap-4 grid-subscribe-card p-1">
|
<draggable
|
||||||
<SubscribeCard
|
v-if="displayList.length > 0"
|
||||||
v-for="data in filteredDataList"
|
v-model="displayList"
|
||||||
:key="data.id"
|
@end="saveSubscribeOrder"
|
||||||
:media="data"
|
handle=".cursor-move"
|
||||||
@remove="fetchData"
|
item-key="id"
|
||||||
@save="fetchData"
|
tag="div"
|
||||||
/>
|
:component-data="{ class: 'mx-3 grid gap-4 grid-subscribe-card p-1' }"
|
||||||
</div>
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
v-if="filteredDataList.length === 0 && isRefreshed"
|
v-if="displayList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
error-title="没有订阅"
|
error-title="没有订阅"
|
||||||
error-description="请通过搜索添加电影、电视剧订阅。"
|
error-description="请通过搜索添加电影、电视剧订阅。"
|
||||||
|
|||||||
Reference in New Issue
Block a user