diff options
| author | hanyuxia <[email protected]> | 2024-09-19 17:27:19 +0800 |
|---|---|---|
| committer | hanyuxia <[email protected]> | 2024-09-19 17:27:19 +0800 |
| commit | 47c09d439efd8720f35f2f88ed2289608251cbc7 (patch) | |
| tree | bf6fd71364251a8d10df559bf093601c67c8b444 | |
| parent | 3cdd54de65a323bcaace75ef32c0fe3028128999 (diff) | |
feat: ASW-81 User profile页面开发
| -rw-r--r-- | src/axios/api/user.js | 26 | ||||
| -rw-r--r-- | src/components/layout/index.vue | 6 | ||||
| -rw-r--r-- | src/components/layout/layoutHeader.vue | 4 | ||||
| -rw-r--r-- | src/components/layout/leftMenu.vue | 4 | ||||
| -rw-r--r-- | src/i18n/en.js | 6 | ||||
| -rw-r--r-- | src/i18n/zh.js | 6 | ||||
| -rw-r--r-- | src/router/permissions.js | 11 | ||||
| -rw-r--r-- | src/utils/constants.js | 36 | ||||
| -rw-r--r-- | src/views/envMgts/detail.vue | 6 | ||||
| -rw-r--r-- | src/views/envMgts/index.vue | 1 | ||||
| -rw-r--r-- | src/views/users/profile.vue | 358 |
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> |
