feat: 页面及按钮权限&环境管理新增环境接口对接

This commit is contained in:
RubyLiu 2024-01-23 20:04:41 +08:00 committed by Craftsman
parent 8ca4f27432
commit 09b4b6756a
33 changed files with 591 additions and 195 deletions

View File

@ -67,6 +67,10 @@
const checkIsLogin = async () => { const checkIsLogin = async () => {
const isLogin = await userStore.isLogin(); const isLogin = await userStore.isLogin();
const isLoginPage = route.name === 'login'; const isLoginPage = route.name === 'login';
if (isLogin && appStore.currentProjectId) {
//
appStore.setCurrentMenuConfig();
}
if (isLoginPage && isLogin) { if (isLoginPage && isLogin) {
// //
router.push(WorkbenchRouteEnum.WORKBENCH); router.push(WorkbenchRouteEnum.WORKBENCH);

View File

@ -12,8 +12,8 @@ import type {
} from '@/models/projectManagement/environmental'; } from '@/models/projectManagement/environmental';
import { OptionsItem } from '@/models/setting/log'; import { OptionsItem } from '@/models/setting/log';
export function updateEnv(data: EnvListItem) { export function updateOrAddEnv(data: EnvDetailItem) {
return MSR.post<EnvListItem>({ url: envURL.updateEnvUrl, data }); return MSR.post<EnvDetailItem>({ url: data.id ? envURL.updateEnvUrl : envURL.addEnvUrl, data });
} }
export function listEnv(data: { projectId: string; keyword: string }) { export function listEnv(data: { projectId: string; keyword: string }) {
return MSR.post<EnvListItem[]>({ url: envURL.listEnvUrl, data }); return MSR.post<EnvListItem[]>({ url: envURL.listEnvUrl, data });
@ -24,8 +24,8 @@ export function importEnv(data: { request: EnvListItem; fileList: FileItem[] })
export function getEntryEnv(data: EnvListItem) { export function getEntryEnv(data: EnvListItem) {
return MSR.post<EnvListItem>({ url: envURL.getEntryEnvUrl, data }); return MSR.post<EnvListItem>({ url: envURL.getEntryEnvUrl, data });
} }
export function exportEnv(data: EnvListItem) { export function exportEnv(id: string) {
return MSR.post<EnvListItem>({ url: envURL.exportEnvUrl, data }); return MSR.get<EnvListItem>({ url: envURL.exportEnvUrl + id, responseType: 'blob' }, { isTransformResponse: false });
} }
export function editPosEnv(data: EnvListItem) { export function editPosEnv(data: EnvListItem) {
return MSR.post<EnvListItem>({ url: envURL.editPosEnvUrl, data }); return MSR.post<EnvListItem>({ url: envURL.editPosEnvUrl, data });
@ -85,5 +85,8 @@ export function getGlobalParamDetail(id: string) {
} }
/** 项目管理-环境-全局参数-导出 */ /** 项目管理-环境-全局参数-导出 */
export function exportGlobalParam(id: string) { export function exportGlobalParam(id: string) {
return MSR.get<BlobPart>({ url: envURL.exportGlobalParamUrl + id }); return MSR.get<BlobPart>(
{ url: envURL.exportGlobalParamUrl + id, responseType: 'blob' },
{ isTransformResponse: false }
);
} }

View File

@ -10,3 +10,7 @@ export function getProjectList(organizationId: string) {
export function switchProject(data: { projectId: string; userId: string }) { export function switchProject(data: { projectId: string; userId: string }) {
return MSR.post({ url: ProjectSwitchUrl, data }); return MSR.post({ url: ProjectSwitchUrl, data });
} }
export function getProjectInfo(projectId: string) {
return MSR.get<ProjectListItem>({ url: `/project/get/${projectId}` });
}

View File

@ -1,2 +1,3 @@
export const ProjectListUrl = '/project/list/options'; // 项目列表 export const ProjectListUrl = '/project/list/options'; // 项目列表
export const ProjectSwitchUrl = '/project/switch'; // 切换项目 export const ProjectSwitchUrl = '/project/switch'; // 切换项目
export const projectModuleInfoUrl = '/project/get/'; // 获取项目模块信息

View File

@ -0,0 +1,246 @@
<template>
<div class="flex h-full w-full flex-col">
<a-radio-group v-model:model-value="condition.scriptType" size="small" class="mb-[16px]">
<a-radio value="manual">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio value="quote">{{ t('apiTestDebug.quote') }}</a-radio>
</a-radio-group>
<div
v-if="scriptType === 'manual'"
class="relative h-full w-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
<a-input
ref="scriptNameInputRef"
v-model:model-value="condition.name"
:placeholder="t('apiTestDebug.preconditionScriptNamePlaceholder')"
:max-length="255"
show-word-limit
size="small"
@press-enter="isShowEditScriptNameInput = false"
@blur="isShowEditScriptNameInput = false"
/>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<a-tooltip :content="condition.name">
<div class="script-name-container">
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
{{ condition.name }}
</div>
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
</div>
</a-tooltip>
<a-popover class="h-auto" position="top">
<div class="text-[rgb(var(--primary-5))]">{{ t('apiTestDebug.scriptEx') }}</div>
<template #content>
<div class="mb-[8px] flex items-center justify-between">
<div class="text-[14px] font-medium text-[var(--color-text-1)]">
{{ t('apiTestDebug.scriptEx') }}
</div>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScriptEx"
>
{{ t('common.copy') }}
</a-button>
</div>
<div class="flex h-[412px]">
<MsCodeEditor
v-model:model-value="scriptEx"
class="flex-1"
theme="MS-text"
width="500px"
height="388px"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
</template>
</a-popover>
</div>
<div class="flex items-center gap-[8px]">
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini">
<template #icon>
<MsIcon type="icon-icon_undo_outlined" class="text-var(--color-text-4)" size="12" />
</template>
{{ t('common.revoke') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="clearScript">
<template #icon>
<MsIcon type="icon-icon_clear" class="text-var(--color-text-4)" size="12" />
</template>
{{ t('common.clear') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="copyCondition">
{{ t('common.copy') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="deleteCondition">
{{ t('common.delete') }}
</a-button>
</div>
</div>
</div>
<div v-else class="flex h-[calc(100%-47px)] flex-col">
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{ condition.quoteScript.name || '-' }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium">
{{ t('apiTestDebug.quote') }}
</MsButton>
</div>
<a-radio-group v-model:model-value="commonScriptShowType" size="small" type="button" class="mb-[8px] w-fit">
<a-radio value="parameters">{{ t('apiTestDebug.parameters') }}</a-radio>
<a-radio value="scriptContent">{{ t('apiTestDebug.scriptContent') }}</a-radio>
</a-radio-group>
<MsBaseTable v-show="commonScriptShowType === 'parameters'" v-bind="propsRes" v-on="propsEvent">
<template #value="{ record }">
<a-tooltip :content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')">
<div
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
>
<div>*</div>
</div>
</a-tooltip>
{{ record.type }}
</template>
</MsBaseTable>
<div v-show="commonScriptShowType === 'scriptContent'" class="h-[calc(100%-76px)]">
<MsCodeEditor
v-model:model-value="condition.quoteScript.script"
theme="MS-text"
height="100%"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
</MsCodeEditor>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useClipboard } from '@vueuse/core';
import { InputInstance, Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const scriptType = defineModel('scriptType', {
type: String,
default: 'manual',
});
const condition = defineModel('modelValue', {
type: Object,
default: () => ({
scriptType: 'manual',
name: '',
script: '',
quoteScript: {
name: '',
script: '',
},
}),
});
const columns: MsTableColumn = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
showTooltip: true,
},
{
title: 'apiTestDebug.paramValue',
dataIndex: 'value',
slotName: 'value',
},
{
title: 'apiTestDebug.desc',
dataIndex: 'desc',
showTooltip: true,
},
];
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
scroll: { x: '100%' },
columns,
});
const emit = defineEmits<{
(e: 'update:data', data: Record<string, any>): void;
(e: 'copy'): void;
(e: 'delete', id: string): void;
(e: 'change'): void;
}>();
//
const isShowEditScriptNameInput = ref(false);
const scriptNameInputRef = ref<InputInstance>();
function showEditScriptNameInput() {
isShowEditScriptNameInput.value = true;
nextTick(() => {
scriptNameInputRef.value?.focus();
});
}
const scriptEx = ref(`2023-12-04 11:19:28 INFO 9026fd6a 1-1 Thread started: 9026fd6a 1-1
2023-12-04 11:19:28 ERROR 9026fd6a 1-1 Problem in JSR223 script JSR223Sampler, message: {}
In file: inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' Encountered "import" at line 2, column 1.
in inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException
org.apache.http.client.method . . . '' at line number 2
`);
const { copy, isSupported } = useClipboard();
const commonScriptShowType = ref<'parameters' | 'scriptContent'>('parameters');
function copyScriptEx() {
if (isSupported) {
copy(scriptEx.value);
Message.success(t('apiTestDebug.scriptExCopySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
function clearScript() {
condition.value.script = '';
}
/**
* 复制条件
*/
function copyCondition() {
emit('copy');
}
/**
* 删除条件
*/
function deleteCondition() {
emit('delete', condition.value.id);
}
</script>
<style lang="less" scoped></style>

View File

@ -63,6 +63,7 @@
<ResponseHeaderTab v-if="valueKey === 'responseHeader'" /> <ResponseHeaderTab v-if="valueKey === 'responseHeader'" />
<ResponseTimeTab v-if="valueKey === 'responseTime'" /> <ResponseTimeTab v-if="valueKey === 'responseTime'" />
<VariableTab v-if="valueKey === 'variable'" /> <VariableTab v-if="valueKey === 'variable'" />
<ScriptTab v-if="valueKey === 'script'" />
</section> </section>
</div> </div>
</div> </div>
@ -77,6 +78,7 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import ResponseHeaderTab from './comp/ResponseHeaderTab.vue'; import ResponseHeaderTab from './comp/ResponseHeaderTab.vue';
import ResponseTimeTab from './comp/ResponseTimeTab.vue'; import ResponseTimeTab from './comp/ResponseTimeTab.vue';
import ScriptTab from './comp/ScriptTab.vue';
import StatusCodeTab from './comp/StatusCodeTab.vue'; import StatusCodeTab from './comp/StatusCodeTab.vue';
import VariableTab from './comp/VariableTab.vue'; import VariableTab from './comp/VariableTab.vue';

View File

@ -89,6 +89,7 @@
}); });
} }
}; };
menuTree.value?.forEach((el: RouteRecordRaw | null) => { menuTree.value?.forEach((el: RouteRecordRaw | null) => {
if (isFind) return; // if (isFind) return; //
backtrack(el, [el?.name as string]); backtrack(el, [el?.name as string]);
@ -336,6 +337,7 @@
} }
return nodes; return nodes;
} }
return travel(menuTree.value); return travel(menuTree.value);
}; };

View File

@ -79,7 +79,9 @@
(item) => name && item?.name && (name as string).includes(item.name as string) (item) => name && item?.name && (name as string).includes(item.name as string)
); );
} }
appStore.setTopMenus(currentParent?.children?.filter((item) => item.meta?.isTopMenu)); appStore.setTopMenus(
currentParent?.children?.filter((item) => permission.accessRouter(item) && item.meta?.isTopMenu)
);
setCurrentTopMenu(name as string); setCurrentTopMenu(name as string);
return; return;
} }

View File

@ -1,22 +1,17 @@
import { DirectiveBinding } from 'vue'; import { DirectiveBinding } from 'vue';
import { useUserStore } from '@/store'; import { hasAnyPermission } from '@/utils/permission';
/** /**
* ,TODO:权限判定按权限点来 *
* @param el dom * @param el dom
* @param binding vue * @param binding vue
*/ */
function checkPermission(el: HTMLElement, binding: DirectiveBinding) { function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding; const { value } = binding;
const userStore = useUserStore();
const { role } = userStore;
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (value.length > 0) { if (value.length > 0) {
const permissionValues = value; const hasPermission = hasAnyPermission(value);
const hasPermission = permissionValues.includes(role);
if (!hasPermission && el.parentNode) { if (!hasPermission && el.parentNode) {
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
} }

View File

@ -23,7 +23,7 @@ export enum CaseManagementRouteEnum {
} }
export enum PerformanceTestRouteEnum { export enum PerformanceTestRouteEnum {
PERFORMANCE_TEST = 'performanceTest', PERFORMANCE_TEST = 'loadTest',
} }
export enum ProjectManagementRouteEnum { export enum ProjectManagementRouteEnum {
@ -57,7 +57,7 @@ export enum UITestRouteEnum {
} }
export enum WorkbenchRouteEnum { export enum WorkbenchRouteEnum {
WORKBENCH = 'workbench', WORKBENCH = 'workstation',
} }
export enum SettingRouteEnum { export enum SettingRouteEnum {

View File

@ -1,13 +1,15 @@
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'; import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
import { includes } from 'lodash-es';
import { useUserStore } from '@/store'; import { hasAnyPermission, hasFirstMenuPermission } from '@/utils/permission';
const firstLevelMenu = ['workstation', 'testPlan', 'bugManagement', 'caseManagement', 'apiTest', 'uiTest', 'loadTest'];
/** /**
* *
* @returns * @returns
*/ */
export default function usePermission() { export default function usePermission() {
const userStore = useUserStore();
return { return {
/** /**
* 访 * 访
@ -15,35 +17,17 @@ export default function usePermission() {
* @returns * @returns
*/ */
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) { accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
if (includes(firstLevelMenu, route.name)) {
// 一级菜单
return hasFirstMenuPermission(route.name as string);
}
return ( return (
route.meta?.requiresAuth === false || route.meta?.requiresAuth === false ||
!route.meta?.roles || !route.meta?.roles ||
route.meta?.roles?.includes('*') || route.meta?.roles?.includes('*') ||
route.meta?.roles?.includes(userStore.role) hasAnyPermission(route.meta?.roles || [])
); );
}, },
/**
* 访
* @param _routers
* @param role
* @returns or null
*/
findFirstPermissionRoute(_routers: any, role = 'admin') {
const cloneRouters = [..._routers];
while (cloneRouters.length) {
const firstElement = cloneRouters.shift();
if (
firstElement?.meta?.roles?.find((el: string[]) => {
return el.includes('*') || el.includes(role);
})
)
return { name: firstElement.name };
if (firstElement?.children) {
cloneRouters.push(...firstElement.children);
}
}
return null;
},
// You can add any rules you want // You can add any rules you want
}; };
} }

View File

@ -38,16 +38,16 @@ export interface EnvConfig {
dataSource?: DataSourceItem[]; dataSource?: DataSourceItem[];
hostConfig?: EnvConfigItem; hostConfig?: EnvConfigItem;
authConfig?: EnvConfigItem; authConfig?: EnvConfigItem;
preScript?: EnvConfigItem; preScript?: EnvConfigItem[];
postScript?: EnvConfigItem; postScript?: EnvConfigItem[];
assertions?: EnvConfigItem; assertions?: EnvConfigItem[];
} }
export interface EnvDetailItem { export interface EnvDetailItem {
id?: string; id?: string;
projectId: string; projectId: string;
name: string; name: string;
config: EnvConfig; config: EnvConfig;
mock?: string; mock?: boolean;
description?: string; description?: string;
} }
export interface GlobalParamsItem { export interface GlobalParamsItem {

View File

@ -13,4 +13,5 @@ export interface ProjectListItem {
deleted: boolean; deleted: boolean;
deleteUser: string; deleteUser: string;
enable: boolean; enable: boolean;
moduleIds: string[];
} }

View File

@ -1,3 +1,5 @@
import { UserState } from '@/store/modules/user/types';
// 登录信息 // 登录信息
export interface LoginData { export interface LoginData {
username: string; username: string;
@ -5,37 +7,11 @@ export interface LoginData {
authenticate: string; authenticate: string;
} }
export interface UserRole {
id: string;
createTime: number;
createUser: string;
roleId: string;
sourceId: string;
userId: string;
}
// 登录返回 // 登录返回
export interface LoginRes { export interface LoginRes extends UserState {
csrfToken: string; csrfToken: string;
createTime: number;
createUser: string;
email: string;
enabled: boolean;
id: string;
language: string;
lastOrganizationId: string;
lastProjectId: string;
name: string;
phone: string;
platformInfo: string;
seleniumServer: string;
sessionId: string; sessionId: string;
source: string; token: string;
updateTime: number;
updateUser: string;
userRolePermissions: UserRole[];
userRoleRelations: UserRole[];
userRoles: UserRole[];
} }
// 更新本地执行配置 // 更新本地执行配置
export interface UpdateLocalConfigParams { export interface UpdateLocalConfigParams {
@ -94,23 +70,6 @@ export interface Resource {
name: string; name: string;
license: boolean; license: boolean;
} }
export interface UserRolePermission {
resource: Resource;
permissions: Permission[];
type: string;
userRole: UserRole;
userRolePermissions: Permission[];
}
export interface UserRoleRelation {
id: string;
userId: string;
roleId: string;
sourceId: string;
organizationId: string;
createTime: number;
createUser: string;
}
// 个人信息 // 个人信息
export interface PersonalOrganization { export interface PersonalOrganization {
id: string; id: string;

View File

@ -270,7 +270,7 @@ const ProjectManagement: AppRouteRecordRaw = {
component: () => import('@/views/project-management/environmental/index.vue'), component: () => import('@/views/project-management/environmental/index.vue'),
meta: { meta: {
locale: 'menu.projectManagement.environmentManagement', locale: 'menu.projectManagement.environmentManagement',
roles: ['*'], roles: ['PROJECT_ENVIRONMENT:READ'],
isTopMenu: true, isTopMenu: true,
}, },
}, },

View File

@ -50,7 +50,7 @@ const Setting: AppRouteRecordRaw = {
component: () => import('@/views/setting/system/organizationAndProject/index.vue'), component: () => import('@/views/setting/system/organizationAndProject/index.vue'),
meta: { meta: {
locale: 'menu.settings.system.organizationAndProject', locale: 'menu.settings.system.organizationAndProject',
roles: ['*'], roles: ['SYSTEM_ORGANIZATION_PROJECT:READ'],
isTopMenu: true, isTopMenu: true,
}, },
}, },

View File

@ -4,6 +4,7 @@ import { cloneDeep } from 'lodash-es';
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types'; import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
import { getProjectInfo } from '@/api/modules/project-management/basicInfo';
import { getPageConfig } from '@/api/modules/setting/config'; import { getPageConfig } from '@/api/modules/setting/config';
import { getSystemVersion } from '@/api/modules/system'; import { getSystemVersion } from '@/api/modules/system';
import { getMenuList } from '@/api/modules/user'; import { getMenuList } from '@/api/modules/user';
@ -51,6 +52,7 @@ const useAppStore = defineStore('app', {
defaultLoginConfig, defaultLoginConfig,
defaultPlatformConfig, defaultPlatformConfig,
innerHeight: 0, innerHeight: 0,
currentMenuConfig: [],
pageConfig: { pageConfig: {
...defaultThemeConfig, ...defaultThemeConfig,
...defaultLoginConfig, ...defaultLoginConfig,
@ -273,6 +275,18 @@ const useAppStore = defineStore('app', {
console.log(error); console.log(error);
} }
}, },
/**
*
*/
async setCurrentMenuConfig() {
try {
const res = await getProjectInfo(this.currentProjectId);
this.currentMenuConfig = res.moduleIds;
} catch (error) {
console.log(error);
}
},
}, },
persist: { persist: {
paths: ['currentOrgId', 'currentProjectId', 'pageConfig'], paths: ['currentOrgId', 'currentProjectId', 'pageConfig'],

View File

@ -2,6 +2,7 @@ import type { MsFileItem } from '@/components/pure/ms-upload/types';
import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types'; import type { BreadcrumbItem } from '@/components/business/ms-breadcrumb/types';
import type { LoginConfig, PageConfig, PlatformConfig, ThemeConfig } from '@/models/setting/config'; import type { LoginConfig, PageConfig, PlatformConfig, ThemeConfig } from '@/models/setting/config';
import { UserGroupAuthSetting } from '@/models/setting/usergroup';
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router'; import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
@ -35,6 +36,7 @@ export interface AppState {
defaultPlatformConfig: PlatformConfig; defaultPlatformConfig: PlatformConfig;
pageConfig: PageConfig; pageConfig: PageConfig;
innerHeight: number; innerHeight: number;
currentMenuConfig: string[];
} }
export interface UploadFileTaskState { export interface UploadFileTaskState {

View File

@ -12,7 +12,8 @@ const useProjectEnvStore = defineStore(
'projectEnv', 'projectEnv',
() => { () => {
const currentId = ref<string>(ALL_PARAM); // 当前选中的key值 const currentId = ref<string>(ALL_PARAM); // 当前选中的key值
const currentEnvDetailInfo = ref<EnvDetailItem>(); // 当前选中的环境详情 const currentEnvDetailInfo = ref<EnvDetailItem>({ projectId: '', name: '', config: {} }); // 当前选中的环境详情
const backupEnvDetailInfo = ref<EnvDetailItem>({ projectId: '', name: '', config: {} }); // 当前选中的环境详情-备份
const allParamDetailInfo = ref<GlobalParams>(); // 全局参数详情 const allParamDetailInfo = ref<GlobalParams>(); // 全局参数详情
const httpNoWarning = ref(true); const httpNoWarning = ref(true);
const getHttpNoWarning = computed(() => httpNoWarning.value); const getHttpNoWarning = computed(() => httpNoWarning.value);
@ -25,10 +26,6 @@ const useProjectEnvStore = defineStore(
function setHttpNoWarning(noWarning: boolean) { function setHttpNoWarning(noWarning: boolean) {
httpNoWarning.value = noWarning; httpNoWarning.value = noWarning;
} }
// 设置环境详情
function setEnvDetailInfo(item: EnvDetailItem) {
currentEnvDetailInfo.value = item;
}
// 设置全局参数 // 设置全局参数
function setAllParamDetailInfo(item: GlobalParams) { function setAllParamDetailInfo(item: GlobalParams) {
allParamDetailInfo.value = item; allParamDetailInfo.value = item;
@ -39,11 +36,14 @@ const useProjectEnvStore = defineStore(
const appStore = useAppStore(); const appStore = useAppStore();
try { try {
if (id === NEW_ENV_PARAM) { if (id === NEW_ENV_PARAM) {
currentEnvDetailInfo.value = undefined; currentEnvDetailInfo.value = { projectId: appStore.currentProjectId, name: '', config: {} };
backupEnvDetailInfo.value = { projectId: appStore.currentProjectId, name: '', config: {} };
} else if (id === ALL_PARAM) { } else if (id === ALL_PARAM) {
allParamDetailInfo.value = await getGlobalParamDetail(appStore.currentProjectId); allParamDetailInfo.value = await getGlobalParamDetail(appStore.currentProjectId);
} else if (id !== ALL_PARAM && id) { } else if (id !== ALL_PARAM && id) {
currentEnvDetailInfo.value = await getDetailEnv(id); const tmpObj = await getDetailEnv(id);
currentEnvDetailInfo.value = tmpObj;
backupEnvDetailInfo.value = JSON.parse(JSON.stringify(tmpObj));
} }
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -56,10 +56,11 @@ const useProjectEnvStore = defineStore(
getHttpNoWarning, getHttpNoWarning,
httpNoWarning, httpNoWarning,
allParamDetailInfo, allParamDetailInfo,
currentEnvDetailInfo,
backupEnvDetailInfo,
groupLength, groupLength,
setCurrentId, setCurrentId,
setHttpNoWarning, setHttpNoWarning,
setEnvDetailInfo,
setAllParamDetailInfo, setAllParamDetailInfo,
initEnvDetail, initEnvDetail,
}; };

View File

@ -4,13 +4,14 @@ import { isLogin as userIsLogin, login as userLogin, logout as userLogout } from
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { getHashParameters } from '@/utils'; import { getHashParameters } from '@/utils';
import { clearToken, setToken } from '@/utils/auth'; import { clearToken, setToken } from '@/utils/auth';
import { composePermissions } from '@/utils/permission';
import { removeRouteListener } from '@/utils/route-listener'; import { removeRouteListener } from '@/utils/route-listener';
import type { LoginData } from '@/models/user'; import type { LoginData } from '@/models/user';
import useAppStore from '../app'; import useAppStore from '../app';
import useLicenseStore from '../setting/license'; import useLicenseStore from '../setting/license';
import type { UserState } from './types'; import { UserState } from './types';
const useUserStore = defineStore('user', { const useUserStore = defineStore('user', {
// 开启数据持久化 // 开启数据持久化
@ -32,12 +33,29 @@ const useUserStore = defineStore('user', {
id: undefined, id: undefined,
certification: undefined, certification: undefined,
role: '', role: '',
userRolePermissions: [],
}), }),
getters: { getters: {
userInfo(state: UserState): UserState { userInfo(state: UserState): UserState {
return { ...state }; return { ...state };
}, },
isAdmin(state: UserState): boolean {
if (!state.userRolePermissions) return false;
return state.userRolePermissions.findIndex((ur) => ur.userRole.id === 'admin') > -1;
},
currentRole(state: UserState): {
projectPermissions: string[];
orgPermissions: string[];
systemPermissions: string[];
} {
const appStore = useAppStore();
return {
projectPermissions: composePermissions(state.userRolePermissions || [], 'PROJECT', appStore.currentProjectId),
orgPermissions: composePermissions(state.userRolePermissions || [], 'ORGANIZATION', appStore.currentOrgId),
systemPermissions: composePermissions(state.userRolePermissions || [], 'SYSTEM', 'global'),
};
},
}, },
actions: { actions: {
@ -51,7 +69,6 @@ const useUserStore = defineStore('user', {
setInfo(partial: Partial<UserState>) { setInfo(partial: Partial<UserState>) {
this.$patch(partial); this.$patch(partial);
}, },
// 重置用户信息 // 重置用户信息
resetInfo() { resetInfo() {
this.$reset(); this.$reset();

View File

@ -1,4 +1,25 @@
export type RoleType = '' | '*' | 'admin' | 'user'; export type RoleType = '' | '*' | 'admin' | 'user';
export type SystemScopeType = 'PROJECT' | 'ORGANIZATION' | 'SYSTEM';
export interface UserRole {
createTime: number;
updateTime: number;
createUser: string;
description?: string;
id: string;
name: string;
scopeId: string; // 项目/组织/系统 id
type: SystemScopeType;
}
export interface permissionsItem {
id: string;
permissionId: string;
roleId: string;
}
export interface UserRolePermissions {
userRole: UserRole;
userRolePermissions: permissionsItem[];
}
export interface UserState { export interface UserState {
name?: string; name?: string;
avatar?: string; avatar?: string;
@ -18,4 +39,5 @@ export interface UserState {
role: RoleType; role: RoleType;
lastOrganizationId?: string; lastOrganizationId?: string;
lastProjectId?: string; lastProjectId?: string;
userRolePermissions?: UserRolePermissions[];
} }

View File

@ -0,0 +1,67 @@
import { useAppStore, useUserStore } from '@/store';
import { SystemScopeType, UserRole, UserRolePermissions } from '@/store/modules/user/types';
export function hasPermission(permission: string, typeList: string[]) {
const userStore = useUserStore();
if (userStore.isAdmin) {
return true;
}
const { projectPermissions, orgPermissions, systemPermissions } = userStore.currentRole;
if (typeList.includes('PROJECT') && projectPermissions.includes(permission)) {
return true;
}
if (typeList.includes('ORGANIZATION') && orgPermissions.includes(permission)) {
return true;
}
if (typeList.includes('SYSTEM') && systemPermissions.includes(permission)) {
return true;
}
return false;
}
// 判断是否有权限
export function hasAnyPermission(permissions: string[], typeList = ['PROJECT', 'ORGANIZATION', 'SYSTEM']) {
return permissions.some((permission) => hasPermission(permission, typeList));
}
function filterProject(role: UserRole, id: string) {
return role && role.type === 'PROJECT' && role.scopeId === id;
}
function filterOrganization(role: UserRole, id: string) {
return role && role.type === 'ORGANIZATION' && role.scopeId === id;
}
function filterSystem(role: UserRole, id: string) {
return role && role.type === 'SYSTEM' && role.scopeId === id;
}
export function composePermissions(userRolePermissions: UserRolePermissions[], type: SystemScopeType, id: string) {
let func: (role: UserRole, val: string) => boolean;
switch (type) {
case 'PROJECT':
func = filterProject;
break;
case 'ORGANIZATION':
func = filterOrganization;
break;
default:
func = filterSystem;
break;
}
return userRolePermissions
.filter((ur) => func(ur.userRole, id))
.flatMap((role) => role.userRolePermissions)
.map((g) => g.permissionId);
}
// 判断当前一级菜单是否有权限
export function hasFirstMenuPermission(menuName: string) {
const userStore = useUserStore();
const appStore = useAppStore();
if (userStore.isAdmin || menuName === 'setting' || menuName === 'projectManagement') {
// 如果是超级管理员,或者是系统设置菜单,或者是项目菜单,都有权限
return true;
}
const { currentMenuConfig } = appStore;
return currentMenuConfig.includes(menuName);
}

View File

@ -15,6 +15,7 @@
:max-length="255" :max-length="255"
class="w-[732px]" class="w-[732px]"
:placeholder="t('project.environmental.envNamePlaceholder')" :placeholder="t('project.environmental.envNamePlaceholder')"
@blur="store.currentEnvDetailInfo.name = form.name"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -32,20 +33,21 @@
<PreTab v-else-if="activeKey === 'pre'" /> <PreTab v-else-if="activeKey === 'pre'" />
<PostTab v-else-if="activeKey === 'post'" /> <PostTab v-else-if="activeKey === 'post'" />
<AssertTab v-else-if="activeKey === 'assert'" /> <AssertTab v-else-if="activeKey === 'assert'" />
<DisplayTab v-else-if="activeKey === 'display'" />
</div> </div>
<div class="footer" :style="{ width: '100%' }"> <div class="footer" :style="{ width: '100%' }">
<a-button :disabled="!canSave" @click="handleReset">{{ t('common.cancel') }}</a-button> <a-button @click="handleReset">{{ t('common.cancel') }}</a-button>
<a-button :disabled="!canSave" type="primary" @click="handleSave">{{ t('common.save') }}</a-button> <a-button :disabled="!canSave" type="primary" @click="handleSave">{{ t('common.save') }}</a-button>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Message } from '@arco-design/web-vue';
import { isEqual } from 'lodash-es';
import AssertTab from './envParams/AssertTab.vue'; import AssertTab from './envParams/AssertTab.vue';
import DataBaseTab from './envParams/DatabaseTab.vue'; import DataBaseTab from './envParams/DatabaseTab.vue';
import DisplayTab from './envParams/DisplayTab.vue';
import EnvParamsTab from './envParams/EnvParamsTab.vue'; import EnvParamsTab from './envParams/EnvParamsTab.vue';
import HostTab from './envParams/HostTab.vue'; import HostTab from './envParams/HostTab.vue';
import HttpTab from './envParams/HttpTab.vue'; import HttpTab from './envParams/HttpTab.vue';
@ -53,17 +55,17 @@
import PreTab from './envParams/PreTab.vue'; import PreTab from './envParams/PreTab.vue';
import TcpTab from './envParams/TcpTab.vue'; import TcpTab from './envParams/TcpTab.vue';
import { updateOrAddEnv } from '@/api/modules/project-management/envManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import useProjectEnvStore, { NEW_ENV_PARAM } from '@/store/modules/setting/useProjectEnvStore';
const activeKey = ref('assert'); const activeKey = ref('assert');
const envForm = ref(); const envForm = ref();
const canSave = ref(false); const canSave = ref(false);
const { t } = useI18n(); const { t } = useI18n();
const loading = ref(false);
const store = useProjectEnvStore(); const store = useProjectEnvStore();
const appStore = useAppStore();
const form = reactive({ const form = reactive({
name: '', name: '',
@ -109,26 +111,42 @@
]; ];
const handleReset = () => { const handleReset = () => {
envForm.value?.resetFields(); envForm.value?.resetFields();
store.initEnvDetail();
}; };
const handleSave = () => { const handleSave = async () => {
envForm.value?.validate(async (valid) => { await envForm.value?.validate(async (valid) => {
if (valid) { if (!valid) {
console.log('form', form); try {
loading.value = true;
store.currentEnvDetailInfo.mock = true;
const res = await updateOrAddEnv(store.currentEnvDetailInfo);
store.currentEnvDetailInfo = res;
Message.success(t('common.saveSuccess'));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
} }
}); });
}; };
const initData = async () => {
await store.initEnvDetail();
};
watchEffect(() => { watchEffect(() => {
if (store.currentId === NEW_ENV_PARAM) { if (store.currentId) {
store.setEnvDetailInfo({ store.initEnvDetail();
name: '', }
projectId: appStore.currentProjectId, });
config: {},
}); watchEffect(() => {
} else { if (store.currentEnvDetailInfo) {
initData(); const { currentEnvDetailInfo } = store;
form.name = currentEnvDetailInfo.name;
}
});
watchEffect(() => {
if (store.currentEnvDetailInfo) {
const { currentEnvDetailInfo, backupEnvDetailInfo } = store;
canSave.value = !isEqual(currentEnvDetailInfo, backupEnvDetailInfo);
} }
}); });
</script> </script>

View File

@ -1,15 +1,20 @@
<template> <template>
<div> <MsAssertion v-model:params="params" />
<ms-assertion />
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MsAssertion from '@/components/business/ms-assertion/index.vue'; import MsAssertion from '@/components/business/ms-assertion/index.vue';
import { useI18n } from '@/hooks/useI18n'; import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
const { t } = useI18n(); const store = useProjectEnvStore();
const params = computed({
set: (value: any) => {
store.currentEnvDetailInfo.config.assertions = value;
},
get: () => store.currentEnvDetailInfo.config.assertions || [],
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -49,9 +49,14 @@
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n(); const { t } = useI18n();
const store = useProjectEnvStore(); const store = useProjectEnvStore();
const innerParam = computed({
get: () => (store.currentEnvDetailInfo.config.dataSource || []) as DataSourceItem[],
set: (value: DataSourceItem[] | undefined) => {
store.currentEnvDetailInfo.config.dataSource = value;
},
});
const keyword = ref(''); const keyword = ref('');
const tableStore = useTableStore(); const tableStore = useTableStore();
const addVisible = ref(false); const addVisible = ref(false);
@ -156,21 +161,8 @@
addVisible.value = true; addVisible.value = true;
}; };
const fetchData = () => {}; const fetchData = () => {};
const handleNoWarning = () => {
store.setHttpNoWarning(false);
};
const initData = () => { const initData = () => {
propsRes.value.data = [ propsRes.value.data = innerParam.value;
{
id: '1',
name: 'test',
desc: 'test',
url: 'test',
username: 'test',
poolMax: 'test',
timeout: 'test',
},
];
}; };
onMounted(() => { onMounted(() => {
initData(); initData();

View File

@ -1,13 +0,0 @@
<template>
<div class="p-[24px]">
<a-divider :margin="0" class="!mb-[16px]" />
</div>
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
</script>
<style lang="less" scoped></style>

View File

@ -1,13 +1,22 @@
<template> <template>
<AllPrams v-model:params="AllParams" :table-key="TableKeyEnum.PROJECT_MANAGEMENT_ENV_ENV_PARAM" /> <AllPrams v-model:params="allParams" :table-key="TableKeyEnum.PROJECT_MANAGEMENT_ENV_ENV_PARAM" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import AllPrams from '../allParams/index.vue'; import AllPrams from '../allParams/index.vue';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
const AllParams = ref<[]>([]); const store = useProjectEnvStore();
const allParams = computed({
set: (value: any) => {
store.currentEnvDetailInfo.config.commmonVariables = value;
},
get: () => store.currentEnvDetailInfo.config.commmonVariables || [],
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="p-[24px]"> <div>
<div class="flex flex-row items-center gap-[8px]"> <div class="flex flex-row items-center gap-[8px]">
<a-switch v-model:model-value="configSwitch" size="small" /> <a-switch type="line" size="small" />
<div class="text-[var(--color-text-1)]">{{ t('project.environmental.host.config') }}</div> <div class="text-[var(--color-text-1)]">{{ t('project.environmental.host.config') }}</div>
</div> </div>
<div class="mt-[8px]"> <div class="mt-[8px]">
@ -19,18 +19,31 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onClickOutside } from '@vueuse/core';
import MsBatchForm from '@/components/business/ms-batch-form/index.vue'; import MsBatchForm from '@/components/business/ms-batch-form/index.vue';
import { FormItemModel } from '@/components/business/ms-batch-form/types'; import { FormItemModel } from '@/components/business/ms-batch-form/types';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { EnvConfigItem } from '@/models/projectManagement/environmental';
import { FakeTableListItem } from '@/models/projectManagement/menuManagement'; import { FakeTableListItem } from '@/models/projectManagement/menuManagement';
const { t } = useI18n(); const { t } = useI18n();
const currentList = ref<FakeTableListItem[]>([]); const store = useProjectEnvStore();
const configSwitch = ref<boolean>(false); const currentList = computed({
get: () => (store.currentEnvDetailInfo.config.hostConfig || []) as FakeTableListItem[],
set: (value: EnvConfigItem[] | undefined) => {
store.currentEnvDetailInfo.config.hostConfig = value;
},
});
const batchFormRef = ref();
onClickOutside(batchFormRef, () => {
currentList.value = batchFormRef.value?.getFormResult();
});
type UserModalMode = 'create' | 'edit'; type UserModalMode = 'create' | 'edit';
const batchFormModels: Ref<FormItemModel[]> = ref([ const batchFormModels: Ref<FormItemModel[]> = ref([
{ {

View File

@ -68,6 +68,10 @@
const tableStore = useTableStore(); const tableStore = useTableStore();
const addVisible = ref(false); const addVisible = ref(false);
const currentId = ref(''); const currentId = ref('');
const innerParam = defineModel<TableData[]>('modelValue', {
type: Array,
default: () => [],
});
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
@ -160,15 +164,7 @@
store.setHttpNoWarning(false); store.setHttpNoWarning(false);
}; };
const initData = () => { const initData = () => {
propsRes.value.data = [ propsRes.value.data = innerParam.value;
{
host: 'http://www.baidu.com',
desc: '百度',
applyScope: '全部',
enableScope: '全部',
value: 'http://www.baidu.com',
},
];
}; };
onMounted(() => { onMounted(() => {
initData(); initData();

View File

@ -1,13 +1,20 @@
<template> <template>
<div class="p-[24px]"> <PostTab v-model:params="params" layout="horizontal" />
<a-divider :margin="0" class="!mb-[16px]" />
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n'; import PostTab from '@/views/api-test/debug/components/debug/postcondition.vue';
const { t } = useI18n(); import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
const store = useProjectEnvStore();
const params = computed({
set: (value: any) => {
store.currentEnvDetailInfo.config.postScript = value;
},
get: () => store.currentEnvDetailInfo.config.postScript || [],
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -1,13 +1,20 @@
<template> <template>
<div class="p-[24px]"> <PreTab v-model:params="params" layout="horizontal" />
<a-divider :margin="0" class="!mb-[16px]" />
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n'; import PreTab from '@/views/api-test/debug/components/debug/precondition.vue';
const { t } = useI18n(); import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
const store = useProjectEnvStore();
const params = computed({
set: (value: any) => {
store.currentEnvDetailInfo.config.preScript = value;
},
get: () => store.currentEnvDetailInfo.config.preScript || [],
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -20,17 +20,33 @@
</template> </template>
<template #operation="{ record }"> <template #operation="{ record }">
<template v-if="record.deleted"> <template v-if="record.deleted">
<MsButton @click="handleRevokeDelete(record)">{{ t('common.revokeDelete') }}</MsButton> <MsButton v-permission="['SYSTEM_ORGANIZATION_PROJECT:READ+RECOVER']" @click="handleRevokeDelete(record)">{{
t('common.revokeDelete')
}}</MsButton>
</template> </template>
<template v-else-if="!record.enable"> <template v-else-if="!record.enable">
<MsButton @click="handleEnableOrDisableOrg(record)">{{ t('common.enable') }}</MsButton> <MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+UPDATE']" @click="handleEnableOrDisableOrg(record)">{{
<MsButton @click="handleDelete(record)">{{ t('common.delete') }}</MsButton> t('common.enable')
}}</MsButton>
<MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+DELETE']" @click="handleDelete(record)">{{
t('common.delete')
}}</MsButton>
</template> </template>
<template v-else> <template v-else>
<MsButton @click="showOrganizationModal(record)">{{ t('common.edit') }}</MsButton> <MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+UPDATE']" @click="showOrganizationModal(record)">{{
<MsButton @click="showAddUserModal(record)">{{ t('system.organization.addMember') }}</MsButton> t('common.edit')
<MsButton @click="handleEnableOrDisableOrg(record, false)">{{ t('common.end') }}</MsButton> }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleMoreAction($event, record)"></MsTableMoreAction> <MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ']" @click="showAddUserModal(record)">{{
t('system.organization.addMember')
}}</MsButton>
<MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ']" @click="handleEnableOrDisableOrg(record, false)">{{
t('common.end')
}}</MsButton>
<MsTableMoreAction
v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+DELETE']"
:list="tableActions"
@select="handleMoreAction($event, record)"
></MsTableMoreAction>
</template> </template>
</template> </template>
</MsBaseTable> </MsBaseTable>

View File

@ -17,17 +17,37 @@
</template> </template>
<template #operation="{ record }"> <template #operation="{ record }">
<template v-if="record.deleted"> <template v-if="record.deleted">
<MsButton @click="handleRevokeDelete(record)">{{ t('common.revokeDelete') }}</MsButton> <MsButton v-permission="['SYSTEM_ORGANIZATION_PROJECT:READ+RECOVER']" @click="handleRevokeDelete(record)">{{
t('common.revokeDelete')
}}</MsButton>
</template> </template>
<template v-else-if="!record.enable"> <template v-else-if="!record.enable">
<MsButton @click="handleEnableOrDisableProject(record)">{{ t('common.enable') }}</MsButton> <MsButton
<MsButton @click="handleDelete(record)">{{ t('common.delete') }}</MsButton> v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+UPDATE']"
@click="handleEnableOrDisableProject(record)"
>{{ t('common.enable') }}</MsButton
>
<MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+DELETE']" @click="handleDelete(record)">{{
t('common.delete')
}}</MsButton>
</template> </template>
<template v-else> <template v-else>
<MsButton @click="showAddProjectModal(record)">{{ t('common.edit') }}</MsButton> <MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+UPDATE']" @click="showAddProjectModal(record)">{{
<MsButton @click="showAddUserModal(record)">{{ t('system.organization.addMember') }}</MsButton> t('common.edit')
<MsButton @click="handleEnableOrDisableProject(record, false)">{{ t('common.end') }}</MsButton> }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleMoreAction($event, record)"></MsTableMoreAction> <MsButton v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ']" @click="showAddUserModal(record)">{{
t('system.organization.addMember')
}}</MsButton>
<MsButton
v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+UPDATE']"
@click="handleEnableOrDisableProject(record, false)"
>{{ t('common.end') }}</MsButton
>
<MsTableMoreAction
v-permission="['SYSTEM_ORGANIZATIN_PROJECT:READ+DELETE']"
:list="tableActions"
@select="handleMoreAction($event, record)"
></MsTableMoreAction>
</template> </template>
</template> </template>
</MsBaseTable> </MsBaseTable>