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 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 { UserListItem, CreateUserParams } from '@/models/system/user';
import type { TableQueryParams } from '@/models/common'; import type { TableQueryParams } from '@/models/common';

View File

@ -1,5 +1,5 @@
import MSR from '@/api/http/index'; 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 { QueryParams, CommonList } from '@/models/common';
import { UserGroupItem } from '@/components/bussiness/usergroup/type'; 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" v-if="appStore.topMenus.length > 0"
class="bg-transparent" class="bg-transparent"
mode="horizontal" 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)"> <a-menu-item v-for="menu of appStore.topMenus" :key="(menu.name as string)" @click="jumpPath(menu.name)">
{{ t(menu.meta?.locale || '') }} {{ t(menu.meta?.locale || '') }}
@ -12,6 +12,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { useRouter, RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router'; import { useRouter, RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
@ -26,6 +27,11 @@
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const defaultActiveMenu = computed(() => {
const { name } = router.currentRoute.value;
return name;
});
/** /**
* 监听路由变化存储打开的三级子路由 * 监听路由变化存储打开的三级子路由
*/ */

View File

@ -6,35 +6,47 @@
</a-space> </a-space>
</div> </div>
<div class="center-side"> <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 /> <TopMenu />
</div> </div>
<ul class="right-side"> <ul class="right-side">
<li> <li>
<a-tooltip :content="$t('settings.language')"> <a-tooltip :content="t('settings.navbar.search')">
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setDropDownVisible"> <a-button type="secondary">
<template #icon> <template #icon>
<icon-language /> <icon-search />
</template> </template>
</a-button> </a-button>
</a-tooltip> </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>
<li> <li>
<a-tooltip :content="$t('settings.navbar.alerts')"> <a-tooltip :content="t('settings.navbar.alerts')">
<div class="message-box-trigger"> <div class="message-box-trigger">
<a-badge :count="9" dot> <a-badge :count="9" dot>
<a-button class="nav-btn" type="outline" :shape="'circle'" @click="setPopoverVisible"> <a-button type="secondary" @click="setPopoverVisible">
<icon-notification /> <template #icon>
<icon-notification />
</template>
</a-button> </a-button>
</a-badge> </a-badge>
</div> </div>
@ -52,7 +64,52 @@
</a-popover> </a-popover>
</li> </li>
<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"> <a-button class="nav-btn" type="outline" :shape="'circle'" @click="toggleFullScreen">
<template #icon> <template #icon>
<icon-fullscreen-exit v-if="isFullscreen" /> <icon-fullscreen-exit v-if="isFullscreen" />
@ -60,9 +117,9 @@
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</li> </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"> <a-button class="nav-btn" type="outline" :shape="'circle'" @click="setVisible">
<template #icon> <template #icon>
<icon-settings /> <icon-settings />
@ -70,7 +127,7 @@
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</li> --> </li> -->
<li> <!-- <li>
<a-dropdown trigger="click"> <a-dropdown trigger="click">
<a-avatar :size="32" :style="{ marginRight: '8px', cursor: 'pointer' }"> <a-avatar :size="32" :style="{ marginRight: '8px', cursor: 'pointer' }">
<img alt="avatar" :src="avatar" /> <img alt="avatar" :src="avatar" />
@ -80,7 +137,7 @@
<a-space @click="switchRoles"> <a-space @click="switchRoles">
<icon-tag /> <icon-tag />
<span> <span>
{{ $t('messageBox.switchRoles') }} {{ t('messageBox.switchRoles') }}
</span> </span>
</a-space> </a-space>
</a-doption> </a-doption>
@ -88,7 +145,7 @@
<a-space @click="$router.push({ name: 'Info' })"> <a-space @click="$router.push({ name: 'Info' })">
<icon-user /> <icon-user />
<span> <span>
{{ $t('messageBox.userCenter') }} {{ t('messageBox.userCenter') }}
</span> </span>
</a-space> </a-space>
</a-doption> </a-doption>
@ -96,7 +153,7 @@
<a-space @click="$router.push({ name: 'Setting' })"> <a-space @click="$router.push({ name: 'Setting' })">
<icon-settings /> <icon-settings />
<span> <span>
{{ $t('messageBox.userSettings') }} {{ t('messageBox.userSettings') }}
</span> </span>
</a-space> </a-space>
</a-doption> </a-doption>
@ -104,41 +161,89 @@
<a-space @click="handleLogout"> <a-space @click="handleLogout">
<icon-export /> <icon-export />
<span> <span>
{{ $t('messageBox.logout') }} {{ t('messageBox.logout') }}
</span> </span>
</a-space> </a-space>
</a-doption> </a-doption>
</template> </template>
</a-dropdown> </a-dropdown>
</li> </li> -->
</ul> </ul>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { ref, computed, Ref, onBeforeMount } from 'vue';
import { Message } from '@arco-design/web-vue'; import { useRoute } from 'vue-router';
import { useFullscreen } from '@vueuse/core'; import { IconCompass, IconQuestionCircle, IconFile, IconInfoCircle } from '@arco-design/web-vue/es/icon';
import { useUserStore } from '@/store'; // import { Message } from '@arco-design/web-vue';
// import { useFullscreen } from '@vueuse/core';
import { useAppStore } from '@/store';
import { LOCALE_OPTIONS } from '@/locale'; import { LOCALE_OPTIONS } from '@/locale';
import useLocale from '@/locale/useLocale'; 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 TopMenu from '@/components/pure/ms-top-menu/index.vue';
import MessageBox from '../message-box/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(); import type { ProjectListItem } from '@/models/system/project';
const { logout } = useUser();
const { changeLocale, currentLocale } = useLocale(); const appStore = useAppStore();
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen(); // const { logout } = useUser();
const locales = [...LOCALE_OPTIONS]; const route = useRoute();
const avatar = computed(() => { const { t } = useI18n();
return userStore.avatar;
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 = () => { // const setVisible = () => {
// appStore.updateSettings({ globalSettings: true }); // appStore.updateSettings({ globalSettings: true });
// }; // };
const refBtn = ref(); const refBtn = ref();
const triggerBtn = ref();
const setPopoverVisible = () => { const setPopoverVisible = () => {
const event = new MouseEvent('click', { const event = new MouseEvent('click', {
view: window, view: window,
@ -147,21 +252,13 @@
}); });
refBtn.value.dispatchEvent(event); refBtn.value.dispatchEvent(event);
}; };
const handleLogout = () => { // const handleLogout = () => {
logout(); // logout();
}; // };
const setDropDownVisible = () => { // const switchRoles = async () => {
const event = new MouseEvent('click', { // const res = await userStore.switchRoles();
view: window, // Message.success(res as string);
bubbles: true, // };
cancelable: true,
});
triggerBtn.value.dispatchEvent(event);
};
const switchRoles = async () => {
const res = await userStore.switchRoles();
Message.success(res as string);
};
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@ -177,7 +274,7 @@
width: 185px; width: 185px;
} }
.center-side { .center-side {
@apply flex-1; @apply flex flex-1 items-center;
} }
.right-side { .right-side {
@apply flex list-none; @apply flex list-none;
@ -189,7 +286,16 @@
li { li {
@apply flex items-center; @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 { a {
@apply no-underline; @apply no-underline;
@ -219,4 +325,29 @@
@apply mt-0; @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> </style>

View File

@ -8,7 +8,14 @@ export default {
'settings.menuWidth': 'Menu Width (px)', 'settings.menuWidth': 'Menu Width (px)',
'settings.navbar.screen.toFull': 'Click to switch to full screen mode', 'settings.navbar.screen.toFull': 'Click to switch to full screen mode',
'settings.navbar.screen.toExit': 'Click to exit the 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.menu': 'Menu',
'settings.tabBar': 'Tab Bar', 'settings.tabBar': 'Tab Bar',
'settings.footer': 'Footer', 'settings.footer': 'Footer',

View File

@ -9,6 +9,13 @@ export default {
'settings.navbar.screen.toFull': '点击切换全屏模式', 'settings.navbar.screen.toFull': '点击切换全屏模式',
'settings.navbar.screen.toExit': '点击退出全屏模式', 'settings.navbar.screen.toExit': '点击退出全屏模式',
'settings.navbar.alerts': '消息通知', '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.menu': '菜单栏',
'settings.tabBar': '多页签', 'settings.tabBar': '多页签',
'settings.footer': '底部', 'settings.footer': '底部',

View File

@ -4,6 +4,7 @@ import './user';
import './message-box'; import './message-box';
import './api-test'; import './api-test';
import './system/user'; import './system/user';
import './system/project';
Mock.setup({ Mock.setup({
timeout: '600-1000', 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 = [ export const WHITE_LIST = [
{ name: 'notFound', children: [] }, { name: 'notFound', children: [] },
{ name: 'login', children: [] }, { name: 'login', children: [] },
{ name: 'invite', children: [] }, { name: 'invite', children: [] },
]; ];
// 左侧菜单底部对齐的菜单数组数组项为一级路由的name
export const BOTTOM_MENU_LIST = ['setting']; export const BOTTOM_MENU_LIST = ['setting'];
// 404 路由
export const NOT_FOUND = { export const NOT_FOUND = {
name: 'notFound', name: 'notFound',
}; };
// 重定向中转站路由
export const REDIRECT_ROUTE_NAME = 'Redirect'; export const REDIRECT_ROUTE_NAME = 'Redirect';
// 首页路由
export const DEFAULT_ROUTE_NAME = 'Workplace'; export const DEFAULT_ROUTE_NAME = 'Workplace';
// 默认 tab-bar 路,多页签模式下,打开的第一个页面
export const DEFAULT_ROUTE = { export const DEFAULT_ROUTE = {
title: 'menu.dashboard.workplace', title: 'menu.dashboard.workplace',
name: DEFAULT_ROUTE_NAME, name: DEFAULT_ROUTE_NAME,
fullPath: '/dashboard/workplace', fullPath: '/dashboard/workplace',
}; };
// 不需要显示项目选择器的模块数组项为一级路由的path
export const NOT_SHOW_PROJECT_SELECT_MODULE = ['setting'];

View File

@ -9,11 +9,12 @@ const ApiTest: AppRouteRecordRaw = {
locale: 'menu.apiTest', locale: 'menu.apiTest',
icon: 'icon-dashboard', icon: 'icon-dashboard',
order: 0, order: 0,
hideChildrenInMenu: true,
}, },
children: [ children: [
{ {
path: 'list', path: 'list',
name: 'apiTest', name: 'apiTestList',
component: () => import('@/views/api-test/index.vue'), component: () => import('@/views/api-test/index.vue'),
meta: { meta: {
locale: 'menu.apiTest', 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'; import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
const useAppStore = defineStore('app', { 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: { getters: {
appCurrentSetting(state: AppState): AppState { appCurrentSetting(state: AppState): AppState {
@ -30,6 +37,12 @@ const useAppStore = defineStore('app', {
getTopMenus(state: AppState): RouteRecordRaw[] { getTopMenus(state: AppState): RouteRecordRaw[] {
return state.topMenus; return state.topMenus;
}, },
getCurrentOrgId(state: AppState): string {
return state.currentOrgId;
},
getCurrentProjectId(state: AppState): string {
return state.currentProjectId;
},
}, },
actions: { actions: {
@ -111,6 +124,21 @@ const useAppStore = defineStore('app', {
setTopMenus(menus: RouteRecordRaw[] | undefined) { setTopMenus(menus: RouteRecordRaw[] | undefined) {
this.topMenus = menus ? [...menus] : []; 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; loading: boolean;
loadingTip: string; loadingTip: string;
topMenus: RouteRecordRaw[]; topMenus: RouteRecordRaw[];
currentOrgId: string;
currentProjectId: string;
[key: string]: unknown; [key: string]: unknown;
} }

View File

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