feat(系统设置): 用户邀请
This commit is contained in:
parent
6e1229b332
commit
409f1538a3
|
@ -14,6 +14,8 @@ import {
|
|||
GetOrgsUrl,
|
||||
GetProjectsUrl,
|
||||
GetUserInfoUrl,
|
||||
InviteUserUrl,
|
||||
RegisterByInviteUrl,
|
||||
} from '@/api/requrls/setting/user';
|
||||
import type {
|
||||
UserListItem,
|
||||
|
@ -27,6 +29,8 @@ import type {
|
|||
BatchAddParams,
|
||||
ResetUserPasswordParams,
|
||||
OrgsItem,
|
||||
InviteUserParams,
|
||||
RegisterByInviteParams,
|
||||
} from '@/models/setting/user';
|
||||
import type { CommonList, TableQueryParams } from '@/models/common';
|
||||
|
||||
|
@ -100,3 +104,13 @@ export function getSystemOrgs() {
|
|||
export function getSystemProjects() {
|
||||
return MSR.get<OrgsItem[]>({ url: GetProjectsUrl });
|
||||
}
|
||||
|
||||
// 邀请用户
|
||||
export function inviteUser(data: InviteUserParams) {
|
||||
return MSR.post({ url: InviteUserUrl, data });
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
export function registerByInvite(data: RegisterByInviteParams) {
|
||||
return MSR.post({ url: RegisterByInviteUrl, data });
|
||||
}
|
||||
|
|
|
@ -26,3 +26,7 @@ export const BatchAddOrgUrl = '/system/user/add-org-member';
|
|||
export const GetOrgsUrl = '/system/user/get/organization';
|
||||
// 查找项目
|
||||
export const GetProjectsUrl = '/system/user/get/project';
|
||||
// 用户注册
|
||||
export const RegisterByInviteUrl = '/system/user/register-by-invite';
|
||||
// 邀请用户
|
||||
export const InviteUserUrl = '/system/user/invite';
|
||||
|
|
|
@ -108,3 +108,15 @@ export interface OrgsItem {
|
|||
children?: OrgsItem[];
|
||||
leafNode: boolean;
|
||||
}
|
||||
|
||||
export interface InviteUserParams {
|
||||
inviteEmails: string[];
|
||||
userRoleIds: string[];
|
||||
}
|
||||
|
||||
export interface RegisterByInviteParams {
|
||||
inviteId: string;
|
||||
name: string;
|
||||
password: string;
|
||||
phone: string;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Recordable } from '#/global';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
import { isObject } from './is';
|
||||
|
||||
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
||||
|
@ -247,3 +247,16 @@ export function getFilterList(targetMap: Record<string, any>[], sourceMap: Recor
|
|||
});
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
* @param input 输入的字符串
|
||||
* @param publicKey 公钥
|
||||
* @returns
|
||||
*/
|
||||
export function encrypted(input: string) {
|
||||
const publicKey = localStorage.getItem('salt') || '';
|
||||
const encrypt = new JSEncrypt({ default_key_size: '1024' });
|
||||
encrypt.setPublicKey(publicKey);
|
||||
return encrypt.encrypt(input);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,14 @@
|
|||
<div class="invite-page">
|
||||
<div class="form-box w-1/3 rounded-[12px] bg-white">
|
||||
<div class="form-box-title">{{ t('invite.title') }}</div>
|
||||
<a-form class="p-[24px_40px_40px_40px]" :model="form" :rules="rules" layout="vertical" @submit="confirmInvite">
|
||||
<a-form-item field="email" class="hidden-item">
|
||||
<a-input v-model="form.email" :placeholder="t('invite.emailPlaceholder')" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form
|
||||
ref="registerFormRef"
|
||||
class="p-[24px_40px_40px_40px]"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@submit="confirmInvite"
|
||||
>
|
||||
<a-form-item field="name" class="hidden-item">
|
||||
<a-input v-model="form.name" :placeholder="t('invite.namePlaceholder')" allow-clear />
|
||||
</a-form-item>
|
||||
|
@ -39,15 +43,15 @@
|
|||
</template>
|
||||
</a-popover>
|
||||
</a-form-item>
|
||||
<a-form-item field="repassword" class="hidden-item">
|
||||
<a-form-item field="rePassword" class="hidden-item">
|
||||
<a-input-password
|
||||
v-model="form.repassword"
|
||||
v-model="form.rePassword"
|
||||
:placeholder="t('invite.repasswordPlaceholder')"
|
||||
autocomplete="new-password"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-button type="primary" html-type="submit">{{ t('invite.confirm') }}</a-button>
|
||||
<a-button type="primary" :loading="loading" html-type="submit">{{ t('invite.confirm') }}</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,32 +59,29 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { validateEmail, validatePasswordLength, validateWordPassword } from '@/utils/validate';
|
||||
import { validatePasswordLength, validateWordPassword } from '@/utils/validate';
|
||||
import { registerByInvite } from '@/api/modules/setting/user';
|
||||
import { sleep, encrypted } from '@/utils';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
repassword: '',
|
||||
rePassword: '',
|
||||
});
|
||||
|
||||
const pswValidateRes = ref(false);
|
||||
const pswLengthValidateRes = ref(false);
|
||||
const registerFormRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
|
||||
const rules = {
|
||||
email: [
|
||||
{ required: true, message: t('invite.emailNotNull') },
|
||||
{
|
||||
validator: (value: string, callback: (error?: string) => void) => {
|
||||
if (!validateEmail(value)) {
|
||||
callback(t('invite.emailErr'));
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
name: [{ required: true, message: t('invite.nameNotNull') }],
|
||||
password: [
|
||||
{ required: true, message: t('invite.passwordNotNull') },
|
||||
|
@ -96,7 +97,16 @@
|
|||
},
|
||||
},
|
||||
],
|
||||
repassword: [{ required: true, message: t('invite.repasswordNotNull') }],
|
||||
rePassword: [
|
||||
{ required: true, message: t('invite.repasswordNotNull') },
|
||||
{
|
||||
validator: (value: string, callback: (error?: string) => void) => {
|
||||
if (value !== form.value.password) {
|
||||
callback(t('invite.repasswordNotSame'));
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function validatePsw(value: string) {
|
||||
|
@ -104,7 +114,30 @@
|
|||
pswLengthValidateRes.value = validatePasswordLength(value);
|
||||
}
|
||||
|
||||
function confirmInvite() {}
|
||||
function confirmInvite() {
|
||||
registerFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
loading.value = true;
|
||||
await registerByInvite({
|
||||
inviteId: route.query.inviteId as string,
|
||||
name: form.value.name,
|
||||
password: encrypted(form.value.password) || '',
|
||||
phone: '',
|
||||
});
|
||||
Message.success(t('invite.success'));
|
||||
await sleep(300);
|
||||
router.push({
|
||||
name: 'login',
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -14,6 +14,7 @@ export default {
|
|||
'invite.passwordTipTitle': 'The passwords must match both, only the following rules are supported',
|
||||
'invite.passwordLengthRule': 'The length is 8-32 digits',
|
||||
'invite.passwordWordRule': 'Must contain numbers and letters, Chinese or spaces are not allowed',
|
||||
'invite.success': 'Registered successfully',
|
||||
'personal.info': 'Personal Info',
|
||||
'personal.switchOrg': 'Switch Org',
|
||||
'personal.exit': 'Log out',
|
||||
|
|
|
@ -14,6 +14,7 @@ export default {
|
|||
'invite.passwordTipTitle': '密码须同时符合,仅支持以下规则',
|
||||
'invite.passwordLengthRule': '长度为8-32位',
|
||||
'invite.passwordWordRule': '必须包含数字和字母,不允许输入中文或空格',
|
||||
'invite.success': '注册成功',
|
||||
'personal.info': '个人信息',
|
||||
'personal.switchOrg': '切换组织',
|
||||
'personal.exit': '退出系统',
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
import { GetLoginLogoUrl } from '@/api/requrls/setting/config';
|
||||
import type { LoginData } from '@/models/user';
|
||||
import { WorkbenchRouteEnum } from '@/enums/routeEnum';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
import { encrypted } from '@/utils';
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
@ -100,12 +100,6 @@
|
|||
password: 'metersphere',
|
||||
});
|
||||
|
||||
const encrypted = (input: string, publicKey: string) => {
|
||||
const encrypt = new JSEncrypt({ default_key_size: '1024' });
|
||||
encrypt.setPublicKey(publicKey);
|
||||
return encrypt.encrypt(input);
|
||||
};
|
||||
|
||||
const handleSubmit = async ({
|
||||
errors,
|
||||
values,
|
||||
|
@ -117,10 +111,9 @@
|
|||
if (!errors) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const publicKey = localStorage.getItem('salt') || '';
|
||||
await userStore.login({
|
||||
username: encrypted(values.username, publicKey),
|
||||
password: encrypted(values.password, publicKey),
|
||||
username: encrypted(values.username),
|
||||
password: encrypted(values.password),
|
||||
authenticate: values.authenticate,
|
||||
} as LoginData);
|
||||
Message.success(t('login.form.login.success'));
|
||||
|
|
|
@ -73,6 +73,11 @@
|
|||
<a-checkbox-group v-model:model-value="form.use">
|
||||
<a-checkbox v-for="use of useList" :key="use.value" :value="use.value">{{ t(use.label) }}</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
<MsFormItemSub
|
||||
v-if="form.use.length === 3"
|
||||
:text="t('system.resourcePool.allUseTip')"
|
||||
:show-fill-icon="false"
|
||||
/>
|
||||
</a-form-item>
|
||||
<template v-if="isCheckedPerformance">
|
||||
<a-form-item :label="t('system.resourcePool.mirror')" field="testResourceDTO.loadTestImage" class="form-item">
|
||||
|
@ -88,14 +93,10 @@
|
|||
:placeholder="t('system.resourcePool.testHeapPlaceholder')"
|
||||
:max-length="250"
|
||||
></a-input>
|
||||
<div class="mt-[4px] text-[12px] text-[var(--color-text-4)]">
|
||||
{{ t('system.resourcePool.testHeapExample', { heap: defaultHeap }) }}
|
||||
<MsIcon
|
||||
type="icon-icon_corner_right_up"
|
||||
class="cursor-pointer text-[rgb(var(--primary-6))]"
|
||||
@click="fillHeapByDefault"
|
||||
></MsIcon>
|
||||
</div>
|
||||
<MsFormItemSub
|
||||
:text="t('system.resourcePool.testHeapExample', { heap: defaultHeap })"
|
||||
@fill="fillHeapByDefault"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
@ -344,6 +345,8 @@
|
|||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsBatchForm from '@/components/business/ms-batch-form/index.vue';
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
|
||||
import JobTemplateDrawer from './components/jobTemplateDrawer.vue';
|
||||
import { getYaml, YamlType, job } from './template';
|
||||
import { downloadStringFile, sleep } from '@/utils';
|
||||
|
|
|
@ -84,41 +84,37 @@
|
|||
title: 'system.resourcePool.tableColumnName',
|
||||
slotName: 'name',
|
||||
dataIndex: 'name',
|
||||
width: 200,
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'system.resourcePool.tableColumnStatus',
|
||||
slotName: 'enable',
|
||||
dataIndex: 'enable',
|
||||
showInTable: true,
|
||||
},
|
||||
{
|
||||
title: 'system.resourcePool.tableColumnDescription',
|
||||
dataIndex: 'description',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'system.resourcePool.tableColumnType',
|
||||
dataIndex: 'type',
|
||||
showInTable: true,
|
||||
},
|
||||
{
|
||||
title: 'system.resourcePool.tableColumnCreateTime',
|
||||
dataIndex: 'createTime',
|
||||
showInTable: true,
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
title: 'system.resourcePool.tableColumnUpdateTime',
|
||||
dataIndex: 'updateTime',
|
||||
showInTable: true,
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
title: 'system.resourcePool.tableColumnActions',
|
||||
slotName: 'action',
|
||||
fixed: 'right',
|
||||
width: 140,
|
||||
showInTable: true,
|
||||
},
|
||||
];
|
||||
const tableStore = useTableStore();
|
||||
|
|
|
@ -69,6 +69,7 @@ export default {
|
|||
'system.resourcePool.batchAddResource': '批量添加资源',
|
||||
'system.resourcePool.changeAddTypeTip': '切换后,已添加资源内容将继续现在 yaml 内;可批量修改已添加资源',
|
||||
'system.resourcePool.changeAddTypePopTitle': '切换添加资源类型?',
|
||||
'system.resourcePool.allUseTip': '如果配置多个测试类型,会存在抢占资源的情况,建议一种测试类型配置一个资源池',
|
||||
'system.resourcePool.ip': 'IP',
|
||||
'system.resourcePool.ipRequired': 'IP 地址不能为空',
|
||||
'system.resourcePool.ipPlaceholder': '请输入 IP 地址',
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
:title="t('system.user.invite')"
|
||||
title-align="start"
|
||||
class="ms-modal-form ms-modal-medium"
|
||||
:loading="inviteLoading"
|
||||
>
|
||||
<a-form ref="inviteFormRef" class="rounded-[4px]" :model="emailForm" layout="vertical">
|
||||
<a-form-item
|
||||
|
@ -42,8 +41,10 @@
|
|||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button type="secondary" @click="cancelInvite">{{ t('system.user.inviteCancel') }}</a-button>
|
||||
<a-button type="primary" @click="emailInvite">
|
||||
<a-button type="secondary" :disabled="inviteLoading" @click="cancelInvite">
|
||||
{{ t('system.user.inviteCancel') }}
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="inviteLoading" @click="emailInvite">
|
||||
{{ t('system.user.inviteSendEmail') }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
@ -56,6 +57,7 @@
|
|||
import { FormInstance, Message, ValidatedError } from '@arco-design/web-vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
|
||||
import { inviteUser } from '@/api/modules/setting/user';
|
||||
|
||||
import type { SystemRole } from '@/models/setting/user';
|
||||
|
||||
|
@ -103,7 +105,10 @@
|
|||
function cancelInvite() {
|
||||
inviteVisible.value = false;
|
||||
inviteFormRef.value?.resetFields();
|
||||
emailForm.value = cloneDeep(defaultInviteForm);
|
||||
emailForm.value.emails = [];
|
||||
emailForm.value.userGroup = props.userGroupOptions
|
||||
.filter((e: SystemRole) => e.selected === true)
|
||||
.map((e: SystemRole) => e.id);
|
||||
}
|
||||
|
||||
function emailInvite() {
|
||||
|
@ -111,8 +116,12 @@
|
|||
if (!errors) {
|
||||
try {
|
||||
inviteLoading.value = true;
|
||||
cancelInvite();
|
||||
await inviteUser({
|
||||
inviteEmails: emailForm.value.emails,
|
||||
userRoleIds: emailForm.value.userGroup,
|
||||
});
|
||||
Message.success(t('system.user.inviteSuccess'));
|
||||
inviteVisible.value = false;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
:mask-closable="false"
|
||||
@close="handleUserModalClose"
|
||||
>
|
||||
<a-alert class="mb-[16px]">{{ t('system.user.createUserTip') }}</a-alert>
|
||||
<a-form ref="userFormRef" class="rounded-[4px]" :model="userForm" layout="vertical">
|
||||
<MsBatchForm
|
||||
ref="batchFormRef"
|
||||
|
|
|
@ -39,6 +39,7 @@ export default {
|
|||
'system.user.addUser': 'Add user',
|
||||
'system.user.addUserSuccess': 'Added successfully',
|
||||
'system.user.updateUserSuccess': 'Updated successfully',
|
||||
'system.user.createUserTip': 'The initial password is email address',
|
||||
'system.user.createUserName': 'Name',
|
||||
'system.user.createUserNameNotNull': 'Name cannot be blank',
|
||||
'system.user.createUserNameOverLength': 'Name length cannot exceed 50',
|
||||
|
|
|
@ -38,6 +38,7 @@ export default {
|
|||
'system.user.addUser': '添加用户',
|
||||
'system.user.addUserSuccess': '添加成功',
|
||||
'system.user.updateUserSuccess': '更新成功',
|
||||
'system.user.createUserTip': '初始密码为邮箱地址',
|
||||
'system.user.createUserName': '姓名',
|
||||
'system.user.createUserNameNotNull': '姓名不能为空',
|
||||
'system.user.createUserNameOverLength': '姓名长度不能超过50',
|
||||
|
|
Loading…
Reference in New Issue