mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-07 08:40:46 +08:00
refactor: enhance dynamic button system to support menus, reactive properties, and improved PWA floating action button integration
This commit is contained in:
@@ -17,7 +17,7 @@ import { usePWA } from '@/composables/usePWA'
|
||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -79,26 +79,6 @@ registerHeaderTab({
|
||||
},
|
||||
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',
|
||||
@@ -915,12 +895,56 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 使用动态按钮钩子
|
||||
function openPluginSearchDialog() {
|
||||
SearchDialog.value = true
|
||||
}
|
||||
|
||||
function openMarketSettingDialog() {
|
||||
MarketSettingDialog.value = true
|
||||
}
|
||||
|
||||
const showSearchAction = computed(() => activeTab.value === 'installed' || activeTab.value === 'market')
|
||||
const showNewFolderAction = computed(() => activeTab.value === 'installed' && !currentFolder.value)
|
||||
const showMarketSettingAction = computed(() => activeTab.value === 'market')
|
||||
|
||||
const pluginDynamicMenuItems = computed(() => {
|
||||
locale.value
|
||||
|
||||
if (!appMode.value) return undefined
|
||||
if (!showSearchAction.value) return undefined
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: t('plugin.searchPlugins'),
|
||||
icon: 'mdi-magnify',
|
||||
action: openPluginSearchDialog,
|
||||
},
|
||||
]
|
||||
|
||||
if (showNewFolderAction.value) {
|
||||
items.push({
|
||||
title: t('plugin.newFolder'),
|
||||
icon: 'mdi-folder-plus',
|
||||
action: showNewFolderDialog,
|
||||
})
|
||||
}
|
||||
|
||||
if (showMarketSettingAction.value) {
|
||||
items.push({
|
||||
title: t('components.pluginMarketSetting.title'),
|
||||
icon: 'mdi-store-cog',
|
||||
action: openMarketSettingDialog,
|
||||
})
|
||||
}
|
||||
|
||||
return items.length > 1 ? items : undefined
|
||||
})
|
||||
|
||||
useDynamicButton({
|
||||
icon: 'mdi-magnify',
|
||||
onClick: () => {
|
||||
SearchDialog.value = true
|
||||
},
|
||||
onClick: openPluginSearchDialog,
|
||||
menuItems: pluginDynamicMenuItems,
|
||||
show: computed(() => appMode.value && showSearchAction.value && isRefreshed.value),
|
||||
})
|
||||
|
||||
// 获取插件文件夹配置
|
||||
@@ -1585,9 +1609,8 @@ function onDragStartPlugin(evt: any) {
|
||||
|
||||
<!-- 插件搜索图标 -->
|
||||
<Teleport to="body" v-if="route.path === '/plugins'">
|
||||
<div v-if="isRefreshed">
|
||||
<div v-if="isRefreshed && !appMode && showSearchAction">
|
||||
<VFab
|
||||
v-if="!appMode"
|
||||
icon="mdi-magnify"
|
||||
color="info"
|
||||
location="bottom"
|
||||
@@ -1595,8 +1618,31 @@ function onDragStartPlugin(evt: any) {
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="SearchDialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
@click="openPluginSearchDialog"
|
||||
/>
|
||||
<VFab
|
||||
v-if="showNewFolderAction"
|
||||
icon="mdi-folder-plus"
|
||||
color="primary"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
class="mb-16"
|
||||
@click="showNewFolderDialog"
|
||||
/>
|
||||
<VFab
|
||||
v-if="showMarketSettingAction"
|
||||
icon="mdi-store-cog"
|
||||
color="warning"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
class="mb-16"
|
||||
@click="openMarketSettingDialog"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -12,70 +12,13 @@ import { useDisplay } from 'vuetify'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
|
||||
// i18n
|
||||
const { t } = useI18n()
|
||||
|
||||
// 动态按钮相关
|
||||
interface DynamicButton {
|
||||
icon: string
|
||||
action: () => void
|
||||
show: boolean
|
||||
routePath?: string
|
||||
menuItems?: {
|
||||
title: string
|
||||
icon?: string
|
||||
color?: string
|
||||
action: () => void
|
||||
}[]
|
||||
}
|
||||
|
||||
const injectedRegisterDynamicButton = inject<((button: DynamicButton) => void) | null>('registerDynamicButton', null)
|
||||
const injectedUnregisterDynamicButton = inject<(() => void) | null>('unregisterDynamicButton', null)
|
||||
|
||||
function registerHistoryDynamicButton(show: boolean) {
|
||||
const button: DynamicButton = {
|
||||
icon: 'mdi-chevron-up',
|
||||
show,
|
||||
action: () => {},
|
||||
menuItems: [
|
||||
{
|
||||
title: t('transferHistory.actions.batchRedo'),
|
||||
icon: 'mdi-redo-variant',
|
||||
action: () => {
|
||||
retransferBatch()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('transferHistory.actions.batchDelete'),
|
||||
icon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
action: () => {
|
||||
removeHistoryBatch()
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
if (injectedRegisterDynamicButton) {
|
||||
injectedRegisterDynamicButton(button)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && (window as any).__VUE_INJECT_DYNAMIC_BUTTON__) {
|
||||
;(window as any).__VUE_INJECT_DYNAMIC_BUTTON__(button)
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterHistoryDynamicButton() {
|
||||
if (injectedUnregisterDynamicButton) {
|
||||
injectedUnregisterDynamicButton()
|
||||
return
|
||||
}
|
||||
}
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
// 全局设置
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
@@ -698,32 +641,55 @@ const toggleGroupSelection = (checked: boolean | null, items: readonly any[]) =>
|
||||
}
|
||||
}
|
||||
|
||||
// 监听选中项变化,更新动态按钮
|
||||
watch(
|
||||
() => selected.value.length,
|
||||
newLength => {
|
||||
if (appMode) {
|
||||
registerHistoryDynamicButton(newLength > 0)
|
||||
}
|
||||
const historyDynamicIcon = computed(() => (selected.value.length > 0 ? 'mdi-chevron-up' : 'mdi-timer-sand-paused'))
|
||||
const historyDynamicMenuItems = computed(() => {
|
||||
locale.value
|
||||
|
||||
if (selected.value.length === 0) return undefined
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('components.transferQueue.title'),
|
||||
icon: 'mdi-timer-sand-paused',
|
||||
action: () => {
|
||||
transferQueueDialog.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('transferHistory.actions.batchRedo'),
|
||||
icon: 'mdi-redo-variant',
|
||||
action: () => {
|
||||
retransferBatch()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('transferHistory.actions.batchDelete'),
|
||||
icon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
action: () => {
|
||||
removeHistoryBatch()
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
useDynamicButton({
|
||||
icon: historyDynamicIcon,
|
||||
onClick: () => {
|
||||
transferQueueDialog.value = true
|
||||
},
|
||||
)
|
||||
menuItems: historyDynamicMenuItems,
|
||||
show: computed(() => appMode.value),
|
||||
})
|
||||
|
||||
// 初始加载数据
|
||||
onMounted(() => {
|
||||
loadStorages()
|
||||
fetchData()
|
||||
|
||||
// 仅在 Docker 模式下注册动态按钮
|
||||
if (appMode) {
|
||||
registerHistoryDynamicButton(selected.value.length > 0)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAiRedoProgress()
|
||||
if (appMode) {
|
||||
unregisterHistoryDynamicButton()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -754,7 +720,6 @@ onUnmounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="4" md="6" class="text-end">
|
||||
<VBtnGroup variant="outlined" divided rounded>
|
||||
<VBtn icon="mdi-timer-sand-paused" @click="transferQueueDialog = true" />
|
||||
<VBtn :icon="group ? 'mdi-format-list-bulleted' : 'mdi-format-list-group'" @click="group = !group" />
|
||||
</VBtnGroup>
|
||||
</VCol>
|
||||
@@ -1013,10 +978,33 @@ onUnmounted(() => {
|
||||
<!-- 整理队列进度弹窗 -->
|
||||
<TransferQueueDialog v-if="transferQueueDialog" v-model="transferQueueDialog" @close="transferQueueDialog = false" />
|
||||
|
||||
<!-- 非 Docker 模式下的 FAB 按钮 -->
|
||||
<!-- 非 app 模式下的 FAB 按钮 -->
|
||||
<Teleport to="body" v-if="!appMode && route.path === '/history'">
|
||||
<div v-if="isRefreshed && selected.length > 0">
|
||||
<div v-if="isRefreshed">
|
||||
<VFab
|
||||
icon="mdi-timer-sand-paused"
|
||||
color="info"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="transferQueueDialog = true"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
class="mb-16"
|
||||
icon="mdi-redo-variant"
|
||||
color="primary"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
location="bottom"
|
||||
@@ -1025,17 +1013,7 @@ onUnmounted(() => {
|
||||
app
|
||||
appear
|
||||
@click="removeHistoryBatch"
|
||||
class="mb-16"
|
||||
/>
|
||||
<VFab
|
||||
class="mb-32"
|
||||
icon="mdi-redo-variant"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -6,21 +6,13 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
|
||||
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -185,6 +177,10 @@ function historyDone() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function openHistoryDialog() {
|
||||
historyDialog.value = true
|
||||
}
|
||||
|
||||
// 批量管理相关函数
|
||||
// 切换批量模式
|
||||
function toggleBatchMode() {
|
||||
@@ -381,12 +377,8 @@ onActivated(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 使用动态按钮钩子
|
||||
useDynamicButton({
|
||||
icon: 'mdi-history',
|
||||
onClick: () => {
|
||||
historyDialog.value = true
|
||||
},
|
||||
defineExpose({
|
||||
openHistoryDialog,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -477,23 +469,6 @@ useDynamicButton({
|
||||
:error-title="errorTitle"
|
||||
:error-description="errorDescription"
|
||||
/>
|
||||
<!-- 底部操作按钮 -->
|
||||
<Teleport to="body" v-if="route.path.startsWith(`/subscribe/${props.type === '电影' ? 'movie' : 'tv'}`)">
|
||||
<div v-if="isRefreshed">
|
||||
<VFab
|
||||
v-if="userStore.superUser && !appMode"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="historyDialog = true"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
<!-- 历史记录弹窗 -->
|
||||
<SubscribeHistoryDialog
|
||||
v-if="historyDialog"
|
||||
|
||||
@@ -5,8 +5,6 @@ import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue
|
||||
import WorkflowTaskCard from '@/components/cards/WorkflowTaskCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -14,12 +12,6 @@ const { t } = useI18n()
|
||||
// 是否刷新
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
|
||||
// 新增对话框
|
||||
const addDialog = ref(false)
|
||||
|
||||
@@ -54,14 +46,6 @@ function addDone() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 使用动态按钮钩子 新增
|
||||
useDynamicButton({
|
||||
icon: 'mdi-plus',
|
||||
onClick: () => {
|
||||
addDialog.value = true
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadEventTypes()
|
||||
fetchData()
|
||||
@@ -70,6 +54,14 @@ onMounted(() => {
|
||||
onActivated(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
function openAddDialog() {
|
||||
addDialog.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openAddDialog,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
@@ -83,20 +75,6 @@ onActivated(() => {
|
||||
:error-title="t('workflow.noWorkflow')"
|
||||
:error-description="t('workflow.noWorkflowDescription')"
|
||||
/>
|
||||
<!-- 新增按钮 -->
|
||||
<Teleport to="body" v-if="route.path === '/workflow'">
|
||||
<VFab
|
||||
v-if="isRefreshed && !appMode"
|
||||
icon="mdi-plus"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
:class="{ 'mb-12': appMode }"
|
||||
@click="addDialog = true"
|
||||
/>
|
||||
</Teleport>
|
||||
<!-- 新增对话框 -->
|
||||
<WorkflowAddEditDialog v-if="addDialog" v-model="addDialog" @close="addDialog = false" @save="addDone" />
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ const page = ref(1)
|
||||
|
||||
// 搜索关键字
|
||||
const keyword = ref(props.keyword)
|
||||
const currentKey = ref(0)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
@@ -53,6 +54,17 @@ async function loadEventTypes() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.keyword,
|
||||
newKeyword => {
|
||||
keyword.value = newKeyword || ''
|
||||
page.value = 1
|
||||
dataList.value = []
|
||||
isRefreshed.value = false
|
||||
currentKey.value++
|
||||
},
|
||||
)
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
@@ -141,7 +153,7 @@ onActivated(() => {
|
||||
<template>
|
||||
<VPageContentTitle v-if="keyword" :title="`${t('common.search')}:${keyword}`" />
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-2" @load="fetchData">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-2" @load="fetchData" :key="currentKey">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-workflow-share-card" tabindex="0">
|
||||
|
||||
Reference in New Issue
Block a user