Files
cn-rdms-web/src/views/workbench/modules/workbench-shortcut.vue

224 lines
5.9 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import type { VNode } from 'vue';
import { computed, ref } from 'vue';
import { objectContextDomainConfigs } from '@/constants/object-context';
import { useRouteStore } from '@/store/modules/route';
import { useWorkbenchStore } from '@/store/modules/workbench';
import { useRouterPush } from '@/hooks/common/router';
import { useWorkbenchRefresh } from '../composables/use-workbench-refresh';
import WorkbenchModuleCard from './workbench-module-card.vue';
import WorkbenchShortcutPicker from './workbench-shortcut-picker.vue';
interface Props {
editing?: boolean;
}
withDefaults(defineProps<Props>(), {
editing: false
});
defineEmits<{
(e: 'hide'): void;
}>();
const routeStore = useRouteStore();
const workbench = useWorkbenchStore();
const { routerPushByKey } = useRouterPush();
const { loading, refresh } = useWorkbenchRefresh();
interface FlatMenu {
key: string;
label: string;
/** 来自 routeStore.menus 的 icon VNodeSvgIconVNode 渲染产物,与侧栏菜单同源) */
icon?: VNode;
}
// 与 picker 保持一致对象域入口project_list / product_list当叶子
// 其下的对象上下文页不进入快捷入口候选范围。
const OBJECT_DOMAIN_ENTRY_KEYS = new Set(objectContextDomainConfigs.map(c => c.entryRouteKey));
function flatten(menus: typeof routeStore.menus): FlatMenu[] {
const out: FlatMenu[] = [];
function walk(list: typeof menus) {
list.forEach((m: any) => {
const isDomainEntry = OBJECT_DOMAIN_ENTRY_KEYS.has(m.key);
const hasChildren = !isDomainEntry && m.children && m.children.length > 0;
if (hasChildren) {
walk(m.children);
} else {
out.push({
key: m.key,
label: m.label as string,
icon: m.icon
});
}
});
}
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 });
}
function handleRemove(key: string) {
workbench.updateModuleSettings('shortcut', { menuKeys: selectedKeys.value.filter(k => k !== key) });
}
</script>
<template>
<WorkbenchModuleCard
v-loading="loading"
title="快捷入口"
icon="mdi:rocket-launch-outline"
:badge-count="selected.length || undefined"
:editing="editing"
has-settings
@hide="$emit('hide')"
@open-settings="openPicker"
@refresh="refresh"
>
<div v-if="selected.length === 0" class="shortcut-empty">
<ElEmpty description="还未选择菜单" :image-size="48">
<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)">
<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>
<span class="shortcut-item__remove" title="移除此快捷入口" @click.stop="handleRemove(item.key)">
<SvgIcon icon="mdi:close" />
</span>
</button>
<button class="shortcut-item shortcut-item--add" title="添加快捷入口" @click="openPicker">
<SvgIcon icon="mdi:plus" />
<span>添加</span>
</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;
flex: 1;
min-height: 0;
overflow: auto;
align-content: start;
}
.shortcut-item {
position: relative;
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;
}
.shortcut-item__remove {
position: absolute;
top: 4px;
right: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
color: var(--el-text-color-placeholder);
font-size: 12px;
opacity: 0;
transition:
opacity 120ms,
color 120ms,
background-color 120ms;
}
.shortcut-item:hover .shortcut-item__remove {
opacity: 1;
}
.shortcut-item__remove:hover {
background-color: var(--el-color-danger-light-9);
color: var(--el-color-danger);
}
.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;
}
.shortcut-item:hover {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
.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);
}
.shortcut-empty {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
}
/* 压缩 ElEmpty 默认大 padding空态在最小高度下也不溢出 */
.shortcut-empty :deep(.el-empty) {
padding: 12px 0;
}
</style>