feat: 登录的盐&组织的用户组创建编辑保存权限

This commit is contained in:
RubyLiu 2023-08-24 19:11:17 +08:00 committed by 刘瑞斌
parent 14d401342b
commit 383e3fcf78
46 changed files with 2222 additions and 80 deletions

View File

@ -18,10 +18,13 @@
import { watchStyle, watchTheme, setFavicon } from '@/utils/theme';
import { GetPlatformIconUrl } from '@/api/requrls/setting/config';
import { useUserStore } from '@/store';
import { useRouter } from 'vue-router';
import { WorkbenchRouteEnum } from './enums/routeEnum';
const appStore = useAppStore();
const userStore = useUserStore();
const licenseStore = useLicenseStore();
const router = useRouter();
const { currentLocale } = useLocale();
const locale = computed(() => {
@ -61,7 +64,15 @@
console.log(error);
}
});
onMounted(() => {
userStore.isLogin();
const checkIsLogin = async () => {
const isLogin = await userStore.isLogin();
const isLoginPage = window.location.hash === '#/login';
if (isLoginPage && isLogin) {
//
router.push(WorkbenchRouteEnum.WORKBENCH);
}
};
onMounted(async () => {
await checkIsLogin();
});
</script>

View File

@ -5,15 +5,19 @@ import useUser from '@/hooks/useUser';
export default function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = 'message'): void {
const { t } = useI18n();
const { logout } = useUser();
const { logout, setSalt, isLoginPage } = useUser();
let errMessage = '';
switch (status) {
case 400:
errMessage = `${msg}`;
break;
case 401: {
errMessage = msg || t('api.errMsg401');
setSalt(msg);
if (!isLoginPage()) {
// 不是登录页再调用logout
logout();
errMessage = t('api.errMsg401');
}
break;
}
case 403:

View File

@ -155,7 +155,6 @@ const transform: AxiosTransform = {
} catch (e) {
throw new Error(e as unknown as string);
}
checkStatus(error?.response?.status, msg, errorMessageMode);
return Promise.reject(error);
},

View File

@ -1,65 +1,85 @@
import MSR from '@/api/http/index';
import {
updateUserGroupU,
getUserGroupU,
addUserGroupU,
deleteUserGroupU,
getGlobalUSettingUrl,
editGlobalUSettingUrl,
postUserByUserGroupUrl,
deleteUserFromUserGroupUrl,
getUserListUrl,
addUserToUserGroupUrl,
} from '@/api/requrls/setting/usergroup';
import * as ugUrl from '@/api/requrls/setting/usergroup';
import { TableQueryParams, CommonList } from '@/models/common';
import { UserGroupItem, UserGroupAuthSetting, SaveGlobalUSettingData, UserTableItem } from '@/models/setting/usergroup';
import {
UserGroupItem,
SystemUserGroupParams,
OrgUserGroupParams,
UserGroupAuthSetting,
SaveGlobalUSettingData,
UserTableItem,
} from '@/models/setting/usergroup';
export function updateOrAddUserGroup(data: Partial<UserGroupItem>) {
// 系统-创建或修改用户组
export function updateOrAddUserGroup(data: SystemUserGroupParams) {
return MSR.post<UserGroupItem>({
url: data.id ? updateUserGroupU : addUserGroupU,
url: data.id ? ugUrl.updateUserGroupU : ugUrl.addUserGroupU,
data,
});
}
// 组织-创建或修改用户组
export function updateOrAddOrgUserGroup(data: OrgUserGroupParams) {
return MSR.post<UserGroupItem>({
url: data.id ? ugUrl.updateOrgUserGroupU : ugUrl.addOrgUserGroupU,
data,
});
}
export function updateSocpe(data: Partial<UserGroupItem>) {
export function updateSocpe(data: Partial<SystemUserGroupParams>) {
return MSR.post<UserGroupItem>({
url: updateUserGroupU,
url: ugUrl.updateUserGroupU,
data,
});
}
// 系统-获取用户组列表
export function getUserGroupList() {
return MSR.get<UserGroupItem[]>({ url: getUserGroupU });
return MSR.get<UserGroupItem[]>({ url: ugUrl.getUserGroupU });
}
// 组织-获取用户组列表
export function getOrgUserGroupList(organizationId: string) {
return MSR.get<UserGroupItem[]>({ url: `${ugUrl.getOrgUserGroupU}${organizationId}` });
}
// 系统-删除用户组
export function deleteUserGroup(id: string) {
return MSR.get<string>({ url: `${ugUrl.deleteUserGroupU}${id}` });
}
export function deleteUserGroup(id: string) {
return MSR.get<string>({ url: `${deleteUserGroupU}${id}` });
// 组织-删除用户组
export function deleteOrgUserGroup(id: string) {
return MSR.get<string>({ url: `${ugUrl.deleteOrgUserGroupU}${id}` });
}
export function getUsergroupInfo(id: string) {
return MSR.get<UserGroupItem>({ url: `${getUserGroupU}${id}` });
return MSR.get<UserGroupItem>({ url: `${ugUrl.getUserGroupU}${id}` });
}
// 系统-获取用户组对应的权限配置
export function getGlobalUSetting(id: string) {
return MSR.get<UserGroupAuthSetting[]>({ url: `${getGlobalUSettingUrl}${id}` });
return MSR.get<UserGroupAuthSetting[]>({ url: `${ugUrl.getGlobalUSettingUrl}${id}` });
}
// 组织-获取用户组对应的权限配置
export function getOrgUSetting(id: string) {
return MSR.get<UserGroupAuthSetting[]>({ url: `${ugUrl.getOrgUSettingUrl}${id}` });
}
// 系统-编辑用户组对应的权限配置
export function saveGlobalUSetting(data: SaveGlobalUSettingData) {
return MSR.post<UserGroupAuthSetting[]>({ url: editGlobalUSettingUrl, data });
return MSR.post<UserGroupAuthSetting[]>({ url: ugUrl.editGlobalUSettingUrl, data });
}
// 组织-编辑用户组对应的权限配置
export function saveOrgUSetting(data: SaveGlobalUSettingData) {
return MSR.post<UserGroupAuthSetting[]>({ url: ugUrl.editOrgUSettingUrl, data });
}
export function postUserByUserGroup(data: TableQueryParams) {
return MSR.post<CommonList<UserTableItem[]>>({ url: postUserByUserGroupUrl, data });
return MSR.post<CommonList<UserTableItem[]>>({ url: ugUrl.postUserByUserGroupUrl, data });
}
export function deleteUserFromUserGroup(id: string) {
return MSR.get<string>({ url: `${deleteUserFromUserGroupUrl}${id}` });
}
export function getUserList() {
return MSR.get<UserTableItem[]>({ url: getUserListUrl });
return MSR.get<string>({ url: `${ugUrl.deleteUserFromUserGroupUrl}${id}` });
}
export function addUserToUserGroup(data: { roleId: string; userIds: string[] }) {
return MSR.post<string>({ url: addUserToUserGroupUrl, data });
return MSR.post<string>({ url: ugUrl.addUserToUserGroupUrl, data });
}

View File

@ -31,7 +31,7 @@ export const postModifyProjectUrl = '/system/project/update';
// 获取项目列表
export const postProjectTableUrl = '/system/project/page';
// 获取项目成员
export const postProjectMemberUrl = '/system/project/member/list';
export const postProjectMemberUrl = '/system/project/member-list';
// 添加项目
export const postAddProjectUrl = '/system/project/add';
// 添加项目成员
@ -45,7 +45,7 @@ export const getProjectInfoUrl = '/system/project/get/';
// 删除项目
export const getDeleteProjectUrl = '/system/project/delete/';
// 系统-组织及项目,获取用户下拉选项
export const getUserByOrgOrProjectUrl = '/system/user/get-option/';
export const getUserByOrgOrProjectUrl = '/system/organization/get-option/';
// 启用项目
export const getEnableProjectUrl = '/system/project/enable/';
// 禁用项目

View File

@ -19,5 +19,26 @@ export const postUserByUserGroupUrl = `/user/role/relation/global/list`;
export const addUserToUserGroupUrl = `/user/role/relation/global/add`;
/** 删除用户组用户 */
export const deleteUserFromUserGroupUrl = `/user/role/relation/global/delete/`;
/** 获取所有用户 */
export const getUserListUrl = '/system/user/list';
// 组织下的用户组
export const updateOrgUserGroupU = `/user/role/organization/update`;
/** 编辑用户组对应的权限配置 */
export const editOrgUSettingUrl = `/user/role/organization/permission/update`;
/** 添加用户组 */
export const addOrgUserGroupU = `/user/role/organization/add`;
/** 获取用户组对应的权限配置 */
export const getOrgUSettingUrl = `/user/role/organization/permission/setting/`;
/** 获取用户组 */
export const getOrgUserGroupU = `/user/role/organization/list/`;
/** 获取单个用户组信息 */
export const getOrgUsergroupInfoU = `/user/role/organization/get/`;
/** 删除用户组 */
export const deleteOrgUserGroupU = `/user/role/organization/delete/`;
/** 根据用户组获取用户列表 */
export const postOrgUserByUserGroupUrl = `/user/role/relation/organization/list`;
/** 创建用户组添加用户 */
export const addOrgUserToUserGroupUrl = `/user/role/relation/organization/add`;
/** 删除用户组用户 */
export const deleteOrgUserFromUserGroupUrl = `/user/role/relation/organization/delete/`;

View File

@ -100,4 +100,3 @@
}
);
</script>
@/api/modules/setting/organizationAndProject

View File

@ -128,6 +128,20 @@ export const pathMap = [
permission: [],
level: MENU_LEVEL[1],
},
{
key: 'SETTING_ORGANIZATION_USER_ROLE', // 系统设置-组织-用户组
locale: 'menu.settings.organization.userGroup',
route: RouteEnum.SETTING_ORGANIZATION_USER_GROUP,
permission: [],
level: MENU_LEVEL[1],
},
{
key: 'SETTING_ORGANIZATION_PROJECT', // 系统设置-组织-项目
locale: 'menu.settings.organization.project',
route: RouteEnum.SETTING_ORGANIZATION_PROJECT,
permission: [],
level: MENU_LEVEL[1],
},
{
key: 'SETTING_ORGANIZATION_SERVICE', // 系统设置-组织-服务集成
locale: 'menu.settings.organization.serviceIntegration',

View File

@ -46,6 +46,8 @@ export enum SettingRouteEnum {
SETTING_SYSTEM_PLUGIN_MANAGEMENT = 'settingSystemPluginManagement',
SETTING_ORGANIZATION = 'settingOrganization',
SETTING_ORGANIZATION_MEMBER = 'settingOrganizationMember',
SETTING_ORGANIZATION_USER_GROUP = 'settingOrganizationUserGroup',
SETTING_ORGANIZATION_PROJECT = 'settingOrganizationProject',
SETTING_ORGANIZATION_SERVICE = 'settingOrganizationService',
SETTING_ORGANIZATION_LOG = 'settingOrganizationLog',
}

View File

@ -21,7 +21,17 @@ export default function useUser() {
});
};
const setSalt = (salt: string) => {
userStore.setSalt(salt);
};
const isLoginPage = () => {
return router.currentRoute.value.name === 'login';
};
return {
logout,
setSalt,
isLoginPage,
};
}

View File

@ -43,4 +43,5 @@ export default {
'common.unSaveLeaveTitle': 'Leave this page?',
'common.unSaveLeaveContent': 'The system may not save your changes',
'common.leave': 'Leave',
'common.rename': 'Rename',
};

View File

@ -42,6 +42,8 @@ export default {
'menu.settings.system.log': 'System Log',
'menu.settings.organization': 'Organization',
'menu.settings.organization.member': 'Member',
'menu.settings.organization.userGroup': 'User Group',
'menu.settings.organization.project': 'Project',
'menu.settings.organization.serviceIntegration': 'Service Integration',
'menu.settings.organization.log': 'Org Log',
'navbar.action.locale': 'Switch to English',

View File

@ -43,4 +43,5 @@ export default {
'common.unSaveLeaveTitle': '离开此页面?',
'common.unSaveLeaveContent': '系统可能不会保存你所做的更改',
'common.leave': '离开',
'common.rename': '重命名',
};

View File

@ -41,6 +41,8 @@ export default {
'menu.settings.system.log': '系统日志',
'menu.settings.organization': '组织',
'menu.settings.organization.member': '成员',
'menu.settings.organization.userGroup': '用户组',
'menu.settings.organization.project': '项目',
'menu.settings.organization.serviceIntegration': '服务集成',
'menu.settings.organization.log': '组织日志',
'navbar.action.locale': '切换为中文',

View File

@ -40,6 +40,18 @@ export interface UserGroupItem {
pos: number;
}
export interface SystemUserGroupParams {
id?: string; // 组ID
name?: string; // 名称
scopeId?: string; // 组织ID
type?: string; // 组类型SYSTEM | PROJECT | ORGANIZATION
}
export interface OrgUserGroupParams {
id?: string; // 组ID
name: string;
scopeId: string; // 组织ID
}
export interface UserGroupPermissionItem {
id: string;
name: string;

View File

@ -148,6 +148,26 @@ const Setting: AppRouteRecordRaw = {
isTopMenu: true,
},
},
{
path: 'usergroup',
name: SettingRouteEnum.SETTING_ORGANIZATION_USER_GROUP,
component: () => import('@/views/setting/organization/usergroup/orgUserGroup.vue'),
meta: {
locale: 'menu.settings.organization.userGroup',
roles: ['*'],
isTopMenu: true,
},
},
{
path: 'project',
name: SettingRouteEnum.SETTING_ORGANIZATION_PROJECT,
component: () => import('@/views/setting/organization/project/orgProject.vue'),
meta: {
locale: 'menu.settings.organization.project',
roles: ['*'],
isTopMenu: true,
},
},
{
path: 'serviceIntegration',
name: SettingRouteEnum.SETTING_ORGANIZATION_SERVICE,

View File

@ -4,10 +4,9 @@ import useAppStore from './modules/app';
import useVisitStore from './modules/app/visit';
import useUserStore from './modules/user';
import { debouncePlugin } from './plugins';
import useUserGroupStore from './modules/setting/usergroup';
import useTableStore from './modules/ms-table';
const pinia = createPinia().use(debouncePlugin).use(piniaPluginPersistedstate);
export { useAppStore, useUserStore, useVisitStore, useUserGroupStore, useTableStore };
export { useAppStore, useUserStore, useVisitStore, useTableStore };
export default pinia;

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { UserGroupState } from './types';
import { UserGroupState } from '../types';
const useUserGroupStore = defineStore('userGroup', {
const useOrgUserGroupStore = defineStore('orgUserGroup', {
state: (): UserGroupState => ({
currentName: '',
currentTitle: '',
@ -27,4 +27,4 @@ const useUserGroupStore = defineStore('userGroup', {
},
});
export default useUserGroupStore;
export default useOrgUserGroupStore;

View File

@ -0,0 +1,30 @@
import { defineStore } from 'pinia';
import { UserGroupState } from '../types';
const useProjectUserGroupStore = defineStore('projectUserGroup', {
state: (): UserGroupState => ({
currentName: '',
currentTitle: '',
currentId: '',
currentType: '',
currentInternal: false,
collapse: true,
}),
getters: {
userGroupInfo(state: UserGroupState): UserGroupState {
return state;
},
},
actions: {
// 设置当前用户组信息
setInfo(partial: Partial<UserGroupState>) {
this.$patch(partial);
},
// 设置用户组菜单开启关闭
setCollapse(collapse: boolean) {
this.collapse = collapse;
},
},
});
export default useProjectUserGroupStore;

View File

@ -0,0 +1,30 @@
import { defineStore } from 'pinia';
import { UserGroupState } from '../types';
const useSystemUserGroupStore = defineStore('systemUserGroup', {
state: (): UserGroupState => ({
currentName: '',
currentTitle: '',
currentId: '',
currentType: '',
currentInternal: false,
collapse: true,
}),
getters: {
userGroupInfo(state: UserGroupState): UserGroupState {
return state;
},
},
actions: {
// 设置当前用户组信息
setInfo(partial: Partial<UserGroupState>) {
this.$patch(partial);
},
// 设置用户组菜单开启关闭
setCollapse(collapse: boolean) {
this.collapse = collapse;
},
},
});
export default useSystemUserGroupStore;

View File

@ -8,7 +8,6 @@ import { useI18n } from '@/hooks/useI18n';
import type { LoginData } from '@/models/user';
import type { UserState } from './types';
import { Message } from '@arco-design/web-vue';
const useUserStore = defineStore('user', {
// 开启数据持久化
@ -30,6 +29,7 @@ const useUserStore = defineStore('user', {
accountId: undefined,
certification: undefined,
role: '',
salt: '',
}),
getters: {
@ -100,12 +100,15 @@ const useUserStore = defineStore('user', {
if (appStore.currentOrgId === '') {
appStore.setCurrentOrgId(res.lastOrganizationId || '');
}
return true;
} catch (err) {
const { t } = useI18n();
Message.error(t('message.loginExpired'));
this.logoutCallBack();
return false;
}
},
// 加盐
setSalt(salt: string) {
this.$patch({ salt });
},
},
});

View File

@ -17,4 +17,6 @@ export interface UserState {
certification?: number;
role: RoleType;
lastOrganizationId?: string;
// 盐
salt: string;
}

View File

@ -62,8 +62,8 @@
import useLoading from '@/hooks/useLoading';
import { setLoginExpires } from '@/utils/auth';
import { GetLoginLogoUrl } from '@/api/requrls/setting/config';
import type { LoginData } from '@/models/user';
import { WorkbenchRouteEnum } from '@/enums/routeEnum';
const router = useRouter();
const { t } = useI18n();
@ -121,7 +121,7 @@
const { redirect, ...othersQuery } = router.currentRoute.value.query;
setLoginExpires();
router.push({
name: (redirect as string) || 'settingSystemUser',
name: (redirect as string) || WorkbenchRouteEnum.WORKBENCH,
query: {
...othersQuery,
},

View File

@ -0,0 +1,187 @@
<template>
<a-modal
v-model:visible="currentVisible"
width="680px"
class="ms-modal-form ms-modal-medium"
:ok-text="isEdit ? t('common.update') : t('common.create')"
unmount-on-close
@cancel="handleCancel"
>
<template #title>
<span v-if="isEdit">
{{ t('system.project.updateProject') }}
<span class="text-[var(--color-text-4)]">({{ props.currentProject?.name }})</span>
</span>
<span v-else>
{{ t('system.project.createProject') }}
</span>
</template>
<div class="form">
<a-form ref="formRef" :model="form" size="large" :style="{ width: '600px' }" layout="vertical">
<a-form-item
field="name"
required
:label="t('system.project.name')"
:rules="[{ required: true, message: t('system.project.projectNameRequired') }]"
>
<a-input v-model="form.name" :placeholder="t('system.project.projectNamePlaceholder')" />
</a-form-item>
<a-form-item
required
field="organizationId"
:label="t('system.project.affiliatedOrg')"
:rules="[{ required: true, message: t('system.project.affiliatedOrgRequired') }]"
>
<a-select
v-model="form.organizationId"
:disabled="!isXpack"
allow-search
:options="affiliatedOrgOption"
:default-value="isXpack ? '' : 'default_organization'"
:placeholder="t('system.project.affiliatedOrgPlaceholder')"
:field-names="{ label: 'name', value: 'id' }"
>
</a-select>
</a-form-item>
<a-form-item field="userIds" :label="t('system.project.projectAdmin')">
<MsUserSelector v-model:value="form.userIds" placeholder="system.project.projectAdminPlaceholder" />
</a-form-item>
<a-form-item field="description" :label="t('system.organization.description')">
<a-input v-model="form.description" :placeholder="t('system.organization.descriptionPlaceholder')" />
</a-form-item>
<a-form-item field="module" :label="t('system.organization.description')">
<a-checkbox-group v-model="form.moduleIds" :options="moduleOption">
<template #label="{ data }">
<span>{{ t(data.label) }}</span>
</template>
</a-checkbox-group>
</a-form-item>
</a-form>
</div>
<template #footer>
<div class="flex flex-row justify-between">
<div class="flex flex-row items-center gap-[4px]">
<a-switch v-model="form.enable" />
<span>{{ t('system.organization.status') }}</span>
<a-tooltip :content="t('system.project.createTip')" position="top">
<MsIcon type="icon-icon-maybe_outlined" class="text-[var(--color-text-4)]" />
</a-tooltip>
</div>
<div class="flex flex-row gap-[14px]">
<a-button type="secondary" :loading="loading" @click="handleCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleBeforeOk">
{{ isEdit ? t('common.confirm') : t('common.create') }}
</a-button>
</div>
</div>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import { reactive, ref, watchEffect, computed } from 'vue';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
import MsUserSelector from '@/components/business/ms-user-selector/index.vue';
import { createOrUpdateProject, getSystemOrgOption } from '@/api/modules/setting/organizationAndProject';
import { Message } from '@arco-design/web-vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { CreateOrUpdateSystemProjectParams, SystemOrgOption } from '@/models/setting/system/orgAndProject';
import useLicenseStore from '@/store/modules/setting/license';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
currentProject?: CreateOrUpdateSystemProjectParams;
}>();
const formRef = ref<FormInstance>();
const loading = ref(false);
const isEdit = computed(() => !!props.currentProject?.id);
const affiliatedOrgOption = ref<SystemOrgOption[]>([]);
const licenseStore = useLicenseStore();
const moduleOption = [
{ label: 'menu.workbench', value: 'workstation' },
{ label: 'menu.testPlan', value: 'testPlan' },
{ label: 'menu.bugManagement', value: 'bugManagement' },
{ label: 'menu.featureTest', value: 'caseManagement' },
{ label: 'menu.apiTest', value: 'apiTest' },
{ label: 'menu.uiTest', value: 'uiTest' },
{ label: 'menu.performanceTest', value: 'loadTest' },
];
const emit = defineEmits<{
(e: 'cancel'): void;
}>();
const form = reactive<CreateOrUpdateSystemProjectParams>({
name: '',
userIds: [],
organizationId: '',
description: '',
enable: true,
moduleIds: [],
});
const currentVisible = ref(props.visible);
const isXpack = computed(() => {
return licenseStore.hasLicense();
});
watchEffect(() => {
currentVisible.value = props.visible;
});
const handleCancel = () => {
formRef.value?.resetFields();
emit('cancel');
};
const handleBeforeOk = async () => {
await formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (errors) {
return;
}
try {
loading.value = true;
await createOrUpdateProject({ id: props.currentProject?.id, ...form });
Message.success(
isEdit.value
? t('system.organization.updateOrganizationSuccess')
: t('system.organization.createOrganizationSuccess')
);
handleCancel();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
} finally {
loading.value = false;
}
});
};
const initAffiliatedOrgOption = async () => {
try {
const res = await getSystemOrgOption();
affiliatedOrgOption.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
watchEffect(() => {
initAffiliatedOrgOption();
if (props.currentProject) {
form.id = props.currentProject.id;
form.name = props.currentProject.name;
form.description = props.currentProject.description;
form.enable = props.currentProject.enable;
form.userIds = props.currentProject.userIds;
form.organizationId = props.currentProject.organizationId;
form.moduleIds = props.currentProject.moduleIds;
}
});
</script>
@/api/modules/setting/organizationAndProject

View File

@ -0,0 +1,104 @@
<template>
<a-modal
v-model:visible="currentVisible"
title-align="start"
class="ms-modal-form ms-modal-medium"
:ok-text="t('system.organization.addMember')"
unmount-on-close
@cancel="handleCancel"
>
<template #title> {{ t('system.organization.addMember') }} </template>
<div class="form">
<a-form ref="formRef" :model="form" size="large" :style="{ width: '600px' }" layout="vertical">
<a-form-item
field="name"
:label="t('system.organization.member')"
:rules="[{ required: true, message: t('system.organization.addMemberRequired') }]"
>
<MsUserSelector v-model:value="form.name" type="organization" :source-id="organizationId || projectId" />
</a-form-item>
</a-form>
</div>
<template #footer>
<a-button type="secondary" :loading="loading" @click="handleCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="loading" :disabled="form.name.length === 0" @click="handleAddMember">
{{ t('common.add') }}
</a-button>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import { reactive, ref, watchEffect, onUnmounted } from 'vue';
import { addUserToOrgOrProject } from '@/api/modules/setting/organizationAndProject';
import { Message, type FormInstance, type ValidatedError } from '@arco-design/web-vue';
import MsUserSelector from '@/components/business/ms-user-selector/index.vue';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
organizationId?: string;
projectId?: string;
}>();
const emit = defineEmits<{
(e: 'cancel'): void;
(e: 'submit', value: string[]): void;
}>();
const currentVisible = ref(props.visible);
const loading = ref(false);
const form = reactive({
name: [],
});
const formRef = ref<FormInstance>();
watchEffect(() => {
currentVisible.value = props.visible;
});
const handleCancel = () => {
form.name = [];
emit('cancel');
};
const handleAddMember = () => {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (errors) {
loading.value = false;
}
const { organizationId, projectId } = props;
try {
loading.value = true;
await addUserToOrgOrProject({ userIds: form.name, organizationId, projectId });
Message.success(t('system.organization.addSuccess'));
handleCancel();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
} finally {
loading.value = false;
}
});
};
onUnmounted(() => {
form.name = [];
loading.value = false;
});
</script>
<style lang="less" scoped>
.option-name {
color: var(--color-text-1);
}
.option-email {
color: var(--color-text-4);
}
</style>
@/api/modules/setting/organizationAndProject

View File

@ -0,0 +1,164 @@
<template>
<MsDrawer
:mask="false"
:width="680"
:visible="currentVisible"
unmount-on-close
:footer="false"
:title="t('system.organization.addMember')"
@cancel="handleCancel"
>
<div>
<div class="flex flex-row justify-between">
<a-button type="primary" @click="handleAddMember">
{{ t('system.organization.addMember') }}
</a-button>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('system.user.searchUser')"
class="w-[230px]"
@search="searchUser"
@press-enter="searchUser"
></a-input-search>
</div>
<ms-base-table class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<span>{{ record.name }}</span>
<span v-if="record.adminFlag" class="ml-[4px] text-[var(--color-text-4)]">{{
`(${t('common.admin')})`
}}</span>
</template>
<template #operation="{ record }">
<MsRemoveButton
:title="t('system.organization.removeName', { name: record.name })"
:sub-title-tip="t('system.organization.removeTip')"
@ok="handleRemove(record)"
/>
</template>
</ms-base-table>
</div>
</MsDrawer>
<AddUserModal
:project-id="props.projectId"
:organization-id="props.organizationId"
:visible="userVisible"
@cancel="handleHideUserModal"
/>
</template>
<script lang="ts" setup>
import {
postUserTableByOrgIdOrProjectId,
deleteUserFromOrgOrProject,
} from '@/api/modules/setting/organizationAndProject';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { useI18n } from '@/hooks/useI18n';
import { watch, ref } from 'vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import AddUserModal from './addUserModal.vue';
import { TableData, Message } from '@arco-design/web-vue';
import MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
export interface projectDrawerProps {
visible: boolean;
organizationId?: string;
projectId?: string;
}
const { t } = useI18n();
const props = defineProps<projectDrawerProps>();
const emit = defineEmits<{
(e: 'cancel'): void;
}>();
const currentVisible = ref(props.visible);
const userVisible = ref(false);
const keyword = ref('');
const projectColumn: MsTableColumn = [
{
title: 'system.organization.userName',
slotName: 'name',
},
{
title: 'system.organization.email',
dataIndex: 'email',
width: 200,
},
{
title: 'system.organization.phone',
dataIndex: 'phone',
},
{ title: 'system.organization.operation', slotName: 'operation' },
];
const { propsRes, propsEvent, loadList, setLoadListParams, setKeyword } = useTable(postUserTableByOrgIdOrProjectId, {
columns: projectColumn,
showSetting: false,
scroll: { y: 'auto', x: '600px' },
selectable: false,
size: 'small',
noDisable: false,
});
async function searchUser() {
setKeyword(keyword.value);
await loadList();
}
const handleCancel = () => {
emit('cancel');
};
const fetchData = async () => {
setLoadListParams({ organizationId: props.organizationId });
await loadList();
};
const handleAddMember = () => {
userVisible.value = true;
};
const handleHideUserModal = () => {
userVisible.value = false;
};
const handleRemove = async (record: TableData) => {
try {
if (props.organizationId) {
await deleteUserFromOrgOrProject(props.organizationId, record.id);
}
if (props.projectId) {
await deleteUserFromOrgOrProject(props.projectId, record.id, true);
}
Message.success(t('common.removeSuccess'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
watch(
() => props.organizationId,
() => {
fetchData();
}
);
watch(
() => props.projectId,
() => {
fetchData();
}
);
watch(
() => props.visible,
(visible) => {
currentVisible.value = visible;
}
);
</script>
@/api/modules/setting/organizationAndProject

View File

@ -0,0 +1,74 @@
export default {
'system.organization.create': 'Create',
'system.organization.cancel': 'Cancel',
'system.organization.add': 'Add',
'system.organization.delete': 'Delete',
'system.organization.edit': 'Edit',
'system.organization.save': 'Save',
'system.organization.end': 'End',
'system.organization.removeName': 'Confirm to remove {name} this user',
'system.organization.removeTip': 'After removal, the organization permission will be lost',
'system.organization.addMember': 'Add Member',
'system.organization.addMemberPlaceholder': 'Please select member',
'system.organization.addMemberRequired': 'Please select member',
'system.organization.searchPlaceholder': 'Please enter the organization name to query',
'system.organization.addOrganization': 'Add organization',
'system.organization.createOrganization': 'Create organization',
'system.organization.organizationName': 'Organization name',
'system.organization.organizationNamePlaceholder':
'Please enter the organization name, which cannot be duplicated with other organization names',
'system.organization.organizationNameRequired': 'Organization name cannot be empty',
'system.organization.organizationNameDuplicate': 'Already have {name} please change',
'system.organization.organizationAdmin': 'Organization administrator',
'system.organization.organizationAdminPlaceholder':
'The organization administrator defaults to the person who created the organization',
'system.organization.description': 'Description',
'system.organization.descriptionPlaceholder': 'Please describe the organization',
'system.organization.ID': 'ID',
'system.organization.name': 'Name',
'system.organization.member': 'Member',
'system.organization.project': 'Project',
'system.organization.status': 'Status',
'system.organization.creator': 'Creator',
'system.organization.createTime': 'Create Time',
'system.organization.operation': 'Operation',
'system.organization.organizationCount': 'Organization({count})',
'system.organization.projectCount': 'Project({count})',
'system.organization.projectName': 'Project({name})',
'system.project.name': 'Project name',
'system.organization.userName': 'Name',
'system.organization.email': 'Email',
'system.organization.phone': 'Phone',
'system.organization.addSuccess': 'Add success',
'system.organization.deleteName': 'Are you sure to delete {name}',
'system.organization.deleteTip':
'Delete the organization and delete the project data under that organization together. Please be cautious!',
'system.organization.revokeDeleteToolTip': 'The organization will be deleted automatically after 30 days',
'system.organization.createOrganizationSuccess': 'Create organization success',
'system.organization.enableTitle': 'Start organization',
'system.organization.endTitle': 'Close organization',
'system.organization.enableContent': 'The organization after opening is displayed in the organization switching list',
'system.organization.endContent':
'The organization after closing is not displayed in the organization switching list',
'system.organization.updateOrganization': 'Update organization',
'system.organization.updateOrganizationSuccess': 'Update organization success',
'system.organization.createProject': 'Create project',
'system.organization.subordinateOrg': 'Subordinate organization',
'system.project.revokeDeleteTitle': 'Confirm revoke {name} ?',
'system.project.enableTitle': 'Start project',
'system.project.endTitle': 'Close project',
'system.project.enableContent': 'The project after opening is displayed in the organization switching list',
'system.project.endContent': 'The project after closing is not displayed in the project switching list',
'system.project.projectNamePlaceholder':
'Please enter the project name, which cannot be duplicated with other project names',
'system.project.updateProject': 'Update project',
'system.project.createProject': 'Create project',
'system.project.affiliatedOrg': 'Affiliated organization',
'system.project.affiliatedOrgPlaceholder': 'Please select affiliated organization',
'system.project.projectAdmin': 'Project administrator',
'system.project.projectAdminPlaceholder': 'The project administrator defaults to the person who created the project',
'system.project.moduleSetting': 'Module setting',
'system.project.projectNameRequired': 'Project name cannot be empty',
'system.project.createTip': 'After the project is enabled, it will be displayed in the project switching list',
'system.project.affiliatedOrgRequired': 'Affiliated organization cannot be empty',
};

View File

@ -0,0 +1,69 @@
export default {
'system.organization.create': '创建',
'system.organization.cancel': '取消',
'system.organization.add': '添加',
'system.organization.delete': '删除',
'system.organization.edit': '编辑',
'system.organization.save': '保存',
'system.organization.end': '结束',
'system.organization.removeName': '确认移除 {name} 这个用户吗',
'system.organization.removeTip': '移除后,将失去组织权限',
'system.organization.addMember': '添加成员',
'system.organization.addMemberPlaceholder': '请选择成员',
'system.organization.addMemberRequired': '请选择成员',
'system.organization.searchPlaceholder': '请输入组织名称回车查询',
'system.organization.addOrganization': '添加组织',
'system.organization.createOrganization': '创建组织',
'system.organization.organizationName': '组织名称',
'system.organization.organizationNamePlaceholder': '请输入组织名称,不可与其他组织名称重复',
'system.organization.organizationNameRequired': '组织名称不能为空',
'system.organization.organizationNameDuplicate': '已有 {name} 请更改',
'system.organization.organizationAdmin': '组织管理员',
'system.organization.organizationAdminPlaceholder': '默认选择创建组织人为组织管理员',
'system.organization.description': '描述',
'system.organization.descriptionPlaceholder': '请对组织进行描述',
'system.organization.ID': 'ID',
'system.organization.name': '名称',
'system.organization.member': '成员',
'system.organization.project': '项目',
'system.organization.status': '状态',
'system.organization.creator': '创建人',
'system.organization.createTime': '创建时间',
'system.organization.operation': '操作',
'system.organization.organizationCount': '组织({count})',
'system.organization.projectCount': '项目({count})',
'system.organization.projectName': '项目({name})',
'system.project.name': '项目名称',
'system.organization.userName': '姓名',
'system.organization.email': '邮箱',
'system.organization.phone': '手机',
'system.organization.addSuccess': '添加成功',
'system.organization.deleteName': '确认删除 {name} 这个组织吗',
'system.organization.deleteTip': '删除组织同时将该组织下的项目数据一起删除,请谨慎操作!',
'system.organization.revokeDeleteToolTip': '该组织将与 30 天后自动删除',
'system.organization.createOrganizationSuccess': '创建组织成功',
'system.organization.enableTitle': '开启组织',
'system.organization.endTitle': '关闭组织',
'system.organization.enableContent': '开启后的组织展示在组织切换列表',
'system.organization.endContent': '关闭后的组织不展示在组织切换列表',
'system.organization.updateOrganization': '更新组织',
'system.organization.updateOrganizationSuccess': '更新组织成功',
'system.organization.createProject': '创建项目',
'system.organization.subordinateOrg': '所属组织',
'system.project.revokeDeleteTitle': '确认恢复 {name} 这个项目吗?',
'system.project.enableTitle': '开启项目',
'system.project.endTitle': '关闭项目',
'system.project.enableContent': '开启后的项目展示在项目切换列表',
'system.project.endContent': '关闭后的项目不展示在项目切换列表',
'system.project.projectNamePlaceholder': '请输入项目名称,不可与其他项目名称重复',
'system.project.updateProject': '更新项目',
'system.project.createProject': '创建项目',
'system.project.affiliatedOrg': '所属组织',
'system.project.affiliatedOrgPlaceholder': '请选择所属组织',
'system.project.projectAdmin': '项目管理员',
'system.project.projectAdminPlaceholder': '默认选择创建项目人为项目管理员',
'system.project.moduleSetting': '模块设置',
'system.project.projectNameRequired': '项目名称不能为空',
'system.project.createTip': '项目启用后,将展示在项目切换列表',
'system.project.affiliatedOrgRequired': '所属组织不能为空',
};

View File

@ -0,0 +1,290 @@
<template>
<MsCard simple>
<div class="mb-4 flex items-center justify-between">
<a-button type="primary" @click="showAddProject">{{ t('system.organization.createProject') }}</a-button>
<a-input-search
v-model="keyword"
:placeholder="t('system.user.searchUser')"
class="w-[240px]"
@press-enter="fetchData"
@search="fetchData"
></a-input-search>
</div>
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<span>{{ record.name }}</span>
<a-tooltip background-color="#FFFFFF">
<template #content>
<span class="text-[var(--color-text-1)]">{{ t('system.organization.revokeDeleteToolTip') }}</span>
<MsButton class="ml-[8px]" @click="handleRevokeDelete(record)">{{ t('common.revokeDelete') }}</MsButton>
</template>
<MsIcon v-if="record.deleted" type="icon-icon_alarm_clock" class="ml-[4px] text-[rgb(var(--danger-6))]" />
</a-tooltip>
</template>
<template #creator="{ record }">
<span>{{ record.createUser }}</span>
<span v-if="record.projectCreateUserIsAdmin" class="ml-[8px] text-[var(--color-text-4)]">{{
`(${t('common.admin')})`
}}</span>
</template>
<template #memberCount="{ record }">
<span class="primary-color" @click="showUserDrawer(record)">{{ record.memberCount }}</span>
</template>
<template #operation="{ record }">
<template v-if="record.deleted">
<MsButton @click="handleRevokeDelete(record)">{{ t('common.revokeDelete') }}</MsButton>
</template>
<template v-else-if="!record.enable">
<MsButton @click="handleEnableOrDisableProject(record)">{{ t('common.enable') }}</MsButton>
<MsButton @click="handleDelete(record)">{{ t('common.delete') }}</MsButton>
</template>
<template v-else>
<MsButton @click="showAddProjectModal(record)">{{ t('common.edit') }}</MsButton>
<MsButton @click="showAddUserModal(record)">{{ t('system.organization.addMember') }}</MsButton>
<MsButton @click="handleEnableOrDisableProject(record, false)">{{ t('common.end') }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleMoreAction($event, record)"></MsTableMoreAction>
</template>
</template>
</MsBaseTable>
<AddProjectModal
:current-project="currentUpdateProject"
:visible="addProjectVisible"
@cancel="handleAddProjectModalCancel"
/>
<AddUserModal :project-id="currentProjectId" :visible="userVisible" @cancel="handleAddUserModalCancel" />
<UserDrawer :project-id="currentProjectId" v-bind="currentUserDrawer" @cancel="handleUserDrawerCancel" />
</MsCard>
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import useTable from '@/components/pure/ms-table/useTable';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { useTableStore } from '@/store';
import { ref, reactive } from 'vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import {
postProjectTable,
deleteProject,
enableOrDisableProject,
revokeDeleteProject,
} from '@/api/modules/setting/organizationAndProject';
import { TableKeyEnum } from '@/enums/tableEnum';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import { Message, TableData } from '@arco-design/web-vue';
import UserDrawer from './components/userDrawer.vue';
import AddUserModal from './components/addUserModal.vue';
import useModal from '@/hooks/useModal';
import { CreateOrUpdateSystemProjectParams } from '@/models/setting/system/orgAndProject';
import AddProjectModal from './components/addProjectModal.vue';
import { UserItem } from '@/components/business/ms-user-selector/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
const { t } = useI18n();
const tableStore = useTableStore();
const userVisible = ref(false);
const addProjectVisible = ref(false);
const currentProjectId = ref('');
const currentUpdateProject = ref<CreateOrUpdateSystemProjectParams>();
const { openDeleteModal, openModal } = useModal();
const keyword = ref('');
const organizationColumns: MsTableColumn = [
{
title: 'system.organization.ID',
dataIndex: 'num',
width: 100,
},
{
title: 'system.organization.name',
slotName: 'name',
editable: true,
},
{
title: 'system.organization.member',
slotName: 'memberCount',
},
{
title: 'system.organization.status',
dataIndex: 'enable',
},
{
title: 'system.organization.description',
dataIndex: 'description',
ellipsis: true,
tooltip: true,
},
{
title: 'system.organization.subordinateOrg',
dataIndex: 'organizationName',
sortable: {
sortDirections: ['ascend', 'descend'],
},
},
{
title: 'system.organization.creator',
slotName: 'creator',
dataIndex: 'createUser',
},
{
title: 'system.organization.createTime',
dataIndex: 'createTime',
width: 230,
},
{
title: 'system.organization.operation',
slotName: 'operation',
fixed: 'right',
width: 208,
},
];
tableStore.initColumn(TableKeyEnum.SYSTEM_PROJECT, organizationColumns, 'drawer');
const { propsRes, propsEvent, loadList, setKeyword } = useTable(postProjectTable, {
tableKey: TableKeyEnum.SYSTEM_PROJECT,
scroll: { y: 'auto', x: '1300px' },
selectable: false,
noDisable: false,
size: 'default',
showSetting: true,
});
const fetchData = async () => {
setKeyword(keyword.value);
await loadList();
};
const currentUserDrawer = reactive({
visible: false,
organizationId: '',
});
const tableActions: ActionsItem[] = [
{
label: 'system.user.delete',
eventTag: 'delete',
danger: true,
},
];
const showAddProject = () => {
addProjectVisible.value = true;
};
const handleDelete = (record: TableData) => {
openDeleteModal({
title: t('system.organization.deleteName', { name: record.name }),
content: t('system.organization.deleteTip'),
onBeforeOk: async () => {
try {
await deleteProject(record.id);
Message.success(t('common.deleteSuccess'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
});
};
const handleMoreAction = (tag: ActionsItem, record: TableData) => {
if (tag.eventTag === 'delete') {
handleDelete(record);
}
};
const handleEnableOrDisableProject = async (record: any, isEnable = true) => {
const title = isEnable ? t('system.project.enableTitle') : t('system.project.endTitle');
const content = isEnable ? t('system.project.enableContent') : t('system.project.endContent');
const okText = isEnable ? t('common.confirmEnable') : t('common.confirmClose');
openModal({
type: 'error',
cancelText: t('common.cancel'),
title,
content,
okText,
onBeforeOk: async () => {
try {
await enableOrDisableProject(record.id, isEnable);
Message.success(isEnable ? t('common.enableSuccess') : t('common.closeSuccess'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
hideCancel: false,
});
};
const showAddProjectModal = (record: any) => {
const { id, name, description, enable, adminList, organizationId, moduleIds } = record;
addProjectVisible.value = true;
currentUpdateProject.value = {
id,
name,
description,
enable,
userIds: adminList.map((item: UserItem) => item.id),
organizationId,
moduleIds,
};
};
const showAddUserModal = (record: any) => {
currentProjectId.value = record.id;
userVisible.value = true;
};
const showUserDrawer = (record: TableData) => {
currentUserDrawer.visible = true;
currentUserDrawer.organizationId = record.id;
};
const handleUserDrawerCancel = () => {
currentUserDrawer.visible = false;
fetchData();
};
const handleAddUserModalCancel = () => {
userVisible.value = false;
fetchData();
};
const handleAddProjectModalCancel = () => {
addProjectVisible.value = false;
fetchData();
};
const handleRevokeDelete = async (record: TableData) => {
openModal({
type: 'error',
cancelText: t('common.cancel'),
title: t('system.project.revokeDeleteTitle', { name: record.name }),
content: t('system.organization.enableContent'),
okText: t('common.revokeDelete'),
onBeforeOk: async () => {
try {
await revokeDeleteProject(record.id);
Message.success(t('common.revokeDeleteSuccess'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
},
hideCancel: false,
});
};
</script>
<style lang="scss" scoped>
.primary-color {
color: rgb(var(--primary-5));
cursor: pointer;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<a-popconfirm
:popup-visible="currentVisible"
:ok-text="props.id ? t('common.rename') : t('common.create')"
unmount-on-close
:on-before-ok="handleBeforeOk"
:ok-loading="loading"
:cancel-button-props="{ disabled: loading }"
position="bl"
class="w-[276px]"
@cancel="handleCancel"
>
<template #icon>{{ null }}</template>
<template #content>
<div class="form">
<a-form
ref="formRef"
:model="form"
size="large"
layout="vertical"
:label-col-props="{ span: 0 }"
:wrapper-col-props="{ span: 24 }"
>
<a-form-item>
<div class="text-[var(color-text-1)]">{{
props.id ? t('system.userGroup.rename') : t('system.userGroup.createUserGroup')
}}</div>
</a-form-item>
<a-form-item field="name" :rules="[{ validator: validateName }]">
<a-input
v-model="form.name"
class="w-[228px]"
:placeholder="t('system.userGroup.pleaseInputUserGroupName')"
/>
</a-form-item>
</a-form>
</div>
</template>
<slot></slot>
</a-popconfirm>
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import { reactive, ref, computed, watchEffect } from 'vue';
import { UserGroupItem } from '@/models/setting/usergroup';
import { Message } from '@arco-design/web-vue';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
import { updateOrAddOrgUserGroup } from '@/api/modules/setting/usergroup';
import { useAppStore } from '@/store';
const { t } = useI18n();
const props = defineProps<{
id?: string;
list: UserGroupItem[];
visible: boolean;
defaultName?: string;
}>();
const emit = defineEmits<{
(e: 'cancel', value: boolean): void;
}>();
const formRef = ref<FormInstance>();
const currentVisible = ref(props.visible);
const form = reactive({
name: '',
});
const appStore = useAppStore();
const currentOrgId = computed(() => appStore.currentOrgId);
const loading = ref(false);
const validateName = (value: string | undefined, callback: (error?: string) => void) => {
if (value === undefined || value === '') {
callback(t('system.userGroup.userGroupNameIsNotNone'));
} else {
if (value === props.defaultName) {
callback();
} else {
const isExist = props.list.some((item) => item.name === value);
if (isExist) {
callback(t('system.userGroup.userGroupNameIsExist', { name: value }));
}
}
callback();
}
};
const handleCancel = () => {
form.name = '';
emit('cancel', false);
};
const handleBeforeOk = () => {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (errors) {
return false;
}
try {
loading.value = true;
const res = await updateOrAddOrgUserGroup({ id: props.id, name: form.name, scopeId: currentOrgId.value });
if (res) {
Message.success(
props.id ? t('system.userGroup.updateUserGroupSuccess') : t('system.userGroup.addUserGroupSuccess')
);
loading.value = false;
handleCancel();
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
} finally {
loading.value = false;
}
});
};
watchEffect(() => {
currentVisible.value = props.visible;
});
</script>

View File

@ -0,0 +1,101 @@
<template>
<a-modal
v-model:visible="currentVisible"
class="ms-modal-form ms-modal-medium"
width="680px"
text-align="start"
:ok-text="t('system.userGroup.add')"
unmount-on-close
@cancel="handleCancel"
>
<template #title> {{ t('system.userGroup.addUser') }} </template>
<div class="form">
<a-form ref="formRef" :model="form" size="large" :style="{ width: '600px' }" layout="vertical">
<a-form-item
field="name"
:label="t('system.userGroup.user')"
:rules="[{ required: true, message: t('system.userGroup.pleaseSelectUser') }]"
>
<MsUserSelector v-model:value="form.name" />
</a-form-item>
</a-form>
</div>
<template #footer>
<a-button type="secondary" :loading="loading" @click="handleCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="loading" :disabled="form.name.length === 0" @click="handleBeforeOk">
{{ t('common.add') }}
</a-button>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import { reactive, ref, watchEffect } from 'vue';
import useUserGroupStore from '@/store/modules/setting/organization/usergroup';
import { addUserToUserGroup } from '@/api/modules/setting/usergroup';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
import MsUserSelector from '@/components/business/ms-user-selector/index.vue';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
}>();
const store = useUserGroupStore();
const emit = defineEmits<{
(e: 'cancel'): void;
(e: 'submit', value: string[]): void;
}>();
const currentVisible = ref(props.visible);
const loading = ref(false);
const form = reactive({
name: [],
});
const labelCache = new Map();
const formRef = ref<FormInstance>();
watchEffect(() => {
currentVisible.value = props.visible;
});
const handleCancel = () => {
labelCache.clear();
form.name = [];
emit('cancel');
};
const handleBeforeOk = () => {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (errors) {
return;
}
try {
loading.value = true;
await addUserToUserGroup({ roleId: store.currentId, userIds: form.name });
handleCancel();
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
} finally {
loading.value = false;
}
});
};
</script>
<style lang="less" scoped>
.option-name {
color: var(--color-text-1);
}
.option-email {
color: var(--color-text-4);
}
</style>

View File

@ -0,0 +1,322 @@
<template>
<div class="usergroup-auth-table">
<a-table
:span-method="dataSpanMethod"
:scroll="{ y: '500px', x: '800px' }"
:data="tableData"
:loading="loading"
:bordered="{ wrapper: true, cell: true }"
size="small"
:pagination="false"
>
<template #columns>
<a-table-column :width="100" :title="t('system.userGroup.function')" data-index="ability" />
<a-table-column :width="150" :title="t('system.userGroup.operationObject')" data-index="operationObject" />
<a-table-column :title="t('system.userGroup.auth')">
<template #cell="{ record, rowIndex }">
<a-checkbox-group v-model="record.perChecked" @change="(v) => handleAuthChange(v, rowIndex)">
<a-checkbox
v-for="item in record.permissions"
:key="item.id"
:disabled="item.license || currentInternal"
:value="item.id"
>{{ t(item.name) }}</a-checkbox
>
</a-checkbox-group>
</template>
</a-table-column>
<a-table-column :width="50" fixed="right" align="center" :bordered="false">
<template #title>
<a-checkbox
v-if="tableData && tableData?.length > 0"
:model-value="allChecked"
:indeterminate="allIndeterminate"
:disabled="currentInternal"
@change="handleAllChangeByCheckbox"
></a-checkbox>
</template>
<template #cell="{ record, rowIndex }">
<a-checkbox
:model-value="record.enable"
:indeterminate="record.indeterminate"
:disabled="currentInternal"
@change="(value) => handleActionChangeAll(value, rowIndex)"
/>
</template>
</a-table-column>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { RenderFunction, VNodeChild, ref, watchEffect, computed } from 'vue';
import { Message, type TableColumnData, type TableData } from '@arco-design/web-vue';
import useUserGroupStore from '@/store/modules/setting/organization/usergroup';
import { getGlobalUSetting, getOrgUSetting, saveOrgUSetting } from '@/api/modules/setting/usergroup';
import { UserGroupAuthSetting, AuthTableItem, type AuthScopeType, SavePermissions } from '@/models/setting/usergroup';
export declare type OperationName = 'selection-checkbox' | 'selection-radio' | 'expand' | 'drag-handle';
export interface TableOperationColumn {
name: OperationName | string;
title?: string | RenderFunction;
width?: number;
fixed?: boolean;
render?: (record: TableData) => VNodeChild;
isLastLeftFixed?: boolean;
}
const loading = ref(false);
const store = useUserGroupStore();
const systemSpan = ref(1);
const projectSpan = ref(1);
const organizationSpan = ref(1);
//
const allChecked = ref(false);
const allIndeterminate = ref(false);
const tableData = ref<AuthTableItem[]>();
//
const canSave = ref(false);
//
const currentInternal = computed(() => {
return store.userGroupInfo.currentInternal;
});
const dataSpanMethod = (data: {
record: TableData;
column: TableColumnData | TableOperationColumn;
rowIndex: number;
columnIndex: number;
}) => {
const { record, column } = data;
if ((column as TableColumnData).dataIndex === 'ability') {
if (record.isSystem) {
return {
rowspan: systemSpan.value,
};
}
if (record.isOrganization) {
return {
rowspan: organizationSpan.value,
};
}
if (record.isProject) {
return {
rowspan: projectSpan.value,
};
}
}
};
const { t } = useI18n();
/**
* 生成数据
* @param type
* @param idx
*/
const makeData = (item: UserGroupAuthSetting, type: AuthScopeType) => {
const result: AuthTableItem[] = [];
item.children?.forEach((child, index) => {
const perChecked =
child?.permissions?.reduce((acc: string[], cur) => {
if (cur.enable) {
acc.push(cur.id);
}
return acc;
}, []) || [];
result.push({
id: child?.id,
license: child?.license,
enable: child?.enable,
permissions: child?.permissions,
indeterminate: perChecked?.length > 0,
perChecked,
ability: index === 0 ? t(`system.userGroup.${type}`) : undefined,
operationObject: t(child.name),
isSystem: index === 0 && type === 'SYSTEM',
isOrganization: index === 0 && type === 'ORGANIZATION',
isProject: index === 0 && type === 'PROJECT',
});
});
return result;
};
const transformData = (data: UserGroupAuthSetting[]) => {
const result: AuthTableItem[] = [];
data.forEach((item) => {
if (item.type === 'SYSTEM') {
systemSpan.value = item.children?.length || 0;
}
if (item.type === 'PROJECT') {
projectSpan.value = item.children?.length || 0;
}
if (item.type === 'ORGANIZATION') {
organizationSpan.value = item.children?.length || 0;
}
result.push(...makeData(item, item.id));
});
return result;
};
const initData = async (id: string, internal: boolean) => {
try {
let tmpArr = [];
let res: UserGroupAuthSetting[] = [];
loading.value = true;
if (internal) {
res = await getGlobalUSetting(id);
} else {
res = await getOrgUSetting(id);
}
tmpArr = transformData(res);
tableData.value = tmpArr;
} catch (error) {
tableData.value = [];
} finally {
loading.value = false;
}
};
// change
const handleAllChangeByCheckbox = () => {
if (!tableData.value) return;
allChecked.value = !allChecked.value;
allIndeterminate.value = false;
const tmpArr = tableData.value;
tmpArr.forEach((item) => {
item.enable = allChecked.value;
item.indeterminate = false;
item.perChecked = allChecked.value ? item.permissions?.map((ele) => ele.id) : [];
});
if (!canSave.value) canSave.value = true;
};
//
const handleAllChange = () => {
if (!tableData.value) return;
const tmpArr = tableData.value;
const { length: allLength } = tmpArr;
const { length } = tmpArr.filter((item) => item.enable);
if (length === allLength) {
allChecked.value = true;
allIndeterminate.value = false;
} else if (length === 0) {
allChecked.value = false;
allIndeterminate.value = false;
} else {
allChecked.value = false;
allIndeterminate.value = true;
}
if (!canSave.value) canSave.value = true;
};
// change
const handleActionChangeAll = (value: boolean | (string | number | boolean)[], rowIndex: number) => {
if (!tableData.value) return;
const tmpArr = tableData.value;
tmpArr[rowIndex].indeterminate = false;
if (value) {
tmpArr[rowIndex].enable = true;
tmpArr[rowIndex].perChecked = tmpArr[rowIndex].permissions?.map((item) => item.id);
} else {
tmpArr[rowIndex].enable = false;
tmpArr[rowIndex].perChecked = [];
}
tableData.value = [...tmpArr];
handleAllChange();
if (!canSave.value) canSave.value = true;
};
// change
const handleAuthChange = (values: (string | number | boolean)[], rowIndex: number) => {
if (!tableData.value) return;
const tmpArr = tableData.value;
const length = tmpArr[rowIndex].permissions?.length || 0;
if (values.length === length) {
tmpArr[rowIndex].enable = true;
tmpArr[rowIndex].indeterminate = false;
handleAllChange();
} else if (values.length === 0) {
tmpArr[rowIndex].enable = false;
tmpArr[rowIndex].indeterminate = false;
handleAllChange();
} else {
tmpArr[rowIndex].enable = false;
tmpArr[rowIndex].indeterminate = true;
}
if (!canSave.value) canSave.value = true;
};
//
const handleSave = async () => {
if (!tableData.value) return;
const permissions: SavePermissions[] = [];
const tmpArr = tableData.value;
tmpArr.forEach((item) => {
item.permissions?.forEach((ele) => {
ele.enable = item.perChecked?.includes(ele.id) || false;
permissions.push({
id: ele.id,
enable: ele.enable,
});
});
});
try {
await saveOrgUSetting({
userRoleId: store.currentId,
permissions,
});
Message.success(t('common.saveSuccess'));
initData(store.currentId, store.currentInternal);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
};
//
const handleReset = () => {
if (store.currentId) {
initData(store.currentId, store.currentInternal);
}
};
watchEffect(() => {
if (store.currentId) {
initData(store.currentId, store.currentInternal);
}
});
defineExpose({
handleReset,
handleSave,
canSave,
});
</script>
<style scoped lang="less">
.usergroup-auth-table {
position: relative;
min-height: calc(100vh - 230px);
:deep(.arco-table-container) {
border-top: 1px solid var(--color-text-n8) !important;
border-right: 1px solid var(--color-text-n8) !important;
border-left: 1px solid var(--color-text-n8) !important;
}
.action {
position: absolute;
right: 24px;
bottom: 0;
left: 24px;
display: flex;
justify-content: space-between;
align-items: center;
width: calc(100% - 24px);
}
}
</style>
@/store/modules/setting/system/usergroup

View File

@ -0,0 +1,284 @@
<template>
<div class="user-group-left">
<a-input-search
class="w-[252px]"
:placeholder="t('system.userGroup.searchHolder')"
@press-enter="enterData"
@search="searchData"
/>
<div class="mt-2 flex flex-col">
<div class="flex h-[38px] items-center px-[8px] leading-[24px]">
<div class="second-color"> {{ t('system.userGroup.global') }}</div>
</div>
<div>
<div
v-for="element in globalUserGroupList"
:key="element.id"
:class="{
'flex': true,
'h-[38px]': true,
'items-center': true,
'px-[8px]': true,
'is-active': element.id === currentId,
}"
@click="handleListItemClick(element)"
>
<div class="flex grow flex-row">
<div class="usergroup-title leading-[24px]">
<span class="n1">{{ element.name }}</span>
<span v-if="element.type" class="n4">{{ t(`system.userGroup.${element.type}`) }}</span>
</div>
</div>
</div>
</div>
</div>
<a-divider class="mt-2" />
<div class="mt-2 flex flex-col">
<AddOrUpdateUserGroupPopup
:visible="addUserGroupVisible"
:list="customUserGroupList"
@cancel="handleAddUserGroupCancel"
>
<div class="flex h-[38px] items-center justify-between px-[8px] leading-[24px]">
<div class="second-color"> {{ t('system.userGroup.custom') }}</div>
<div class="primary-color"><icon-plus-circle-fill style="font-size: 20px" @click="addUserGroup" /></div>
</div>
</AddOrUpdateUserGroupPopup>
<div>
<div
v-for="element in customUserGroupList"
:key="element.id"
class="flex h-[38px] items-center px-[8px]"
:class="{ 'is-active': element.id === currentId }"
@click="handleListItemClick(element)"
>
<AddOrUpdateUserGroupPopup
:id="element.id"
:visible="popVisible[element.id]"
:default-name="popDefaultName"
:list="customUserGroupList"
@cancel="() => handlePopConfirmCancel(element.id)"
>
<div class="draglist-item flex grow flex-row justify-between">
<div class="usergroup-title leading-[24px]">
<span class="n1">{{ element.name }}</span>
<span v-if="element.type" class="n4">{{ t(`system.userGroup.${element.type}`) }}</span>
</div>
<div v-if="element.id === currentId && !element.internal">
<MsTableMoreAction :list="customAction" @select="(value) => handleMoreAction(value, element.id)" />
</div>
</div>
</AddOrUpdateUserGroupPopup>
</div>
</div>
</div>
</div>
<AddUserModal :visible="addUserVisible" @cancel="addUserVisible = false" />
</template>
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { CustomMoreActionItem, PopVisibleItem, RenameType, UserGroupItem } from '@/models/setting/usergroup';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import AddUserModal from './addUserModal.vue';
import AddOrUpdateUserGroupPopup from './addOrUpdateUserGroupPopup.vue';
import useModal from '@/hooks/useModal';
import { Message } from '@arco-design/web-vue';
import useUserGroupStore from '@/store/modules/setting/organization/usergroup';
import { useAppStore } from '@/store';
import { getOrgUserGroupList, updateOrAddOrgUserGroup, deleteOrgUserGroup } from '@/api/modules/setting/usergroup';
import { characterLimit } from '@/utils';
const { t } = useI18n();
const store = useUserGroupStore();
const appStore = useAppStore();
const { openModal } = useModal();
// loading
const currentId = ref('');
const addUserVisible = ref(false);
const addUserGroupVisible = ref(false);
//
const popVisible = ref<PopVisibleItem>({});
//
const popType = ref<RenameType>('rename');
const popDefaultName = ref('');
//
const userGroupList = ref<UserGroupItem[]>([]);
const currentOrgId = computed(() => appStore.currentOrgId);
const globalUserGroupList = computed(() => {
return userGroupList.value.filter((ele) => ele.internal);
});
const customUserGroupList = computed(() => {
return userGroupList.value.filter((ele) => !ele.internal);
});
const customAction: ActionsItem[] = [
{
label: 'system.userGroup.rename',
danger: false,
eventTag: 'rename',
},
{
isDivider: true,
},
{
label: 'system.userGroup.delete',
danger: true,
eventTag: 'delete',
},
];
//
const handleListItemClick = (element: UserGroupItem) => {
const { id, name, type, internal } = element;
currentId.value = id;
store.setInfo({
currentName: name,
currentTitle: type,
currentId: id,
currentType: type,
currentInternal: internal,
});
};
//
const initData = async () => {
try {
const res = await getOrgUserGroupList(currentOrgId.value);
if (res.length > 0) {
userGroupList.value = res;
handleListItemClick(res[0]);
//
const tmpObj: PopVisibleItem = {};
res.forEach((element) => {
tmpObj[element.id] = false;
});
popVisible.value = tmpObj;
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
//
const addUserGroup = () => {
addUserGroupVisible.value = true;
};
//
const handleAddUserGroupCancel = () => {
addUserGroupVisible.value = false;
initData();
};
//
const handleMoreAction = (item: ActionsItem, id: string) => {
if (item.eventTag !== 'delete') {
popType.value = item.eventTag as RenameType;
const tmpObj = userGroupList.value.filter((ele) => ele.id === id)[0];
popVisible.value = { ...popVisible.value, [id]: true };
if (item.eventTag === 'rename') {
popDefaultName.value = tmpObj.name;
}
} else {
openModal({
type: 'error',
title: t('system.userGroup.isDeleteUserGroup', { name: characterLimit(store.currentName) }),
content: t('system.userGroup.beforeDeleteUserGroup'),
okText: t('system.userGroup.confirmDelete'),
cancelText: t('system.userGroup.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await deleteOrgUserGroup(id);
Message.success(t('system.user.deleteUserSuccess'));
initData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
hideCancel: false,
});
}
};
// confirm
const handlePopConfirmCancel = (id: string) => {
popVisible.value = { ...popVisible.value, [id]: false };
initData();
};
function enterData(eve: Event) {
if (!(eve.target as HTMLInputElement).value) {
initData();
return;
}
const keyword = (eve.target as HTMLInputElement).value;
const tmpArr = userGroupList.value.filter((ele) => ele.name.includes(keyword));
userGroupList.value = tmpArr;
}
function searchData(value: string) {
if (!value) {
initData();
return;
}
const keyword = value;
const tmpArr = userGroupList.value.filter((ele) => ele.name.includes(keyword));
userGroupList.value = tmpArr;
}
onMounted(() => {
initData();
});
</script>
<style scoped lang="less">
.primary-color {
color: rgb(var(--primary-5));
}
.n1 {
color: var(--color-text-1);
}
.n4 {
color: var(--color-text-4);
}
.second-color {
color: var(--color-text-input-border);
}
.handle {
cursor: move;
opacity: 0.3;
}
.is-active {
background-color: rgb(var(--primary-1));
}
.custom-empty {
padding: 8px;
font-size: 12px;
font-family: 'PingFang SC';
font-weight: 400;
border-radius: 4px;
color: #8f959e;
background: #f7f9fc;
font-style: normal;
line-height: 20px;
overflow-wrap: break-word;
}
.button-icon {
display: flex;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
color: rgb(var(--primary-5));
background-color: rgb(var(--primary-9));
}
.usergroup-title {
color: var(--color-text-1);
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<a-button type="primary" @click="handleAddUser">{{ t('system.userGroup.quickAddUser') }}</a-button>
<MsBaseTable class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
<template #action="{ record }">
<MsRemoveButton
:title="t('system.userGroup.removeName', { name: record.name })"
:sub-title-tip="t('system.userGroup.removeTip')"
@ok="handleRemove(record)"
/>
</template>
</MsBaseTable>
<AddUserModal :visible="userVisible" @cancel="handleAddUserModalCancel" />
</template>
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import useTable from '@/components/pure/ms-table/useTable';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { useTableStore } from '@/store';
import useUserGroupStore from '@/store/modules/setting/organization/usergroup';
import { watchEffect, ref, watch } from 'vue';
import { postUserByUserGroup, deleteUserFromUserGroup } from '@/api/modules/setting/usergroup';
import { UserTableItem } from '@/models/setting/usergroup';
import { TableKeyEnum } from '@/enums/tableEnum';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import AddUserModal from './addUserModal.vue';
import MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
const { t } = useI18n();
const store = useUserGroupStore();
const tableStore = useTableStore();
const userVisible = ref(false);
const props = defineProps<{
keyword: string;
}>();
const userGroupUsercolumns: MsTableColumn = [
{
title: 'system.userGroup.name',
dataIndex: 'name',
},
{
title: 'system.userGroup.email',
dataIndex: 'email',
},
{
title: 'system.userGroup.phone',
dataIndex: 'email',
},
{
title: 'system.userGroup.operation',
slotName: 'action',
fixed: 'right',
width: 200,
},
];
tableStore.initColumn(TableKeyEnum.USERGROUPUSER, userGroupUsercolumns, 'drawer');
const { propsRes, propsEvent, loadList, setLoadListParams, setKeyword } = useTable(postUserByUserGroup, {
tableKey: TableKeyEnum.USERGROUPUSER,
scroll: { x: '600px' },
selectable: true,
noDisable: true,
});
const fetchData = async () => {
setKeyword(props.keyword);
await loadList();
};
const handleRemove = async (record: UserTableItem) => {
try {
await deleteUserFromUserGroup(record.id);
await fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
};
const handleAddUser = () => {
userVisible.value = true;
};
const handleAddUserModalCancel = () => {
fetchData();
userVisible.value = false;
};
watchEffect(() => {
if (store.currentId) {
setLoadListParams({ roleId: store.currentId });
fetchData();
}
});
watch(
() => props.keyword,
() => {
fetchData();
}
);
</script>

View File

@ -0,0 +1,148 @@
<template>
<MsCard simple>
<div class="flex flex-row">
<div class="user-group-left" :style="{ padding: collapse ? '24px 24px 24px 0' : 0 }">
<user-group-left v-if="collapse" />
<div class="usergroup-collapse" @click="handleCollapse">
<MsIcon v-if="collapse" type="icon-icon_up-left_outlined" class="icon" />
<MsIcon v-else type="icon-icon_down-right_outlined" class="icon" />
</div>
</div>
<div class="relative w-[100%] overflow-x-scroll p-[24px]">
<div class="flex flex-row items-center justify-between">
<div class="title">{{ store.userGroupInfo.currentName }}</div>
<div class="flex items-center">
<a-input-search
v-if="currentTable === 'user'"
:placeholder="t('system.user.searchUser')"
class="w-[240px]"
@press-enter="handleEnter"
@search="handleSearch"
></a-input-search>
<a-radio-group v-if="couldShowUser && couldShowAuth" v-model="currentTable" class="ml-[14px]" type="button">
<a-radio v-if="couldShowAuth" value="auth">{{ t('system.userGroup.auth') }}</a-radio>
<a-radio v-if="couldShowUser" value="user">{{ t('system.userGroup.user') }}</a-radio>
</a-radio-group>
</div>
</div>
<div class="mt-[16px]">
<user-table v-if="currentTable === 'user' && couldShowUser" :keyword="currentKeyword" />
<auth-table v-if="currentTable === 'auth' && couldShowAuth" ref="authRef" />
</div>
</div>
</div>
</MsCard>
<div
v-if="currentTable === 'auth'"
class="fixed bottom-[16px] right-[16px] z-[999] flex justify-between bg-white p-[24px] shadow-[0_-1px_4px_rgba(2,2,2,0.1)]"
:style="{ width: `calc(100% - ${menuWidth + 16}px)` }"
>
<ms-button class="btn" :disabled="!canSave" @click="handleReset">{{ t('system.userGroup.reset') }}</ms-button>
<a-button class="btn" :disabled="!canSave" type="primary" @click="handleSave">{{
t('system.userGroup.save')
}}</a-button>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watchEffect } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import MsCard from '@/components/pure/ms-card/index.vue';
import useUserGroupStore from '@/store/modules/setting/organization/usergroup';
import UserGroupLeft from './components/index.vue';
import UserTable from './components/userTable.vue';
import AuthTable from './components/authTable.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import { useAppStore } from '@/store';
const currentTable = ref('auth');
const { t } = useI18n();
const currentKeyword = ref('');
const authRef = ref<{
handleReset: () => void;
handleSave: () => void;
canSave: boolean;
}>();
const appStore = useAppStore();
const handleSearch = (value: string) => {
currentKeyword.value = value;
};
const handleEnter = (eve: Event) => {
currentKeyword.value = (eve.target as HTMLInputElement).value;
};
const store = useUserGroupStore();
const couldShowUser = computed(() => store.userGroupInfo.currentType === 'ORGANIZATION');
const couldShowAuth = computed(() => store.userGroupInfo.currentId !== 'admin');
const handleCollapse = () => {
store.setCollapse(!store.collapse);
};
const collapse = computed(() => store.collapse);
const menuWidth = computed(() => {
const width = appStore.menuCollapse ? 86 : appStore.menuWidth;
if (store.collapse) {
return width + 300;
}
return width + 24;
});
const handleReset = () => {
authRef.value?.handleReset();
};
const handleSave = () => {
authRef.value?.handleSave();
};
const canSave = computed(() => {
if (currentTable.value === 'auth') {
return authRef.value?.canSave;
}
return true;
});
watchEffect(() => {
if (!couldShowAuth.value) {
currentTable.value = 'user';
} else if (!couldShowUser.value) {
currentTable.value = 'auth';
} else {
currentTable.value = 'auth';
}
});
</script>
<style lang="scss" scoped>
.title {
color: var(--color-text-1);
}
.user-group {
height: calc(100vh - 72px);
}
.user-group-left {
position: relative;
border-right: 1px solid var(--color-border);
.usergroup-collapse {
position: absolute;
top: 50%;
right: -16px;
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
width: 16px;
height: 36px;
background-color: var(--color-text-n8);
opacity: 0;
cursor: pointer;
&:hover {
opacity: 1;
}
.icon {
font-size: 12px;
color: var(--color-text-brand);
}
}
}
</style>
@/store/modules/setting/system/usergroup

View File

@ -4,7 +4,7 @@
<span>{{ record.name }}</span>
<a-tooltip background-color="#FFFFFF">
<template #content>
<span class="text-[var(--color-text-1)]">{{ t('system.organization.revokeDeleteToolTip') }}</span>
<span class="text-[var(--color-text-1)]">{{ t('system.project.revokeDeleteToolTip') }}</span>
<MsButton class="ml-[8px]" @click="handleRevokeDelete(record)">{{ t('common.revokeDelete') }}</MsButton>
</template>
<MsIcon v-if="record.deleted" type="icon-icon_alarm_clock" class="ml-[4px] text-[rgb(var(--danger-6))]" />
@ -252,7 +252,7 @@
type: 'error',
cancelText: t('common.cancel'),
title: t('system.project.revokeDeleteTitle', { name: record.name }),
content: t('system.organization.enableContent'),
content: t('system.project.enableContent'),
okText: t('common.revokeDelete'),
onBeforeOk: async () => {
try {

View File

@ -114,7 +114,12 @@
};
const fetchData = async () => {
if (props.organizationId) {
setLoadListParams({ organizationId: props.organizationId });
}
if (props.projectId) {
setLoadListParams({ projectId: props.projectId });
}
await loadList();
};
@ -161,4 +166,3 @@
}
);
</script>
@/api/modules/setting/organizationAndProject

View File

@ -71,4 +71,5 @@ export default {
'system.project.projectNameRequired': 'Project name cannot be empty',
'system.project.createTip': 'After the project is enabled, it will be displayed in the project switching list',
'system.project.affiliatedOrgRequired': 'Affiliated organization cannot be empty',
'system.project.revokeDeleteToolTip': 'The project will be deleted automatically after 30 days',
};

View File

@ -66,4 +66,5 @@ export default {
'system.project.projectNameRequired': '项目名称不能为空',
'system.project.createTip': '项目启用后,将展示在项目切换列表',
'system.project.affiliatedOrgRequired': '所属组织不能为空',
'system.project.revokeDeleteToolTip': '该项目将与 30 天后自动删除',
};

View File

@ -33,10 +33,9 @@
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import { reactive, ref, watchEffect, onMounted } from 'vue';
import { UserTableItem } from '@/models/setting/usergroup';
import { useUserGroupStore } from '@/store';
import { getUserList, addUserToUserGroup } from '@/api/modules/setting/usergroup';
import { reactive, ref, watchEffect } from 'vue';
import useUserGroupStore from '@/store/modules/setting/system/usergroup';
import { addUserToUserGroup } from '@/api/modules/setting/usergroup';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
import MsUserSelector from '@/components/business/ms-user-selector/index.vue';
@ -61,18 +60,8 @@
const labelCache = new Map();
const allOption = ref<UserTableItem[]>([]);
const userOptions = ref<UserTableItem[]>([]);
const formRef = ref<FormInstance>();
const initUserList = async () => {
const res = await getUserList();
allOption.value = res;
userOptions.value = res;
};
watchEffect(() => {
currentVisible.value = props.visible;
});
@ -100,10 +89,6 @@
}
});
};
onMounted(() => {
initUserList();
});
</script>
<style lang="less" scoped>

View File

@ -53,7 +53,7 @@
import { useI18n } from '@/hooks/useI18n';
import { RenderFunction, VNodeChild, ref, watchEffect, computed } from 'vue';
import { type TableColumnData, type TableData } from '@arco-design/web-vue';
import useUserGroupStore from '@/store/modules/setting/usergroup';
import useUserGroupStore from '@/store/modules/setting/system/usergroup';
import { getGlobalUSetting, saveGlobalUSetting } from '@/api/modules/setting/usergroup';
import { UserGroupAuthSetting, AuthTableItem, type AuthScopeType, SavePermissions } from '@/models/setting/usergroup';
@ -313,3 +313,4 @@
}
}
</style>
@/store/modules/setting/system/usergroup

View File

@ -68,7 +68,7 @@
import useModal from '@/hooks/useModal';
import { Message } from '@arco-design/web-vue';
import popconfirm from './popconfirm.vue';
import useUserGroupStore from '@/store/modules/setting/usergroup';
import useUserGroupStore from '@/store/modules/setting/system/usergroup';
import { getUserGroupList, updateOrAddUserGroup, deleteUserGroup } from '@/api/modules/setting/usergroup';
import { characterLimit } from '@/utils';
@ -174,6 +174,7 @@
Message.success(t('system.user.deleteUserSuccess'));
initData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
@ -301,4 +302,4 @@
color: var(--color-text-1);
}
</style>
@/models/setting/usergroup @/api/modules/setting/usergroup
@/models/setting/usergroup @/api/modules/setting/usergroup @/store/modules/setting/system/usergroup

View File

@ -16,7 +16,8 @@
import { useI18n } from '@/hooks/useI18n';
import useTable from '@/components/pure/ms-table/useTable';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { useUserGroupStore, useTableStore } from '@/store';
import { useTableStore } from '@/store';
import useUserGroupStore from '@/store/modules/setting/system/usergroup';
import { watchEffect, ref, watch } from 'vue';
import { postUserByUserGroup, deleteUserFromUserGroup } from '@/api/modules/setting/usergroup';
import { UserTableItem } from '@/models/setting/usergroup';

View File

@ -48,7 +48,7 @@
import { ref, computed, watchEffect } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import MsCard from '@/components/pure/ms-card/index.vue';
import useUserGroupStore from '@/store/modules/setting/usergroup';
import useUserGroupStore from '@/store/modules/setting/system/usergroup';
import UserGroupLeft from './components/index.vue';
import UserTable from './components/userTable.vue';
import AuthTable from './components/authTable.vue';
@ -145,3 +145,4 @@
}
}
</style>
@/store/modules/setting/system/usergroup

View File

@ -56,6 +56,7 @@ export default {
quickAddUser: 'Quick add user',
removeName: 'Confirm to remove {name} this user',
removeTip: 'After removal, the User Group permission will be lost',
custom: 'Custom user group',
},
},
permission: {

View File

@ -55,6 +55,7 @@ export default {
quickAddUser: '快速添加用户',
removeName: '确认移除 {name} 这个用户吗',
removeTip: '移除后,将失去用户组权限',
custom: '自定义用户组',
},
},
permission: {