feat(other): The menu supports jumping to external links and fixing some known problems

This commit is contained in:
vben
2024-05-21 21:45:48 +08:00
parent 399334ac57
commit c31d21be50
43 changed files with 505 additions and 243 deletions

View File

@@ -80,6 +80,10 @@ declare module 'vue-router' {
* 用于路由->菜单排序
*/
orderNo?: number;
/**
* 外链-跳转路径
*/
target?: string;
/**
* 标题名称
*/

View File

@@ -100,12 +100,7 @@ onBeforeUnmount(() => {
</VbenTooltip>
<div v-show="!showTooltip" :class="[e('content')]">
<VbenMenuBadge v-bind="props" />
<VbenIcon
v-if="isTopLevelMenuItem"
:class="nsMenu.e('icon')"
:icon="icon"
fallback
/>
<VbenIcon :class="nsMenu.e('icon')" :icon="icon" fallback />
<slot></slot>
<slot name="title"></slot>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { IcRoundMoreHoriz } from '@vben-core/iconify';
import { useNamespace } from '@vben-core/toolkit';
import { isHttpUrl, useNamespace } from '@vben-core/toolkit';
import { UseResizeObserverReturn, useResizeObserver } from '@vueuse/core';
import {
@@ -241,7 +241,10 @@ function handleMenuItemClick(data: MenuItemClicked) {
if (!path || !parentPaths) {
return;
}
activePath.value = path;
if (!isHttpUrl(path)) {
activePath.value = path;
}
emit('select', path, parentPaths);
}

View File

@@ -86,7 +86,7 @@ const iconArrowStyle = computed(() => {
<slot></slot>
<VbenIcon
v-if="isTopLevelMenuSubmenu && !isMenuMore"
v-if="!isMenuMore"
:class="nsMenu.e('icon')"
:icon="icon"
fallback

View File

@@ -16,7 +16,7 @@
--menu-item-collapse-margin-y: 4px;
--menu-item-collapse-margin-x: 0px;
--menu-item-radius: 0px;
--menu-item-indent: 24px;
--menu-item-indent: 16px;
--menu-font-size: 14px;
--menu-dark-background: 0deg 0% 100% / 10%;
--menu-light-background: 192deg 1% 93%;

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Icon } from '@vben-core/iconify';
import { VbenIcon } from '@vben-core/shadcn-ui';
import type { IBreadcrumb } from './interface';
@@ -32,7 +32,7 @@ function handleClick(path?: string) {
<li>
<a href="javascript:void 0" @click.stop="handleClick(item.path)">
<span class="flex-center h-full">
<Icon
<VbenIcon
v-if="item.icon && showIcon"
class="mr-1 size-5 flex-shrink-0"
:icon="item.icon"

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import { IcRoundKeyboardArrowDown, Icon } from '@vben-core/iconify';
import { IcRoundKeyboardArrowDown } from '@vben-core/iconify';
import { VbenIcon } from '@vben-core/shadcn-ui';
import {
Breadcrumb,
@@ -49,7 +50,7 @@ function handleClick(path?: string) {
<div v-if="item.items?.length ?? 0 > 0">
<DropdownMenu>
<DropdownMenuTrigger class="flex items-center gap-1">
<Icon
<VbenIcon
v-if="item.icon && showIcon"
class="size-5"
:icon="item.icon"
@@ -74,21 +75,26 @@ function handleClick(path?: string) {
href="javascript:void 0"
@click.stop="handleClick(item.path)"
>
<Icon
v-if="item.icon && showIcon"
class="size-4"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
/>
{{ item.title }}
<div class="flex-center">
<VbenIcon
v-if="item.icon && showIcon"
class="mr-1 size-4"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
/>
{{ item.title }}
</div>
</BreadcrumbLink>
<BreadcrumbPage v-else>
<Icon
v-if="item.icon && showIcon"
class="size-4"
:icon="item.icon"
/>
{{ item.title }}
<div class="flex-center">
<VbenIcon
v-if="item.icon && showIcon"
class="mr-1 size-4"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
/>
{{ item.title }}
</div>
</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator

View File

@@ -29,7 +29,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{ close: [string]; unPushPin: [TabItem] }>();
const gap = 7;
const gap = 6;
const active = defineModel<string>('active');
const { b, e, is } = useNamespace('tabs-ui');

View File

@@ -2,7 +2,7 @@
@include b('tabs-ui') {
--tabs-background: hsl(var(--color-background));
--tabs-gap: 8px;
--tabs-gap: 10px;
--tabs-divider: hsl(var(--color-border));
--tabs-hover: hsl(var(--color-heavy));
--tabs-active-background: hsl(var(--color-primary) / 15%);
@@ -30,7 +30,6 @@
color: hsl(var(--color-muted-foreground));
cursor: pointer;
user-select: none;
// mask-image: linear-gradient(to right, #000 calc(100% - 0px), transparent);
@include is('active') {
z-index: 1;
@@ -49,10 +48,10 @@
background-color: var(--tabs-active-background);
}
// .#{$namespace}-tab-background__before,
// .#{$namespace}-tab-background__after {
// fill: var(--tabs-active-background);
// }
.#{$namespace}-tab-background__before,
.#{$namespace}-tab-background__after {
fill: var(--tabs-active-background);
}
}
@include e('content') {
@@ -95,7 +94,7 @@
height: 100%;
font-size: 12px;
border-radius: 50%;
// transition: all 0.15s ease;
transition: all 0.15s ease;
&:hover {
color: hsl(var(--color-foreground));
@@ -185,15 +184,15 @@
height: 100%;
margin: 0 7px;
&::before {
position: absolute;
top: 20%;
right: 100%;
width: 1px;
height: 60%;
content: '';
background-color: var(--tabs-divider);
}
// &::before {
// position: absolute;
// top: 20%;
// right: 100%;
// width: 1px;
// height: 60%;
// content: '';
// background-color: var(--tabs-divider);
// }
// &::after {
// position: absolute;

View File

@@ -1,8 +1,11 @@
<script lang="ts" setup>
import { VbenAdminLayout } from '@vben-core/layout-ui';
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import { mapTree } from '@vben-core/toolkit';
import { MenuRecordRaw } from '@vben-core/typings';
import { PreferenceWidget } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { preference, updatePreference, usePreference } from '@vben/preference';
import { computed } from 'vue';
@@ -80,6 +83,15 @@ const {
sideMenus,
sideVisible,
} = useMixedMenu();
function wrapperMenus(menus: MenuRecordRaw[]) {
return mapTree(menus, (item) => {
return {
...item,
name: $t(item.name),
};
});
}
</script>
<template>
@@ -154,7 +166,7 @@ const {
:rounded="isMenuRounded"
mode="horizontal"
:theme="headerMenuTheme"
:menus="headerMenus"
:menus="wrapperMenus(headerMenus)"
:default-active="headerActive"
@select="handleMenuSelect"
/>
@@ -175,7 +187,7 @@ const {
:collapse-show-title="preference.sideCollapseShowTitle"
:collapse="preference.sideCollapse"
:theme="theme"
:menus="sideMenus"
:menus="wrapperMenus(sideMenus)"
:default-active="sideActive"
@select="handleMenuSelect"
/>
@@ -195,7 +207,7 @@ const {
<template #side-extra>
<LayoutExtraMenu
:rounded="isMenuRounded"
:menus="extraMenus"
:menus="wrapperMenus(extraMenus)"
:collapse="preference.sideExtraCollapse"
:theme="theme"
/>

View File

@@ -3,7 +3,9 @@ import type { MenuRecordRaw } from '@vben-core/typings';
import { Menu, MenuProps } from '@vben-core/menu-ui';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import { useNavigation } from './use-navigation';
interface Props extends MenuProps {
collspae?: boolean;
@@ -13,10 +15,10 @@ interface Props extends MenuProps {
defineProps<Props>();
const route = useRoute();
const router = useRouter();
const { navigation } = useNavigation();
function handleSelect(key: string) {
router.push(key);
async function handleSelect(key: string) {
await navigation(key);
}
</script>

View File

@@ -3,17 +3,18 @@ import type { MenuRecordRaw } from '@vben-core/typings';
import { preference } from '@vben/preference';
import { useAccessStore } from '@vben/stores';
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import { findRootMenuByPath } from './helper';
import { useNavigation } from './use-navigation';
function useExtraMenu() {
const accessStore = useAccessStore();
const { navigation } = useNavigation();
const menus = computed(() => accessStore.getAccessMenus);
const route = useRoute();
const router = useRouter();
const extraMenus = ref<MenuRecordRaw[]>([]);
const extraVisible = ref<boolean>(false);
const extraActiveMenu = ref('');
@@ -22,14 +23,14 @@ function useExtraMenu() {
* 选择混合菜单事件
* @param menu
*/
const handleMixedMenuSelect = (menu: MenuRecordRaw) => {
const handleMixedMenuSelect = async (menu: MenuRecordRaw) => {
extraMenus.value = menu?.children ?? [];
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
const hasChildren = extraMenus.value.length > 0;
extraVisible.value = hasChildren;
if (!hasChildren) {
router.push(menu.path);
await navigation(menu.path);
}
};

View File

@@ -3,15 +3,15 @@ import type { MenuRecordRaw } from '@vben-core/typings';
import { preference, usePreference } from '@vben/preference';
import { useAccessStore } from '@vben/stores';
import { computed, onBeforeMount, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import { findRootMenuByPath } from './helper';
import { useNavigation } from './use-navigation';
function useMixedMenu() {
const accessStore = useAccessStore();
const { navigation } = useNavigation();
const route = useRoute();
const router = useRouter();
const splitSideMenus = ref<MenuRecordRaw[]>([]);
const rootMenuPath = ref<string>('');
@@ -75,7 +75,7 @@ function useMixedMenu() {
*/
const handleMenuSelect = (key: string, mode?: string) => {
if (!isMixedNav.value || mode === 'vertical') {
router.push(key);
navigation(key);
return;
}
@@ -83,7 +83,7 @@ function useMixedMenu() {
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = rootMenu?.children ?? [];
if (splitSideMenus.value.length === 0) {
router.push(key);
navigation(key);
}
};

View File

@@ -0,0 +1,19 @@
import { isHttpUrl, openWindow } from '@vben-core/toolkit';
import { useRouter } from 'vue-router';
function useNavigation() {
const router = useRouter();
const navigation = async (path: string) => {
if (isHttpUrl(path)) {
openWindow(path, { target: '_blank' });
} else {
await router.push(path);
}
};
return { navigation };
}
export { useNavigation };

View File

@@ -12,7 +12,12 @@ import {
MdiPinOff,
} from '@vben-core/iconify';
import { filterTree } from '@vben-core/toolkit';
import type {
RouteLocationNormalized,
RouteRecordNormalized,
} from 'vue-router';
import { $t } from '@vben/locales';
import { storeToRefs, useAccessStore, useTabsStore } from '@vben/stores';
import { computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -39,6 +44,9 @@ function useTabs() {
const affixTabs = filterTree(router.getRoutes(), (route) => {
return !!route.meta?.affixTab;
});
affixTabs.forEach((tab) => {
Object.assign(tab, wrapperTabLocale(tab));
});
tabsStore.setAffixTabs(affixTabs);
};
@@ -52,6 +60,18 @@ function useTabs() {
await tabsStore.closeTabByKey(key, router);
};
function wrapperTabLocale(
tab: RouteLocationNormalized | RouteRecordNormalized,
) {
return {
...tab,
meta: {
...tab.meta,
title: $t(tab.meta.title as string),
},
};
}
watch(
() => accessMenus.value,
() => {
@@ -63,7 +83,7 @@ function useTabs() {
watch(
() => route.path,
() => {
tabsStore.addTab(route);
tabsStore.addTab(wrapperTabLocale(route) as RouteLocationNormalized);
},
{ immediate: true },
);

View File

@@ -4,6 +4,7 @@ import type { IBreadcrumb } from '@vben-core/shadcn-ui';
import { VbenBackgroundBreadcrumb, VbenBreadcrumb } from '@vben-core/shadcn-ui';
import { BreadcrumbStyle } from '@vben-core/typings';
import { $t } from '@vben/locales';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -43,7 +44,7 @@ const breadcrumbs = computed((): IBreadcrumb[] => {
resultBreadcrumb.push({
icon: icon as string,
path: path || route.path,
title: (title || name) as string,
title: $t((title || name) as string),
// items: children.map((child) => {
// return {
// icon: child?.meta?.icon as string,

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,45 @@
{
"name": "@vben/constants",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/constants"
},
"bugs": {
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
},
"scripts": {
"build": "pnpm unbuild",
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"imports": {
"#*": "./src/*"
},
"exports": {
".": {
"development": "./src/index.ts",
"types": "./src/index.ts",
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
}
}

View File

@@ -0,0 +1 @@
export * from './vben';

View File

@@ -0,0 +1 @@
export const VBEN_GITHUB_URL = 'https://github.com/vbenjs/vue-vben-admin';

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"include": ["src"]
}

View File

@@ -5,6 +5,7 @@ export default defineBuildConfig({
declaration: true,
entries: [
'src/index',
'src/helper',
{
builder: 'mkdist',
input: './src/langs',

View File

@@ -35,6 +35,11 @@
},
"./langs/*": {
"default": "./dist/langs/*"
},
"./helper": {
"development": "./src/helper.ts",
"types": "./src/helper.ts",
"default": "./dist/helper.mjs"
}
},
"publishConfig": {

View File

@@ -0,0 +1,8 @@
/**
* 没有任何实际作用,只用于 IDE 对I18的提示
*/
function $t(key: string) {
return key;
}
export { $t };

View File

@@ -149,3 +149,7 @@ authentication:
send-code: Get Security code
send-text: "Reacquire in {0}s"
third-party-login: Or continue with
page:
about: About
document: Document

View File

@@ -148,3 +148,7 @@ authentication:
send-code: 获取验证码
send-text: "{0}秒后重新获取"
third-party-login: 其他登录方式
page:
about: 关于
document: 文档

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,48 @@
{
"name": "@vben/utils",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/utils"
},
"bugs": {
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
},
"scripts": {
"build": "pnpm unbuild",
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"imports": {
"#*": "./src/*"
},
"exports": {
".": {
"development": "./src/index.ts",
"types": "./src/index.ts",
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@vben-core/toolkit": "workspace:*"
}
}

View File

@@ -0,0 +1 @@
export * from '@vben-core/toolkit';

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"include": ["src"]
}