mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 08:06:43 +08:00
优化动态标签页注册逻辑
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
// 磨砂渐变效果
|
// 磨砂渐变效果
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
block-size: calc(env(safe-area-inset-top, 0px) + 5rem);
|
block-size: calc(env(safe-area-inset-top, 0px) + var(--navbar-height) + 5rem);
|
||||||
content: "";
|
content: "";
|
||||||
inset-block-start: 0;
|
inset-block-start: 0;
|
||||||
inset-inline: 0;
|
inset-inline: 0;
|
||||||
@@ -18,10 +18,10 @@
|
|||||||
mask: linear-gradient(
|
mask: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(0, 0, 0, 100%) 0%,
|
rgba(0, 0, 0, 100%) 0%,
|
||||||
rgba(0, 0, 0, 90%) calc(env(safe-area-inset-top, 0px) + 1rem),
|
rgba(0, 0, 0, 90%) calc(env(safe-area-inset-top, 0px) + var(--navbar-height) + 1rem),
|
||||||
rgba(0, 0, 0, 70%) calc(env(safe-area-inset-top, 0px) + 2rem),
|
rgba(0, 0, 0, 70%) calc(env(safe-area-inset-top, 0px) + var(--navbar-height) + 2rem),
|
||||||
rgba(0, 0, 0, 50%) calc(env(safe-area-inset-top, 0px) + 3rem),
|
rgba(0, 0, 0, 50%) calc(env(safe-area-inset-top, 0px) + var(--navbar-height) + 3rem),
|
||||||
rgba(0, 0, 0, 20%) calc(env(safe-area-inset-top, 0px) + 4rem),
|
rgba(0, 0, 0, 20%) calc(env(safe-area-inset-top, 0px) + var(--navbar-height) + 4rem),
|
||||||
rgba(0, 0, 0, 0%) 100%
|
rgba(0, 0, 0, 0%) 100%
|
||||||
);
|
);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -38,15 +38,23 @@ export default defineComponent({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 👉 Navbar
|
// 👉 Navbar
|
||||||
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
|
const navbar = h(
|
||||||
h(
|
'header',
|
||||||
'div',
|
{ class: ['layout-navbar navbar-blur'] },
|
||||||
{ class: 'navbar-content-container' },
|
[
|
||||||
slots.navbar?.({
|
h(
|
||||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
'div',
|
||||||
}),
|
{ class: 'navbar-content-container' },
|
||||||
),
|
slots.navbar?.({
|
||||||
])
|
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
// 👉 Dynamic Header Tab in NavBar
|
||||||
|
slots['dynamic-header-tab']?.()
|
||||||
|
? h('div', { class: 'layout-dynamic-header-tab' }, slots['dynamic-header-tab']?.())
|
||||||
|
: null,
|
||||||
|
].filter(Boolean),
|
||||||
|
)
|
||||||
|
|
||||||
const main = h(
|
const main = h(
|
||||||
'main',
|
'main',
|
||||||
|
|||||||
@@ -243,14 +243,14 @@ function stopDrag() {
|
|||||||
|
|
||||||
// 外层DIV大小控制
|
// 外层DIV大小控制
|
||||||
const scrollStyle = computed(() => {
|
const scrollStyle = computed(() => {
|
||||||
return appMode
|
return appMode.value
|
||||||
? 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom) - 7rem)'
|
? 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom) - 7rem)'
|
||||||
: 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom)'
|
: 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom)'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 文件列表大小限制
|
// 文件列表大小限制
|
||||||
const fileListStyle = computed(() => {
|
const fileListStyle = computed(() => {
|
||||||
return appMode
|
return appMode.value
|
||||||
? 'height: calc(100vh - 14rem - env(safe-area-inset-bottom) - 7rem)'
|
? 'height: calc(100vh - 14rem - env(safe-area-inset-bottom) - 7rem)'
|
||||||
: 'height: calc(100vh - 14rem - env(safe-area-inset-bottom)'
|
: 'height: calc(100vh - 14rem - env(safe-area-inset-bottom)'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -62,23 +62,50 @@ export function useDynamicHeaderTab() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 在组件卸载时取消注册
|
// 注册函数
|
||||||
onUnmounted(() => {
|
const doRegister = () => {
|
||||||
|
// 确保路由路径是最新的
|
||||||
|
tabConfig.routePath = route.path
|
||||||
|
|
||||||
|
if (registerDynamicHeaderTab) {
|
||||||
|
registerDynamicHeaderTab(tabConfig)
|
||||||
|
} else if (typeof window !== 'undefined') {
|
||||||
|
// 使用全局方法作为备用
|
||||||
|
const globalRegister = (window as any).__VUE_INJECT_DYNAMIC_HEADER_TAB__
|
||||||
|
if (globalRegister) {
|
||||||
|
globalRegister(tabConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消注册函数
|
||||||
|
const doUnregister = () => {
|
||||||
if (unregisterDynamicHeaderTab) {
|
if (unregisterDynamicHeaderTab) {
|
||||||
unregisterDynamicHeaderTab()
|
unregisterDynamicHeaderTab()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始注册(延迟到下个tick,确保路由已经完全切换)
|
||||||
|
nextTick(() => {
|
||||||
|
doRegister()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 初始注册
|
// 处理页面激活时重新注册(支持keep-alive缓存的页面)
|
||||||
if (registerDynamicHeaderTab) {
|
onActivated(() => {
|
||||||
registerDynamicHeaderTab(tabConfig)
|
nextTick(() => {
|
||||||
} else if (typeof window !== 'undefined') {
|
doRegister()
|
||||||
// 使用全局方法作为备用
|
})
|
||||||
const globalRegister = (window as any).__VUE_INJECT_DYNAMIC_HEADER_TAB__
|
})
|
||||||
if (globalRegister) {
|
|
||||||
globalRegister(tabConfig)
|
// 处理页面失活时取消注册(支持keep-alive缓存的页面)
|
||||||
}
|
onDeactivated(() => {
|
||||||
}
|
doUnregister()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在组件卸载时取消注册
|
||||||
|
onUnmounted(() => {
|
||||||
|
doUnregister()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消注册
|
// 取消注册
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ const dynamicHeaderTab = ref<DynamicHeaderTab | null>(null)
|
|||||||
const registerDynamicHeaderTab = (tab: DynamicHeaderTab) => {
|
const registerDynamicHeaderTab = (tab: DynamicHeaderTab) => {
|
||||||
// 保存注册标签页的路由路径
|
// 保存注册标签页的路由路径
|
||||||
tab.routePath = route.path
|
tab.routePath = route.path
|
||||||
dynamicHeaderTab.value = tab
|
// 强制更新,确保响应式系统能检测到变化
|
||||||
|
dynamicHeaderTab.value = { ...tab }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提供一个方法让其他组件取消注册动态标签页
|
// 提供一个方法让其他组件取消注册动态标签页
|
||||||
@@ -123,11 +124,17 @@ provide('unregisterDynamicHeaderTab', unregisterDynamicHeaderTab)
|
|||||||
// 监听路由变化来清除动态标签页
|
// 监听路由变化来清除动态标签页
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
newPath => {
|
(newPath, oldPath) => {
|
||||||
// 当路由变化时,清除动态标签页(如果不是同一个路由注册的)
|
// 使用nextTick确保新页面的组件已经挂载完成
|
||||||
if (dynamicHeaderTab.value && dynamicHeaderTab.value.routePath !== newPath) {
|
nextTick(() => {
|
||||||
dynamicHeaderTab.value = null
|
// 延迟一小段时间,让新页面有机会注册标签页
|
||||||
}
|
setTimeout(() => {
|
||||||
|
// 如果当前标签页不属于新路由,则清除
|
||||||
|
if (dynamicHeaderTab.value && dynamicHeaderTab.value.routePath !== route.path) {
|
||||||
|
dynamicHeaderTab.value = null
|
||||||
|
}
|
||||||
|
}, 50) // 减少延迟时间,但仍然给新页面注册机会
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
@@ -138,7 +145,7 @@ const showDynamicHeaderTab = computed(() => {
|
|||||||
dynamicHeaderTab.value &&
|
dynamicHeaderTab.value &&
|
||||||
dynamicHeaderTab.value.items.length > 0 &&
|
dynamicHeaderTab.value.items.length > 0 &&
|
||||||
// 确保只在注册的路由路径下显示标签页
|
// 确保只在注册的路由路径下显示标签页
|
||||||
(!dynamicHeaderTab.value.routePath || dynamicHeaderTab.value.routePath === route.path)
|
dynamicHeaderTab.value.routePath === route.path
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -286,7 +293,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VerticalNavLayout>
|
<VerticalNavLayout :style="{ '--navbar-height': showDynamicHeaderTab ? '2.5rem' : '0px' }">
|
||||||
<!-- 👉 Navbar -->
|
<!-- 👉 Navbar -->
|
||||||
<template #navbar="{ toggleVerticalOverlayNavActive }">
|
<template #navbar="{ toggleVerticalOverlayNavActive }">
|
||||||
<div class="d-flex h-100 align-center mx-1">
|
<div class="d-flex h-100 align-center mx-1">
|
||||||
@@ -349,16 +356,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template #after-vertical-nav-items />
|
<template #after-vertical-nav-items />
|
||||||
|
|
||||||
<!-- 👉 下拉跟随动画 -->
|
<!-- 👉 Dynamic Header Tab -->
|
||||||
<div
|
<template #dynamic-header-tab>
|
||||||
class="main-content-wrapper"
|
<div v-if="showDynamicHeaderTab">
|
||||||
:style="{
|
|
||||||
transform: contentTransform,
|
|
||||||
transition: contentTransition,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<!-- 👉 Dynamic Header Tab -->
|
|
||||||
<div v-if="showDynamicHeaderTab" class="dynamic-header-tab-container">
|
|
||||||
<HeaderTab
|
<HeaderTab
|
||||||
:items="dynamicHeaderTab!.items"
|
:items="dynamicHeaderTab!.items"
|
||||||
:model-value="dynamicHeaderTab!.modelValue"
|
:model-value="dynamicHeaderTab!.modelValue"
|
||||||
@@ -380,7 +380,17 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</HeaderTab>
|
</HeaderTab>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 👉 下拉跟随动画 -->
|
||||||
|
<div
|
||||||
|
class="main-content-wrapper"
|
||||||
|
:style="{
|
||||||
|
transform: contentTransform,
|
||||||
|
transition: contentTransition,
|
||||||
|
paddingTop: showDynamicHeaderTab ? '3rem' : '0px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -122,8 +122,6 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-block-end: 16px;
|
|
||||||
padding-block: 8px;
|
|
||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { DownloaderConf } from '@/api/types'
|
|||||||
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
|
import DownloadingListView from '@/views/reorganize/DownloadingListView.vue'
|
||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const activeTab = ref(route.query.tab)
|
const activeTab = ref<string>((route.query.tab as string) || '')
|
||||||
|
|
||||||
// 下载器
|
// 下载器
|
||||||
const downloaders = ref<DownloaderConf[]>([])
|
const downloaders = ref<DownloaderConf[]>([])
|
||||||
@@ -22,6 +23,9 @@ const downloaderItems = computed(() => {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 使用动态标签页
|
||||||
|
const { registerHeaderTab } = useDynamicHeaderTab()
|
||||||
|
|
||||||
// 调用API查询下载器设置
|
// 调用API查询下载器设置
|
||||||
async function loadDownloaderSetting() {
|
async function loadDownloaderSetting() {
|
||||||
try {
|
try {
|
||||||
@@ -33,19 +37,30 @@ async function loadDownloaderSetting() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 注册动态标签页
|
||||||
|
const registerTabs = () => {
|
||||||
|
if (downloaderItems.value.length > 0) {
|
||||||
|
registerHeaderTab({
|
||||||
|
items: downloaderItems.value,
|
||||||
|
modelValue: activeTab,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadDownloaderSetting()
|
await loadDownloaderSetting()
|
||||||
|
registerTabs()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
loadDownloaderSetting()
|
await loadDownloaderSetting()
|
||||||
|
registerTabs()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="downloaders.length > 0">
|
<div v-if="downloaders.length > 0">
|
||||||
<VHeaderTab :items="downloaderItems" v-model="activeTab" />
|
<VWindow v-model="activeTab" class="disable-tab-transition" :touch="false">
|
||||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
|
||||||
<VWindowItem v-for="item in downloaders" :value="item.name">
|
<VWindowItem v-for="item in downloaders" :value="item.name">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+49
-38
@@ -4,6 +4,7 @@ import { RecommendSource } from '@/api/types'
|
|||||||
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||||
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ async function saveConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 标签图标映射
|
// 标签图标映射
|
||||||
const categoryItems: Record<string, string>[] = [
|
const categoryItems = computed(() => [
|
||||||
{
|
{
|
||||||
title: t('recommend.all'),
|
title: t('recommend.all'),
|
||||||
icon: 'mdi-filmstrip-box-multiple',
|
icon: 'mdi-filmstrip-box-multiple',
|
||||||
@@ -191,7 +192,10 @@ const categoryItems: Record<string, string>[] = [
|
|||||||
icon: 'mdi-trophy',
|
icon: 'mdi-trophy',
|
||||||
tab: t('recommend.categoryRankings'),
|
tab: t('recommend.categoryRankings'),
|
||||||
},
|
},
|
||||||
]
|
])
|
||||||
|
|
||||||
|
// 使用动态标签页
|
||||||
|
const { registerHeaderTab } = useDynamicHeaderTab()
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
await loadConfig()
|
await loadConfig()
|
||||||
@@ -199,6 +203,23 @@ onBeforeMount(async () => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadExtraRecommendSources()
|
await loadExtraRecommendSources()
|
||||||
|
|
||||||
|
// 注册动态标签页
|
||||||
|
registerHeaderTab({
|
||||||
|
items: categoryItems.value,
|
||||||
|
modelValue: currentCategory,
|
||||||
|
appendButtons: [
|
||||||
|
{
|
||||||
|
icon: 'mdi-tune',
|
||||||
|
variant: 'text',
|
||||||
|
color: 'grey',
|
||||||
|
class: 'settings-icon-button',
|
||||||
|
action: () => {
|
||||||
|
dialog.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
@@ -208,20 +229,6 @@ onActivated(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mp-recommend">
|
<div class="mp-recommend">
|
||||||
<!-- 页面顶部控制栏 -->
|
|
||||||
<VHeaderTab :items="categoryItems" v-model="currentCategory">
|
|
||||||
<template #append>
|
|
||||||
<VBtn
|
|
||||||
icon="mdi-tune"
|
|
||||||
variant="text"
|
|
||||||
color="grey"
|
|
||||||
size="default"
|
|
||||||
class="settings-icon-button"
|
|
||||||
@click="dialog = true"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</VHeaderTab>
|
|
||||||
|
|
||||||
<!-- 滚动内容区域 -->
|
<!-- 滚动内容区域 -->
|
||||||
<div class="recommend-content">
|
<div class="recommend-content">
|
||||||
<TransitionGroup name="fade">
|
<TransitionGroup name="fade">
|
||||||
@@ -362,12 +369,6 @@ onActivated(async () => {
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-label {
|
|
||||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-item {
|
.setting-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -399,37 +400,47 @@ onActivated(async () => {
|
|||||||
&.动漫::before {
|
&.动漫::before {
|
||||||
background-color: #ff9800;
|
background-color: #ff9800;
|
||||||
} // Orange
|
} // Orange
|
||||||
&.榜单::before {
|
&.排行榜::before {
|
||||||
background-color: #9c27b0;
|
background-color: #9c27b0;
|
||||||
} // Purple
|
} // Purple
|
||||||
|
|
||||||
&:hover {
|
&.enabled {
|
||||||
border-color: rgba(var(--v-theme-on-surface), 0.15);
|
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||||
background-color: rgba(var(--v-theme-surface-variant), 0.6);
|
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.enabled {
|
&:hover {
|
||||||
border-color: rgba(var(--v-theme-primary), 0.5);
|
box-shadow: 0 4px 12px rgba(var(--v-theme-on-surface), 0.1);
|
||||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
transform: translateY(-2px);
|
||||||
|
|
||||||
.setting-label {
|
|
||||||
color: rgb(var(--v-theme-primary));
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item-inner {
|
.setting-item-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-check {
|
.setting-check {
|
||||||
margin-inline-end: 8px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove old tune button styles if they exist */
|
.setting-label {
|
||||||
.tune-button {
|
flex: 1;
|
||||||
display: none; // Hide the old button definitively
|
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enabled .setting-label {
|
||||||
|
color: rgba(var(--v-theme-primary), 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.settings-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+18
-3
@@ -13,17 +13,32 @@ import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue
|
|||||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||||
import AccountSettingCache from '@/views/setting/AccountSettingCache.vue'
|
import AccountSettingCache from '@/views/setting/AccountSettingCache.vue'
|
||||||
import { getSettingTabs } from '@/router/i18n-menu'
|
import { getSettingTabs } from '@/router/i18n-menu'
|
||||||
|
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const activeTab = ref(route.query.tab)
|
const activeTab = ref((route.query.tab as string) || '')
|
||||||
const settingTabs = computed(() => getSettingTabs())
|
const settingTabs = computed(() => getSettingTabs())
|
||||||
|
|
||||||
|
// 使用动态标签页
|
||||||
|
const { registerHeaderTab } = useDynamicHeaderTab()
|
||||||
|
|
||||||
|
// 注册动态标签页
|
||||||
|
onMounted(() => {
|
||||||
|
// 设置初始activeTab值
|
||||||
|
if (!activeTab.value && settingTabs.value.length > 0) {
|
||||||
|
activeTab.value = settingTabs.value[0].tab
|
||||||
|
}
|
||||||
|
registerHeaderTab({
|
||||||
|
items: settingTabs.value,
|
||||||
|
modelValue: activeTab,
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VHeaderTab :items="settingTabs" v-model="activeTab" />
|
<VWindow v-model="activeTab" class="disable-tab-transition" :touch="false">
|
||||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
|
||||||
<!-- 系统 -->
|
<!-- 系统 -->
|
||||||
<VWindowItem value="system">
|
<VWindowItem value="system">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useDynamicButton } from '@/composables/useDynamicButton'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue'
|
import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
|
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -33,6 +34,81 @@ const activeTab = ref('installed')
|
|||||||
// 获取插件标签页
|
// 获取插件标签页
|
||||||
const pluginTabs = computed(() => getPluginTabs())
|
const pluginTabs = computed(() => getPluginTabs())
|
||||||
|
|
||||||
|
// 使用动态标签页
|
||||||
|
const { registerHeaderTab } = useDynamicHeaderTab()
|
||||||
|
|
||||||
|
// 注册动态标签页(在setup顶层立即执行)
|
||||||
|
registerHeaderTab({
|
||||||
|
items: pluginTabs.value,
|
||||||
|
modelValue: activeTab,
|
||||||
|
appendButtons: [
|
||||||
|
{
|
||||||
|
icon: 'mdi-filter-multiple-outline',
|
||||||
|
variant: 'text',
|
||||||
|
color: computed(() =>
|
||||||
|
installedFilter.value || hasUpdateFilter.value || enabledFilter.value ? 'primary' : 'gray',
|
||||||
|
),
|
||||||
|
class: 'settings-icon-button',
|
||||||
|
dataAttr: 'installed-filter-btn',
|
||||||
|
action: () => {
|
||||||
|
filterInstalledPluginDialog.value = true
|
||||||
|
},
|
||||||
|
show: computed(() => activeTab.value === 'installed'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-filter-multiple-outline',
|
||||||
|
variant: 'text',
|
||||||
|
color: computed(() => (isFilterFormEmpty.value ? 'gray' : 'primary')),
|
||||||
|
class: 'settings-icon-button',
|
||||||
|
dataAttr: 'market-filter-btn',
|
||||||
|
action: () => {
|
||||||
|
filterMarketPluginDialog.value = true
|
||||||
|
},
|
||||||
|
show: computed(() => activeTab.value === 'market'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-refresh',
|
||||||
|
variant: 'text',
|
||||||
|
color: 'gray',
|
||||||
|
class: 'settings-icon-button',
|
||||||
|
action: () => {
|
||||||
|
refreshMarket()
|
||||||
|
},
|
||||||
|
show: computed(() => activeTab.value === 'market'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-store-cog',
|
||||||
|
variant: 'text',
|
||||||
|
color: 'gray',
|
||||||
|
class: 'settings-icon-button',
|
||||||
|
action: () => {
|
||||||
|
MarketSettingDialog.value = true
|
||||||
|
},
|
||||||
|
show: computed(() => activeTab.value === 'market'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-folder-plus',
|
||||||
|
variant: 'text',
|
||||||
|
color: 'gray',
|
||||||
|
class: 'settings-icon-button',
|
||||||
|
action: () => {
|
||||||
|
showNewFolderDialog()
|
||||||
|
},
|
||||||
|
show: computed(() => activeTab.value === 'installed' && !currentFolder.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-arrow-left',
|
||||||
|
variant: 'text',
|
||||||
|
color: 'gray',
|
||||||
|
class: 'settings-icon-button',
|
||||||
|
action: () => {
|
||||||
|
backToMain()
|
||||||
|
},
|
||||||
|
show: computed(() => activeTab.value === 'installed' && !!currentFolder.value),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
// 插件ID参数
|
// 插件ID参数
|
||||||
const pluginId = ref(route.query.id)
|
const pluginId = ref(route.query.id)
|
||||||
|
|
||||||
@@ -798,6 +874,7 @@ function loadMarketMore({ done }: { done: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载后
|
// 组件挂载后
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadPluginOrderConfig()
|
await loadPluginOrderConfig()
|
||||||
await loadPluginFolders() // 加载文件夹配置
|
await loadPluginFolders() // 加载文件夹配置
|
||||||
@@ -1215,173 +1292,118 @@ function onDragStartPlugin(evt: any) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VHeaderTab :items="pluginTabs" v-model="activeTab">
|
<!-- 过滤弹窗 -->
|
||||||
<template #append>
|
<Teleport to="body" v-if="filterInstalledPluginDialog">
|
||||||
<VMenu
|
<VMenu
|
||||||
v-if="activeTab === 'installed'"
|
v-model="filterInstalledPluginDialog"
|
||||||
v-model="filterInstalledPluginDialog"
|
width="20rem"
|
||||||
width="20rem"
|
:close-on-content-click="false"
|
||||||
:close-on-content-click="false"
|
:activator="'[data-menu-activator=installed-filter-btn]'"
|
||||||
scrim
|
location="bottom end"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<VCard>
|
||||||
<VBtn
|
<VCardItem>
|
||||||
icon="mdi-filter-multiple-outline"
|
<VCardTitle>
|
||||||
variant="text"
|
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
|
||||||
:color="installedFilter || hasUpdateFilter || enabledFilter ? 'primary' : 'gray'"
|
{{ t('plugin.filterPlugins') }}
|
||||||
size="default"
|
</VCardTitle>
|
||||||
class="settings-icon-button"
|
<VDialogCloseBtn @click="filterInstalledPluginDialog = false" />
|
||||||
v-bind="props"
|
</VCardItem>
|
||||||
/>
|
<VCardText>
|
||||||
</template>
|
<VRow>
|
||||||
<VCard>
|
<VCol cols="12">
|
||||||
<VCardItem>
|
<VCombobox
|
||||||
<VCardTitle>
|
v-model="installedFilter"
|
||||||
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
|
:items="installedPluginNames"
|
||||||
{{ t('plugin.filterPlugins') }}
|
:label="t('plugin.name')"
|
||||||
</VCardTitle>
|
density="comfortable"
|
||||||
<VDialogCloseBtn @click="filterInstalledPluginDialog = false" />
|
clearable
|
||||||
</VCardItem>
|
/>
|
||||||
<VCardText>
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSwitch v-model="enabledFilter" :label="t('plugin.running')" />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSwitch v-model="hasUpdateFilter" :label="t('plugin.hasNewVersion')" />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VMenu>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<Teleport to="body" v-if="filterMarketPluginDialog">
|
||||||
|
<VMenu
|
||||||
|
v-model="filterMarketPluginDialog"
|
||||||
|
width="25rem"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
:activator="'[data-menu-activator=market-filter-btn]'"
|
||||||
|
location="bottom end"
|
||||||
|
>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>
|
||||||
|
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
|
||||||
|
{{ t('plugin.filterPlugins') }}
|
||||||
|
</VCardTitle>
|
||||||
|
<VDialogCloseBtn @click="filterMarketPluginDialog = false" />
|
||||||
|
</VCardItem>
|
||||||
|
<VCardText>
|
||||||
|
<!-- 过滤表单 -->
|
||||||
|
<div v-if="isAppMarketLoaded">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12">
|
<VCol cols="6">
|
||||||
<VCombobox
|
<VTextField v-model="filterForm.name" density="comfortable" :label="t('plugin.name')" clearable />
|
||||||
v-model="installedFilter"
|
</VCol>
|
||||||
:items="installedPluginNames"
|
<VCol v-if="authorFilterOptions.length > 0" cols="6">
|
||||||
:label="t('plugin.name')"
|
<VSelect
|
||||||
|
v-model="filterForm.author"
|
||||||
|
:items="authorFilterOptions"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
|
chips
|
||||||
|
:label="t('plugin.author')"
|
||||||
|
multiple
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6">
|
<VCol v-if="labelFilterOptions.length > 0" cols="6">
|
||||||
<VSwitch v-model="enabledFilter" :label="t('plugin.running')" />
|
<VSelect
|
||||||
|
v-model="filterForm.label"
|
||||||
|
:items="labelFilterOptions"
|
||||||
|
density="comfortable"
|
||||||
|
chips
|
||||||
|
:label="t('plugin.label')"
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6">
|
<VCol v-if="repoFilterOptions.length > 0" cols="6">
|
||||||
<VSwitch v-model="hasUpdateFilter" :label="t('plugin.hasNewVersion')" />
|
<VSelect
|
||||||
|
v-model="filterForm.repo"
|
||||||
|
:items="repoFilterOptions"
|
||||||
|
density="comfortable"
|
||||||
|
chips
|
||||||
|
:label="t('plugin.repository')"
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol v-if="sortOptions.length > 0" cols="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="activeSort"
|
||||||
|
:items="sortOptions"
|
||||||
|
density="comfortable"
|
||||||
|
:label="t('plugin.sortTitle')"
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VCardText>
|
</div>
|
||||||
</VCard>
|
</VCardText>
|
||||||
</VMenu>
|
</VCard>
|
||||||
<VMenu
|
</VMenu>
|
||||||
v-if="activeTab === 'market'"
|
</Teleport>
|
||||||
v-model="filterMarketPluginDialog"
|
|
||||||
width="25rem"
|
|
||||||
:close-on-content-click="false"
|
|
||||||
scrim
|
|
||||||
>
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<VBtn
|
|
||||||
icon="mdi-filter-multiple-outline"
|
|
||||||
variant="text"
|
|
||||||
:color="isFilterFormEmpty ? 'gray' : 'primary'"
|
|
||||||
size="default"
|
|
||||||
class="settings-icon-button"
|
|
||||||
v-bind="props"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<VCard>
|
|
||||||
<VCardItem>
|
|
||||||
<VCardTitle>
|
|
||||||
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
|
|
||||||
{{ t('plugin.filterPlugins') }}
|
|
||||||
</VCardTitle>
|
|
||||||
<VDialogCloseBtn @click="filterMarketPluginDialog = false" />
|
|
||||||
</VCardItem>
|
|
||||||
<VCardText>
|
|
||||||
<!-- 过滤表单 -->
|
|
||||||
<div v-if="isAppMarketLoaded">
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="6">
|
|
||||||
<VTextField v-model="filterForm.name" density="comfortable" :label="t('plugin.name')" clearable />
|
|
||||||
</VCol>
|
|
||||||
<VCol v-if="authorFilterOptions.length > 0" cols="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="filterForm.author"
|
|
||||||
:items="authorFilterOptions"
|
|
||||||
density="comfortable"
|
|
||||||
chips
|
|
||||||
:label="t('plugin.author')"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol v-if="labelFilterOptions.length > 0" cols="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="filterForm.label"
|
|
||||||
:items="labelFilterOptions"
|
|
||||||
density="comfortable"
|
|
||||||
chips
|
|
||||||
:label="t('plugin.label')"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol v-if="repoFilterOptions.length > 0" cols="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="filterForm.repo"
|
|
||||||
:items="repoFilterOptions"
|
|
||||||
density="comfortable"
|
|
||||||
chips
|
|
||||||
:label="t('plugin.repository')"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol v-if="sortOptions.length > 0" cols="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="activeSort"
|
|
||||||
:items="sortOptions"
|
|
||||||
density="comfortable"
|
|
||||||
:label="t('plugin.sortTitle')"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VMenu>
|
|
||||||
<VBtn
|
|
||||||
v-if="activeTab === 'market'"
|
|
||||||
icon="mdi-refresh"
|
|
||||||
variant="text"
|
|
||||||
color="gray"
|
|
||||||
size="default"
|
|
||||||
class="settings-icon-button"
|
|
||||||
:loading="isMarketRefreshing"
|
|
||||||
@click="refreshMarket"
|
|
||||||
/>
|
|
||||||
<VBtn
|
|
||||||
v-if="activeTab === 'market'"
|
|
||||||
icon="mdi-store-cog"
|
|
||||||
variant="text"
|
|
||||||
color="gray"
|
|
||||||
size="default"
|
|
||||||
class="settings-icon-button"
|
|
||||||
@click="MarketSettingDialog = true"
|
|
||||||
/>
|
|
||||||
<VBtn
|
|
||||||
v-if="activeTab === 'installed' && !currentFolder"
|
|
||||||
icon="mdi-folder-plus"
|
|
||||||
variant="text"
|
|
||||||
color="gray"
|
|
||||||
size="default"
|
|
||||||
class="settings-icon-button"
|
|
||||||
@click="showNewFolderDialog"
|
|
||||||
/>
|
|
||||||
<VBtn
|
|
||||||
v-if="activeTab === 'installed' && currentFolder"
|
|
||||||
icon="mdi-arrow-left"
|
|
||||||
variant="text"
|
|
||||||
color="gray"
|
|
||||||
size="default"
|
|
||||||
class="settings-icon-button"
|
|
||||||
@click="backToMain"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</VHeaderTab>
|
|
||||||
|
|
||||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition px-2" :touch="false">
|
<VWindow v-model="activeTab" class="disable-tab-transition px-2" :touch="false">
|
||||||
<!-- 我的插件 -->
|
<!-- 我的插件 -->
|
||||||
<VWindowItem value="installed">
|
<VWindowItem value="installed">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
@@ -1626,7 +1648,3 @@ function onDragStartPlugin(evt: any) {
|
|||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
// 样式已移至 PluginMixedSortCard 组件
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ const TransferDict: { [key: string]: string } = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tableStyle = computed(() => {
|
const tableStyle = computed(() => {
|
||||||
return appMode
|
return appMode.value
|
||||||
? 'height: calc(100vh - 15rem - env(safe-area-inset-bottom) - 7rem)'
|
? 'height: calc(100vh - 15rem - env(safe-area-inset-bottom) - 7rem)'
|
||||||
: 'height: calc(100vh - 15rem - env(safe-area-inset-bottom)'
|
: 'height: calc(100vh - 15rem - env(safe-area-inset-bottom)'
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user