summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhanyuxia <[email protected]>2024-09-19 17:27:19 +0800
committerhanyuxia <[email protected]>2024-09-19 17:27:19 +0800
commit47c09d439efd8720f35f2f88ed2289608251cbc7 (patch)
treebf6fd71364251a8d10df559bf093601c67c8b444
parent3cdd54de65a323bcaace75ef32c0fe3028128999 (diff)
feat: ASW-81 User profile页面开发
-rw-r--r--src/axios/api/user.js26
-rw-r--r--src/components/layout/index.vue6
-rw-r--r--src/components/layout/layoutHeader.vue4
-rw-r--r--src/components/layout/leftMenu.vue4
-rw-r--r--src/i18n/en.js6
-rw-r--r--src/i18n/zh.js6
-rw-r--r--src/router/permissions.js11
-rw-r--r--src/utils/constants.js36
-rw-r--r--src/views/envMgts/detail.vue6
-rw-r--r--src/views/envMgts/index.vue1
-rw-r--r--src/views/users/profile.vue358
11 files changed, 447 insertions, 17 deletions
diff --git a/src/axios/api/user.js b/src/axios/api/user.js
index 392812d..392b1ef 100644
--- a/src/axios/api/user.js
+++ b/src/axios/api/user.js
@@ -69,3 +69,29 @@ export const userDeleteApi = async (data) => {
}
};
+// user profile 详情
+export const userProfileDetailApi = async () => {
+ try {
+ const res = await axiosInstance({
+ url: '/api/v1/user/profile',
+ method: 'GET'
+ });
+ return res.data;
+ } catch (err) {
+ return err.data;
+ }
+};
+
+// user profile 修改
+export const userProfileEditApi = async (data) => {
+ try {
+ const res = await axiosInstance({
+ url: '/api/v1/user/profile',
+ method: 'PUT',
+ data: data,
+ });
+ return res.data;
+ } catch (err) {
+ return err.data;
+ }
+};
diff --git a/src/components/layout/index.vue b/src/components/layout/index.vue
index 525552a..d854c1b 100644
--- a/src/components/layout/index.vue
+++ b/src/components/layout/index.vue
@@ -66,8 +66,8 @@ onBeforeMount(() => {
router.currentRoute.value.path.startsWith('/users/'))
) {
curMenuType.value = userMenuType;
- } else if (router.currentRoute.value.path === '/envMgts' ||
- router.currentRoute.value.path.startsWith('/envMgts/')
+ } else if (router.currentRoute.value.path === '/profile' ||
+ router.currentRoute.value.path.startsWith('/profile/')
) {
curMenuType.value = profileMenuType;
} else {
@@ -90,7 +90,7 @@ const gotoUserManage = (val) => {
const gotoProfileManage = (val) => {
curMenuType.value = profileMenuType;
router.push({
- name: 'envMgts',
+ name: 'profile',
});
};
</script>
diff --git a/src/components/layout/layoutHeader.vue b/src/components/layout/layoutHeader.vue
index f48cb0f..4c81d3d 100644
--- a/src/components/layout/layoutHeader.vue
+++ b/src/components/layout/layoutHeader.vue
@@ -244,8 +244,8 @@ watch(
(value) => {
if (
(isAdministrator && (router.currentRoute.value.path === '/users' || router.currentRoute.value.path.startsWith('/users/'))) ||
- router.currentRoute.value.path === '/envMgts' ||
- router.currentRoute.value.path.startsWith('/envMgts/')
+ router.currentRoute.value.path === '/profile' ||
+ router.currentRoute.value.path.startsWith('/profile/')
) {
isGoBackWorkspace.value = true;
} else {
diff --git a/src/components/layout/leftMenu.vue b/src/components/layout/leftMenu.vue
index f303e06..6a179b4 100644
--- a/src/components/layout/leftMenu.vue
+++ b/src/components/layout/leftMenu.vue
@@ -81,8 +81,8 @@ watch(
if(isAdministrator && (router.currentRoute.value.path === '/users'
|| router.currentRoute.value.path.startsWith('/users/'))) {
menus = userMenus
- } else if (router.currentRoute.value.path === '/envMgts' ||
- router.currentRoute.value.path.startsWith('/envMgts/')
+ } else if (router.currentRoute.value.path === '/profile' ||
+ router.currentRoute.value.path.startsWith('/profile/')
) {
menus = profileMenus
} else {
diff --git a/src/i18n/en.js b/src/i18n/en.js
index 302b4a9..a257df8 100644
--- a/src/i18n/en.js
+++ b/src/i18n/en.js
@@ -152,6 +152,12 @@ export default {
language_zh: '简体中文',
},
},
+ profile: {
+ last_login_ip: 'Last login IP',
+ last_login_time: 'Last login Time',
+ current_password: 'Current Password',
+ new_password: 'New Password',
+ },
workspace: {
member: {
account:'Account',
diff --git a/src/i18n/zh.js b/src/i18n/zh.js
index 5079e4f..6dbdf1a 100644
--- a/src/i18n/zh.js
+++ b/src/i18n/zh.js
@@ -152,6 +152,12 @@ export default {
language_zh: '简体中文',
},
},
+ profile: {
+ last_login_ip: '最后登录IP',
+ last_login_time: '最后登录时间',
+ current_password: '当前密码',
+ new_password: '新密码',
+ },
workspace: {
member: {
account:'账户',
diff --git a/src/router/permissions.js b/src/router/permissions.js
index 6f913bf..e311164 100644
--- a/src/router/permissions.js
+++ b/src/router/permissions.js
@@ -62,6 +62,11 @@ const systemRoutes = {
component: () => import('@/views/users/detail.vue'),
},
{
+ path: '/profile',
+ name: 'profile',
+ component: () => import('@/views/users/profile.vue'),
+ },
+ {
path: '/:workspace?/pcaps',
name: 'pcaps',
component: () => import('@/views/pcaps/index.vue'),
@@ -77,17 +82,17 @@ const systemRoutes = {
component: () => import('@/views/environments/start.vue'),
},
{
- path: '/envMgts',
+ path: '/profile/envMgts',
name: 'envMgts',
component: () => import('@/views/envMgts/index.vue'),
},
{
- path: '/envMgts/add',
+ path: '/profile/envMgts/add',
name: 'envMgt_add',
component: () => import('@/views/envMgts/detail.vue'),
},
{
- path: '/envMgts/:id/edit',
+ path: '/profile/envMgts/:id/edit',
name: 'envMgt_edit',
component: () => import('@/views/envMgts/detail.vue'),
},
diff --git a/src/utils/constants.js b/src/utils/constants.js
index fe219e7..32d7e4c 100644
--- a/src/utils/constants.js
+++ b/src/utils/constants.js
@@ -2,7 +2,7 @@ export const administrator = 'administrator'
export const userMenus = [{
"id": "1000",
"name": "users",
- "i18n": "user.user",
+ "i18n": "overall.users",
"pid": "0",
"type": "menu",
"perms": "",
@@ -44,6 +44,34 @@ export const userMenus = [{
}]
export const profileMenus = [{
"id": "1000",
+ "name": "profile",
+ "i18n": "overall.profile",
+ "pid": "0",
+ "type": "menu",
+ "perms": "",
+ "route": "/profile",
+ "icon": "asw-icon icon-Personal",
+ "order": 1,
+ "state": 1,
+ "createTimestamp": 1702534225000,
+ "children": [
+ {
+ "id": "1002",
+ "name": "profile_edit",
+ "i18n": "buttons.edit",
+ "pid": "1000",
+ "type": "button",
+ "perms": "",
+ "route": "",
+ "icon": "",
+ "order": 2,
+ "state": 1,
+ "createTimestamp": 1722478572000,
+ "children": null
+ }
+ ]
+},{
+ "id": "2000",
"name": "envMgts",
"i18n": "overall.environments",
"pid": "0",
@@ -56,7 +84,7 @@ export const profileMenus = [{
"createTimestamp": 1702534225000,
"children": [
{
- "id": "1001",
+ "id": "2001",
"name": "envMgt_add",
"i18n": "buttons.add",
"pid": "1000",
@@ -70,7 +98,7 @@ export const profileMenus = [{
"children": null
},
{
- "id": "1002",
+ "id": "2002",
"name": "envMgt_edit",
"i18n": "buttons.edit",
"pid": "1000",
@@ -84,7 +112,7 @@ export const profileMenus = [{
"children": null
}
]
-}]
+},]
export const normalMenuType = 0 // 登陆后默认菜单
export const userMenuType = 1
export const profileMenuType = 2
diff --git a/src/views/envMgts/detail.vue b/src/views/envMgts/detail.vue
index 2172f7c..c485b92 100644
--- a/src/views/envMgts/detail.vue
+++ b/src/views/envMgts/detail.vue
@@ -116,7 +116,8 @@
<span>{{ t('overall.workspace') }}</span>
</div>
<div class="detail-form detail-workspace" >
- <div v-for="(item,index) in ruleForm.workspaces" style="display: flex;flex-direction: row;margin-bottom:20px;">
+ <div v-for="(item,index) in ruleForm.workspaces"
+ style="display: flex;flex-direction: row;margin-bottom:20px;">
<!-- workspace name -->
<div class="workspace-item">
<el-form-item
@@ -126,7 +127,8 @@
:prop="`workspaces[${index}]`"
size="default"
>
- <el-select v-model="ruleForm.workspaces[index]" value-key="id" style="width: 566px;" :key="'workspace'+index" :ref="'workspace'+index">
+ <el-select v-model="ruleForm.workspaces[index]" value-key="id" style="width: 566px;"
+ :key="'workspace'+index" :ref="'workspace'+index">
<template #label>
<span>{{ item.name }} </span>
</template>
diff --git a/src/views/envMgts/index.vue b/src/views/envMgts/index.vue
index 87e0b1a..df051fe 100644
--- a/src/views/envMgts/index.vue
+++ b/src/views/envMgts/index.vue
@@ -352,7 +352,6 @@ onBeforeMount(() => {
align-items: center;
.circle {
border-radius: 50%;
- //box-shadow: 0.375em 0.375em 0 0 rgba(15, 28, 63, 0.125);
height: 6px;
width: 6px;
margin-right: 6px;
diff --git a/src/views/users/profile.vue b/src/views/users/profile.vue
new file mode 100644
index 0000000..c2a6457
--- /dev/null
+++ b/src/views/users/profile.vue
@@ -0,0 +1,358 @@
+<template>
+ <div id="profile_edit" class="detail">
+ <p class="detail-title">
+ <span>{{ t('overall.profile') }}</span>
+ </p>
+ <div class="detail-content profile-content" v-loading="loading">
+ <el-form
+ ref="ruleFormRef"
+ :model="ruleForm"
+ :rules="editRules"
+ label-position="left"
+ label-width="auto"
+ style="width:calc(100% - 400px);"
+ >
+ <!-- left -->
+ <div class="detail-left">
+ <!-- baseInfo -->
+ <div class="form-title">
+ <span>{{ t('user.basic') }}</span>
+ </div>
+ <div class="detail-form" >
+ <!-- userName -->
+ <el-form-item
+ :label="t('user.user_name') + ':'"
+ prop="userName"
+ size="default"
+ >
+ <el-input
+ v-model="ruleForm.userName"
+ show-word-limit
+ disabled
+ maxlength="64"
+ :placeholder="t('overall.please_input')"
+ />
+ </el-form-item>
+ <!-- name -->
+ <el-form-item
+ :label="t('user.add.name') + ':'"
+ prop="name"
+ size="default"
+ >
+ <el-input
+ v-model="ruleForm.name"
+ show-word-limit
+ maxlength="32"
+ :placeholder="t('overall.please_input')"
+ />
+ </el-form-item>
+ <!-- language -->
+ <el-form-item
+ :label="t('user.add.language') + ':'"
+ prop="language"
+ size="default"
+ >
+ <el-select v-model="ruleForm.language" >
+ <el-option
+ key="en"
+ :label="t('user.add.language_en')"
+ value="en"
+ />
+ <el-option
+ key="zh"
+ :label="t('user.add.language_zh')"
+ value="zh"
+ />
+ </el-select>
+ </el-form-item>
+ <!-- accessLevel -->
+ <el-form-item
+ :label="t('user.add.access_level') + ':'"
+ prop="accessLevel"
+ size="default"
+ >
+ <el-radio-group v-model="ruleForm.accessLevel" style="display:flex;flex-direction: column;align-items: flex-start;">
+ <div class="detail-item-desc">
+ <el-radio value="regular" :disabled="ruleForm.accessLevel !== 'regular'" size="large">{{t('user.add.regular')}}</el-radio>
+ <span class="item-desc">{{t('user.add.regular_tips')}}</span>
+ </div>
+ <div class="detail-item-desc">
+ <el-radio value="administrator" :disabled="ruleForm.accessLevel !== 'administrator'" size="large">{{t('user.add.administrator')}}</el-radio>
+ <span class="item-desc">{{t('user.add.administrator_tips')}}</span>
+ </div>
+ </el-radio-group>
+ </el-form-item>
+ </div>
+ <!-- password -->
+ <div class="form-title">
+ <span>{{ t('overall.password') }}</span>
+ </div>
+ <div class="detail-form" >
+ <!-- current password -->
+ <el-form-item
+ :label="t('profile.current_password') + ':'"
+ prop="oldPwd"
+ size="default"
+ >
+ <div class="detail-item-desc">
+ <el-input
+ v-model="ruleForm.oldPwd"
+ autocomplete="new-password"
+ show-password
+ :placeholder="t('overall.please_input')"
+ />
+ </div>
+ </el-form-item>
+ <!-- new password -->
+ <el-form-item
+ :label="t('profile.new_password') + ':'"
+ prop="pwd"
+ size="default"
+ >
+ <div class="detail-item-desc">
+ <el-input
+ v-model="ruleForm.pwd"
+ style="margin-bottom:10px;"
+ autocomplete="new-password"
+ show-password
+ :placeholder="t('overall.please_input')"
+ />
+ <span class="item-desc">{{t('user.add.password_tips')}}</span>
+ </div>
+ </el-form-item>
+ <!-- confirm password -->
+ <el-form-item
+ :label="t('user.add.confirm_password') + ':'"
+ prop="confirmPassword"
+ size="default"
+ >
+ <el-input
+ v-model="ruleForm.confirmPassword"
+ style="margin-bottom:10px;"
+ show-password
+ :placeholder="t('overall.please_input')"
+ />
+ </el-form-item>
+ </div>
+ </div>
+ </el-form>
+ <!-- right -->
+ <div class="detail-log-info">
+ <p class="detail-info-title">{{ t('profile.last_login_ip') }}</p>
+ <p class="detail-info-text">
+ {{ get(ruleForm, 'lastLoginIp', '-') }}
+ </p>
+ <p class="detail-info-title">{{ t('profile.last_login_time') }}</p>
+ <p class="detail-info-text">
+ {{
+ ruleForm.lastLoginTimestamp
+ ? moment(ruleForm.lastLoginTimestamp).format('YYYY-MM-DD HH:mm:ss')
+ : '-'
+ }}
+ </p>
+ </div>
+ </div>
+ <div class="detail-footer" >
+ <el-button
+ size="large"
+ type="primary"
+ :disabled="submitting"
+ @click="save"
+ >
+ {{ t('overall.save') }}
+ </el-button>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { useRouter } from 'vue-router';
+ import { ref, reactive, computed, onBeforeMount } from 'vue';
+ import {
+ userProfileDetailApi,
+ userProfileEditApi
+ } from '@/axios/api';
+ import { ElMessage } from 'element-plus';
+ import { useI18n } from 'vue-i18n';
+ import { isEmpty, get } from 'lodash';
+ import moment from 'moment-timezone';
+
+ const router = useRouter();
+ const { t } = useI18n();
+
+ // base
+ const ruleFormRef = ref(null);
+ const ruleForm = reactive({
+ userName: '',
+ name: '',
+ accessLevel: 'regular',
+ oldPwd: '',
+ pwd: '',
+ confirmPassword: '',
+ language: ''
+ });
+
+ const bitTotal = (H) => {
+ let I = 0
+ for (let j = 0; j < 4; j++) {
+ if (H & 1) {
+ I++
+ }
+ H >>>= 1
+ }
+ return I
+ };
+
+ const passwordLevel = (I) => {
+ let H = 0
+ for (let a = 0; a < I.length; a++) {
+ H |= CharMode(I.charCodeAt(a))
+ }
+ return bitTotal(H)
+ };
+
+ const CharMode = (H) => {
+ if (H >= 48 && H <= 57) { // 数字
+ return 1
+ }
+ if (H >= 65 && H <= 90) { // 大写
+ return 2
+ }
+ if (H >= 97 && H <= 122) { // 小写
+ return 4
+ } else {
+ return 8
+ }
+ };
+
+ const validatePwd = (rule, value, callback) => { // 确认密码
+ if (value) {
+ const reg = /^[-\d\w/~!@#$%^&*.?]+$/g
+ const isValid = value.match(reg) // 返回匹配到的值
+ if (value && value.length < 8) {
+ callback(t('validator.at_least_eight'));
+ } else if (!isValid) {
+ callback(t('validator.password_error'));
+ } else if (passwordLevel(ruleForm.pwd) === 1) {
+ callback(t('validator.password_two_types'));
+ } else {
+ callback();
+ }
+ } else {
+ callback()
+ }
+ };
+
+ const validateConfirmPwd = (rule, value, callback) => { // 确认密码的二次校验
+ if (isEmpty(value) && isEmpty(ruleForm.pwd)) { // 编辑时,密码和确认密码均没内容则不做校验
+ callback()
+ } else if (isEmpty(value) && !isEmpty(ruleForm.pwd)) { // 密码有内容,确认密码没内容
+ callback(t('validator.confirm_pwd'));
+ } else if (!isEmpty(value) && !isEmpty(ruleForm.pwd) && value !== ruleForm.pwd) { // 密码有内容,确认密码也有内容,内容不一致
+ callback(t('validator.confirm_pwd_err'));
+ } else if (!isEmpty(value) && isEmpty(ruleForm.pwd)) { // 确认密码有内容,密码没内容
+ callback(t('validator.confirm_no_pwd'));
+ } else {
+ callback()
+ }
+ };
+
+ const editRules = reactive({
+ userName: [{ required: true, message: t('validator.required'), trigger: 'blur' }],
+ name: [{ required: true, message: t('validator.required'), trigger: 'blur' }],
+ pwd: [
+ { validator: validatePwd, trigger: 'blur' }
+ ],
+ confirmPassword: [
+ { validator: validateConfirmPwd, trigger: 'blur' }
+ ]
+ });
+
+ const submitting = ref(false);
+ const save = async () => {
+ if (submitting.value) {
+ return;
+ }
+ submitting.value = true;
+ await ruleFormRef.value.validate(async (valid, fields) => {
+ if (valid) {
+ let params = {
+ userName: ruleForm.userName,
+ name: ruleForm.name,
+ accessLevel: ruleForm.accessLevel,
+ pwd: ruleForm.pwd,
+ oldPwd: ruleForm.oldPwd,
+ language: ruleForm.language
+ };
+ let res = await userProfileEditApi(params);
+
+ if (res.code == 200) {
+ jumpBack();
+ ElMessage.success(t('message.save_success'));
+ } else {
+ ElMessage.error(res.msg || res.error);
+ }
+ }
+ });
+ submitting.value = false;
+ };
+
+ const loading = ref(false);
+ const getData = async () => {
+ loading.value = true;
+ const res = await userProfileDetailApi();
+ if (res.code == 200) {
+ Object.assign(ruleForm, res.data.record);
+ } else {
+ ElMessage.error(res.msg || res.error);
+ }
+ loading.value = false;
+ };
+
+ const jumpBack = () => {
+ router.push({
+ name: 'profile',
+ params: {},
+ });
+ };
+
+ onBeforeMount(() => {
+ getData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .profile-content {
+ border-bottom: 1px #edf0f3 solid;
+ .el-input,.el-select {
+ width: 392px;
+ }
+ .user-accessLevel {
+ display:flex;
+ lex-direction: column;
+ align-items: flex-start;
+ }
+ }
+ .detail-log-info {
+ overflow: auto;
+ width: 400px;
+ height: 100%;
+ border-radius: 2px;
+ padding: 25px 20px;
+ .detail-info-title {
+ font-size: 16px;
+ color: var(--text);
+ font-weight: 600;
+ margin-bottom: 10px;
+ }
+ .detail-info-text {
+ font-size: 14px;
+ color: var(--text);
+ font-weight: 500;
+ margin-bottom: 20px;
+ white-space: noWrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+</style>