1、对接谷歌验证器

This commit is contained in:
2025-07-18 18:07:44 +08:00
parent 722409056e
commit 34867c170c
10 changed files with 744 additions and 289 deletions

View File

@ -64,6 +64,7 @@
"normalize.css": "8.0.1",
"nprogress": "0.2.0",
"path-to-regexp": "6.1.0",
"qrcode": "^1.5.4",
"remixicon": "^2.5.0",
"sass-resources-loader": "^2.0.3",
"screenfull": "5.0.2",

View File

@ -53,3 +53,12 @@ export function clearAllAlarmLog() {
})
}
//导出告警记录
export function exportMmAlarmLog(query) {
return request({
url: '/api/v1/mm-alarm-log/export',
method: 'get',
params: query,
responseType: 'blob',
})
}

View File

@ -1,142 +1,178 @@
import request from '@/utils/request'
import request from "@/utils/request";
// 查询用户列表
export function listUser(query) {
return request({
url: '/api/v1/sys-user',
method: 'get',
params: query
})
url: "/api/v1/sys-user",
method: "get",
params: query,
});
}
// 查询用户详细
export function getUser(userId) {
return request({
url: '/api/v1/sys-user/' + userId,
method: 'get'
})
url: "/api/v1/sys-user/" + userId,
method: "get",
});
}
export function getUserInit() {
return request({
url: '/api/v1/sys-user/',
method: 'get'
})
url: "/api/v1/sys-user/",
method: "get",
});
}
// 新增用户
export function addUser(data) {
return request({
url: '/api/v1/sys-user',
method: 'post',
data: data
})
url: "/api/v1/sys-user",
method: "post",
data: data,
});
}
// 修改用户
export function updateUser(data) {
return request({
url: '/api/v1/sys-user',
method: 'put',
data: data
})
url: "/api/v1/sys-user",
method: "put",
data: data,
});
}
// 删除用户
export function delUser(data) {
return request({
url: '/api/v1/sys-user',
method: 'delete',
data: data
})
url: "/api/v1/sys-user",
method: "delete",
data: data,
});
}
// 导出用户
export function exportUser(query) {
return request({
url: '/api/v1/sys-user/export',
method: 'get',
params: query
})
url: "/api/v1/sys-user/export",
method: "get",
params: query,
});
}
// 用户密码重置
export function resetUserPwd(userId, password) {
const data = {
userId,
password
}
password,
};
return request({
url: '/api/v1/user/pwd/reset',
method: 'put',
data: data
})
url: "/api/v1/user/pwd/reset",
method: "put",
data: data,
});
}
// 用户状态修改
export function changeUserStatus(e) {
const data = {
userId: e.userId,
status: e.status
}
status: e.status,
};
return request({
url: '/api/v1/user/status',
method: 'put',
data: data
})
url: "/api/v1/user/status",
method: "put",
data: data,
});
}
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: '/api/v1/sys-user/profile',
method: 'put',
data: data
})
url: "/api/v1/sys-user/profile",
method: "put",
data: data,
});
}
// 下载用户导入模板
export function importTemplate() {
return request({
url: '/api/v1/sys-user/importTemplate',
method: 'get'
})
url: "/api/v1/sys-user/importTemplate",
method: "get",
});
}
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword
}
newPassword,
};
return request({
url: '/api/v1/user/pwd/set',
method: 'put',
data: data
})
url: "/api/v1/user/pwd/set",
method: "put",
data: data,
});
}
// 用户头像上传
export function uploadAvatar(data) {
return request({
url: '/api/v1/user/avatar',
method: 'post',
data: data
})
url: "/api/v1/user/avatar",
method: "post",
data: data,
});
}
// 查询用户个人信息
export function getUserProfile() {
return request({
url: '/api/v1/user/profile',
method: 'get'
})
url: "/api/v1/user/profile",
method: "get",
});
}
// 查询用户列表
export function getUserOptions() {
return request({
url: '/api/v1/sys-user/options',
method: 'get'
})
url: "/api/v1/sys-user/options",
method: "get",
});
}
// 检查是否需要谷歌验证
export function needCheckGoogleAuth(headers) {
return request({
url: "/api/v1/need-check-google-auth",
method: "get",
headers: headers,
});
}
//验证google验证码
export function checkGoogleCode(params,headers) {
return request({
url: "/api/v1/google-code",
method: "get",
params: params,
headers: headers,
});
}
//判断是否需要谷歌验证
export function needGoolAuth() {
return request({
url: "/api/v1/need-auth",
method: "get",
});
}
//绑定谷歌认证器
export function setGoogleAuth(data) {
return request({
url: "/api/v1/set-google-auth",
method: "put",
data: data,
});
}

View File

@ -0,0 +1,149 @@
<template>
<el-dialog :visible.sync="dialogVisible" title="绑定谷歌验证器" width="680px" :show-close="false" :close-on-press-escape="false"
:close-on-click-modal="false" class="google-bind-dialog">
<div class="dialog-body">
<p class="tip-text">1. 使用 <strong>Google Authenticator</strong> 扫描二维码或手动输入密钥添加账号</p>
<div class="qr-section">
<canvas ref="qrCanvas" class="qr-canvas"></canvas>
<div class="manual-section">
<!-- <p><strong>账户</strong> {{ account }}</p> -->
<p>
<strong>密钥</strong>
<el-tag size="small" type="success">{{ secret }}</el-tag>
<el-button type="text" size="mini" @click="copySecret">复制</el-button>
</p>
</div>
</div>
<el-input v-model="code" placeholder="请输入 6 位验证码" maxlength="6" class="code-input"
@keyup.enter.native="submit" ref="codeInput" />
<div class="error-text" v-if="errorMsg">{{ errorMsg }}</div>
</div>
<template #footer>
<el-button type="primary" :loading="loading" :disabled="code.length !== 6" @click="submit">
确认绑定
</el-button>
</template>
</el-dialog>
</template>
<script>
import QRCode from 'qrcode'
import { setGoogleAuth } from '@/api/admin/sys-user'
export default {
name: 'BindGoogleDialog',
props: {
visible: { type: Boolean, default: true },
otpAuthUrl: { type: String, default: '' },
secret: { type: String, default: '' },
account: { type: String, default: '' },
},
data() {
return {
code: '',
loading: false,
errorMsg: ''
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
watch: {
visible(val) {
if (val) {
this.code = ''
this.errorMsg = ''
this.$nextTick(() => {
QRCode.toCanvas(this.$refs.qrCanvas, this.otpAuthUrl, { width: 180 })
this.$refs.codeInput.focus()
})
}
}
},
methods: {
submit() {
this.loading = true
setGoogleAuth({
secret: this.secret,
code: this.code
})
.then(res => {
if (res && res.code === 200) {
this.dialogVisible = false
this.$message.success('绑定成功')
} else {
this.errorMsg = res.message || '绑定失败'
}
})
.finally(() => {
this.loading = false
})
},
copySecret() {
navigator.clipboard.writeText(this.secret).then(() => {
this.$message.success('密钥已复制')
}).catch(() => {
this.$message.error('复制失败,请手动复制')
})
}
}
}
</script>
<style scoped>
.google-bind-dialog ::v-deep .el-dialog {
border-radius: 10px;
}
.dialog-body {
padding: 10px 20px;
}
.tip-text {
font-size: 14px;
color: #666;
margin-bottom: 15px;
}
.qr-section {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 15px;
}
.qr-canvas {
width: 180px;
height: 180px;
border: 1px solid #eee;
}
.manual-section {
flex: 1;
font-size: 13px;
color: #333;
}
.code-input {
width: 240px;
display: block;
margin: 0 auto;
}
.error-text {
text-align: center;
margin-top: 8px;
color: red;
}
</style>

View File

@ -13,16 +13,22 @@
<settings />
</right-panel>
</div>
<!-- 绑定谷歌验证码弹窗 -->
<set-google-secret :visible.sync="showSetGooleSecret" :otpAuthUrl="needGoolAuthData.otpAuthUrl"
:secret="needGoolAuthData.secret" @success="handleBindSuccess"></set-google-secret>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { needGoolAuth, setGoogleAuth } from '@/api/admin/sys-user'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import SetGoogleSecret from './SetGoogleSecret'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import variables from '@/styles/variables.scss'
import dingSound from '@/assets/tiktok/sisfus.mp3'
// import dingSound from '@/assets/tiktok/sisfus.mp3'
import checkPermisAction from '@/utils/permisaction'
export default {
@ -33,11 +39,14 @@ export default {
RightPanel,
Settings,
Sidebar,
TagsView
TagsView,
SetGoogleSecret
},
data() {
return {
voice: null,
showSetGooleSecret: false,
needGoolAuthData: {}
}
},
mixins: [ResizeMixin],
@ -63,35 +72,52 @@ export default {
},
created() {
// if (!this.roles.includes('admin')) {
// this.currentRole = 'editorDashboard'
// }
this.getNeedGoolAuth()
if (checkPermisAction(['admin:mmAlarmLog:notice'])) {
this.$confirm('是否接收警告?', '提示', {
distinguishCancelAndClose: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.voice = new Audio(dingSound)
// this.$confirm('是否接收警告?', '提示', {
// distinguishCancelAndClose: true,
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
// this.voice = new Audio(dingSound)
this.initWebSocket()
}).catch(() => {
console.log('取消')
});
this.initWebSocket()
// }).catch(() => {
// console.log('取消')
// });
}
},
destroyed() {
console.log('断开websocket连接')
this.websock.close() // 离开路由之后断开websocket连接
// unWsLogout(this.id, this.group).then(response => {
// console.log(response.data)
// }
// )
},
methods: {
checkPermisAction,
getNeedGoolAuth() {
const loading = this.$loading({
lock: true,
text: 'Loading',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
needGoolAuth()
.then(response => {
console.log("needGoogleAuth", response)
if (response.code === 200) {
if (response.data.needGooglAuth) {
this.needGoolAuthData.otpAuthUrl = response.data.otpAuthUrl
this.needGoolAuthData.secret = response.data.secret
this.showSetGooleSecret = true
}
}
})
.finally(() => {
loading.close()
})
},
initWebSocket() { // 初始化weosocket
const wsuri = `ws://${process.env.VUE_APP_WEBSOCKET_URL}/ws?token=${this.$store.state.user.token}`
@ -110,7 +136,6 @@ export default {
this.initWebSocket()
},
websocketonmessage(e) { // 数据接收
console.log("ws", e.data)
try {
let data = JSON.parse(e.data)
@ -122,7 +147,7 @@ export default {
duration: 0
});
this.playVoice("钱包告警")
// this.playVoice("钱包告警")
} catch (err) {
console.log("接收websocket数据失败:", err)
}
@ -146,6 +171,9 @@ export default {
},
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
},
handleBindSuccess() {
this.showSetGooleSecret = false
}
}
}

View File

@ -1,153 +1,174 @@
import { login, logout, getInfo, refreshtoken } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import router, { resetRouter } from '@/router'
import storage from '@/utils/storage'
import { login, logout, getInfo, refreshtoken } from "@/api/user";
import { getToken, setToken, removeToken } from "@/utils/auth";
import router, { resetRouter } from "@/router";
import storage from "@/utils/storage";
const state = {
token: getToken(),
name: '',
avatar: '',
introduction: '',
name: "",
avatar: "",
introduction: "",
roles: [],
permissions: [],
permisaction: []
}
permisaction: [],
};
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
state.token = token;
},
SET_INTRODUCTION: (state, introduction) => {
state.introduction = introduction
state.introduction = introduction;
},
SET_NAME: (state, name) => {
state.name = name
state.name = name;
},
SET_AVATAR: (state, avatar) => {
if (avatar.indexOf('http') !== -1) {
state.avatar = avatar
if (avatar.indexOf("http") !== -1) {
state.avatar = avatar;
} else {
state.avatar = process.env.VUE_APP_BASE_API + avatar
state.avatar = process.env.VUE_APP_BASE_API + avatar;
}
},
SET_ROLES: (state, roles) => {
state.roles = roles
state.roles = roles;
},
SET_PERMISSIONS: (state, permisaction) => {
state.permisaction = permisaction
}
}
state.permisaction = permisaction;
},
};
const actions = {
// user login
login({ commit }, userInfo) {
login({ commit }, { userInfo, handleResponse }) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
const { token } = response
commit('SET_TOKEN', token)
setToken(token)
resolve()
}).catch(error => {
reject(error)
})
})
login(userInfo)
.then(async(res) => {
// 自定义回调处理(判断是否需要谷歌验证)
if (handleResponse) {
const shouldSetToken =await handleResponse(res);
console.log("shouldSetToken", shouldSetToken);
if (!shouldSetToken) return resolve(); // 不设置 token
}
const { token } = res;
if (token) {
commit("SET_TOKEN", token);
setToken(token);
resolve(res);
} else {
reject(new Error("登录失败:无 token"));
}
})
.catch(reject);
});
},
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
if (!response || !response.data) {
commit('SET_TOKEN', '')
removeToken()
resolve()
}
getInfo()
.then((response) => {
if (!response || !response.data) {
commit("SET_TOKEN", "");
removeToken();
resolve();
}
const { roles, name, avatar, introduction, permissions } = response.data
const { roles, name, avatar, introduction, permissions } =
response.data;
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
commit('SET_PERMISSIONS', permissions)
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(response)
}).catch(error => {
reject(error)
})
})
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject("getInfo: roles must be a non-null array!");
}
commit("SET_PERMISSIONS", permissions);
commit("SET_ROLES", roles);
commit("SET_NAME", name);
commit("SET_AVATAR", avatar);
commit("SET_INTRODUCTION", introduction);
resolve(response);
})
.catch((error) => {
reject(error);
});
});
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
storage.clear()
resolve()
}).catch(error => {
reject(error)
})
})
logout(state.token)
.then(() => {
commit("SET_TOKEN", "");
commit("SET_ROLES", []);
commit("SET_PERMISSIONS", []);
removeToken();
storage.clear();
resolve();
})
.catch((error) => {
reject(error);
});
});
},
// 刷新token
refreshToken({ commit, state }) {
return new Promise((resolve, reject) => {
refreshtoken({ token: state.token }).then(response => {
const { token } = response
commit('SET_TOKEN', token)
setToken(token)
resolve()
}).catch(error => {
reject(error)
})
})
refreshtoken({ token: state.token })
.then((response) => {
const { token } = response;
commit("SET_TOKEN", token);
setToken(token);
resolve();
})
.catch((error) => {
reject(error);
});
});
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
return new Promise((resolve) => {
commit("SET_TOKEN", "");
removeToken();
resolve();
});
},
// dynamically modify permissions
changeRoles({ commit, dispatch }, role) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const token = role + '-token'
return new Promise(async (resolve) => {
const token = role + "-token";
commit('SET_TOKEN', token)
setToken(token)
commit("SET_TOKEN", token);
setToken(token);
const { roles } = await dispatch('getInfo')
const { roles } = await dispatch("getInfo");
resetRouter()
resetRouter();
// generate accessible routes map based on roles
const accessRoutes = await dispatch('permission/generateRoutes', roles, { root: true })
const accessRoutes = await dispatch("permission/generateRoutes", roles, {
root: true,
});
// dynamically add accessible routes
router.addRoutes(accessRoutes)
router.addRoutes(accessRoutes);
// reset visited views and cached views
dispatch('tagsView/delAllViews', null, { root: true })
dispatch("tagsView/delAllViews", null, { root: true });
resolve()
})
}
}
resolve();
});
},
};
export default {
namespaced: true,
state,
mutations,
actions
}
actions,
};

View File

@ -48,6 +48,8 @@ service.interceptors.response.use(
return response.data
}
const code = response.data.code
console.log("xx",code)
if (code === 401) {
store.dispatch('user/resetToken')
if (location.href.indexOf('login') !== -1) {

View File

@ -15,6 +15,11 @@
clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item> -->
<el-form-item label="时间范围">
<el-date-picker v-model="times" type="daterange" align="right" unlink-panels range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期" :picker-options="pickerOptions">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@ -37,6 +42,14 @@
size="mini" :disabled="multiple" @click="handleDelete">删除
</el-button>
</el-col>
<el-col :span="1.5">
<el-popconfirm class="delete-popconfirm" title="确定要导出记录吗?" confirm-button-text="确定"
@confirm="handleExport">
<el-button slot="reference" v-permisaction="['admin:mmAlarmLog:export']"
icon="el-icon-files" size="mini">导出
</el-button>
</el-popconfirm>
</el-col>
<el-col :span="1.5">
<el-popconfirm class="delete-popconfirm" title="确认要清除所有吗?" confirm-button-text="清除"
@confirm="handleClearAll()">
@ -107,8 +120,9 @@
</template>
<script>
import { addMmAlarmLog, delMmAlarmLog, getMmAlarmLog, listMmAlarmLog, updateMmAlarmLog, clearAllAlarmLog } from '@/api/admin/mm-alarm-log'
import { addMmAlarmLog, delMmAlarmLog, getMmAlarmLog, listMmAlarmLog, updateMmAlarmLog, clearAllAlarmLog, exportMmAlarmLog } from '@/api/admin/mm-alarm-log'
import { getMmMachineList } from '@/api/admin/mm-machine'
import { resolveBlob } from '@/utils/zipdownload'
export default {
name: 'MmAlarmLog',
@ -137,11 +151,13 @@ export default {
mmAlarmLogList: [],
// 关系表类型
times: [],
// 查询参数
queryParams: {
pageIndex: 1,
pageSize: 10,
startTime: undefined,
endTime: undefined,
machineId: undefined,
biosId: undefined,
idOrder: 'desc',
@ -153,7 +169,34 @@ export default {
rules: {
machineId: [{ required: true, message: '设备id不能为空', trigger: 'blur' }],
biosId: [{ required: true, message: '设备码不能为空', trigger: 'blur' }],
}
},
pickerOptions: {
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
},
}
},
created() {
@ -164,7 +207,20 @@ export default {
/** 查询参数列表 */
getList() {
this.loading = true
listMmAlarmLog(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
this.queryParams.startTime = undefined
this.queryParams.endTime = undefined
if (this.times) {
console.log(this.times.length)
if (this.times.length >= 0) {
this.queryParams.startTime = this.times[0]
}
if (this.times.length >= 1) {
this.queryParams.endTime = this.times[1]
}
}
listMmAlarmLog(this.queryParams).then(response => {
this.mmAlarmLogList = response.data.list
this.total = response.data.count
this.loading = false
@ -300,6 +356,36 @@ export default {
}).finally(() => {
this.loading = false
})
},
// 导出按钮
handleExport() {
this.loading = true
this.queryParams.startTime = undefined
this.queryParams.endTime = undefined
if (this.times) {
console.log(this.times.length)
if (this.times.length >= 0) {
this.queryParams.startTime = this.times[0]
}
if (this.times.length >= 1) {
this.queryParams.endTime = this.times[1]
}
}
exportMmAlarmLog(this.queryParams)
.then(response => {
console.log(response)
resolveBlob(response, '钱包告警记录.xlsx')
this.msgSuccess('导出成功')
})
.catch(err => {
console.log(err)
})
.finally(() => {
this.loading = false
})
}
}
}

View File

@ -0,0 +1,90 @@
<template>
<el-dialog
title="谷歌验证码验证"
:visible.sync="visible"
width="400px"
:close-on-click-modal="false"
:before-close="handleCancel"
:show-close="false"
:close-on-press-escape="false"
center
class="google-dialog"
>
<div class="dialog-body">
<p class="tip-text">为了您的账户安全请输入谷歌验证码</p>
<el-input
v-model="code"
placeholder="请输入6位谷歌验证码"
maxlength="6"
class="code-input"
ref="codeInput"
@keyup.enter.native="handleConfirm"
></el-input>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button
type="primary"
:loading="loading"
:disabled="code.length !== 6"
@click="handleConfirm"
>验证</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "GoogleAuth",
props: {
visible: {
type: Boolean,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data() {
return {
code: ""
};
},
watch: {
visible(val) {
if (val) {
this.code = "";
this.$nextTick(() => this.$refs.codeInput.focus());
}
}
},
methods: {
handleConfirm() {
this.$emit("confirm", this.code);
},
handleCancel() {
this.$emit("cancel");
}
}
};
</script>
<style scoped>
.google-dialog ::v-deep .el-dialog {
border-radius: 10px;
}
.dialog-body {
text-align: center;
}
.tip-text {
font-size: 14px;
color: #666;
margin-bottom: 15px;
}
.code-input {
width: 80%;
text-align: center;
}
</style>

View File

@ -30,58 +30,27 @@
<div class="login-border">
<div class="login-main">
<div class="login-title">用户登录</div>
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
autocomplete="on"
label-position="left"
>
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on"
label-position="left">
<el-form-item prop="username">
<span class="svg-container">
<i class="el-icon-user" />
</span>
<el-input
ref="username"
v-model="loginForm.username"
placeholder="用户名"
name="username"
type="text"
tabindex="1"
autocomplete="on"
/>
<el-input ref="username" v-model="loginForm.username" placeholder="用户名" name="username" type="text"
tabindex="1" autocomplete="on" />
</el-form-item>
<el-tooltip
v-model="capsTooltip"
content="Caps lock is On"
placement="right"
manual
>
<el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="密码"
name="password"
tabindex="2"
autocomplete="on"
@keyup.native="checkCapslock"
@blur="capsTooltip = false"
@keyup.enter.native="handleLogin"
/>
<el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType"
placeholder="密码" name="password" tabindex="2" autocomplete="on" @keyup.native="checkCapslock"
@blur="capsTooltip = false" @keyup.enter.native="handleLogin" />
<span class="show-pwd" @click="showPwd">
<svg-icon
:icon-class="
passwordType === 'password' ? 'eye' : 'eye-open'
"
/>
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'
" />
</span>
</el-form-item>
</el-tooltip>
@ -89,47 +58,26 @@
<span class="svg-container">
<svg-icon icon-class="validCode" />
</span>
<el-input
ref="username"
v-model="loginForm.code"
placeholder="验证码"
name="username"
type="text"
tabindex="3"
maxlength="5"
autocomplete="off"
style="width: 75%"
@keyup.enter.native="handleLogin"
/>
<el-input ref="username" v-model="loginForm.code" placeholder="验证码" name="username" type="text"
tabindex="3" maxlength="5" autocomplete="off" style="width: 75%" @keyup.enter.native="handleLogin" />
</el-form-item>
<div
class="login-code"
style="
<div class="login-code" style="
cursor: pointer;
width: 30%;
height: 48px;
float: right;
background-color: #f0f1f5;
"
>
<img
style="
">
<img style="
height: 48px;
width: 100%;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 5px;
"
:src="codeUrl"
@click="getCode"
>
" :src="codeUrl" @click="getCode">
</div>
<el-button
:loading="loading"
type="primary"
style="width: 100%; padding: 12px 20px; margin-bottom: 30px"
@click.native.prevent="handleLogin"
>
<el-button :loading="loading" type="primary" style="width: 100%; padding: 12px 20px; margin-bottom: 30px"
@click.native.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
@ -146,11 +94,7 @@
<br>
<social-sign />
</el-dialog>
<div
id="bottom_layer"
class="s-bottom-layer s-isindex-wrap"
style="visibility: visible; width: 100%"
>
<div id="bottom_layer" class="s-bottom-layer s-isindex-wrap" style="visibility: visible; width: 100%">
<div class="s-bottom-layer-content">
<div class="lh">
@ -163,11 +107,7 @@
<div class="rest_info_tip">
<div class="tip-wrapper">
<div class="lh tip-item" style="display: none">
<a
class="text-color"
href="https://beian.miit.gov.cn"
target="_blank"
>
<a class="text-color" href="https://beian.miit.gov.cn" target="_blank">
沪ICP备XXXXXXXXX号-1
</a>
</div>
@ -177,17 +117,26 @@
</div>
</div>
</div>
<GoogleAuth :visible.sync="showGoogleVerifyDialog" :loading="googleVerifying" @confirm="submitGoogleCode"
@cancel="showGoogleVerifyDialog = false" />
</div>
</template>
<script>
import { getCodeImg } from '@/api/login'
import { needCheckGoogleAuth, checkGoogleCode } from '@/api/admin/sys-user'
import moment from 'moment'
import SocialSign from './components/SocialSignin'
import GoogleAuth from './google_auth'
import { setToken } from "@/utils/auth";
export default {
name: 'Login',
components: { SocialSign },
components: {
SocialSign,
GoogleAuth
},
data() {
return {
codeUrl: '',
@ -218,12 +167,15 @@ export default {
redirect: undefined,
otherQuery: {},
currentTime: null,
sysInfo: ''
sysInfo: '',
showGoogleVerifyDialog: false,
googleVerifying: false,
googleTokenCache: ''
}
},
watch: {
$route: {
handler: function(route) {
handler: function (route) {
const query = route.query
if (query) {
this.redirect = query.redirect
@ -252,10 +204,11 @@ export default {
},
destroyed() {
clearInterval(this.timer)
window.removeEventListener('resize', () => {})
window.removeEventListener('resize', () => { })
// window.removeEventListener('storage', this.afterQRScan)
},
methods: {
setToken,
getSystemSetting() {
this.$store.dispatch('system/settingDetail').then((ret) => {
this.sysInfo = ret
@ -300,26 +253,55 @@ export default {
this.$refs.password.focus()
})
},
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true
this.$store
.dispatch('user/login', this.loginForm)
.then(() => {
this.$router
.push({ path: this.redirect || '/', query: this.otherQuery })
.catch(() => {})
})
.catch(() => {
this.loading = false
this.getCode()
})
} else {
console.log('error submit!!')
return false
async validateForm() {
return new Promise((resolve) => {
this.$refs.loginForm.validate((valid) => {
resolve(valid);
});
});
},
async handleLogin() {
const valid = await this.validateForm();
if (!valid) return;
this.loading = true;
try {
const res = await this.$store.dispatch('user/login', {
userInfo: this.loginForm,
handleResponse: async (res) => {
if (res.code === 200) {
let headers = {
"Authorization": "Bearer " + res.token
}
const res2 = await needCheckGoogleAuth(headers);
let cacheToken = false;
if (res2 && res2.code === 200 && res2.data) {
this.googleTokenCache = res.token;
this.showGoogleVerifyDialog = true;
cacheToken = false;
} else {
cacheToken = true;
}
return cacheToken;
} else {
this.$message.error(res.message || '登录失败');
return false;
}
}
});
if (res) {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery }).catch(() => { });
}
})
} catch (error) {
this.getCode();
} finally {
this.loading = false;
}
},
getOtherQuery(query) {
return Object.keys(query).reduce((acc, cur) => {
@ -328,6 +310,30 @@ export default {
}
return acc
}, {})
},
submitGoogleCode(code) {
this.googleVerifying = true
let headers = {
"Authorization": "Bearer " + this.googleTokenCache
}
checkGoogleCode({ code }, headers)
.then((response) => {
if (response && response.code === 200) {
const token = this.googleTokenCache
this.$store.commit("user/SET_TOKEN", token)
setToken(token)
this.$message.success("登录成功")
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
}
})
.catch((err) => {
console.log("err", err)
})
.finally(() => {
this.googleVerifying = false
})
}
}
}
@ -355,76 +361,95 @@ $cursor: #fff;
line-height: 39px;
// background: #0e6cff;
}
#bottom_layer .lh {
display: inline-block;
margin-right: 14px;
}
#bottom_layer .lh .emphasize {
text-decoration: underline;
font-weight: 700;
}
#bottom_layer .lh:last-child {
margin-left: -2px;
margin-right: 0;
}
#bottom_layer .lh.activity {
font-weight: 700;
text-decoration: underline;
}
#bottom_layer a {
font-size: 12px;
text-decoration: none;
}
#bottom_layer .text-color {
color: #bbb;
}
#bottom_layer .aria-img {
width: 49px;
height: 20px;
margin-bottom: -5px;
}
#bottom_layer a:hover {
color: #fff;
}
#bottom_layer .s-bottom-layer-content {
margin: 0 17px;
text-align: center;
}
#bottom_layer .s-bottom-layer-content .auto-transform-line {
display: inline;
}
#bottom_layer .s-bottom-layer-content .auto-transform-line:first-child {
margin-right: 14px;
}
.s-bottom-space {
position: static;
width: 100%;
height: 40px;
margin: 23px auto 12px;
}
#bottom_layer .open-content-info a:hover {
color: #fff;
}
#bottom_layer .open-content-info .text-color {
color: #626675;
}
.open-content-info {
position: relative;
display: inline-block;
width: 20px;
}
.open-content-info > span {
.open-content-info>span {
cursor: pointer;
font-size: 14px;
}
.open-content-info > span:hover {
.open-content-info>span:hover {
color: #fff;
}
.open-content-info .tip-hover-panel {
position: absolute;
display: none;
padding-bottom: 18px;
}
.open-content-info .tip-hover-panel .rest_info_tip {
max-width: 560px;
padding: 8px 12px 8px 12px;
@ -434,29 +459,31 @@ $cursor: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
text-align: left;
}
.open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper {
white-space: nowrap;
line-height: 20px;
}
.open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper .tip-item {
height: 20px;
line-height: 20px;
}
.open-content-info
.tip-hover-panel
.rest_info_tip
.tip-wrapper
.tip-item:last-child {
.open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper .tip-item:last-child {
margin-right: 0;
}
@media screen and (max-width: 515px) {
.open-content-info {
width: 16px;
}
.open-content-info .tip-hover-panel {
right: -16px !important;
}
}
.footer {
background-color: #0e6cff;
margin-bottom: -20px;
@ -517,6 +544,7 @@ $cursor: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
.login-time {
position: absolute;
left: 25px;
@ -613,6 +641,7 @@ $cursor: #fff;
color: #454545;
}
}
$bg: #2d3a4b;
$dark_gray: #889aa4;
$light_gray: #eee;
@ -670,18 +699,22 @@ $light_gray: #eee;
.thirdparty-button {
display: none;
}
.login-weaper {
width: 100%;
padding: 0 30px;
box-sizing: border-box;
box-shadow: none;
}
.login-main {
width: 80%;
}
.login-left {
display: none !important;
}
.login-border {
width: 100%;
}