feat: 顶部菜单&项目选择器&右上角图标

This commit is contained in:
baiqi 2023-06-27 09:58:59 +08:00 committed by 刘瑞斌
parent 38f798f27e
commit 28525002f6
18 changed files with 347 additions and 63 deletions

View File

@ -0,0 +1,9 @@
import MSR from '@/api/http/index';
import { ProjectListUrl } from '@/api/requrls/system/project';
import type { ProjectListItem } from '@/models/system/project';
export function getProjectList(organizationId: string) {
return MSR.get<ProjectListItem[]>({ url: `${ProjectListUrl}/${organizationId}` });
}
export default {};

View File

@ -1,5 +1,5 @@
import MSR from '@/api/http/index';
import { GetUserListUrl, CreateUserUrl, UpdateUserUrl } from '@/api/requrls/system';
import { GetUserListUrl, CreateUserUrl, UpdateUserUrl } from '@/api/requrls/system/user';
import type { UserListItem, CreateUserParams } from '@/models/system/user';
import type { TableQueryParams } from '@/models/common';

View File

@ -1,5 +1,5 @@
import MSR from '@/api/http/index';
import { updateUserGroupU, getUserGroupU, addUserGroupU, deleteUserGroupU } from '@/api/requrls/usergroup';
import { updateUserGroupU, getUserGroupU, addUserGroupU, deleteUserGroupU } from '@/api/requrls/system/usergroup';
// import { QueryParams, CommonList } from '@/models/common';
import { UserGroupItem } from '@/components/bussiness/usergroup/type';

View File

@ -0,0 +1,3 @@
export const ProjectListUrl = '/system/project/list';
export default {};

View File

@ -3,7 +3,7 @@
v-if="appStore.topMenus.length > 0"
class="bg-transparent"
mode="horizontal"
:default-selected-keys="[appStore.topMenus[0].name]"
:default-selected-keys="[defaultActiveMenu]"
>
<a-menu-item v-for="menu of appStore.topMenus" :key="(menu.name as string)" @click="jumpPath(menu.name)">
{{ t(menu.meta?.locale || '') }}
@ -12,6 +12,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter, RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router';
import { cloneDeep } from 'lodash-es';
import { useAppStore } from '@/store';
@ -26,6 +27,11 @@
const router = useRouter();
const { t } = useI18n();
const defaultActiveMenu = computed(() => {
const { name } = router.currentRoute.value;
return name;
});
/**
* 监听路由变化存储打开的三级子路由
*/

View File

@ -6,35 +6,47 @@
</a-space>
</div>
<div class="center-side">
<template v-if="showProjectSelect">
<a-divider direction="vertical" class="ml-0" />
<a-select
class="w-auto max-w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
:default-value="appStore.getCurrentProjectId"
:bordered="false"
@change="selectProject"
>
<template #arrow-icon>
<icon-caret-down />
</template>
<a-tooltip v-for="project of projectList" :key="project.id" :mouse-enter-delay="500" :content="project.name">
<a-option
:value="project.id"
:class="project.id === appStore.getCurrentProjectId ? 'arco-select-option-selected' : ''"
>{{ project.name }}</a-option
>
</a-tooltip>
</a-select>
<a-divider direction="vertical" class="mr-0" />
</template>
<TopMenu />
</div>
<ul class="right-side">
<li>
<a-tooltip :content="$t('settings.language')">
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setDropDownVisible">
<a-tooltip :content="t('settings.navbar.search')">
<a-button type="secondary">
<template #icon>
<icon-language />
<icon-search />
</template>
</a-button>
</a-tooltip>
<a-dropdown trigger="click" @select="changeLocale as any">
<div ref="triggerBtn" class="trigger-btn"></div>
<template #content>
<a-doption v-for="item in locales" :key="item.value" :value="item.value">
<template #icon>
<icon-check v-show="item.value === currentLocale" />
</template>
{{ item.label }}
</a-doption>
</template>
</a-dropdown>
</li>
<li>
<a-tooltip :content="$t('settings.navbar.alerts')">
<a-tooltip :content="t('settings.navbar.alerts')">
<div class="message-box-trigger">
<a-badge :count="9" dot>
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setPopoverVisible">
<icon-notification />
<a-button type="secondary" @click="setPopoverVisible">
<template #icon>
<icon-notification />
</template>
</a-button>
</a-badge>
</div>
@ -52,7 +64,52 @@
</a-popover>
</li>
<li>
<a-tooltip :content="isFullscreen ? $t('settings.navbar.screen.toExit') : $t('settings.navbar.screen.toFull')">
<a-tooltip :content="t('settings.navbar.task')">
<a-button type="secondary">
<template #icon>
<icon-calendar-clock />
</template>
</a-button>
</a-tooltip>
</li>
<li>
<a-dropdown trigger="click" position="br">
<a-tooltip :content="t('settings.navbar.help')">
<a-button type="secondary">
<template #icon>
<icon-question-circle />
</template>
</a-button>
</a-tooltip>
<template #content>
<a-doption v-for="item in helpCenterList" :key="item.name" :value="item.name">
<component :is="item.icon"></component>
{{ t(item.name) }}
</a-doption>
</template>
</a-dropdown>
</li>
<li>
<a-dropdown trigger="click" position="br" @select="changeLocale as any">
<a-tooltip :content="t('settings.language')">
<a-button type="secondary">
<template #icon>
<icon-translate />
</template>
</a-button>
</a-tooltip>
<template #content>
<a-doption v-for="item in locales" :key="item.value" :value="item.value">
<template #icon>
<icon-check v-show="item.value === currentLocale" />
</template>
{{ item.label }}
</a-doption>
</template>
</a-dropdown>
</li>
<!-- <li>
<a-tooltip :content="isFullscreen ? t('settings.navbar.screen.toExit') : t('settings.navbar.screen.toFull')">
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="toggleFullScreen">
<template #icon>
<icon-fullscreen-exit v-if="isFullscreen" />
@ -60,9 +117,9 @@
</template>
</a-button>
</a-tooltip>
</li>
</li> -->
<!-- <li>
<a-tooltip :content="$t('settings.title')">
<a-tooltip :content="t('settings.title')">
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setVisible">
<template #icon>
<icon-settings />
@ -70,7 +127,7 @@
</a-button>
</a-tooltip>
</li> -->
<li>
<!-- <li>
<a-dropdown trigger="click">
<a-avatar :size="32" :style="{ marginRight: '8px', cursor: 'pointer' }">
<img alt="avatar" :src="avatar" />
@ -80,7 +137,7 @@
<a-space @click="switchRoles">
<icon-tag />
<span>
{{ $t('messageBox.switchRoles') }}
{{ t('messageBox.switchRoles') }}
</span>
</a-space>
</a-doption>
@ -88,7 +145,7 @@
<a-space @click="$router.push({ name: 'Info' })">
<icon-user />
<span>
{{ $t('messageBox.userCenter') }}
{{ t('messageBox.userCenter') }}
</span>
</a-space>
</a-doption>
@ -96,7 +153,7 @@
<a-space @click="$router.push({ name: 'Setting' })">
<icon-settings />
<span>
{{ $t('messageBox.userSettings') }}
{{ t('messageBox.userSettings') }}
</span>
</a-space>
</a-doption>
@ -104,41 +161,89 @@
<a-space @click="handleLogout">
<icon-export />
<span>
{{ $t('messageBox.logout') }}
{{ t('messageBox.logout') }}
</span>
</a-space>
</a-doption>
</template>
</a-dropdown>
</li>
</li> -->
</ul>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useFullscreen } from '@vueuse/core';
import { useUserStore } from '@/store';
import { ref, computed, Ref, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { IconCompass, IconQuestionCircle, IconFile, IconInfoCircle } from '@arco-design/web-vue/es/icon';
// import { Message } from '@arco-design/web-vue';
// import { useFullscreen } from '@vueuse/core';
import { useAppStore } from '@/store';
import { LOCALE_OPTIONS } from '@/locale';
import useLocale from '@/locale/useLocale';
import useUser from '@/hooks/useUser';
// import useUser from '@/hooks/useUser';
import TopMenu from '@/components/pure/ms-top-menu/index.vue';
import MessageBox from '../message-box/index.vue';
import { NOT_SHOW_PROJECT_SELECT_MODULE } from '@/router/constants';
import { getProjectList } from '@/api/modules/system/project';
import { useI18n } from '@/hooks/useI18n';
const userStore = useUserStore();
const { logout } = useUser();
const { changeLocale, currentLocale } = useLocale();
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const locales = [...LOCALE_OPTIONS];
const avatar = computed(() => {
return userStore.avatar;
import type { ProjectListItem } from '@/models/system/project';
const appStore = useAppStore();
// const { logout } = useUser();
const route = useRoute();
const { t } = useI18n();
const projectList: Ref<ProjectListItem[]> = ref([]);
onBeforeMount(async () => {
const res = await getProjectList(appStore.getCurrentOrgId);
projectList.value = res;
});
const showProjectSelect = computed(() => {
//
return !NOT_SHOW_PROJECT_SELECT_MODULE.includes(route.fullPath.split('/')[1]);
});
function selectProject(value: string | number | Record<string, any> | undefined) {
appStore.setCurrentProjectId(value as string);
}
const helpCenterList = [
{
name: 'settings.help.guide',
icon: IconCompass,
route: '/help-center/guide',
},
{
name: 'settings.help.doc',
icon: IconQuestionCircle,
route: '/help-center/guide',
},
{
name: 'settings.help.APIDoc',
icon: IconFile,
route: '/help-center/guide',
},
{
name: 'settings.help.version',
icon: IconInfoCircle,
route: '/help-center/guide',
},
];
const { changeLocale, currentLocale } = useLocale();
// const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const locales = [...LOCALE_OPTIONS];
// const avatar = computed(() => {
// return userStore.avatar;
// });
// const setVisible = () => {
// appStore.updateSettings({ globalSettings: true });
// };
const refBtn = ref();
const triggerBtn = ref();
const setPopoverVisible = () => {
const event = new MouseEvent('click', {
view: window,
@ -147,21 +252,13 @@
});
refBtn.value.dispatchEvent(event);
};
const handleLogout = () => {
logout();
};
const setDropDownVisible = () => {
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
});
triggerBtn.value.dispatchEvent(event);
};
const switchRoles = async () => {
const res = await userStore.switchRoles();
Message.success(res as string);
};
// const handleLogout = () => {
// logout();
// };
// const switchRoles = async () => {
// const res = await userStore.switchRoles();
// Message.success(res as string);
// };
</script>
<style scoped lang="less">
@ -177,7 +274,7 @@
width: 185px;
}
.center-side {
@apply flex-1;
@apply flex flex-1 items-center;
}
.right-side {
@apply flex list-none;
@ -189,7 +286,16 @@
li {
@apply flex items-center;
padding: 0 10px;
padding-left: 10px;
.arco-btn-secondary {
@apply !bg-transparent;
color: var(--color-text-4) !important;
&:hover,
&:focus-visible {
color: var(--color-text-1) !important;
}
}
}
a {
@apply no-underline;
@ -219,4 +325,29 @@
@apply mt-0;
}
}
.arco-menu-horizontal {
.arco-menu-inner {
.arco-menu-item,
.arco-menu-overflow-sub-menu {
@apply !bg-transparent;
}
.arco-menu-selected {
@apply !font-normal;
color: rgb(var(--primary-5)) !important;
.arco-menu-selected-label {
bottom: -11px;
background-color: rgb(var(--primary-5)) !important;
}
}
}
}
.arco-trigger-menu-vertical {
max-height: 500px;
.arco-trigger-menu-selected {
@apply !font-normal;
color: rgb(var(--primary-5)) !important;
}
}
</style>

View File

@ -8,7 +8,14 @@ export default {
'settings.menuWidth': 'Menu Width (px)',
'settings.navbar.screen.toFull': 'Click to switch to full screen mode',
'settings.navbar.screen.toExit': 'Click to exit the full screen mode',
'settings.navbar.alerts': 'alerts',
'settings.navbar.alerts': 'Alerts',
'settings.navbar.search': 'Search',
'settings.navbar.task': 'Task center',
'settings.navbar.help': 'Help center',
'settings.help.guide': 'Use Guide',
'settings.help.doc': 'Help docs',
'settings.help.APIDoc': 'API docs',
'settings.help.version': 'Version',
'settings.menu': 'Menu',
'settings.tabBar': 'Tab Bar',
'settings.footer': 'Footer',

View File

@ -9,6 +9,13 @@ export default {
'settings.navbar.screen.toFull': '点击切换全屏模式',
'settings.navbar.screen.toExit': '点击退出全屏模式',
'settings.navbar.alerts': '消息通知',
'settings.navbar.search': '搜索',
'settings.navbar.task': '任务中心',
'settings.navbar.help': '帮助中心',
'settings.help.guide': '新手指引',
'settings.help.doc': '帮助文档',
'settings.help.APIDoc': 'API文档',
'settings.help.version': '版本信息',
'settings.menu': '菜单栏',
'settings.tabBar': '多页签',
'settings.footer': '底部',

View File

@ -4,6 +4,7 @@ import './user';
import './message-box';
import './api-test';
import './system/user';
import './system/project';
Mock.setup({
timeout: '600-1000',

View File

@ -0,0 +1,60 @@
import Mock from 'mockjs';
import setupMock, { successResponseWrap } from '@/utils/setup-mock';
const getProjectList = () => {
return [
{
id: '0283f238hf2',
num: 0,
organizationId: 'v3v4h434c3',
name: '发了多少',
description: 'string',
createTime: 0,
updateTime: 0,
updateUser: 'string',
createUser: 'string',
deleteTime: 0,
deleted: true,
deleteUser: 'string',
enable: true,
},
{
id: 'f9h832',
num: 0,
organizationId: 'v3v4h434c3',
name: '你了大 V',
description: 'string',
createTime: 0,
updateTime: 0,
updateUser: 'string',
createUser: 'string',
deleteTime: 0,
deleted: true,
deleteUser: 'string',
enable: true,
},
{
id: '0v023i92',
num: 0,
organizationId: 'v3v4h434c3',
name: '代付款就是快递方式觉得都是就',
description: 'string',
createTime: 0,
updateTime: 0,
updateUser: 'string',
createUser: 'string',
deleteTime: 0,
deleted: true,
deleteUser: 'string',
enable: true,
},
];
};
setupMock({
setup: () => {
Mock.mock(new RegExp('/system/project/list'), () => {
return successResponseWrap(getProjectList());
});
},
});

View File

@ -0,0 +1,16 @@
// 项目列表项
export interface ProjectListItem {
id: string;
num: number;
organizationId: string;
name: string;
description: string;
createTime: number;
updateTime: number;
updateUser: string;
createUser: string;
deleteTime: number;
deleted: boolean;
deleteUser: string;
enable: boolean;
}

View File

@ -1,21 +1,30 @@
// 路由白名单,无需校验权限与登录状态
export const WHITE_LIST = [
{ name: 'notFound', children: [] },
{ name: 'login', children: [] },
{ name: 'invite', children: [] },
];
// 左侧菜单底部对齐的菜单数组数组项为一级路由的name
export const BOTTOM_MENU_LIST = ['setting'];
// 404 路由
export const NOT_FOUND = {
name: 'notFound',
};
// 重定向中转站路由
export const REDIRECT_ROUTE_NAME = 'Redirect';
// 首页路由
export const DEFAULT_ROUTE_NAME = 'Workplace';
// 默认 tab-bar 路,多页签模式下,打开的第一个页面
export const DEFAULT_ROUTE = {
title: 'menu.dashboard.workplace',
name: DEFAULT_ROUTE_NAME,
fullPath: '/dashboard/workplace',
};
// 不需要显示项目选择器的模块数组项为一级路由的path
export const NOT_SHOW_PROJECT_SELECT_MODULE = ['setting'];

View File

@ -9,11 +9,12 @@ const ApiTest: AppRouteRecordRaw = {
locale: 'menu.apiTest',
icon: 'icon-dashboard',
order: 0,
hideChildrenInMenu: true,
},
children: [
{
path: 'list',
name: 'apiTest',
name: 'apiTestList',
component: () => import('@/views/api-test/index.vue'),
meta: {
locale: 'menu.apiTest',

View File

@ -9,7 +9,14 @@ import type { NotificationReturn } from '@arco-design/web-vue/es/notification/in
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
const useAppStore = defineStore('app', {
state: (): AppState => ({ ...defaultSettings, loading: false, loadingTip: '', topMenus: [] as RouteRecordRaw[] }),
state: (): AppState => ({
...defaultSettings,
loading: false,
loadingTip: '',
topMenus: [] as RouteRecordRaw[],
currentOrgId: '',
currentProjectId: '',
}),
getters: {
appCurrentSetting(state: AppState): AppState {
@ -30,6 +37,12 @@ const useAppStore = defineStore('app', {
getTopMenus(state: AppState): RouteRecordRaw[] {
return state.topMenus;
},
getCurrentOrgId(state: AppState): string {
return state.currentOrgId;
},
getCurrentProjectId(state: AppState): string {
return state.currentProjectId;
},
},
actions: {
@ -111,6 +124,21 @@ const useAppStore = defineStore('app', {
setTopMenus(menus: RouteRecordRaw[] | undefined) {
this.topMenus = menus ? [...menus] : [];
},
/**
* ID
*/
setCurrentOrgId(id: string) {
this.currentOrgId = id;
},
/**
* ID
*/
setCurrentProjectId(id: string) {
this.currentProjectId = id;
},
},
persist: {
paths: ['currentOrgId', 'currentProjectId'],
},
});

View File

@ -17,6 +17,8 @@ export interface AppState {
loading: boolean;
loadingTip: string;
topMenus: RouteRecordRaw[];
currentOrgId: string;
currentProjectId: string;
[key: string]: unknown;
}

View File

@ -54,7 +54,11 @@ const useUserStore = defineStore('user', {
// 获取用户信息
async info() {
const res = await getUserInfo();
const appStore = useAppStore();
if (appStore.currentOrgId === '') {
// 第一次进系统才设置组织 ID后续已经持久化存储了
appStore.setCurrentOrgId(res.organization || '');
}
this.setInfo(res);
},