This commit is contained in:
oneao committed 2025-10-14 17:30:21 +08:00
1 parent 35951777b0
commit 63f1d4d1dc
39 files changed
+880 -133

No files matched your search

+5 -5
View File
@@ -21,15 +21,15 @@
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="aliyun" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="aliyun" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public" />
</remote-repository>
</component>
</project>
+1 -1
View File
@@ -8,5 +8,5 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="temurin-1.8" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
</project>
+5 -5
View File
@@ -58,11 +58,11 @@
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 可选依赖,不打包到生产 -->
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-devtools</artifactId>-->
<!-- <optional>true</optional> &lt;!&ndash; 可选依赖,不打包到生产 &ndash;&gt;-->
<!-- </dependency>-->
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -24,6 +24,7 @@ public class AuthController {
response.put("message", "Missing parameters");
return response;
}
UserInfo userInfo = UserRepository.getUserPasswordById(orgId, userId);
if (userInfo != null && userInfo.getPassword().equals(password)) {
String token = JwtUtil.generateToken(userId, orgId);
@@ -33,6 +34,7 @@ public class AuthController {
} else {
response.put("message", "Invalid credentials");
}
return response;
}
@@ -19,10 +19,7 @@ public class ColumnConfigController {
*/
@GetMapping("/api/columns")
public Result<?> getColumnConfig(@RequestParam String tableName, HttpServletRequest request) {
try {
String orgId = UserContext.getOrgId();
System.out.println("api/columns:"+orgId);
List<Map<String, Object>> data = JsonUtils.queryColumnConfig(orgId, tableName);
@@ -32,10 +29,8 @@ public class ColumnConfigController {
}
}
@GetMapping("/api/columnsEdit")
public Result<?> getColumnEditConfig(@RequestParam String tableName, HttpServletRequest request) {
try {
String orgId = UserContext.getOrgId();
List<Map<String, Object>> data = JsonUtils.queryColumnEditConfig(orgId, tableName);
@@ -8,7 +8,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:5173", "http://localhost:8089","http://localhost:5174") // 前端地址
.allowedOriginPatterns("*") // 前端地址
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
@@ -3,11 +3,15 @@ package com.link.api.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.link.im.manager.UserChannelManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.sql.*;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class PaginationUtils {
@@ -470,5 +474,5 @@ public class PaginationUtils {
}
}
}
}
@@ -0,0 +1,43 @@
package com.link.im.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.link.api.util.DruidDataSourceManager;
import com.link.api.util.PaginationUtils;
import com.link.api.util.Result;
import com.link.api.util.UserContext;
import com.link.im.manager.UserChannelManager;
import com.link.im.utils.DbUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@CrossOrigin
public class CompanyController {
@GetMapping("/api/loadCompanyList")
public Result<?> getCompanyList(){
try {
String orgId = UserContext.getOrgId();
String sql = "select b_id,b_name from b_user where b_canuse = '1' ";
List<Map<String, Object>> maps = DbUtils.queryForList(orgId, sql);
for (Map<String, Object> map : maps) {
String bId = map.get("b_id").toString();
map.put("_online",UserChannelManager.isOnline(bId));
}
return Result.ok("成功",maps);
} catch (Exception e) {
return Result.fail("公司列表查询异常:" + e.getMessage());
}
}
}
@@ -0,0 +1,72 @@
package com.link.im.handler;
import com.link.api.util.UserContext;
import com.link.im.manager.UserChannelManager;
import com.link.im.manager.WriteManager;
import com.link.im.model.ImRequest;
import com.link.im.model.ImResponse;
import com.link.im.utils.CommonUtils;
import com.link.im.utils.DbUtils;
import io.netty.channel.ChannelHandlerContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
@Component
public class ImMessageProcessor {
public void process(ChannelHandlerContext ctx, ImRequest request) {
String type = request.getType();
if (type == null) {
WriteManager.writeToChannel(ctx.channel(), "消息类型不能为空");
return;
}
switch (type) {
case "ping":
handlePing(ctx);
break;
case "login":
handleLogin(ctx, request);
break;
default:
WriteManager.writeToChannel(ctx.channel(), "未知的消息类型: " + type);
break;
}
}
private void handlePing(ChannelHandlerContext ctx) {
WriteManager.writeToChannel(ctx.channel(), ImResponse.success("pong",null));
}
private void handleLogin(ChannelHandlerContext ctx,ImRequest request){
String fromUserid = request.getFrom();
UserChannelManager.addUser(fromUserid,ctx.channel());
String orgId = request.getOrgId();
System.out.println(orgId);
// 1.通知给好友
String sql1 = "select b_frienduser_Id from IM_Friends where b_user_id = '" + fromUserid +"'";
try {
List<Map<String, Object>> maps = DbUtils.queryForList(orgId, sql1);
for (Map<String, Object> map : maps) {
String bFrienduserId = map.get("b_frienduser_Id").toString();
if (CommonUtils.isNotEmpty(bFrienduserId)) {
boolean friendOnline = UserChannelManager.isOnline(bFrienduserId);
if(friendOnline){
}
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
// 2.通知给群组
// 3.通知给全部(测试用,先留,后面会删)
}
}
@@ -5,6 +5,8 @@ import com.link.im.manager.GroupManager;
import com.link.im.manager.UserChannelManager;
import com.link.im.manager.WriteManager;
import com.link.im.model.ImMessage;
import com.link.im.model.ImRequest;
import com.link.im.model.ImResponse;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
@@ -17,6 +19,7 @@ import org.slf4j.LoggerFactory;
public class ImWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final Logger logger = LoggerFactory.getLogger(ImWebSocketHandler.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private final ImMessageProcessor imMessageProcessor = new ImMessageProcessor();
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
@@ -35,14 +38,15 @@ public class ImWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocke
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String text = msg.text();
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
try {
ImMessage imMessage = MAPPER.readValue(text, ImMessage.class);
System.out.println(imMessage);
System.out.println(msg.text());
ImRequest request = MAPPER.readValue(msg.text(), ImRequest.class);
imMessageProcessor.process(ctx,request);
} catch (Exception e) {
WriteManager.writeToChannel(ctx.channel(),"JSON格式错误");
logger.error("channelRead0数据处理失败", e);
WriteManager.writeToChannel(ctx.channel(),ImResponse.error("error",e.toString()));
}
}
@@ -1,8 +1,6 @@
package com.link.im.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -14,20 +12,45 @@ public class UserChannelManager {
private static final ConcurrentHashMap<String, Channel> userChannelMap = new ConcurrentHashMap<>();
/**
* 添加用户与Channel绑定
* 添加用户与Channel绑定,如果已有绑定,先移除旧的
*/
public static void addUser(String userId, Channel channel) {
if (userId != null && channel != null) {
userChannelMap.put(userId, channel);
if (userId == null || channel == null) return;
// 如果已有旧的 channel,先移除
Channel oldChannel = userChannelMap.put(userId, channel);
if (oldChannel != null && oldChannel != channel) {
try {
if (oldChannel.isActive()) {
oldChannel.close();
}
logger.info("用户 {} 已有旧 channel,被替换: oldChannelId={}, newChannelId={}",
userId, oldChannel.id().asShortText(), channel.id().asShortText());
} catch (Exception e) {
logger.error("关闭旧 channel 异常: userId={}", userId, e);
}
}
logger.info("用户上线: userId={}, channelId={}", userId, channel.id().asShortText());
}
/**
* 移除用户绑定
*/
public static void removeUserById(String userId) {
if (userId != null) {
userChannelMap.remove(userId);
if (userId == null) return;
Channel removed = userChannelMap.remove(userId);
if (removed != null) {
try {
if (removed.isActive()) {
removed.close();
}
} catch (Exception e) {
logger.error("关闭 channel 异常: userId={}", userId, e);
}
logger.info("用户下线: userId={}, channelId={}", userId, removed.id().asShortText());
}
}
@@ -60,5 +83,4 @@ public class UserChannelManager {
Channel ch = userChannelMap.get(userId);
return ch != null && ch.isActive();
}
}
@@ -24,6 +24,18 @@ public class ImMessage {
return type;
}
@Override
public String toString() {
return "ImMessage{" +
"type='" + type + '\'' +
", msg='" + msg + '\'' +
", from='" + from + '\'' +
", to='" + to + '\'' +
", content='" + content + '\'' +
", timestamp=" + timestamp +
'}';
}
public void setType(String type) {
this.type = type;
}
@@ -0,0 +1,82 @@
package com.link.im.model;
public class ImRequest {
private String type;
private String from;
private String to;
private String content;
private String orgId;
private long timestamp;
// 无参构造
public ImRequest() {}
// 全参构造
public ImRequest(String type, String from, String to, String content, long timestamp,String orgId) {
this.type = type;
this.from = from;
this.to = to;
this.content = content;
this.timestamp = timestamp;
this.orgId = orgId;
}
public String getOrgId() {
return orgId;
}
public void setOrgId(String orgId) {
this.orgId = orgId;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
@Override
public String toString() {
return "ImRequest{" +
"type='" + type + '\'' +
", from='" + from + '\'' +
", to='" + to + '\'' +
", content='" + content + '\'' +
", timestamp=" + timestamp +
'}';
}
}
@@ -0,0 +1,89 @@
package com.link.im.model;
public class ImResponse {
private String type;
private int code;
private String message;
private long timestamp;
private Object data;
// 无参构造
public ImResponse() {}
// 全参构造
public ImResponse(String type, int code, String message, long timestamp, Object data) {
this.type = type;
this.code = code;
this.message = message;
this.timestamp = timestamp;
this.data = data;
}
public static ImResponse success(String type, Object data) {
return new ImResponse(type, 200, "success", System.currentTimeMillis(), data);
}
public static ImResponse success(String type, String message, Object data) {
return new ImResponse(type, 200, message, System.currentTimeMillis(), data);
}
public static ImResponse error(String type, String message) {
return new ImResponse(type, 500, message, System.currentTimeMillis(), null);
}
public static ImResponse error(String type, int code, String message) {
return new ImResponse(type, code, message, System.currentTimeMillis(), null);
}
// Getter & Setter
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "ImResponse{" +
"type='" + type + '\'' +
", code=" + code +
", message='" + message + '\'' +
", timestamp=" + timestamp +
", data=" + data +
'}';
}
}
@@ -0,0 +1,21 @@
package com.link.im.utils;
public class CommonUtils {
/**
* 判断字符串是否为空(null 或 "")
* @param str 待判断的字符串
* @return true 如果字符串为 null 或 空串
*/
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
/**
* 判断字符串是否非空(不是 null 且不为空串)
* @param str 待判断的字符串
* @return true 如果字符串非空
*/
public static boolean isNotEmpty(String str) {
return !isEmpty(str);
}
}
@@ -0,0 +1,41 @@
package com.link.im.utils;
import com.link.api.util.DruidDataSourceManager;
import java.sql.*;
import java.util.*;
public class DbUtils {
/**
* 根据 SQL 查询数据
*
* @param orgCode 数据源组织编码
* @param sql 查询 SQL
* @return List<Map<String,Object>>,每一行作为 Map
*/
public static List<Map<String, Object>> queryForList(String orgCode, String sql) throws SQLException {
List<Map<String, Object>> resultList = new ArrayList<>();
try (Connection conn = DruidDataSourceManager.getConnection(orgCode);
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
while (rs.next()) {
Map<String, Object> rowMap = new LinkedHashMap<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i);
Object value = rs.getObject(i);
rowMap.put(columnName, value);
}
resultList.add(rowMap);
}
}
return resultList;
}
}
@@ -1,4 +1,4 @@
url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3HY2025
url=jdbc:sqlserver://192.168.1.189:1433;databaseName=G3-Link;encrypt=false
username=sa
password=g3soft
driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
@@ -1,4 +1,4 @@
url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3HY2025
url=jdbc:sqlserver://192.168.1.189:1433;databaseName=G3-Link;encrypt=false
username=sa
password=g3soft
driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
+4 -7
View File
@@ -1,18 +1,15 @@
<script setup>
import { onBeforeUnmount, onMounted } from 'vue'
import { onBeforeUnmount } from 'vue'
import Layout from '@/layout/index.vue'
import { useWebSocket } from './stores/websocketStore'
const websocket = useWebSocket()
onMounted(() => {
websocket.connect('ws://127.0.0.1:8090/ws')
})
onBeforeUnmount(() => {
websocket.disconnect()
// websocket.disconnect()
})
</script>
<template>
<RouterView />
<Layout />
</template>
@@ -0,0 +1,97 @@
<script setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '../stores/userStore'
import { useWebSocket } from '../stores/websocketStore'
const websocket = useWebSocket()
const userStore = useUserStore()
const router = useRouter()
const activeIndex = ref('/home')
onMounted(() => {
router.afterEach((to) => {
activeIndex.value = to.path
})
})
function toPath(url) {
if (url === '/login') {
console.log(userStore.isLogined())
if (userStore.isLogined()) {
websocket.disconnect()
userStore.resetUserInfo()
}
router.push(url)
}
else {
router.push(url)
}
}
</script>
<template>
<div class="layout">
<el-menu
v-model="activeIndex"
:default-active="activeIndex"
mode="horizontal"
active-text-color="#1553f1"
>
<el-menu-item index="/home" @click="toPath('/home')">
首页
</el-menu-item>
<el-menu-item index="/im" @click="toPath('/im')">
IM即时通信
</el-menu-item>
<el-menu-item index="/login" @click="toPath('/login')">
登录
</el-menu-item>
</el-menu>
<div class="layout-main">
<router-view />
</div>
</div>
</template>
<style scoped>
.layout {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
& *::-webkit-scrollbar {
width: 6px;
height: 6px;
}
& *::-webkit-scrollbar-track {
background-color: #fff;
border-radius: 3px;
}
& *::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
transition: background-color 0.2s ease;
}
& *::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.4);
}
.el-menu--horizontal {
width: 100%;
}
.layout-main {
flex: 1;
min-height: 0;
overflow: auto;
scrollbar-gutter: stable both-edges;
}
}
</style>
+42 -6
View File
@@ -1,14 +1,30 @@
import { ElMessage } from 'element-plus'
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/userStore'
import Login from '@/views/auth/index.vue'
import Home from '@/views/home/index.vue'
import Im from '@/views/im/index.vue'
import { useWebSocket } from '../stores/websocketStore'
// 新增登录页面
const routes = [
{
path: '/',
redirect: '/im',
redirect: '/home',
},
{
path: '/home',
component: Home,
meta: { title: '首页' },
},
{
path: '/im',
component: Im,
meta: { title: 'IM聊天' },
},
{
path: '/login',
component: Login,
meta: { title: '登录' },
},
]
@@ -17,16 +33,36 @@ const router = createRouter({
routes,
})
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - 系统名称` : '系统名称'
// 通过检查,继续跳转
const userStore = useUserStore()
const websocket = useWebSocket()
// 登录页直接放行,不主动断开
if (to.path === '/login') {
next()
return
}
if (!userStore.isLogined()) {
ElMessage.warning('请先登录')
userStore.resetUserInfo()
next({ path: '/login', query: { redirect: to.fullPath } })
return
}
// 登录状态下,确保连接
if (!websocket.isConnected.value) {
websocket.connect()
}
next()
})
router.afterEach(() => {
router.afterEach((to, from) => {
})
export default router
@@ -4,18 +4,19 @@ import { ref } from 'vue'
export const useUserStore = defineStore(
'userInfo',
() => {
const userInfo = ref({
orgid: '',
orgid: 'g3hd',
userid: '',
token: '',
username: '',
})
// 设置用户信息
function setUserInfo(orgid, userid, token) {
function setUserInfo(orgid, userid, username, token) {
userInfo.value.orgid = orgid
userInfo.value.userid = userid
userInfo.value.token = token
userInfo.value.username = username
}
// 重置用户信息
@@ -1,7 +1,15 @@
// stores/websocket.js
import { ElNotification } from 'element-plus'
import { onScopeDispose, ref } from 'vue'
import { ref } from 'vue'
import { useUserStore } from './userStore'
let instance = null // 单例缓存
export function useWebSocket() {
if (instance)
return instance // 已存在则直接返回
const userStore = useUserStore()
const socket = ref(null)
const isConnected = ref(false)
const listeners = ref(new Set())
@@ -9,25 +17,19 @@ export function useWebSocket() {
let reconnectAttempts = 0
let pingTimer = null
let pongTimeout = null
const PING_INTERVAL = 10000 // 收到PONG后间隔再发送下一次PING
const PONG_TIMEOUT = 15000 // PING发送后15秒未收到PONG就重连
const PING_INTERVAL = 10000
const PONG_TIMEOUT = 15000
const currentUrl = 'ws://127.0.0.1:8090/ws'
let reconnectNotification = null
let currentUrl = null
let isReconnecting = false
let manualClose = false // ✅ 标记是否为手动关闭
// --------------------
// 私有方法
// --------------------
// ---------------- 清理 ----------------
function cleanup() {
if (pingTimer) {
clearTimeout(pingTimer)
pingTimer = null
}
if (pongTimeout) {
clearTimeout(pongTimeout)
pongTimeout = null
}
clearTimeout(pingTimer)
clearTimeout(pongTimeout)
pingTimer = null
pongTimeout = null
}
function handleDisconnect() {
@@ -39,23 +41,14 @@ export function useWebSocket() {
listeners.value.forEach(cb => cb(message))
}
// --------------------
// 心跳逻辑(链式)
// --------------------
// ---------------- 心跳 ----------------
function sendPing() {
if (!socket.value || socket.value.readyState !== WebSocket.OPEN)
return
try {
sendMessage('PING')
}
catch (err) {
console.error('发送 PING 失败', err)
}
sendMessage({ type: 'ping' })
// 设置 PONG 超时
if (pongTimeout)
clearTimeout(pongTimeout)
clearTimeout(pongTimeout)
pongTimeout = setTimeout(() => {
console.warn('未收到PONG,触发重连')
handleReconnect()
@@ -63,15 +56,8 @@ export function useWebSocket() {
}
function handlePong() {
// 收到 PONG,清理超时
if (pongTimeout) {
clearTimeout(pongTimeout)
pongTimeout = null
}
// 等 PING_INTERVAL 后再发送下一次 PING
if (pingTimer)
clearTimeout(pingTimer)
clearTimeout(pongTimeout)
clearTimeout(pingTimer)
pingTimer = setTimeout(sendPing, PING_INTERVAL)
}
@@ -79,62 +65,57 @@ export function useWebSocket() {
sendPing()
}
// --------------------
// 重连逻辑
// --------------------
// ---------------- 重连 ----------------
function handleReconnect() {
if (isReconnecting)
return
if (isReconnecting || manualClose)
return // ✅ 如果是手动断开则不重连
isReconnecting = true
cleanup()
isConnected.value = false
socket.value?.close()
reconnectAttempts++
if (reconnectNotification)
reconnectNotification.close()
reconnectNotification?.close()
reconnectNotification = ElNotification({
title: '提示',
message: `WebSocket 连接失败,正在第 ${reconnectAttempts} 次重连...`,
type: 'warning',
duration: 0,
key: 'websocket-reconnect',
})
const delay = Math.min(3000 * 2 ** (reconnectAttempts - 1), 30000)
setTimeout(() => {
isReconnecting = false
connect(currentUrl)
connect()
}, delay)
}
function clearReconnectNotification() {
if (reconnectNotification) {
reconnectNotification.close()
reconnectNotification = null
}
reconnectNotification?.close()
reconnectNotification = null
}
// --------------------
// 公共方法
// --------------------
function connect(url) {
currentUrl = url
// ---------------- 公共方法 ----------------
function connect() {
if (isConnected.value)
return
if (socket.value)
disconnect()
const newSocket = new WebSocket(url)
manualClose = false // ✅ 每次连接前重置为 false
const newSocket = new WebSocket(currentUrl)
newSocket.onopen = () => {
isConnected.value = true
isReconnecting = false
clearReconnectNotification()
if (userStore.isLogined)
sendLogin(userStore.userInfo.userid)
ElNotification({
title: '提示',
message: 'WebSocket 连接成功',
message: '上线成功',
type: 'success',
duration: 2000,
})
@@ -144,26 +125,42 @@ export function useWebSocket() {
}
newSocket.onmessage = (event) => {
let message
let resData
try {
message = JSON.parse(event.data)
console.log(event.data)
if (message.type === 'PONG') {
handlePong()
return
}
notifyListeners(message)
resData = JSON.parse(event.data)
}
catch (err) {
console.error('消息解析失败:', err)
notifyListeners({ type: 'ERROR', data: '消息解析失败' })
return
}
if (resData.code !== 200) {
ElNotification({
title: '错误',
message: resData.message || '发生未知错误',
type: 'error',
showClose: true,
duration: 3000,
})
return
}
if (resData.type === 'pong') {
handlePong()
}
else {
notifyListeners(resData)
}
}
newSocket.onclose = () => {
handleDisconnect()
if (!isReconnecting)
// ✅ 非手动断开才重连
if (!manualClose && !isReconnecting)
handleReconnect()
}
@@ -175,14 +172,25 @@ export function useWebSocket() {
}
function disconnect() {
if (!socket.value)
return
manualClose = true
socket.value?.close()
cleanup()
clearReconnectNotification()
ElNotification({
type: 'warning',
title: '提示',
message: '已下线',
})
}
function sendMessage(type, data) {
function sendMessage(data) {
if (isConnected.value && socket.value) {
const message = { type, data }
socket.value.send(JSON.stringify(message))
data.orgId = userStore.userInfo.orgid || ''
socket.value.send(JSON.stringify(data))
}
}
@@ -191,13 +199,23 @@ export function useWebSocket() {
return () => listeners.value.delete(callback)
}
onScopeDispose(() => disconnect())
function sendLogin(userId) {
if (!isConnected.value)
return
sendMessage({
type: 'login',
from: userId,
})
}
return {
instance = {
isConnected,
connect,
disconnect,
sendMessage,
onMessage,
sendLogin,
}
return instance
}
+27 -10
View File
@@ -1,34 +1,51 @@
// src/utils/axios.js
import axios from 'axios'
import { useUserStore } from '@/stores/userStore'
const instance = axios.create({
baseURL: 'http://localhost:8089/api', // 后端接口地址
timeout: 5000
})
timeout: 5000,
})
// 请求拦截器:自动添加 token
instance.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
(config) => {
const userStore = useUserStore()
if (userStore.userInfo.token) {
config.headers.Authorization = `Bearer ${userStore.userInfo.token}`
}
return config
},
error => Promise.reject(error)
error => Promise.reject(error),
)
// 响应拦截器:统一处理错误或过期 token
instance.interceptors.response.use(
response => response,
error => {
(error) => {
const userStore = useUserStore()
if (error.response && error.response.status === 401) {
alert('登录失效,请重新登录')
window.localStorage.removeItem('token')
userStore.resetUserInfo()
window.location.href = '/login'
}
return Promise.reject(error)
}
},
)
export function axiosPost(url, data = {}, config = {}) {
return instance.post(url, data, {
headers: { 'Content-Type': 'application/json' },
...config,
}).then(res => res.data)
}
export function axiosGet(url, params = {}, config = {}) {
return instance.get(url, {
params,
...config,
}).then(res => res.data)
}
export default instance
@@ -0,0 +1,127 @@
<script setup>
import { ElMessage } from 'element-plus'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/userStore'
import { axiosPost } from '@/utils/axios'
const userStore = useUserStore()
const router = useRouter()
const form = ref({
orgid: userStore.userInfo.orgid,
userid: userStore.userInfo.userid,
password: '',
})
const loginLoading = ref(false)
async function onLogin() {
loginLoading.value = true
try {
const resData = await axiosPost(
'/login',
{
orgid: form.value.orgid,
userid: form.value.userid,
password: form.value.password,
},
)
if (resData.token) {
userStore.setUserInfo(form.value.orgid, form.value.userid, resData.userInfo.username, resData.token)
ElMessage.success(resData.message || '登录成功')
router.push('/home')
}
else {
ElMessage.error(resData.message || '登录失败')
}
}
catch (err) {
ElMessage.error(`请求错误:${err.response?.data?.message || err.message}`)
}
finally {
loginLoading.value = false
}
}
</script>
<template>
<div class="app">
<div class="login">
<div class="login-left">
<img src="/vite.svg" alt="Logo">
</div>
<div class="login-right">
<div class="login-right-title">
Online
</div>
<el-form :model="form" label-width="auto" label-position="top" size="large">
<el-form-item label="用户名" required>
<el-input v-model="form.userid" style="height: 36px !important" />
</el-form-item>
<el-form-item label="密码">
<el-input
v-model="form.password" type="password" show-password style="height: 36px !important"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary" :loading="loginLoading" style="width: 100%; margin-top: 16px"
@click="onLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<style lang="css">
.app {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.login {
display: flex;
width: 1000px;
height: 500px;
overflow: hidden;
border-radius: 12px;
box-shadow: 0px 6px 24px -8px rgba(0, 0, 0, 0.1);
}
.login-left {
display: flex;
align-items: center;
justify-content: center;
width: 60%;
padding: 32px;
border-right: 1px solid #e9e9e9;
}
.login-left img {
width: 100%;
height: 100%;
}
.login-right {
display: flex;
flex-direction: column;
gap: 24px;
justify-content: center;
width: 50%;
padding: 32px;
}
.login-right-title {
font-size: 20px;
font-weight: bold;
}
</style>
@@ -0,0 +1,67 @@
<script setup>
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
import { useUserStore } from '@/stores/userStore'
import { axiosGet, axiosPost } from '@/utils/axios'
import { useWebSocket } from '../../stores/websocketStore'
const userStore = useUserStore()
const websocket = useWebSocket()
const tableData = ref([])
onMounted(async () => {
try {
const res = await axiosGet('/loadCompanyList')
tableData.value = res.data
}
catch (err) {
ElMessage.error(`请求错误:${err.response?.data?.message || err.message}`)
}
})
</script>
<template>
<div class="home-container">
<div class="welcome">
{{ userStore.userInfo.username }},欢迎登录
</div>
<div class="home-header">
IM 聊天列表(支持多公司,单公司聊天)
</div>
<el-table :data="tableData" border stripe style="width: 50%;">
<el-table-column prop="b_name" label="用户名称" />
<el-table-column prop="_online" label="在线状态" />
<el-table-column fixed="right" label="操作">
<template #default="scope">
<el-button v-if="userStore.userInfo.userid !== scope.row.b_id" link type="primary" size="small">
聊天
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<style lang="scss" scoped>
.home-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 40px 20px;
}
.welcome {
font-size: 16px;
color: #555;
margin-bottom: 16px;
}
.home-header {
font-size: 18px;
font-weight: 600;
margin-bottom: 24px;
color: #333;
}
</style>