2026-05-21 21:42:23 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-05-28 08:20:01 +08:00
|
|
|
|
import type { VNode } from 'vue';
|
2026-05-21 21:42:23 +08:00
|
|
|
|
import { computed, ref } from 'vue';
|
2026-05-28 08:20:01 +08:00
|
|
|
|
import { objectContextDomainConfigs } from '@/constants/object-context';
|
2026-05-21 21:42:23 +08:00
|
|
|
|
import { useRouteStore } from '@/store/modules/route';
|
|
|
|
|
|
import { useWorkbenchStore } from '@/store/modules/workbench';
|
|
|
|
|
|
import { useRouterPush } from '@/hooks/common/router';
|
|
|
|
|
|
import WorkbenchModuleCard from './workbench-module-card.vue';
|
|
|
|
|
|
import WorkbenchShortcutPicker from './workbench-shortcut-picker.vue';
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
editing?: boolean;
|
|
|
|
|
|
collapsed?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
withDefaults(defineProps<Props>(), {
|
|
|
|
|
|
editing: false,
|
|
|
|
|
|
collapsed: false
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
defineEmits<{
|
|
|
|
|
|
(e: 'hide'): void;
|
|
|
|
|
|
(e: 'toggle-collapse'): void;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const routeStore = useRouteStore();
|
|
|
|
|
|
const workbench = useWorkbenchStore();
|
|
|
|
|
|
const { routerPushByKey } = useRouterPush();
|
|
|
|
|
|
|
|
|
|
|
|
interface FlatMenu {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
label: string;
|
2026-05-28 08:20:01 +08:00
|
|
|
|
/** 来自 routeStore.menus 的 icon VNode(SvgIconVNode 渲染产物,与侧栏菜单同源) */
|
|
|
|
|
|
icon?: VNode;
|
2026-05-21 21:42:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
// 与 picker 保持一致:对象域入口(project_list / product_list)当叶子,
|
|
|
|
|
|
// 其下的对象上下文页不进入快捷入口候选范围。
|
|
|
|
|
|
const OBJECT_DOMAIN_ENTRY_KEYS = new Set(objectContextDomainConfigs.map(c => c.entryRouteKey));
|
|
|
|
|
|
|
2026-05-21 21:42:23 +08:00
|
|
|
|
function flatten(menus: typeof routeStore.menus): FlatMenu[] {
|
|
|
|
|
|
const out: FlatMenu[] = [];
|
|
|
|
|
|
function walk(list: typeof menus) {
|
|
|
|
|
|
list.forEach((m: any) => {
|
2026-05-28 08:20:01 +08:00
|
|
|
|
const isDomainEntry = OBJECT_DOMAIN_ENTRY_KEYS.has(m.key);
|
|
|
|
|
|
const hasChildren = !isDomainEntry && m.children && m.children.length > 0;
|
|
|
|
|
|
if (hasChildren) {
|
2026-05-21 21:42:23 +08:00
|
|
|
|
walk(m.children);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
out.push({
|
|
|
|
|
|
key: m.key,
|
|
|
|
|
|
label: m.label as string,
|
2026-05-28 08:20:01 +08:00
|
|
|
|
icon: m.icon
|
2026-05-21 21:42:23 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
walk(menus);
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const flatMenus = computed(() => flatten(routeStore.menus));
|
|
|
|
|
|
const selectedKeys = computed(() => workbench.layout.settings.shortcut?.menuKeys ?? []);
|
|
|
|
|
|
const selected = computed(() =>
|
|
|
|
|
|
selectedKeys.value.map(k => flatMenus.value.find(m => m.key === k)).filter((x): x is FlatMenu => Boolean(x))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const pickerOpen = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
function openPicker() {
|
|
|
|
|
|
pickerOpen.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleClick(key: string) {
|
|
|
|
|
|
routerPushByKey(key as any);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleConfirm(keys: string[]) {
|
|
|
|
|
|
workbench.updateModuleSettings('shortcut', { menuKeys: keys });
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<WorkbenchModuleCard
|
|
|
|
|
|
title="快捷入口"
|
|
|
|
|
|
icon="mdi:rocket-launch-outline"
|
|
|
|
|
|
:badge-count="selected.length || undefined"
|
|
|
|
|
|
:editing="editing"
|
|
|
|
|
|
:collapsed="collapsed"
|
|
|
|
|
|
has-settings
|
|
|
|
|
|
@hide="$emit('hide')"
|
|
|
|
|
|
@toggle-collapse="$emit('toggle-collapse')"
|
|
|
|
|
|
@open-settings="openPicker"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-if="selected.length === 0" class="shortcut-empty">
|
|
|
|
|
|
<ElEmpty description="还未选择菜单" :image-size="60">
|
|
|
|
|
|
<ElButton type="primary" size="small" @click="openPicker">+ 选择菜单</ElButton>
|
|
|
|
|
|
</ElEmpty>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="shortcut-grid">
|
|
|
|
|
|
<button v-for="item in selected" :key="item.key" class="shortcut-item" @click="handleClick(item.key)">
|
2026-05-28 08:20:01 +08:00
|
|
|
|
<ElIcon v-if="item.icon" class="shortcut-item__icon">
|
|
|
|
|
|
<component :is="item.icon" />
|
|
|
|
|
|
</ElIcon>
|
|
|
|
|
|
<SvgIcon v-else icon="mdi:link-variant" class="shortcut-item__icon" />
|
|
|
|
|
|
<span class="shortcut-item__label">{{ item.label }}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="shortcut-item shortcut-item--add" title="添加快捷入口" @click="openPicker">
|
|
|
|
|
|
<SvgIcon icon="mdi:plus" />
|
|
|
|
|
|
<span>添加</span>
|
2026-05-21 21:42:23 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<WorkbenchShortcutPicker v-model="pickerOpen" :initial-selected="selectedKeys" @confirm="handleConfirm" />
|
|
|
|
|
|
</WorkbenchModuleCard>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.shortcut-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.shortcut-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 12px 8px;
|
|
|
|
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
|
|
|
|
background: var(--el-fill-color-lighter);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
|
transition: all 120ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
.shortcut-item__icon {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
transition: color 120ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.shortcut-item__label {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 21:42:23 +08:00
|
|
|
|
.shortcut-item:hover {
|
|
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 08:20:01 +08:00
|
|
|
|
.shortcut-item--add {
|
|
|
|
|
|
border-style: dashed;
|
|
|
|
|
|
border-color: var(--el-border-color);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.shortcut-item--add:hover {
|
|
|
|
|
|
border-color: var(--el-color-primary);
|
|
|
|
|
|
background: var(--el-color-primary-light-9);
|
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 21:42:23 +08:00
|
|
|
|
.shortcut-empty {
|
|
|
|
|
|
padding: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|