diff --git a/src/composables/useDynamicButton.ts b/src/composables/useDynamicButton.ts new file mode 100644 index 00000000..559a4f57 --- /dev/null +++ b/src/composables/useDynamicButton.ts @@ -0,0 +1,124 @@ +import { ref, inject, nextTick, onMounted, onActivated, onDeactivated, onUnmounted } from 'vue' + +// 声明全局变量类型 +declare global { + interface Window { + __VUE_INJECT_DYNAMIC_BUTTON__?: (button: any) => void + } +} + +/** + * 动态按钮钩子函数 + * + * @param options 配置选项 + * @returns 控制函数和状态 + * + * @example + * // 在页面中使用 + * const { openDialog } = useDynamicButton({ + * icon: 'mdi-cog', + * onClick: () => { + * dialog.value = true + * } + * }) + */ +export function useDynamicButton(options: { + icon: string + onClick: () => void + autoRegister?: boolean // 是否自动注册,默认为true +}) { + // 提取配置 + const { icon, onClick, autoRegister = true } = options + + // 动态按钮相关 + const registerDynamicButton = inject<((button: any) => void) | null>('registerDynamicButton', null) + const unregisterDynamicButton = inject<(() => void) | null>('unregisterDynamicButton', null) + + // 按钮注册状态 + const dynamicButtonRegistered = ref(false) + + // 注册动态按钮 + function setupDynamicButton() { + // 避免重复注册 + if (dynamicButtonRegistered.value) return + + // 确保注册方法存在 + if (!registerDynamicButton) { + // 尝试获取全局注册方法 + const tryUseGlobalMethod = () => { + if (typeof window !== 'undefined' && window.__VUE_INJECT_DYNAMIC_BUTTON__) { + window.__VUE_INJECT_DYNAMIC_BUTTON__({ + icon, + action: onClick, + show: true, + }) + dynamicButtonRegistered.value = true + return true + } + return false + } + + // 立即尝试一次 + if (!tryUseGlobalMethod()) { + // 如果失败,延迟再试一次 + setTimeout(tryUseGlobalMethod, 1000) + } + return + } + + // 如果注册方法存在,直接注册 + nextTick(() => { + registerDynamicButton({ + icon, + action: onClick, + show: true, + }) + dynamicButtonRegistered.value = true + }) + } + + // 取消注册动态按钮 + function cleanupDynamicButton() { + if (unregisterDynamicButton && dynamicButtonRegistered.value) { + unregisterDynamicButton() + dynamicButtonRegistered.value = false + } + } + + // 暴露方法:手动打开对话框 + function openDialog() { + onClick() + } + + // 生命周期钩子 + if (autoRegister) { + onMounted(() => { + // 延迟执行,确保Footer组件已加载 + setTimeout(() => { + setupDynamicButton() + }, 500) + }) + + onActivated(() => { + // 重置注册状态,确保每次激活时都重新注册 + dynamicButtonRegistered.value = false + setupDynamicButton() + }) + + onDeactivated(() => { + cleanupDynamicButton() + }) + + onUnmounted(() => { + cleanupDynamicButton() + }) + } + + // 返回控制函数和状态 + return { + setupDynamicButton, // 手动注册按钮 + cleanupDynamicButton, // 手动取消注册 + openDialog, // 手动触发点击事件 + isRegistered: dynamicButtonRegistered, // 注册状态 + } +} diff --git a/src/layouts/components/Footer.vue b/src/layouts/components/Footer.vue index 3a630080..2e32b4e3 100644 --- a/src/layouts/components/Footer.vue +++ b/src/layouts/components/Footer.vue @@ -26,9 +26,64 @@ watch( () => route.path, newPath => { currentMenu.value = getMenuPathFromRoute(newPath) + // 当路由变化时,清除动态按钮 + dynamicButton.value = null }, { immediate: false }, ) + +// 动态按钮相关 +// 定义动态按钮类型 +interface DynamicButton { + icon: string + action: () => void + show: boolean + routePath?: string // 添加路径属性,用于标识哪个路由注册的 +} + +// 提供动态按钮注册和获取的方法 +const dynamicButton = ref(null) + +// 提供一个方法让其他组件注册动态按钮 +const registerDynamicButton = (button: DynamicButton) => { + // 保存注册按钮的路由路径 + button.routePath = route.path + dynamicButton.value = button +} + +// 提供一个方法让其他组件取消注册动态按钮 +const unregisterDynamicButton = () => { + dynamicButton.value = null +} + +// 添加全局注册方法,解决注入不可用的问题 +if (typeof window !== 'undefined') { + // 确保在浏览器环境中 + ;(window as any).__VUE_INJECT_DYNAMIC_BUTTON__ = registerDynamicButton +} + +// 提供给其他组件使用 +provide('registerDynamicButton', registerDynamicButton) +provide('unregisterDynamicButton', unregisterDynamicButton) + +// 在组件销毁时清理 +onUnmounted(() => { + dynamicButton.value = null + // 清理全局方法 + if (typeof window !== 'undefined') { + delete (window as any).__VUE_INJECT_DYNAMIC_BUTTON__ + } +}) + +// 显示动态按钮 +const showDynamicButton = computed(() => { + return ( + dynamicButton.value && + dynamicButton.value.show && + // 确保只在注册的路由路径下显示按钮 + (!dynamicButton.value.routePath || dynamicButton.value.routePath === route.path) + ) +}) @@ -91,7 +163,13 @@ watch( padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)); display: flex; justify-content: center; + align-items: center; pointer-events: none; + + // 按钮卡片之间的间距 + > .v-card + .v-card { + margin-left: 4px; + } } .footer-nav-card { @@ -103,6 +181,32 @@ watch( position: relative; } +// 动态按钮卡片样式 +.dynamic-btn-card { + min-height: 0; + height: auto; + width: auto; + + .footer-card-content { + padding: 3px; + } + + .footer-nav-btn { + min-width: 40px; + width: 40px; + height: 40px; + padding: 0; + + .btn-content { + margin: 0; + } + + .v-icon { + margin-bottom: 0; + } + } +} + .footer-card-content { padding: 6px 8px; position: relative; diff --git a/src/pages/dashboard.vue b/src/pages/dashboard.vue index 6a818d46..063a88ba 100644 --- a/src/pages/dashboard.vue +++ b/src/pages/dashboard.vue @@ -6,6 +6,7 @@ import { DashboardItem } from '@/api/types' import { useUserStore } from '@/stores' import DashboardElement from '@/components/misc/DashboardElement.vue' import { useDisplay } from 'vuetify' +import { useDynamicButton } from '@/composables/useDynamicButton' // APP const display = useDisplay() @@ -141,6 +142,14 @@ const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({}) // 弹窗 const dialog = ref(false) +// 使用动态按钮钩子 +useDynamicButton({ + icon: 'mdi-view-dashboard-edit', + onClick: () => { + dialog.value = true + }, +}) + // 加载用户监控面板配置(本地无配置时才加载) async function loadDashboardConfig() { // 显示配置 @@ -297,7 +306,7 @@ onBeforeMount(async () => { getPluginDashboardMeta() }) -onActivated(async () => { +onActivated(() => { isRequest.value = true }) @@ -327,8 +336,9 @@ onDeactivated(() => { - + { app appear @click="dialog = true" - :class="{ 'mb-12': appMode }" />