feat:订阅和插件支持手动排序

This commit is contained in:
jxxghp
2024-12-09 10:43:00 +08:00
parent 4576ef854d
commit e4b8ff0a64
6 changed files with 480 additions and 344 deletions

View File

@@ -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 到服务端失败')
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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="请通过搜索添加电影电视剧订阅"