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,28 +149,8 @@ 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 <div
class="relative flex flex-row items-start pa-3 justify-between grow" class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }" :style="{ background: `${backgroundColor}` }"
@@ -213,6 +193,27 @@ const dropdownItems = ref([
<VIcon icon="mdi-download" /> <VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span> <span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</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"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</VCardText> </VCardText>
</VCard> </VCard>
<!-- 安装插件进度框 --> <!-- 安装插件进度框 -->
@@ -225,4 +226,5 @@ const dropdownItems = ref([
<VersionHistory :history="props.plugin?.history" /> <VersionHistory :history="props.plugin?.history" />
</VCard> </VCard>
</VDialog> </VDialog>
</div>
</template> </template>

View File

@@ -390,30 +390,18 @@ watch(
</script> </script>
<template> <template>
<div>
<!-- 插件卡片 --> <!-- 插件卡片 -->
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col"> <VHover>
<div class="me-n3 absolute bottom-0 right-3"> <template #default="hover">
<IconBtn> <VCard
<VIcon icon="mdi-dots-vertical" /> v-if="isVisible"
<VMenu activator="parent" close-on-content-click> v-bind="hover.props"
<VList> :width="props.width"
<VListItem :height="props.height"
v-for="(item, i) in dropdownItems" @click="openPluginDetail"
v-show="item.show" class="flex flex-col h-full"
: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>
<div <div
class="relative flex flex-row items-start pa-3 justify-between grow" class="relative flex flex-row items-start pa-3 justify-between grow"
:style="{ background: `${backgroundColor}` }" :style="{ background: `${backgroundColor}` }"
@@ -459,11 +447,38 @@ watch(
<VIcon icon="mdi-download" /> <VIcon icon="mdi-download" />
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span> <span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
</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> </VCardText>
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5"> <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" /> <VIcon icon="mdi-new-box" class="text-white" />
</div> </div>
</VCard> </VCard>
</template>
</VHover>
<!-- 插件配置页面 --> <!-- 插件配置页面 -->
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value"> <VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
@@ -514,6 +529,7 @@ watch(
</VCardText> </VCardText>
</VCard> </VCard>
</VDialog> </VDialog>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -271,12 +271,13 @@ function onSubscribeEditRemove() {
</script> </script>
<template> <template>
<div>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <VCard
v-bind="hover.props" v-bind="hover.props"
:key="props.media?.id" :key="props.media?.id"
class="flex flex-col rounded-lg" class="flex flex-col rounded-lg h-full"
:class="{ :class="{
'outline-dashed outline-1': props.media?.best_version && imageLoaded, 'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering, 'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
@@ -316,10 +317,7 @@ function onSubscribeEditRemove() {
</template> </template>
<div class="absolute inset-0 subscribe-card-background"></div> <div class="absolute inset-0 subscribe-card-background"></div>
</VImg> </VImg>
<div <div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
v-if="subscribeState === 'P'"
class="absolute inset-0 bg-yellow-900 opacity-40 pointer-events-none"
></div>
</template> </template>
<div> <div>
<VCardText class="flex items-center"> <VCardText class="flex items-center">
@@ -371,6 +369,9 @@ function onSubscribeEditRemove() {
color="success" color="success"
/> />
</div> </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> </div>
</VCard> </VCard>
</template> </template>
@@ -399,6 +400,7 @@ function onSubscribeEditRemove() {
: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="请通过搜索添加电影电视剧订阅"