feat: 无权限&无资源页面开发

This commit is contained in:
RubyLiu 2024-01-24 21:53:12 +08:00 committed by CaptainB
parent d30f30151f
commit 144b26f213
17 changed files with 355 additions and 65 deletions

View File

@ -10,6 +10,7 @@
import { useRoute, useRouter } from 'vue-router';
import { useEventListener, useWindowSize } from '@vueuse/core';
import { getProjectInfo } from '@/api/modules/project-management/basicInfo';
import { saveBaseUrl } from '@/api/modules/setting/config';
import { GetPlatformIconUrl } from '@/api/requrls/setting/config';
// import GlobalSetting from '@/components/pure/global-setting/index.vue';
@ -69,7 +70,19 @@
const isLoginPage = route.name === 'login';
if (isLogin && appStore.currentProjectId) {
//
appStore.setCurrentMenuConfig();
try {
const res = await getProjectInfo(appStore.currentProjectId);
if (res.deleted || !res.enable) {
//
router.push(WorkbenchRouteEnum.WORKBENCH);
return;
}
appStore.setCurrentMenuConfig(res.moduleIds);
} catch (err) {
appStore.setCurrentMenuConfig([]);
// eslint-disable-next-line no-console
console.log(err);
}
}
if (isLoginPage && isLogin) {
//

View File

@ -1,3 +1,5 @@
import { MsUserSelectorOption } from '@/components/business/ms-user-selector/types';
import MSR from '@/api/http/index';
import * as orgUrl from '@/api/requrls/setting/organizationAndProject';
@ -172,7 +174,10 @@ export function addProjectMemberByOrg(data: AddUserToOrgOrProjectParams) {
// 组织-获取项目下的管理员选项
export function getAdminByProjectByOrg(organizationId: string, keyword: string) {
return MSR.get({ url: `${orgUrl.getAdminByOrganizationOrProjectUrl}${organizationId}`, params: { keyword } });
return MSR.get<MsUserSelectorOption[]>({
url: `${orgUrl.getAdminByOrganizationOrProjectUrl}${organizationId}`,
params: { keyword },
});
}
// 组织-获取成员下的成员选项

View File

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="251" height="203" viewBox="0 0 251 203" fill="none">
<g filter="url(#filter0_d_16553_74273)">
<path d="M9 9C9 6.79086 10.7909 5 13 5H237.088C239.297 5 241.088 6.79086 241.088 9V185.615C241.088 187.824 239.297 189.615 237.088 189.615H13C10.7909 189.615 9 187.824 9 185.615V9Z" fill="#F9F9FE"/>
<path d="M9.5 9C9.5 7.06701 11.067 5.5 13 5.5H237.088C239.021 5.5 240.588 7.067 240.588 9V185.615C240.588 187.548 239.021 189.115 237.088 189.115H13C11.067 189.115 9.5 187.548 9.5 185.615V9Z" stroke="white"/>
</g>
<path d="M10 9C10 7.34315 11.3431 6 13 6H237C238.657 6 240 7.34315 240 9V25H10V9Z" fill="#EDEDF1"/>
<circle cx="36.4697" cy="15.1207" r="4.33734" fill="white"/>
<circle cx="50.9277" cy="15.1207" r="4.33734" fill="white"/>
<circle cx="22.0114" cy="15.1207" r="4.33734" fill="white"/>
<path d="M18 36C18 34.8954 18.8954 34 20 34H230C231.105 34 232 34.8954 232 36V177C232 178.105 231.105 179 230 179H20C18.8954 179 18 178.105 18 177V36Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.333 81.1667C114.333 74.9995 119.333 70 125.5 70C131.667 70 136.667 74.9995 136.667 81.1667V90.5H114.333V81.1667ZM108.333 90.5V81.1667C108.333 71.6858 116.019 64 125.5 64C134.981 64 142.667 71.6858 142.667 81.1667V90.5H148.167C150.836 90.5 153 92.664 153 95.3333V120.833C153 123.503 150.836 125.667 148.167 125.667H102.833C100.164 125.667 98 123.503 98 120.833V95.3333C98 92.664 100.164 90.5 102.833 90.5H108.333ZM125.5 101.833C126.605 101.833 127.5 102.729 127.5 103.833V112.333C127.5 113.438 126.605 114.333 125.5 114.333C124.395 114.333 123.5 113.438 123.5 112.333V103.833C123.5 102.729 124.395 101.833 125.5 101.833Z" fill="#F2E9F6"/>
<defs>
<filter id="filter0_d_16553_74273" x="0" y="0" width="250.088" height="202.615" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="erode" in="SourceAlpha" result="effect1_dropShadow_16553_74273"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.392157 0 0 0 0 0.392157 0 0 0 0 0.4 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_16553_74273"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_16553_74273" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,7 @@
export interface MsUserSelectorOption {
id: string;
name: string;
email: string;
disabled?: boolean;
[key: string]: string | number | boolean | undefined;
}

View File

@ -3,55 +3,57 @@
<div v-if="navbar" class="layout-navbar z-[100]">
<NavBar :is-preview="innerProps.isPreview" :logo="innerLogo" :name="innerName" />
</div>
<a-layout>
<slot name="body">
<a-layout>
<a-layout-sider
v-if="renderMenu && !innerProps.isPreview"
v-show="!hideMenu"
class="layout-sider z-[99]"
breakpoint="xl"
:collapsed="collapsed"
:collapsible="true"
:width="menuWidth"
:collapsed-width="collapsedWidth"
:style="{ paddingTop: navbar ? navbarHeight : '' }"
:hide-trigger="true"
@collapse="setCollapsed"
>
<div class="menu-wrapper">
<a-layout>
<a-layout-sider
v-if="renderMenu && !innerProps.isPreview"
v-show="!hideMenu"
class="layout-sider z-[99]"
breakpoint="xl"
:collapsed="collapsed"
:collapsible="true"
:width="menuWidth"
:collapsed-width="collapsedWidth"
:style="{ paddingTop: navbar ? navbarHeight : '' }"
:hide-trigger="true"
@collapse="setCollapsed"
>
<div class="menu-wrapper">
<MsMenu />
</div>
</a-layout-sider>
<a-drawer
v-if="hideMenu"
:visible="drawerVisible"
placement="left"
:footer="false"
mask-closable
:closable="false"
@cancel="drawerCancel"
>
<MsMenu />
</div>
</a-layout-sider>
<a-drawer
v-if="hideMenu"
:visible="drawerVisible"
placement="left"
:footer="false"
mask-closable
:closable="false"
@cancel="drawerCancel"
>
<MsMenu />
</a-drawer>
<a-layout class="layout-content" :style="paddingStyle">
<a-spin :loading="appStore.loading" :tip="appStore.loadingTip">
<a-scrollbar
:style="{
overflow: 'auto',
height: 'calc(100vh - 64px)',
}"
>
<MsBreadCrumb />
<a-layout-content>
<PageLayout v-if="!props.isPreview" />
<slot></slot>
</a-layout-content>
<Footer v-if="footer" />
</a-scrollbar>
</a-spin>
</a-drawer>
<a-layout class="layout-content" :style="paddingStyle">
<a-spin :loading="appStore.loading" :tip="appStore.loadingTip">
<a-scrollbar
:style="{
overflow: 'auto',
height: 'calc(100vh - 64px)',
}"
>
<MsBreadCrumb />
<a-layout-content>
<PageLayout v-if="!props.isPreview" />
<slot></slot>
</a-layout-content>
<Footer v-if="footer" />
</a-scrollbar>
</a-spin>
</a-layout>
</a-layout>
</a-layout>
</a-layout>
</slot>
</a-layout>
</template>
@ -73,6 +75,7 @@
isPreview?: boolean;
logo?: string;
name?: string;
singleLogo?: boolean;
}
const props = defineProps<Props>();

View File

@ -0,0 +1,111 @@
<template>
<div class="single-logo-layout">
<DefaultLayout
:logo="pageConfig.logoPlatform[0]?.url || defaultPlatformLogo"
:name="pageConfig.platformName"
class="overflow-hidden"
is-preview
>
<template #body>
<div class="body">
<div class="content-wrapper">
<div class="content">
<div class="icon">
<div class="icon-svg">
<svg-icon width="232px" height="184px" name="no_resource" />
</div>
<div class="radius-box"></div>
</div>
<div class="title">
<span>{{ props.isProject ? t('common.noProject') : t('common.noResource') }}</span>
<span class="user">
{{ adminStr }}
</span>
</div>
<slot></slot>
</div>
</div>
</div>
</template>
</DefaultLayout>
</div>
</template>
<script setup lang="ts">
import DefaultLayout from './default-layout.vue';
import { getAdminByProjectByOrg } from '@/api/modules/setting/organizationAndProject';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
const defaultPlatformLogo = `${import.meta.env.BASE_URL}images/MS-full-logo.svg`;
const appStore = useAppStore();
const pageConfig = ref({ ...appStore.pageConfig });
const adminStr = ref<string>('');
const props = defineProps<{
isProject?: boolean;
}>();
const { t } = useI18n();
const initData = async () => {
try {
const res = (await getAdminByProjectByOrg(appStore.currentOrgId, '')) || [];
adminStr.value = res.map((item) => item.name).join(';');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
};
onMounted(() => {
initData();
});
</script>
<style lang="less" scoped>
.single-logo-layout {
height: 100vh;
background-color: var(--color-text-n9);
.body {
margin-top: 56px;
padding: 17px 14px 15px 18px;
height: 100%;
.content-wrapper {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-color: var(--color-text-fff);
.content {
.icon {
position: relative;
display: flex;
align-items: center;
margin-bottom: 40px;
height: 218px;
flex-direction: column;
.icon-svg {
z-index: 100;
}
.radius-box {
position: relative;
bottom: 83px;
width: 355px;
height: 117px;
flex-shrink: 0;
border-radius: 355px;
background: linear-gradient(180deg, #ededf1 0%, rgb(255 255 255 / 0%) 100%);
}
}
.title {
font-size: 16px;
color: var(--color-text-1);
.user {
margin-left: 16px;
}
}
}
}
}
}
</style>

View File

@ -98,4 +98,7 @@ export default {
'common.tag': 'Tag',
'common.success': 'Success',
'common.fail': 'Failed',
'common.noProject': 'No project, please contact the administrator',
'common.noResource': 'No resource, please contact the administrator',
'common.noSelectProject': 'No optional items available',
};

View File

@ -101,4 +101,7 @@ export default {
'common.tag': '标签',
'common.success': '成功',
'common.fail': '失败',
'common.noProject': '暂无项目权限,请联系管理员',
'common.noResource': '暂无资源权限,请联系管理员',
'common.noSelectProject': '无可选项目',
};

View File

@ -18,4 +18,10 @@ export const REDIRECT_ROUTE_NAME = 'Redirect';
// 首页路由
export const DEFAULT_ROUTE_NAME = 'workbench';
// 无资源/权限路由
export const NO_RESOURCE_ROUTE_NAME = 'noResource';
// 无项目路由
export const NO_PROJECT_ROUTE_NAME = 'noProject';
export const WHITE_LIST_NAME = WHITE_LIST.map((el) => el.name);

View File

@ -1,6 +1,6 @@
import usePermission from '@/hooks/usePermission';
import { NOT_FOUND, WHITE_LIST } from '../constants';
import { NO_RESOURCE_ROUTE_NAME, WHITE_LIST } from '../constants';
import NProgress from 'nprogress'; // progress bar
import type { Router } from 'vue-router';
@ -13,7 +13,7 @@ export default function setupPermissionGuard(router: Router) {
if (exist || permissionsAllow) {
next();
} else next(NOT_FOUND);
} else next(NO_RESOURCE_ROUTE_NAME);
NProgress.done();
});
}

View File

@ -3,7 +3,7 @@ import { createRouter, createWebHashHistory } from 'vue-router';
import 'nprogress/nprogress.css';
import createRouteGuard from './guard';
import appRoutes from './routes';
import { INVITE_ROUTE, NOT_FOUND_ROUTE, REDIRECT_MAIN } from './routes/base';
import { INVITE_ROUTE, NO_PROJECT, NO_RESOURCE, NOT_FOUND_ROUTE, REDIRECT_MAIN } from './routes/base';
import NProgress from 'nprogress'; // progress bar
NProgress.configure({ showSpinner: false }); // NProgress Configuration
@ -27,6 +27,8 @@ const router = createRouter({
REDIRECT_MAIN,
NOT_FOUND_ROUTE,
INVITE_ROUTE,
NO_PROJECT,
NO_RESOURCE,
],
scrollBehavior() {
return { top: 0 };

View File

@ -1,4 +1,4 @@
import { REDIRECT_ROUTE_NAME } from '@/router/constants';
import { NO_PROJECT_ROUTE_NAME, NO_RESOURCE_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants';
import type { RouteRecordRaw } from 'vue-router';
@ -38,3 +38,21 @@ export const INVITE_ROUTE: RouteRecordRaw = {
hideInMenu: true,
},
};
export const NO_RESOURCE: RouteRecordRaw = {
path: '/no-resource',
name: NO_RESOURCE_ROUTE_NAME,
component: () => import('@/views/base/no-resource/index.vue'),
meta: {
hideInMenu: true,
},
};
export const NO_PROJECT: RouteRecordRaw = {
path: '/no-project',
name: NO_PROJECT_ROUTE_NAME,
component: () => import('@/views/base/no-project/index.vue'),
meta: {
hideInMenu: true,
},
};

View File

@ -44,7 +44,7 @@ const ProjectManagement: AppRouteRecordRaw = {
component: () => import('@/views/project-management/projectAndPermission/menuManagement/menuManagement.vue'),
meta: {
locale: 'project.permission.menuManagement',
roles: ['*'],
roles: ['PROJECT_APPLICATION_WORKSTATION:READ'],
},
},
// 项目版本
@ -74,7 +74,7 @@ const ProjectManagement: AppRouteRecordRaw = {
component: () => import('@/views/project-management/projectAndPermission/userGroup/projectUserGroup.vue'),
meta: {
locale: 'project.permission.userGroup',
roles: ['*'],
roles: ['PROJECT_GROUP:READ'],
},
},
],

View File

@ -279,13 +279,8 @@ const useAppStore = defineStore('app', {
/**
*
*/
async setCurrentMenuConfig() {
try {
const res = await getProjectInfo(this.currentProjectId);
this.currentMenuConfig = res.moduleIds;
} catch (error) {
console.log(error);
}
async setCurrentMenuConfig(menuConfig: string[]) {
this.currentMenuConfig = menuConfig;
},
},
persist: {

View File

@ -0,0 +1,86 @@
<template>
<single-logo-layout is-project>
<div class="mt-[24px] flex items-center justify-center">
<a-select class="w-[280px]" allow-search @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.currentProjectId ? 'arco-select-option-selected' : ''"
>
{{ project.name }}
</a-option>
</a-tooltip>
<template #empty>
<div class="text-[var(--color-text-4)]">
{{ t('common.noSelectProject') }}
</div>
</template>
</a-select>
</div>
</single-logo-layout>
</template>
<script lang="ts" setup>
import { onBeforeMount, Ref, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import SingleLogoLayout from '@/layout/single-logo-layout.vue';
import { getProjectList, switchProject } from '@/api/modules/project-management/project';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore, useUserStore } from '@/store';
import { SelectValue } from '@/models/projectManagement/menuManagement';
import type { ProjectListItem } from '@/models/setting/project';
const appStore = useAppStore();
const projectList: Ref<ProjectListItem[]> = ref([]);
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
async function initProjects() {
try {
const res = await getProjectList(appStore.getCurrentOrgId);
projectList.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function selectProject(value: SelectValue) {
appStore.setCurrentProjectId(value as string);
try {
await switchProject({
projectId: value as string,
userId: userStore.id || '',
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
router.replace({
path: route.path,
query: {
...route.query,
organizationId: appStore.currentOrgId,
projectId: appStore.currentProjectId,
},
});
}
}
onBeforeMount(() => {
initProjects();
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<single-logo-layout is-project />
</template>
<script lang="ts" setup>
import SingleLogoLayout from '@/layout/single-logo-layout.vue';
</script>

View File

@ -26,11 +26,18 @@
</template>
<template #operation="{ record }">
<div class="flex flex-row flex-nowrap">
<MsButton class="!mr-0" @click="showAuthDrawer(record)">{{ t('project.userGroup.viewAuth') }}</MsButton>
<a-divider v-if="!record.internal" direction="vertical" />
<MsButton v-if="!record.internal" class="!mr-0" status="danger" @click="handleDelete(record)">{{
t('common.delete')
}}</MsButton>
<span v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+UPDATE']" class="flex flex-row">
<MsButton class="!mr-0" @click="showAuthDrawer(record)">{{ t('project.userGroup.viewAuth') }}</MsButton>
<a-divider v-if="!record.internal" direction="vertical" />
</span>
<MsButton
v-if="!record.internal"
v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+UPDATE']"
class="!mr-0"
status="danger"
@click="handleDelete(record)"
>{{ t('common.delete') }}</MsButton
>
</div>
</template>
</MsBaseTable>