新增在线时长、单点登录

This commit is contained in:
不做码农 2023-08-27 20:54:30 +08:00
parent 9b7a1e870c
commit 352161ac96
10 changed files with 281 additions and 43 deletions

View File

@ -28,7 +28,11 @@ watch(
token, token,
(val) => { (val) => {
if (val) { if (val) {
proxy.signalr.start() proxy.signalr.start().then(async (res) => {
if (res) {
await proxy.signalr.SR.invoke('logOut')
}
})
} }
}, },
{ {

View File

@ -109,11 +109,9 @@
} }
.login-scan-container { .login-scan-container {
padding: 0 20px 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
align-items: center; align-items: center;
height: 200px;
justify-content: space-around; justify-content: space-around;
} }

View File

@ -11,7 +11,7 @@ import directive from './directive' // directive
// 注册指令 // 注册指令
import plugins from './plugins' // plugins import plugins from './plugins' // plugins
import { downFile } from '@/utils/request' import { downFile } from '@/utils/request'
import signalR from '@/utils/signalR' import signalR from '@/signalr/signalr'
import vueI18n from './i18n/index' import vueI18n from './i18n/index'
import pinia from '@/store/index' import pinia from '@/store/index'

89
src/signalr/analysis.js Normal file
View File

@ -0,0 +1,89 @@
// import signalr from './signalr'
import { ElNotification, ElMessage, ElMessageBox } from 'element-plus'
import useSocketStore from '@/store/modules/socket'
import useUserStore from '@/store/modules/user'
import { webNotify } from '@/utils/index'
export default {
onMessage(connection) {
connection.on(MsgType.M001, (data) => {
useSocketStore().setOnlineUsers(data)
})
connection.on(MsgType.M002, (data) => {
// useUserStore().saveConnId(data)
})
// 接收后台手动推送消息
connection.on('receiveNotice', (title, data) => {
ElNotification({
type: 'info',
title: title,
message: data,
dangerouslyUseHTMLString: true,
duration: 0
})
webNotify({ title: title, body: data })
})
// 接收系统通知/公告
connection.on('moreNotice', (data) => {
if (data.code == 200) {
useSocketStore().setNoticeList(data.data)
}
})
// 接收在线用户
// connection.on('onlineUser', (data) => {
// useSocketStore().setOnlineUsers(data)
// })
// 接收封锁通知
connection.on('lockUser', (data) => {
ElMessageBox.alert(`你的账号已被锁定,剩余,${data.time}分,原因:${data.reason || '-'}`, '提示', {
confirmButtonText: '确定',
callback: () => {
useUserStore()
.logOut()
.then(() => {
location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + 'index'
})
}
})
})
// 接收聊天数据
connection.on('receiveChat', (data) => {
const title = `来自${data.userName}的消息通知`
useSocketStore().setChat(data)
if (data.userid != useUserStore().userId) {
ElNotification({
title: title,
message: data.message,
type: 'success',
duration: 3000
})
}
webNotify({ title: title, body: data.message })
})
connection.on('onlineInfo', (data) => {
useSocketStore().getOnlineInfo(data)
})
connection.on('logOut', () => {
useUserStore()
.logOut()
.then(() => {
ElMessageBox.alert(`你的账号已在其他设备登录,如果不是你的操作请尽快修改密码`, '提示', {
confirmButtonText: '确定',
callback: () => {
location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + 'index'
}
})
})
})
}
}
const MsgType = {
M001: 'onlineNum',
M002: 'connId'
}

76
src/signalr/signalr.js Normal file
View File

@ -0,0 +1,76 @@
// 官方文档https://docs.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view=aspnetcore-6.0&viewFallbackFrom=aspnetcore-2.2&tabs=visual-studio
import * as signalR from '@microsoft/signalr'
import { getToken } from '@/utils/auth'
import { ElMessage } from 'element-plus'
import analysis from '@/signalr/analysis'
export default {
// signalR对象
SR: {},
// 失败连接重试次数
failNum: 4,
init(url) {
var socketUrl = window.location.origin + url
const connection = new signalR.HubConnectionBuilder()
.withUrl(socketUrl, { accessTokenFactory: () => getToken() })
.withAutomaticReconnect() //自动重新连接
.configureLogging(signalR.LogLevel.Warning)
.build()
this.SR = connection
// 断线重连
connection.onclose(async (error) => {
console.error('断开连接了' + error)
console.assert(connection.state === signalR.HubConnectionState.Disconnected)
// 建议用户重新刷新浏览器
await this.start()
})
connection.onreconnected((connectionId) => {
ElMessage({
message: '与服务器通讯已连接成功',
type: 'success',
duration: 2000
})
console.log('断线重新连接成功' + connectionId)
})
connection.onreconnecting(async () => {
console.log('断线重新连接中... ')
await this.start()
})
analysis.onMessage(connection)
// this.receiveMsg(connection)
// 启动
// this.start();
},
/**
* 调用 this.signalR.start().then(async () => { await this.SR.invoke("method")})
* @returns
*/
async start() {
try {
console.log('signalR-1', this.SR.state)
//使用async和await 或 promise的then 和catch 处理来自服务端的异常
if (this.SR.state === signalR.HubConnectionState.Disconnected) {
await this.SR.start()
}
console.log('signalR-2', this.SR.state)
return true
} catch (error) {
console.error(error)
this.failNum--
// console.log(`失败重试剩余次数${that.failNum}`, error)
if (this.failNum > 0 && this.SR.state.Disconnected) {
setTimeout(async () => {
await this.start()
}, 5000)
}
return false
}
},
// 接收消息处理
receiveMsg(connection) {}
}

View File

@ -3,7 +3,12 @@ const useSocketStore = defineStore('socket', {
onlineNum: 0, onlineNum: 0,
onlineUsers: [], onlineUsers: [],
noticeList: [], noticeList: [],
noticeDot: false noticeDot: false,
//在线用户信息
onlineInfo: {},
// 聊天数据
chatList: [],
leaveUser: {}
}), }),
actions: { actions: {
//更新在线人数 //更新在线人数
@ -15,14 +20,19 @@ const useSocketStore = defineStore('socket', {
this.noticeList = data this.noticeList = data
this.noticeDot = data.length > 0 this.noticeDot = data.length > 0
}, },
// setOnlineUsers(data) { setOnlineUsers(data) {
// const { onlineNum, users } = data const { onlineClients, num, leaveUser } = data
// this.onlineUsers = users this.onlineUsers = onlineClients
// this.onlineNum = onlineNum this.onlineNum = num
// }, if (leaveUser != null) {
sendChat(data) { this.leaveUser = leaveUser
const { proxy } = getCurrentInstance() }
console.log(data) },
getOnlineInfo(data) {
this.onlineInfo = data
},
setChat(data) {
this.chatList.push(data)
} }
} }
}) })

View File

@ -19,20 +19,20 @@ export default {
.build() .build()
this.SR = connection this.SR = connection
// 断线重连 // 断线重连
connection.onclose(async () => { connection.onclose(async (error) => {
console.log('断开连接了') console.error('断开连接了' + error)
console.assert(connection.state === signalR.HubConnectionState.Disconnected) console.assert(connection.state === signalR.HubConnectionState.Disconnected)
// 建议用户重新刷新浏览器 // 建议用户重新刷新浏览器
await this.start() await this.start()
}) })
connection.onreconnected(() => { connection.onreconnected((connectionId) => {
ElMessage({ ElMessage({
message: '与服务器通讯已连接成功', message: '与服务器通讯已连接成功',
type: 'success', type: 'success',
duration: 2000 duration: 2000
}) })
console.log('断线重新连接成功') console.log('断线重新连接成功' + connectionId)
}) })
connection.onreconnecting(async () => { connection.onreconnecting(async () => {
@ -74,7 +74,7 @@ export default {
// 接收消息处理 // 接收消息处理
receiveMsg(connection) { receiveMsg(connection) {
connection.on('onlineNum', (data) => { connection.on('onlineNum', (data) => {
useSocketStore().setOnlineUserNum(data) useSocketStore().setOnlineUsers(data)
}) })
// 接收欢迎语 // 接收欢迎语
connection.on('welcome', (data) => { connection.on('welcome', (data) => {
@ -125,14 +125,35 @@ export default {
// 接收聊天数据 // 接收聊天数据
connection.on('receiveChat', (data) => { connection.on('receiveChat', (data) => {
const title = `来自${data.userName}的消息通知` const title = `来自${data.userName}的消息通知`
useSocketStore().setChat(data)
if (data.userid != useUserStore().userId) {
ElNotification({ ElNotification({
title: title, title: title,
message: data.message, message: data.message,
type: 'success', type: 'success',
duration: 0 duration: 3000
}) })
}
webNotify({ title: title, body: data.message }) webNotify({ title: title, body: data.message })
}) })
connection.on('onlineInfo', (data) => {
console.log('onlineInfo', data)
useSocketStore().getOnlineInfo(data)
})
connection.on('logOut', () => {
useUserStore()
.logOut()
.then(() => {
ElMessageBox.alert(`你的账号已在其他设备登录,如果不是你的操作请尽快修改密码`, '提示', {
confirmButtonText: '确定',
callback: () => {
location.href = import.meta.env.VITE_APP_ROUTER_PREFIX + 'index'
}
})
})
})
} }
} }

View File

@ -2,7 +2,7 @@
<div class="home"> <div class="home">
<!-- 用户信息 --> <!-- 用户信息 -->
<el-row :gutter="15"> <el-row :gutter="15">
<el-col :md="24" :lg="18" :xl="24" class="mb10"> <el-col :md="24" :lg="16" :xl="24" class="mb10">
<el-card shadow="hover"> <el-card shadow="hover">
<div class="user-item"> <div class="user-item">
<div class="user-item-left"> <div class="user-item-left">
@ -12,7 +12,10 @@
<div class="user-item-right"> <div class="user-item-right">
<el-row> <el-row>
<el-col :xs="24" :md="24" class="right-title mb20 one-text-overflow"> <el-col :xs="24" :md="24" class="right-title mb20 one-text-overflow">
{{ userInfo.welcomeMessage }} <strong>{{ userInfo.nickName }}</strong> {{ userInfo.welcomeContent }} <div class="mb10">
{{ userInfo.welcomeMessage }} <strong>{{ userInfo.nickName }}</strong>
<span>({{ userInfo.welcomeContent }})</span>
</div>
</el-col> </el-col>
</el-row> </el-row>
<el-row> <el-row>
@ -24,10 +27,19 @@
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :lg="6" class="mb10"> <el-col :lg="8" class="mb10">
<el-card style="height: 100%"> <el-card style="height: 100%">
<div class="text-warning mb10">{{ currentTime }} {{ weekName }}</div> <div class="text-warning mb10">{{ currentTime }} {{ weekName }}</div>
<div>上次登录时间{{ userInfo.loginDate }}</div> <div class="work-wrap">
<div class="item">
<div class="name">今日工作时长</div>
<div class="mt10">{{ onlineInfo.onlineTime }}</div>
</div>
<div class="item">
<div class="name">在线设备数</div>
<div class="mt10">{{ onlineInfo.clientNum }}</div>
</div>
</div>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
@ -94,6 +106,7 @@ import BarChart from './dashboard/BarChart'
import CommonMenu from './components/CommonMenu' import CommonMenu from './components/CommonMenu'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import useSocketStore from '@/store/modules/socket'
import { getWeek } from '@/utils/ruoyi' import { getWeek } from '@/utils/ruoyi'
const showEdit = ref(false) const showEdit = ref(false)
const data = { const data = {
@ -118,6 +131,9 @@ const { proxy } = getCurrentInstance()
const userInfo = computed(() => { const userInfo = computed(() => {
return useUserStore().userInfo return useUserStore().userInfo
}) })
const onlineInfo = computed(() => {
return useSocketStore().onlineInfo
})
const currentTime = computed(() => { const currentTime = computed(() => {
return proxy.parseTime(new Date(), 'YYYY-MM-DD') return proxy.parseTime(new Date(), 'YYYY-MM-DD')
}) })
@ -165,6 +181,19 @@ function handleAdd() {
height: 200px; height: 200px;
// overflow-y: scroll; // overflow-y: scroll;
} }
.work-wrap {
display: grid;
grid-template-columns: repeat(2, 50%);
.item {
text-align: center;
.name {
color: #606666;
}
}
}
} }
.chart-wrapper { .chart-wrapper {
background: var(--base-bg-main); background: var(--base-bg-main);

View File

@ -31,7 +31,7 @@
</template> </template>
</el-input> </el-input>
<div class="login-code"> <div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" /> <el-image :src="codeUrl" @click="getCode" class="login-code-img" />
</div> </div>
</el-form-item> </el-form-item>
@ -59,6 +59,7 @@
<div class="login-scan-container"> <div class="login-scan-container">
<div ref="imgContainerRef" id="imgContainer"></div> <div ref="imgContainerRef" id="imgContainer"></div>
<div>请使用移动端app扫码登录</div>
</div> </div>
</div> </div>

View File

@ -3,11 +3,10 @@
<el-form :model="queryParams" ref="queryRef" :inline="true"> <el-form :model="queryParams" ref="queryRef" :inline="true">
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">刷新</el-button> <el-button type="primary" icon="Search" @click="handleQuery">刷新</el-button>
<!-- <el-button icon="Refresh" @click="resetQuery">{{ $t('btn.reset') }}</el-button> -->
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table :data="onlineUsers" ref="tableRef" border highlight-current-row> <el-table :data="onlineUsers" v-loading="loading" ref="tableRef" border highlight-current-row>
<!-- <el-table-column prop="connnectionId" label="连接id"></el-table-column> --> <!-- <el-table-column prop="connnectionId" label="连接id"></el-table-column> -->
<el-table-column label="No" type="index" width="50" align="center"> <el-table-column label="No" type="index" width="50" align="center">
<template #default="scope"> <template #default="scope">
@ -17,11 +16,12 @@
<el-table-column prop="name" label="用户名" align="center" /> <el-table-column prop="name" label="用户名" align="center" />
<el-table-column label="登录地点" prop="location" align="center"> </el-table-column> <el-table-column label="登录地点" prop="location" align="center"> </el-table-column>
<el-table-column label="登录IP" prop="userIP" align="center"></el-table-column> <el-table-column label="登录IP" prop="userIP" align="center"></el-table-column>
<el-table-column prop="browser" label="登录浏览器" width="250"></el-table-column> <el-table-column prop="browser" label="登录浏览器" width="210"></el-table-column>
<el-table-column prop="platform" label="登录设备" align="center"></el-table-column> <el-table-column prop="platform" label="登录设备" align="center"></el-table-column>
<el-table-column prop="loginTime" label="登录时间"> <el-table-column prop="loginTime" label="登录时间" witdh="280px">
<template #default="scope"> <template #default="scope">
{{ dayjs(scope.row.loginTime).format('MM/DD日HH:mm:ss') }} {{ dayjs(scope.row.loginTime).format('MM/DD日HH:mm:ss') }}
<div>在线时长{{ scope.row.onlineTime }}分钟</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" width="160"> <el-table-column label="操作" align="center" width="160">
@ -47,12 +47,11 @@ const queryParams = reactive({
pageSize: 10 pageSize: 10
}) })
// const total = computed(() => { const onlineNum = computed(() => {
// return useSocketStore().onlineNum return useSocketStore().onlineNum
// }) })
// const onlineUsers = computed(() => {
// return useSocketStore().onlineUsers const loading = ref(false)
// })
const onlineUsers = ref([]) const onlineUsers = ref([])
const total = ref(0) const total = ref(0)
function handleQuery() { function handleQuery() {
@ -60,13 +59,14 @@ function handleQuery() {
getList() getList()
} }
function getList() { function getList() {
// proxy.signalr.SR.invoke('GetOnlineUsers', queryParams.pageNum, queryParams.pageSize).catch(function (err) { loading.value = true
// console.error(err.toString())
// })
listOnline(queryParams).then((res) => { listOnline(queryParams).then((res) => {
if (res.code == 200) { if (res.code == 200) {
total.value = res.data.totalNum total.value = res.data.totalNum
onlineUsers.value = res.data.result onlineUsers.value = res.data.result
setTimeout(() => {
loading.value = false
}, 200)
} }
}) })
} }
@ -81,7 +81,7 @@ function onChat(item) {
inputErrorMessage: '消息内容不能为空' inputErrorMessage: '消息内容不能为空'
}) })
.then(({ value }) => { .then(({ value }) => {
proxy.signalr.SR.invoke('SendMessage', item.connnectionId, item.name, value).catch(function (err) { proxy.signalr.SR.invoke('sendMessage', item.connnectionId, item.userid, value).catch(function (err) {
console.error(err.toString()) console.error(err.toString())
}) })
}) })
@ -100,4 +100,14 @@ function onLock(row) {
}) })
.catch(() => {}) .catch(() => {})
} }
watch(
onlineNum,
() => {
handleQuery()
},
{
immediate: true
}
)
</script> </script>