Files
MoviePilot-Frontend/src/layouts/components/ShortcutBar.vue
2026-06-09 21:45:51 +08:00

234 lines
7.2 KiB
Vue

<script lang="ts" setup>
import type { Component } from 'vue'
import { getQueryValue } from '@/@core/utils'
import { openSharedDialog } from '@/composables/useSharedDialog'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores'
import { buildUserPermissionContext, filterItemsByPermission, hasItemPermission, type PermissionProtectedItem } from '@/utils/permission'
// 国际化
const { t } = useI18n()
const userStore = useUserStore()
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
// 快捷工具只在弹窗打开时使用,按需加载避免默认布局首屏带上所有 system 视图。
const NameTestView = defineAsyncComponent(() => import('@/views/system/NameTestView.vue'))
const NetTestView = defineAsyncComponent(() => import('@/views/system/NetTestView.vue'))
const RuleTestView = defineAsyncComponent(() => import('@/views/system/RuleTestView.vue'))
const ModuleTestView = defineAsyncComponent(() => import('@/views/system/ModuleTestView.vue'))
const WordsView = defineAsyncComponent(() => import('@/views/system/WordsView.vue'))
const CacheView = defineAsyncComponent(() => import('@/views/system/CacheView.vue'))
const AccountSettingService = defineAsyncComponent(() => import('@/views/system/ServiceView.vue'))
const ShortcutLogDialog = defineAsyncComponent(() => import('@/components/dialog/ShortcutLogDialog.vue'))
const ShortcutMessageDialog = defineAsyncComponent(() => import('@/components/dialog/ShortcutMessageDialog.vue'))
const ShortcutToolDialog = defineAsyncComponent(() => import('@/components/dialog/ShortcutToolDialog.vue'))
type ShortcutItem = PermissionProtectedItem & {
bodyClass?: string
cardClass?: string
component?: Component
customDialog?: Component
dialog: string
dialogSubtitle?: string
icon: string
maxWidth?: string
subtitle: string
title: string
titleText?: string
}
// App捷径
const appsMenu = ref(false)
// 菜单最大宽度
const menuMaxWidth = ref(420)
// 定义捷径列表
const shortcuts: ShortcutItem[] = [
{
title: t('shortcut.recognition.title'),
subtitle: t('shortcut.recognition.subtitle'),
icon: 'mdi-text-recognition',
dialog: 'nameTest',
component: NameTestView,
maxWidth: '45rem',
titleText: t('shortcut.recognition.title'),
},
{
title: t('shortcut.rule.title'),
subtitle: t('shortcut.rule.subtitle'),
icon: 'mdi-filter-cog',
dialog: 'ruleTest',
component: RuleTestView,
titleText: t('shortcut.rule.subtitle'),
},
{
title: t('shortcut.log.title'),
subtitle: t('shortcut.log.subtitle'),
icon: 'mdi-file-document',
dialog: 'logging',
customDialog: ShortcutLogDialog,
},
{
title: t('shortcut.network.title'),
subtitle: t('shortcut.network.subtitle'),
icon: 'mdi-network',
dialog: 'netTest',
component: NetTestView,
titleText: t('shortcut.network.subtitle'),
},
{
title: t('shortcut.words.title'),
subtitle: t('shortcut.words.subtitle'),
icon: 'mdi-file-word-box',
dialog: 'words',
component: WordsView,
maxWidth: '60rem',
titleText: t('shortcut.words.subtitle'),
},
{
title: t('shortcut.cache.title'),
subtitle: t('shortcut.cache.subtitle'),
icon: 'mdi-database',
dialog: 'cache',
component: CacheView,
maxWidth: '90rem',
titleText: t('shortcut.cache.subtitle'),
},
{
title: t('shortcut.scheduler.title'),
subtitle: t('shortcut.scheduler.subtitle'),
icon: 'mdi-list-box',
dialog: 'scheduler',
bodyClass: 'pa-0',
component: AccountSettingService,
maxWidth: '60rem',
titleText: t('shortcut.scheduler.subtitle'),
dialogSubtitle: t('setting.scheduler.subtitle'),
},
{
title: t('shortcut.system.title'),
subtitle: t('shortcut.system.subtitle'),
icon: 'mdi-cog',
dialog: 'systemTest',
bodyClass: 'system-health-dialog-body pa-0',
cardClass: 'system-health-dialog-card',
component: ModuleTestView,
titleText: t('shortcut.system.subtitle'),
},
{
title: t('shortcut.message.title'),
subtitle: t('shortcut.message.subtitle'),
icon: 'mdi-message',
dialog: 'message',
customDialog: ShortcutMessageDialog,
},
].map(item => ({ ...item, permission: 'admin' }))
const visibleShortcuts = computed(() => filterItemsByPermission(shortcuts, userPermissions.value))
/** 打开快捷工具对应的共享弹窗。 */
function openShortcutDialog(item: (typeof shortcuts)[number]) {
if (!hasItemPermission(item, userPermissions.value)) return
appsMenu.value = false
if (item.customDialog) {
openSharedDialog(item.customDialog, {}, {}, { closeOn: ['close', 'update:modelValue'] })
return
}
if (!item.component) return
openSharedDialog(
ShortcutToolDialog,
{
bodyClass: item.bodyClass,
cardClass: item.cardClass,
icon: item.icon,
maxWidth: item.maxWidth ?? '35rem',
subtitle: item.dialogSubtitle,
title: item.titleText ?? item.title,
view: item.component,
},
{},
{ closeOn: ['close', 'update:modelValue'] },
)
}
/** 供外部调用的打开消息弹窗方法。 */
function openMessageDialogFromExternal() {
const messageShortcut = visibleShortcuts.value.find(item => item.dialog === 'message')
if (messageShortcut) openShortcutDialog(messageShortcut)
}
// 暴露方法给父组件
defineExpose({
openMessageDialog: openMessageDialogFromExternal,
})
onMounted(() => {
const shortcut = getQueryValue('shortcut')
if (shortcut) {
const found = visibleShortcuts.value.find(item => item.dialog === shortcut)
if (found) {
openShortcutDialog(found)
}
}
})
</script>
<template>
<VMenu
v-model="appsMenu"
:max-width="menuMaxWidth"
width="100%"
max-height="560"
location="top end"
origin="top end"
close-on-content-click
close-on-back
scrim
>
<!-- Menu Activator -->
<template #activator="{ props }">
<IconBtn class="ms-2" v-bind="props">
<VIcon icon="mdi-card-multiple-outline" />
</IconBtn>
</template>
<!-- Menu Content -->
<VCard class="overflow-hidden">
<VCardItem class="py-3">
<VCardTitle>{{ t('shortcut.title') }}</VCardTitle>
<template #append>
<IconBtn @click="appsMenu = false">
<VIcon icon="mdi-close" />
</IconBtn>
</template>
</VCardItem>
<VDivider />
<div class="pa-3">
<div class="grid grid-cols-2 gap-3">
<!-- 循环渲染快捷方式 -->
<div v-for="(item, index) in visibleShortcuts" :key="index">
<VCard
flat
class="pa-2 d-flex align-center cursor-pointer transition-transform duration-300 hover:-translate-y-1 border h-full"
hover
@click="openShortcutDialog(item)"
>
<VAvatar variant="text" size="48" rounded="lg">
<VIcon color="primary" :icon="item.icon" size="24" />
</VAvatar>
<div>
<div class="text-body-1 text-high-emphasis font-weight-medium">{{ item.title }}</div>
<div class="text-caption text-medium-emphasis">{{ item.subtitle }}</div>
</div>
</VCard>
</div>
</div>
</div>
</VCard>
</VMenu>
</template>