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", "normalize.css": "8.0.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"path-to-regexp": "6.1.0", "path-to-regexp": "6.1.0",
"qrcode": "^1.5.4",
"remixicon": "^2.5.0", "remixicon": "^2.5.0",
"sass-resources-loader": "^2.0.3", "sass-resources-loader": "^2.0.3",
"screenfull": "5.0.2", "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) { export function listUser(query) {
return request({ return request({
url: '/api/v1/sys-user', url: "/api/v1/sys-user",
method: 'get', method: "get",
params: query params: query,
}) });
} }
// 查询用户详细 // 查询用户详细
export function getUser(userId) { export function getUser(userId) {
return request({ return request({
url: '/api/v1/sys-user/' + userId, url: "/api/v1/sys-user/" + userId,
method: 'get' method: "get",
}) });
} }
export function getUserInit() { export function getUserInit() {
return request({ return request({
url: '/api/v1/sys-user/', url: "/api/v1/sys-user/",
method: 'get' method: "get",
}) });
} }
// 新增用户 // 新增用户
export function addUser(data) { export function addUser(data) {
return request({ return request({
url: '/api/v1/sys-user', url: "/api/v1/sys-user",
method: 'post', method: "post",
data: data data: data,
}) });
} }
// 修改用户 // 修改用户
export function updateUser(data) { export function updateUser(data) {
return request({ return request({
url: '/api/v1/sys-user', url: "/api/v1/sys-user",
method: 'put', method: "put",
data: data data: data,
}) });
} }
// 删除用户 // 删除用户
export function delUser(data) { export function delUser(data) {
return request({ return request({
url: '/api/v1/sys-user', url: "/api/v1/sys-user",
method: 'delete', method: "delete",
data: data data: data,
}) });
} }
// 导出用户 // 导出用户
export function exportUser(query) { export function exportUser(query) {
return request({ return request({
url: '/api/v1/sys-user/export', url: "/api/v1/sys-user/export",
method: 'get', method: "get",
params: query params: query,
}) });
} }
// 用户密码重置 // 用户密码重置
export function resetUserPwd(userId, password) { export function resetUserPwd(userId, password) {
const data = { const data = {
userId, userId,
password password,
} };
return request({ return request({
url: '/api/v1/user/pwd/reset', url: "/api/v1/user/pwd/reset",
method: 'put', method: "put",
data: data data: data,
}) });
} }
// 用户状态修改 // 用户状态修改
export function changeUserStatus(e) { export function changeUserStatus(e) {
const data = { const data = {
userId: e.userId, userId: e.userId,
status: e.status status: e.status,
} };
return request({ return request({
url: '/api/v1/user/status', url: "/api/v1/user/status",
method: 'put', method: "put",
data: data data: data,
}) });
} }
// 修改用户个人信息 // 修改用户个人信息
export function updateUserProfile(data) { export function updateUserProfile(data) {
return request({ return request({
url: '/api/v1/sys-user/profile', url: "/api/v1/sys-user/profile",
method: 'put', method: "put",
data: data data: data,
}) });
} }
// 下载用户导入模板 // 下载用户导入模板
export function importTemplate() { export function importTemplate() {
return request({ return request({
url: '/api/v1/sys-user/importTemplate', url: "/api/v1/sys-user/importTemplate",
method: 'get' method: "get",
}) });
} }
// 用户密码重置 // 用户密码重置
export function updateUserPwd(oldPassword, newPassword) { export function updateUserPwd(oldPassword, newPassword) {
const data = { const data = {
oldPassword, oldPassword,
newPassword newPassword,
} };
return request({ return request({
url: '/api/v1/user/pwd/set', url: "/api/v1/user/pwd/set",
method: 'put', method: "put",
data: data data: data,
}) });
} }
// 用户头像上传 // 用户头像上传
export function uploadAvatar(data) { export function uploadAvatar(data) {
return request({ return request({
url: '/api/v1/user/avatar', url: "/api/v1/user/avatar",
method: 'post', method: "post",
data: data data: data,
}) });
} }
// 查询用户个人信息 // 查询用户个人信息
export function getUserProfile() { export function getUserProfile() {
return request({ return request({
url: '/api/v1/user/profile', url: "/api/v1/user/profile",
method: 'get' method: "get",
}) });
} }
// 查询用户列表 // 查询用户列表
export function getUserOptions() { export function getUserOptions() {
return request({ return request({
url: '/api/v1/sys-user/options', url: "/api/v1/sys-user/options",
method: 'get' 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 /> <settings />
</right-panel> </right-panel>
</div> </div>
<!-- 绑定谷歌验证码弹窗 -->
<set-google-secret :visible.sync="showSetGooleSecret" :otpAuthUrl="needGoolAuthData.otpAuthUrl"
:secret="needGoolAuthData.secret" @success="handleBindSuccess"></set-google-secret>
</div> </div>
</template> </template>
<script> <script>
import RightPanel from '@/components/RightPanel' import RightPanel from '@/components/RightPanel'
import { needGoolAuth, setGoogleAuth } from '@/api/admin/sys-user'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components' import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import SetGoogleSecret from './SetGoogleSecret'
import ResizeMixin from './mixin/ResizeHandler' import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import variables from '@/styles/variables.scss' 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' import checkPermisAction from '@/utils/permisaction'
export default { export default {
@ -33,11 +39,14 @@ export default {
RightPanel, RightPanel,
Settings, Settings,
Sidebar, Sidebar,
TagsView TagsView,
SetGoogleSecret
}, },
data() { data() {
return { return {
voice: null, voice: null,
showSetGooleSecret: false,
needGoolAuthData: {}
} }
}, },
mixins: [ResizeMixin], mixins: [ResizeMixin],
@ -63,35 +72,52 @@ export default {
}, },
created() { created() {
// if (!this.roles.includes('admin')) { this.getNeedGoolAuth()
// this.currentRole = 'editorDashboard'
// }
if (checkPermisAction(['admin:mmAlarmLog:notice'])) { if (checkPermisAction(['admin:mmAlarmLog:notice'])) {
this.$confirm('是否接收警告?', '提示', { // this.$confirm('是否接收警告?', '提示', {
distinguishCancelAndClose: true, // distinguishCancelAndClose: true,
confirmButtonText: '确定', // confirmButtonText: '确定',
cancelButtonText: '取消', // cancelButtonText: '取消',
type: 'warning' // type: 'warning'
}).then(() => { // }).then(() => {
this.voice = new Audio(dingSound) // this.voice = new Audio(dingSound)
this.initWebSocket() this.initWebSocket()
}).catch(() => { // }).catch(() => {
console.log('取消') // console.log('取消')
}); // });
} }
}, },
destroyed() { destroyed() {
console.log('断开websocket连接') console.log('断开websocket连接')
this.websock.close() // 离开路由之后断开websocket连接 this.websock.close() // 离开路由之后断开websocket连接
// unWsLogout(this.id, this.group).then(response => {
// console.log(response.data)
// }
// )
}, },
methods: { methods: {
checkPermisAction, 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 initWebSocket() { // 初始化weosocket
const wsuri = `ws://${process.env.VUE_APP_WEBSOCKET_URL}/ws?token=${this.$store.state.user.token}` const wsuri = `ws://${process.env.VUE_APP_WEBSOCKET_URL}/ws?token=${this.$store.state.user.token}`
@ -110,7 +136,6 @@ export default {
this.initWebSocket() this.initWebSocket()
}, },
websocketonmessage(e) { // 数据接收 websocketonmessage(e) { // 数据接收
console.log("ws", e.data)
try { try {
let data = JSON.parse(e.data) let data = JSON.parse(e.data)
@ -122,7 +147,7 @@ export default {
duration: 0 duration: 0
}); });
this.playVoice("钱包告警") // this.playVoice("钱包告警")
} catch (err) { } catch (err) {
console.log("接收websocket数据失败:", err) console.log("接收websocket数据失败:", err)
} }
@ -146,6 +171,9 @@ export default {
}, },
handleClickOutside() { handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) 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 { login, logout, getInfo, refreshtoken } from "@/api/user";
import { getToken, setToken, removeToken } from '@/utils/auth' import { getToken, setToken, removeToken } from "@/utils/auth";
import router, { resetRouter } from '@/router' import router, { resetRouter } from "@/router";
import storage from '@/utils/storage' import storage from "@/utils/storage";
const state = { const state = {
token: getToken(), token: getToken(),
name: '', name: "",
avatar: '', avatar: "",
introduction: '', introduction: "",
roles: [], roles: [],
permissions: [], permissions: [],
permisaction: [] permisaction: [],
} };
const mutations = { const mutations = {
SET_TOKEN: (state, token) => { SET_TOKEN: (state, token) => {
state.token = token state.token = token;
}, },
SET_INTRODUCTION: (state, introduction) => { SET_INTRODUCTION: (state, introduction) => {
state.introduction = introduction state.introduction = introduction;
}, },
SET_NAME: (state, name) => { SET_NAME: (state, name) => {
state.name = name state.name = name;
}, },
SET_AVATAR: (state, avatar) => { SET_AVATAR: (state, avatar) => {
if (avatar.indexOf('http') !== -1) { if (avatar.indexOf("http") !== -1) {
state.avatar = avatar state.avatar = avatar;
} else { } else {
state.avatar = process.env.VUE_APP_BASE_API + avatar state.avatar = process.env.VUE_APP_BASE_API + avatar;
} }
}, },
SET_ROLES: (state, roles) => { SET_ROLES: (state, roles) => {
state.roles = roles state.roles = roles;
}, },
SET_PERMISSIONS: (state, permisaction) => { SET_PERMISSIONS: (state, permisaction) => {
state.permisaction = permisaction state.permisaction = permisaction;
} },
} };
const actions = { const actions = {
// user login // user login
login({ commit }, userInfo) { login({ commit }, { userInfo, handleResponse }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
login(userInfo).then(response => { login(userInfo)
const { token } = response .then(async(res) => {
commit('SET_TOKEN', token) // 自定义回调处理(判断是否需要谷歌验证)
setToken(token) if (handleResponse) {
resolve() const shouldSetToken =await handleResponse(res);
}).catch(error => {
reject(error) 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 // get user info
getInfo({ commit, state }) { getInfo({ commit, state }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getInfo().then(response => { getInfo()
if (!response || !response.data) { .then((response) => {
commit('SET_TOKEN', '') if (!response || !response.data) {
removeToken() commit("SET_TOKEN", "");
resolve() 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 // roles must be a non-empty array
if (!roles || roles.length <= 0) { if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!') reject("getInfo: roles must be a non-null array!");
} }
commit('SET_PERMISSIONS', permissions) commit("SET_PERMISSIONS", permissions);
commit('SET_ROLES', roles) commit("SET_ROLES", roles);
commit('SET_NAME', name) commit("SET_NAME", name);
commit('SET_AVATAR', avatar) commit("SET_AVATAR", avatar);
commit('SET_INTRODUCTION', introduction) commit("SET_INTRODUCTION", introduction);
resolve(response) resolve(response);
}).catch(error => { })
reject(error) .catch((error) => {
}) reject(error);
}) });
});
}, },
// 退出系统 // 退出系统
LogOut({ commit, state }) { LogOut({ commit, state }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logout(state.token).then(() => { logout(state.token)
commit('SET_TOKEN', '') .then(() => {
commit('SET_ROLES', []) commit("SET_TOKEN", "");
commit('SET_PERMISSIONS', []) commit("SET_ROLES", []);
removeToken() commit("SET_PERMISSIONS", []);
storage.clear() removeToken();
resolve() storage.clear();
}).catch(error => { resolve();
reject(error) })
}) .catch((error) => {
}) reject(error);
});
});
}, },
// 刷新token // 刷新token
refreshToken({ commit, state }) { refreshToken({ commit, state }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
refreshtoken({ token: state.token }).then(response => { refreshtoken({ token: state.token })
const { token } = response .then((response) => {
commit('SET_TOKEN', token) const { token } = response;
setToken(token) commit("SET_TOKEN", token);
resolve() setToken(token);
}).catch(error => { resolve();
reject(error) })
}) .catch((error) => {
}) reject(error);
});
});
}, },
// remove token // remove token
resetToken({ commit }) { resetToken({ commit }) {
return new Promise(resolve => { return new Promise((resolve) => {
commit('SET_TOKEN', '') commit("SET_TOKEN", "");
removeToken() removeToken();
resolve() resolve();
}) });
}, },
// dynamically modify permissions // dynamically modify permissions
changeRoles({ commit, dispatch }, role) { changeRoles({ commit, dispatch }, role) {
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => { return new Promise(async (resolve) => {
const token = role + '-token' const token = role + "-token";
commit('SET_TOKEN', token) commit("SET_TOKEN", token);
setToken(token) setToken(token);
const { roles } = await dispatch('getInfo') const { roles } = await dispatch("getInfo");
resetRouter() resetRouter();
// generate accessible routes map based on roles // 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 // dynamically add accessible routes
router.addRoutes(accessRoutes) router.addRoutes(accessRoutes);
// reset visited views and cached views // reset visited views and cached views
dispatch('tagsView/delAllViews', null, { root: true }) dispatch("tagsView/delAllViews", null, { root: true });
resolve() resolve();
}) });
} },
} };
export default { export default {
namespaced: true, namespaced: true,
state, state,
mutations, mutations,
actions actions,
} };

View File

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

View File

@ -15,6 +15,11 @@
clearable size="small" @keyup.enter.native="handleQuery" /> clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item> --> </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-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <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> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@ -37,6 +42,14 @@
size="mini" :disabled="multiple" @click="handleDelete">删除 size="mini" :disabled="multiple" @click="handleDelete">删除
</el-button> </el-button>
</el-col> </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-col :span="1.5">
<el-popconfirm class="delete-popconfirm" title="确认要清除所有吗?" confirm-button-text="清除" <el-popconfirm class="delete-popconfirm" title="确认要清除所有吗?" confirm-button-text="清除"
@confirm="handleClearAll()"> @confirm="handleClearAll()">
@ -107,8 +120,9 @@
</template> </template>
<script> <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 { getMmMachineList } from '@/api/admin/mm-machine'
import { resolveBlob } from '@/utils/zipdownload'
export default { export default {
name: 'MmAlarmLog', name: 'MmAlarmLog',
@ -137,11 +151,13 @@ export default {
mmAlarmLogList: [], mmAlarmLogList: [],
// 关系表类型 // 关系表类型
times: [],
// 查询参数 // 查询参数
queryParams: { queryParams: {
pageIndex: 1, pageIndex: 1,
pageSize: 10, pageSize: 10,
startTime: undefined,
endTime: undefined,
machineId: undefined, machineId: undefined,
biosId: undefined, biosId: undefined,
idOrder: 'desc', idOrder: 'desc',
@ -153,7 +169,34 @@ export default {
rules: { rules: {
machineId: [{ required: true, message: '设备id不能为空', trigger: 'blur' }], machineId: [{ required: true, message: '设备id不能为空', trigger: 'blur' }],
biosId: [{ required: true, message: '设备码不能为空', 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() { created() {
@ -164,7 +207,20 @@ export default {
/** 查询参数列表 */ /** 查询参数列表 */
getList() { getList() {
this.loading = true 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.mmAlarmLogList = response.data.list
this.total = response.data.count this.total = response.data.count
this.loading = false this.loading = false
@ -300,6 +356,36 @@ export default {
}).finally(() => { }).finally(() => {
this.loading = false 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-border">
<div class="login-main"> <div class="login-main">
<div class="login-title">用户登录</div> <div class="login-title">用户登录</div>
<el-form <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on"
ref="loginForm" label-position="left">
:model="loginForm"
:rules="loginRules"
class="login-form"
autocomplete="on"
label-position="left"
>
<el-form-item prop="username"> <el-form-item prop="username">
<span class="svg-container"> <span class="svg-container">
<i class="el-icon-user" /> <i class="el-icon-user" />
</span> </span>
<el-input <el-input ref="username" v-model="loginForm.username" placeholder="用户名" name="username" type="text"
ref="username" tabindex="1" autocomplete="on" />
v-model="loginForm.username"
placeholder="用户名"
name="username"
type="text"
tabindex="1"
autocomplete="on"
/>
</el-form-item> </el-form-item>
<el-tooltip <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
v-model="capsTooltip"
content="Caps lock is On"
placement="right"
manual
>
<el-form-item prop="password"> <el-form-item prop="password">
<span class="svg-container"> <span class="svg-container">
<svg-icon icon-class="password" /> <svg-icon icon-class="password" />
</span> </span>
<el-input <el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType"
:key="passwordType" placeholder="密码" name="password" tabindex="2" autocomplete="on" @keyup.native="checkCapslock"
ref="password" @blur="capsTooltip = false" @keyup.enter.native="handleLogin" />
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"> <span class="show-pwd" @click="showPwd">
<svg-icon <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'
:icon-class=" " />
passwordType === 'password' ? 'eye' : 'eye-open'
"
/>
</span> </span>
</el-form-item> </el-form-item>
</el-tooltip> </el-tooltip>
@ -89,47 +58,26 @@
<span class="svg-container"> <span class="svg-container">
<svg-icon icon-class="validCode" /> <svg-icon icon-class="validCode" />
</span> </span>
<el-input <el-input ref="username" v-model="loginForm.code" placeholder="验证码" name="username" type="text"
ref="username" tabindex="3" maxlength="5" autocomplete="off" style="width: 75%" @keyup.enter.native="handleLogin" />
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> </el-form-item>
<div <div class="login-code" style="
class="login-code"
style="
cursor: pointer; cursor: pointer;
width: 30%; width: 30%;
height: 48px; height: 48px;
float: right; float: right;
background-color: #f0f1f5; background-color: #f0f1f5;
" ">
> <img style="
<img
style="
height: 48px; height: 48px;
width: 100%; width: 100%;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 5px; border-radius: 5px;
" " :src="codeUrl" @click="getCode">
:src="codeUrl"
@click="getCode"
>
</div> </div>
<el-button <el-button :loading="loading" type="primary" style="width: 100%; padding: 12px 20px; margin-bottom: 30px"
:loading="loading" @click.native.prevent="handleLogin">
type="primary"
style="width: 100%; padding: 12px 20px; margin-bottom: 30px"
@click.native.prevent="handleLogin"
>
<span v-if="!loading"> </span> <span v-if="!loading"> </span>
<span v-else> 中...</span> <span v-else> 中...</span>
</el-button> </el-button>
@ -146,11 +94,7 @@
<br> <br>
<social-sign /> <social-sign />
</el-dialog> </el-dialog>
<div <div id="bottom_layer" class="s-bottom-layer s-isindex-wrap" style="visibility: visible; width: 100%">
id="bottom_layer"
class="s-bottom-layer s-isindex-wrap"
style="visibility: visible; width: 100%"
>
<div class="s-bottom-layer-content"> <div class="s-bottom-layer-content">
<div class="lh"> <div class="lh">
@ -163,11 +107,7 @@
<div class="rest_info_tip"> <div class="rest_info_tip">
<div class="tip-wrapper"> <div class="tip-wrapper">
<div class="lh tip-item" style="display: none"> <div class="lh tip-item" style="display: none">
<a <a class="text-color" href="https://beian.miit.gov.cn" target="_blank">
class="text-color"
href="https://beian.miit.gov.cn"
target="_blank"
>
沪ICP备XXXXXXXXX号-1 沪ICP备XXXXXXXXX号-1
</a> </a>
</div> </div>
@ -177,17 +117,26 @@
</div> </div>
</div> </div>
</div> </div>
<GoogleAuth :visible.sync="showGoogleVerifyDialog" :loading="googleVerifying" @confirm="submitGoogleCode"
@cancel="showGoogleVerifyDialog = false" />
</div> </div>
</template> </template>
<script> <script>
import { getCodeImg } from '@/api/login' import { getCodeImg } from '@/api/login'
import { needCheckGoogleAuth, checkGoogleCode } from '@/api/admin/sys-user'
import moment from 'moment' import moment from 'moment'
import SocialSign from './components/SocialSignin' import SocialSign from './components/SocialSignin'
import GoogleAuth from './google_auth'
import { setToken } from "@/utils/auth";
export default { export default {
name: 'Login', name: 'Login',
components: { SocialSign }, components: {
SocialSign,
GoogleAuth
},
data() { data() {
return { return {
codeUrl: '', codeUrl: '',
@ -218,12 +167,15 @@ export default {
redirect: undefined, redirect: undefined,
otherQuery: {}, otherQuery: {},
currentTime: null, currentTime: null,
sysInfo: '' sysInfo: '',
showGoogleVerifyDialog: false,
googleVerifying: false,
googleTokenCache: ''
} }
}, },
watch: { watch: {
$route: { $route: {
handler: function(route) { handler: function (route) {
const query = route.query const query = route.query
if (query) { if (query) {
this.redirect = query.redirect this.redirect = query.redirect
@ -252,10 +204,11 @@ export default {
}, },
destroyed() { destroyed() {
clearInterval(this.timer) clearInterval(this.timer)
window.removeEventListener('resize', () => {}) window.removeEventListener('resize', () => { })
// window.removeEventListener('storage', this.afterQRScan) // window.removeEventListener('storage', this.afterQRScan)
}, },
methods: { methods: {
setToken,
getSystemSetting() { getSystemSetting() {
this.$store.dispatch('system/settingDetail').then((ret) => { this.$store.dispatch('system/settingDetail').then((ret) => {
this.sysInfo = ret this.sysInfo = ret
@ -300,26 +253,55 @@ export default {
this.$refs.password.focus() this.$refs.password.focus()
}) })
}, },
handleLogin() { async validateForm() {
this.$refs.loginForm.validate((valid) => { return new Promise((resolve) => {
if (valid) { this.$refs.loginForm.validate((valid) => {
this.loading = true resolve(valid);
this.$store });
.dispatch('user/login', this.loginForm) });
.then(() => { },
this.$router async handleLogin() {
.push({ path: this.redirect || '/', query: this.otherQuery }) const valid = await this.validateForm();
.catch(() => {}) if (!valid) return;
})
.catch(() => { this.loading = true;
this.loading = false
this.getCode() try {
}) const res = await this.$store.dispatch('user/login', {
} else { userInfo: this.loginForm,
console.log('error submit!!') handleResponse: async (res) => {
return false
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) { getOtherQuery(query) {
return Object.keys(query).reduce((acc, cur) => { return Object.keys(query).reduce((acc, cur) => {
@ -328,6 +310,30 @@ export default {
} }
return acc 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; line-height: 39px;
// background: #0e6cff; // background: #0e6cff;
} }
#bottom_layer .lh { #bottom_layer .lh {
display: inline-block; display: inline-block;
margin-right: 14px; margin-right: 14px;
} }
#bottom_layer .lh .emphasize { #bottom_layer .lh .emphasize {
text-decoration: underline; text-decoration: underline;
font-weight: 700; font-weight: 700;
} }
#bottom_layer .lh:last-child { #bottom_layer .lh:last-child {
margin-left: -2px; margin-left: -2px;
margin-right: 0; margin-right: 0;
} }
#bottom_layer .lh.activity { #bottom_layer .lh.activity {
font-weight: 700; font-weight: 700;
text-decoration: underline; text-decoration: underline;
} }
#bottom_layer a { #bottom_layer a {
font-size: 12px; font-size: 12px;
text-decoration: none; text-decoration: none;
} }
#bottom_layer .text-color { #bottom_layer .text-color {
color: #bbb; color: #bbb;
} }
#bottom_layer .aria-img { #bottom_layer .aria-img {
width: 49px; width: 49px;
height: 20px; height: 20px;
margin-bottom: -5px; margin-bottom: -5px;
} }
#bottom_layer a:hover { #bottom_layer a:hover {
color: #fff; color: #fff;
} }
#bottom_layer .s-bottom-layer-content { #bottom_layer .s-bottom-layer-content {
margin: 0 17px; margin: 0 17px;
text-align: center; text-align: center;
} }
#bottom_layer .s-bottom-layer-content .auto-transform-line { #bottom_layer .s-bottom-layer-content .auto-transform-line {
display: inline; display: inline;
} }
#bottom_layer .s-bottom-layer-content .auto-transform-line:first-child { #bottom_layer .s-bottom-layer-content .auto-transform-line:first-child {
margin-right: 14px; margin-right: 14px;
} }
.s-bottom-space { .s-bottom-space {
position: static; position: static;
width: 100%; width: 100%;
height: 40px; height: 40px;
margin: 23px auto 12px; margin: 23px auto 12px;
} }
#bottom_layer .open-content-info a:hover { #bottom_layer .open-content-info a:hover {
color: #fff; color: #fff;
} }
#bottom_layer .open-content-info .text-color { #bottom_layer .open-content-info .text-color {
color: #626675; color: #626675;
} }
.open-content-info { .open-content-info {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: 20px; width: 20px;
} }
.open-content-info > span {
.open-content-info>span {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
} }
.open-content-info > span:hover {
.open-content-info>span:hover {
color: #fff; color: #fff;
} }
.open-content-info .tip-hover-panel { .open-content-info .tip-hover-panel {
position: absolute; position: absolute;
display: none; display: none;
padding-bottom: 18px; padding-bottom: 18px;
} }
.open-content-info .tip-hover-panel .rest_info_tip { .open-content-info .tip-hover-panel .rest_info_tip {
max-width: 560px; max-width: 560px;
padding: 8px 12px 8px 12px; padding: 8px 12px 8px 12px;
@ -434,29 +459,31 @@ $cursor: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
text-align: left; text-align: left;
} }
.open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper { .open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper {
white-space: nowrap; white-space: nowrap;
line-height: 20px; line-height: 20px;
} }
.open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper .tip-item { .open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper .tip-item {
height: 20px; height: 20px;
line-height: 20px; line-height: 20px;
} }
.open-content-info
.tip-hover-panel .open-content-info .tip-hover-panel .rest_info_tip .tip-wrapper .tip-item:last-child {
.rest_info_tip
.tip-wrapper
.tip-item:last-child {
margin-right: 0; margin-right: 0;
} }
@media screen and (max-width: 515px) { @media screen and (max-width: 515px) {
.open-content-info { .open-content-info {
width: 16px; width: 16px;
} }
.open-content-info .tip-hover-panel { .open-content-info .tip-hover-panel {
right: -16px !important; right: -16px !important;
} }
} }
.footer { .footer {
background-color: #0e6cff; background-color: #0e6cff;
margin-bottom: -20px; margin-bottom: -20px;
@ -517,6 +544,7 @@ $cursor: #fff;
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
.login-time { .login-time {
position: absolute; position: absolute;
left: 25px; left: 25px;
@ -613,6 +641,7 @@ $cursor: #fff;
color: #454545; color: #454545;
} }
} }
$bg: #2d3a4b; $bg: #2d3a4b;
$dark_gray: #889aa4; $dark_gray: #889aa4;
$light_gray: #eee; $light_gray: #eee;
@ -670,18 +699,22 @@ $light_gray: #eee;
.thirdparty-button { .thirdparty-button {
display: none; display: none;
} }
.login-weaper { .login-weaper {
width: 100%; width: 100%;
padding: 0 30px; padding: 0 30px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: none; box-shadow: none;
} }
.login-main { .login-main {
width: 80%; width: 80%;
} }
.login-left { .login-left {
display: none !important; display: none !important;
} }
.login-border { .login-border {
width: 100%; width: 100%;
} }