feat(系统设置): 用户邀请

This commit is contained in:
baiqi 2023-09-08 16:31:25 +08:00 committed by fit2-zhao
parent 6e1229b332
commit 409f1538a3
15 changed files with 137 additions and 54 deletions

View File

@ -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 });
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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',

View File

@ -14,6 +14,7 @@ export default {
'invite.passwordTipTitle': '密码须同时符合,仅支持以下规则',
'invite.passwordLengthRule': '长度为8-32位',
'invite.passwordWordRule': '必须包含数字和字母,不允许输入中文或空格',
'invite.success': '注册成功',
'personal.info': '个人信息',
'personal.switchOrg': '切换组织',
'personal.exit': '退出系统',

View File

@ -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'));

View File

@ -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';

View File

@ -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();

View File

@ -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 地址',

View File

@ -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 {

View File

@ -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"

View File

@ -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',

View File

@ -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',