This repository has been archived on 2025-09-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
nezha-nezha-fronted/nezha-fronted/src/components/page/config/profile.vue
2021-08-24 11:07:14 +08:00

652 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="profile">
<div class="profile-left">
<div class="profile-left__header">
<div class="profile-left__header-avatar">
<span>{{userList.name.substr(0, 1)}}</span>
</div>
<div class="profile-left__header-username">
<el-popover
placement="right-start"
style="line-height: .1;"
trigger="hover"
:content="userList.name">
<div slot="reference">
<span :class="(mfaEnable == '1' || mfaLevel > 0)?'MfaName':'name'">
{{userList.name}}
</span>
<span class="profile-left__header-username__span" v-show="mfaEnable == '1' || mfaLevel > 0">2FA</span>
</div>
</el-popover>
<el-popover
placement="right-start"
trigger="hover"
:content="'@' + userList.username">
<div slot="reference">
<span :class="(mfaEnable == '1' || mfaLevel > 0)?'MfaName':'name'">
@{{userList.username}}
</span>
</div>
</el-popover>
</div>
</div>
<div v-for="item in tableProfile" :key="item.id" class="profile-left__center">
<span style="margin:0 18px 0 0"><i :class="item.icon"></i></span>
<div class="profile-left__center-title">
<div>{{item.label}}</div>
<div v-if="item.prop">{{item.prop}}</div>
<div v-if="!item.prop">-</div>
</div>
</div>
<div class="profile-left__center">
<span style="margin:0 10px 10px 0"><i class="nz-icon nz-icon-yanzhengma" style="color: orange"></i></span>
<div class="profile-left__center-title">
<div>{{$t('profile.twoFactorAuthentication')}}</div>
</div>
</div>
<div class="profile-left__button table-operation-items">
<el-button @click="profileEnable" size="small" class="profile-left__button-footer__btn footer__btn table-operation-item" v-if="mfaEnable != '1' && mfaLevel == 0">
<span>{{$t('profile.enable')}}</span>
</el-button>
<el-button @click="profileDisable" size="small" class="profile-left__button-footer__btn footer__btns table-operation-item" :class="{'footer__btn--disabled': mfaEnable == 1 || mfaLevel == 2}" v-if="mfaEnable == '1' || mfaLevel > 0" :disabled="mfaEnable == 1 || mfaLevel == 2 ">
<span>{{$t('profile.close')}}</span>
</el-button>
<el-button @click="profileMfaLevel" size="small" class="profile-left__button-footer__btn footer__btn table-operation-item" v-if="mfaEnable == '1' || mfaLevel > 0" >
<span>{{$t('profile.update')}}</span>
</el-button>
</div>
<div class="profile-hr" style="height: 1px;background-color: #E7EAED;width: 320px; margin: 40px auto"></div>
<div class="profile-left__bottom">
<div class="profile-left__bottom-title">
<div>{{$t('profile.lastLoginIp')}}</div>
<div>{{userList.lastLoginIp}}</div>
</div>
<div class="profile-left__bottom-title">
<div>{{$t('profile.lastLoginTime')}}</div>
<div>{{userList.lastLoginTime}}</div>
</div>
</div>
</div>
<operation-record style="width: calc(100% - 370px);padding: 11px 10px 8px 10px;pxbox-shadow: 0 1px 2px 0 rgba(0,0,0,0.06);border-radius: 2px;" />
<el-dialog :visible.sync="authBindShow" :title="$t('login.verifyDialogTitle')" :modal-append-to-body='false'
:show-close="true" width="620px" class="nz-dialog" @closed="closeDialog">
<div v-loading="dialogLoading">
<div class="login-dialog-title">
1 Download your preferred authenticator app to your phone (any will work). If you don't
have a preferred app, we recommend using <span @click="jumpDlw" class="verify-link">Google Authenticator.</span>
</div>
<div class="login-dialog-title">
2 Use your app to take a photo of the QR code.
</div>
<div class="qrCode-box">
<div id="qrCode" ref="qrCodeDiv" class="qrCode-content"></div>
<div class="qrCode-text">
<div>Type this code down if you can't take a photo.</div>
<div class="qrCode-authKey">{{authKey}}</div>
</div>
</div>
<div class="login-dialog-title">
3. Enter the 6-digit code provided by your app and then verify.
</div>
<div class="enter-code">Enter Code</div>
<el-input v-model="bindAuthCode" size="small" style="width: 50%" @keydown.enter="bindCode"></el-input>
</div>
<div slot="footer" class="footer">
<button id="asset-edit-cancel" @click="closeDialog" class="footer__btn footer__btn--light">
<span>{{$t('overall.cancel')}}</span>
</button>
<button id="asset-edit-save" :class="{'footer__btn--disabled': prevent_opt.save}" :disabled="prevent_opt.save" class="footer__btn" @click="bindCode">
<span style="text-transform:capitalize">{{$t('login.verify')}}</span>
</button>
</div>
</el-dialog>
<el-dialog :visible.sync="fileShow" :title="$t('login.verifyDialogTitle')" :modal-append-to-body='false'
:show-close="true" width="620px" class="nz-dialog" @closed="fileClosed">
<div>
<div class="login-dialog-title">
Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account.
Please save them in a safe place, or you will lose access to your account.
</div>
<div class="login-dialog-title2">
<div class="login-dialog-recover">
<div v-for="(item, index) in recoveryCode" :key="index" style="color: #999999;">
<span class="circle"></span>
{{item}}
</div>
</div>
<span class="copy-value-content"> <i class="nz-icon nz-icon-override" @click="copyValue"></i></span>
</div>
</div>
<div slot="footer" class="footer">
<button id="asset-edit-download" :class="{'footer__btn--disabled': prevent_opt.save}" :disabled="prevent_opt.save" class="footer__btn" @click="downloadTxt">
<span style="text-transform:capitalize">{{$t('overall.download')}}</span>
</button>
</div>
</el-dialog>
</div>
</template>
<script>
import operationRecord from './operationRecord'
import QRCode from "qrcodejs2";
import MessageBox from "element-ui/packages/message-box/src/main";
import i18n from "@/components/common/i18n";
export default {
name: 'profile',
components: {
operationRecord
},
data () {
return {
username: sessionStorage.getItem('nz-username'),
mfaEnable: localStorage.getItem('nz-mfa-enable'),
authBindShow: false,
fileShow: false,
dialogLoading: false,
recoveryCode: '',
bindAuthCode: '',
mfaLevel: 0,
authKey: '',
authToken: '',
// 右侧列表
tableProfile: [
{
id: 1,
label: this.$t('profile.role'),
prop: '',
icon: 'nz-icon nz-icon-role profile-role'
},
{
id: 2,
label: this.$t('profile.email'),
prop: '',
icon: 'nz-icon nz-icon-email profile-email'
},
{
id: 3,
label: this.$t('profile.mobile'),
prop: '',
icon: 'nz-icon nz-icon-mobile profile-mobile'
},
{
id: 4,
label: this.$t('profile.source'),
prop: '',
icon: 'nz-icon nz-icon-laiyuan profile-laiyuan'
}
],
// 后台数据
userList: {},
rules: {
oldPassword: [
{ required: true, message: this.$t('validate.required') }
],
// newPassword: [
// { required: true, message: this.$t('validate.required'), trigger: 'change' }
// ],
confirmPassword: [
{ required: true, message: this.$t('validate.required'), trigger: 'change' }
]
}
}
},
mounted () {
this.personalCenter()
},
methods: {
// 请求后端数据
personalCenter () {
return new Promise(resolve => {
this.$get('/sys/user/profile').then(response => {
if (response.code === 200) {
this.userList = response.user
this.mfaLevel = response.user.mfaLevel
this.tableProfile[0].prop = this.userList.roles[0].i18n
this.tableProfile[1].prop = this.userList.email
this.tableProfile[2].prop = this.userList.mobile
this.tableProfile[3].prop = this.userList.source
}
resolve()
})
})
},
profileMfaLevel () {
this.dialogLoading = true
this.authBindShow = true
const params = {}
this.$post('/mfa/genkey', params).then(res => {
this.dialogLoading = false
if (res.code === 200) {
this.$nextTick(() => {
this.authToken = res.data.authToken
this.authKey = res.data.authKey
this.bindQRCode(res.data.otpauth)
})
} else {
this.authBindShow = false
this.$message.error(res.message)
}
})
},
profileEnable () {
this.dialogLoading = true
this.authBindShow = true
const params = {
mfaLevel: 1
}
this.$post('/mfa/genkey', params).then(res => {
this.dialogLoading = false
if (res.code === 200) {
this.$nextTick(() => {
this.authToken = res.data.authToken
this.authKey = res.data.authKey
this.bindQRCode(res.data.otpauth)
})
} else {
this.authBindShow = false
this.$message.error(res.message)
}
})
},
profileDisable () {
MessageBox.confirm(i18n.t('tip.resetMfa'), {
confirmButtonText: i18n.t('tip.yes'),
cancelButtonText: i18n.t('tip.no'),
type: 'warning'
}).then(() => {
this.$delete('/sys/user/disableMfa').then(res => {
if (res.code == 200) {
this.personalCenter()
} else {
this.$message.error(res.msg)
}
})
})
},
closeDialog () {
this.authBindShow = false
this.bindAuthCode = ''
},
jumpDlw () {
window.open('https://google-authenticator.en.softonic.com/android')
},
bindCode () {
this.prevent_opt.save = true
this.$post('/mfa/bind', { authToken: this.authToken, authCode: this.bindAuthCode }).then(res => {
this.prevent_opt.save = false
if (res.code === 200) {
this.authBindShow = false
this.bindAuthCode = ''
this.recoveryCode = res.data.recoveryCode
this.fileContent = this.recoveryCode.join(' \n ')
this.fileShow = true
} else {
this.authToken = res.data.authToken
this.$message.error(this.$t('login.bindFail'))
}
})
},
bindQRCode: function (text) {
text = text || 'https://www.baidu.com'
if (!this.QRCode) {
this.QRCode = new QRCode(this.$refs.qrCodeDiv, {
text: text,
width: 124,
height: 124,
colorDark: '#333333', // 二维码颜色
colorLight: '#ffffff', // 二维码背景色
correctLevel: QRCode.CorrectLevel.L// 容错率L/M/H
})
} else {
this.QRCode.makeCode(text)
}
},
downloadTxt () {
const element = document.createElement('a')
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(this.fileContent))
element.setAttribute('download', 'Nezha recovery codes')
element.style.display = 'none'
element.click()
},
copyValue () {
const domUrl = document.createElement('input')
domUrl.value = this.fileContent
domUrl.id = 'creatDom'
document.body.appendChild(domUrl)
domUrl.select() // 选择对象
document.execCommand('Copy') // 执行浏览器复制命令
const creatDom = document.getElementById('creatDom')
creatDom.parentNode.removeChild(creatDom)
this.$message.success(this.$t('overall.copySuccess'))
},
fileClosed () {
this.personalCenter()
}
}
}
</script>
<style lang="scss" scoped>
.profile {
display: flex;
height: calc(100% - 10px);
.profile-left {
margin: 10px 0 10px 10px;
background: #FFFFFF;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.06);
border-radius: 2px;
height: calc(100% - 10px);
width: 360px;
overflow: auto;
.profile-left__header {
height: 200px;
width: 100%;
margin: 10px 0;
text-align: center;
display: flex;
justify-content: flex-start;
align-items: center;
.profile-left__header-avatar {
height: 80px;
width: 80px;
line-height: 80px;
margin-left: 20px;
span {
display: inline-block;
background: rgba(60,146,241,0.10);
font-family: PingFangSC-Medium;
text-transform: capitalize;
width: 100%;
height: 100%;
font-size: 30px;
color: #3C92F1;
letter-spacing: 0;
font-weight: 500;
border-radius: 100%;
}
}
.profile-left__header-username {
height: 80px;
padding-left: 15px;
text-align: left;
.name{
display: inline-block;
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
vertical-align: middle;
padding-top: 4px;
}
.MfaName{
display: inline-block;
max-width: 158px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
vertical-align: middle;
padding-top: 4px;
}
div:nth-of-type(1) {
font-family: Roboto-Medium;
max-width: 200px;
font-size: 18px;
color: #333333;
letter-spacing: 0;
line-height: 22px;
font-weight: 500;
padding: 9px 0 0 0;
}
div:nth-of-type(2) {
font-family: Roboto-Regular;
font-size: 18px;
color: #666666;
letter-spacing: 0;
line-height: 22px;
font-weight: 400;
}
.profile-left__header-username__span {
display: inline-block;
text-align: center;
width: 42px;
height: 24px;
line-height: 25px;
margin-left: 10px;
background-color: #ecf5ff;
border-color: #d9ecff;
border-radius: 10px;
font-family: Roboto-Regular;
font-size: 14px;
color: #409EFF;
letter-spacing: 0;
font-weight: 400;
}
}
}
.profile-left__center:nth-of-type(6) {
margin-bottom: 0;
}
.profile-left__center {
margin: 0 0 20px 26px;
/*border: 1px solid red;*/
display: flex;
justify-content: flex-start;
.profile-role {
color: #00C398;
}
.profile-email {
color: #0093F8;
}
.profile-mobile {
color: #EC7F66;
}
.profile-laiyuan {
color: #8871DB;
}
.profile-left__center-title {
margin-left: 8px;
margin-top: 1px;
div:nth-of-type(1) {
padding-bottom: 5px;
ont-family: Roboto-Regular;
font-size: 14px;
color: #666666;
letter-spacing: 0;
font-weight: 400;
}
div:nth-of-type(2) {
ont-family: Roboto-Regular;
font-size: 14px;
color: #333333;
letter-spacing: 0;
font-weight: 400;
}
}
}
.profile-left__button {
margin-left: 45px;
.profile-left__button-footer__btn {
margin: 0 10px;
height: 30px;
min-width: 74px;
padding: 0 15px;
border-radius: 4px;
}
.footer__btn {
background-color: #FA901C;
color: white;
border: 1px solid #fff;
}
.footer__btns {
background-color: #fff;
color: #333;
border: 1px solid #ccc;
}
.footer__btns:hover {
opacity: .6;
color: #FA901C;
border: 1px solid #FA901C;
}
.footer__btn:hover {
opacity: .6;
}
.is-disabled,.is-disabled:hover{
color: #C0C4CC;
cursor: not-allowed;
background-image: none;
background-color: #FFF;
border-color: #EBEEF5;
}
}
.profile-left__bottom {
width: 320px;
margin: 0 10px 15px;
.profile-left__bottom-title {
width: 275px;
margin: auto;
padding: 0 0 30px 0;
div:nth-of-type(1) {
ont-family: Roboto-Regular;
font-size: 14px;
color: #666666;
letter-spacing: 0;
line-height: 25px;
font-weight: 400;
}
div:nth-of-type(2) {
ont-family: Roboto-Regular;
font-size: 14px;
color: #333333;
letter-spacing: 0;
line-height: 25px;
font-weight: 400;
}
}
}
}
}
.login-dialog-title{
background: #F9F9F9;
font-size: 14px;
color: #666666;
letter-spacing: 0;
line-height: 20px;
font-weight: 400;
padding: 10px;
margin-bottom: 10px;
word-break: keep-all;
}
.login-dialog-title2{
background: rgba(246,248,250, 0.9);
border: 1px solid #E7EAED;
border-radius: 2px;
height: 248px;
padding: 15px;
position: relative;
.copy-value-content{
position: absolute;
right: 5px;
top: 5px;
cursor: pointer;
}
.login-dialog-recover{
height: calc(100% - 30px);overflow-y: auto
}
}
.enter-code{
ont-size: 14px;
color: #666666;
letter-spacing: 0;
font-weight: 400;
margin-bottom: 10px;
}
.qrCode-box{
display: flex;
align-items: center;
}
.qrCode-content{
padding: 4px;
margin-left: 10px;
}
.qrCode-text{
width: 60%;
margin-left: 10px;
font-size: 14px;
color: #666666;
letter-spacing: 0;
font-weight: 400;
}
.qrCode-authKey{
background: rgba(246,248,250,09);
border: 1px solid #E7EAED;
border-radius: 2px;
padding: 5px 12px;
font-size: 14px;
color: #333333;
letter-spacing: 0;
font-weight: 400;
margin-top: 12px;
}
/deep/ .nz-dialog .el-dialog__body{
padding-bottom: 0;
}
.verify-link{
font-family: Roboto-Black;
font-size: 14px;
color: #3C92F1;
letter-spacing: 0;
line-height: 20px;
font-weight: 400;
cursor: pointer;
}
.verify-link:hover{
border-bottom: 1px solid #3C92F1;
}
.circle{
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #CECECE;
margin-right: 10px;
}
.footer {
.footer__btn {
margin: 0 15px;
height: 30px;
min-width: 74px;
padding: 0 15px;
color: white;
background-color: var(--theme-color);
border: none;
border-radius: 4px;
outline: none;
box-sizing: border-box;
font-size: 14px;
cursor: pointer;
transition: background-color linear .2s, color linear .1s;
}
.footer__btn:hover:not(.footer__btn--disabled) {
background-color: var(--theme-color-light-20);
}
.footer__btn--light {
background-color: white;
border: 1px solid $--primary-border-color;
color: #333;
}
.footer__btn.footer__btn--light:hover:not(.footer__btn--disabled) {
background-color: white;
border-color: var(--theme-color-light-50);
color: var(--theme-color);
}
.footer__btn--disabled {
opacity: .6;
cursor: default;
}
}
</style>