wip
This commit is contained in:
131
src/utils/socket.js
Normal file
131
src/utils/socket.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// ============================================================
|
||||
// WebSocket 广播客户端封装(Workerman 2346 端口)
|
||||
// 特性:单例 + 引用计数 + 断线指数退避重连 + 心跳 + 重连后通知
|
||||
// 用法:
|
||||
// import { openSocket, closeSocket, onTicketChanged, onReconnect } from '@/utils/socket'
|
||||
// onMounted(() => {
|
||||
// openSocket()
|
||||
// const off1 = onTicketChanged(() => fetchTickets(true))
|
||||
// const off2 = onReconnect(() => fetchTickets(true))
|
||||
// onBeforeUnmount(() => { off1(); off2(); closeSocket() })
|
||||
// })
|
||||
// ============================================================
|
||||
|
||||
const WS_URL = 'ws://localhost:2346'
|
||||
const MAX_RETRY_DELAY = 30000 // 重连封顶 30s
|
||||
const HEARTBEAT_INTERVAL = 25000
|
||||
|
||||
let socket = null
|
||||
let retryDelay = 1000
|
||||
let reconnectTimer = null
|
||||
let heartbeatTimer = null
|
||||
let manualClosed = false // 手动关闭后不再重连
|
||||
let refCount = 0
|
||||
|
||||
const ticketListeners = new Set() // 收到 ticket_changed 回调
|
||||
const reconnectListeners = new Set() // 重连成功回调
|
||||
|
||||
function connect() {
|
||||
manualClosed = false
|
||||
try {
|
||||
socket = new WebSocket(WS_URL)
|
||||
} catch (e) {
|
||||
scheduleReconnect()
|
||||
return
|
||||
}
|
||||
|
||||
socket.onopen = () => {
|
||||
retryDelay = 1000
|
||||
console.log('[socket] 已连接到工单推送服务 ws://localhost:2346')
|
||||
startHeartbeat()
|
||||
reconnectListeners.forEach((fn) => fn())
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'ticket_changed') {
|
||||
ticketListeners.forEach((fn) => fn(data))
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[socket] 消息解析失败:', event.data)
|
||||
}
|
||||
}
|
||||
|
||||
socket.onclose = () => {
|
||||
stopHeartbeat()
|
||||
if (!manualClosed) scheduleReconnect()
|
||||
}
|
||||
|
||||
socket.onerror = () => {
|
||||
// 连接错误后必然触发 onclose,无需额外处理
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer || manualClosed) return
|
||||
console.log(`[socket] 连接断开,${Math.round(retryDelay / 1000)}s 后重试...`)
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null
|
||||
connect()
|
||||
}, retryDelay)
|
||||
retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY)
|
||||
}
|
||||
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat()
|
||||
heartbeatTimer = setInterval(() => {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send('ping')
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
}
|
||||
|
||||
function stopHeartbeat() {
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer)
|
||||
heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册工单变更监听,返回注销函数 */
|
||||
export function onTicketChanged(callback) {
|
||||
ticketListeners.add(callback)
|
||||
return () => ticketListeners.delete(callback)
|
||||
}
|
||||
|
||||
/** 注册重连成功监听(适合在此主动拉一次数据补齐断线期间变化),返回注销函数 */
|
||||
export function onReconnect(callback) {
|
||||
reconnectListeners.add(callback)
|
||||
return () => reconnectListeners.delete(callback)
|
||||
}
|
||||
|
||||
/** 打开连接(引用计数 +1;多处页面可共享同一连接) */
|
||||
export function openSocket() {
|
||||
refCount += 1
|
||||
if (!socket || socket.readyState === WebSocket.CLOSED) {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭连接(引用计数 -1;归零才真正断开) */
|
||||
export function closeSocket() {
|
||||
refCount = Math.max(0, refCount - 1)
|
||||
if (refCount === 0) {
|
||||
manualClosed = true
|
||||
stopHeartbeat()
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
if (socket) {
|
||||
socket.onclose = null
|
||||
socket.close()
|
||||
socket = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isSocketOpen() {
|
||||
return socket !== null && socket.readyState === WebSocket.OPEN
|
||||
}
|
||||
@@ -209,7 +209,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
@@ -226,6 +226,7 @@ import { ticketApi, userApi } from "@/api/ticket";
|
||||
import { resetRouter } from "@/router";
|
||||
import { formatUrl, formatDate } from "@/utils/format";
|
||||
import SearchInput from "@/components/common/SearchInput.vue";
|
||||
import { openSocket, closeSocket, onTicketChanged, onReconnect } from "@/utils/socket";
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
@@ -258,8 +259,9 @@ const currentUser = computed(() => {
|
||||
|
||||
|
||||
// 获取工单信息
|
||||
const fetchTickets = async () => {
|
||||
loading.value = true; // 显示加载中状态
|
||||
// silent: 静默刷新(WS 推送触发),不显示 loading 遮罩
|
||||
const fetchTickets = async (silent = false) => {
|
||||
if (!silent) loading.value = true; // 显示加载中状态
|
||||
// 优化:不再获取所有工单,而是根据当前视图的状态和用户ID进行筛选
|
||||
try {
|
||||
const statusMap = {
|
||||
@@ -304,7 +306,7 @@ const fetchTickets = async () => {
|
||||
allTickets.value = [];
|
||||
ElMessage.error("获取列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (!silent) loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -446,6 +448,9 @@ const handleLogout = () => {
|
||||
});
|
||||
};
|
||||
|
||||
let stopTicketWatch = null;
|
||||
let stopReconnectWatch = null;
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await ticketApi.getUserStaffs();
|
||||
@@ -455,6 +460,17 @@ onMounted(async () => {
|
||||
}
|
||||
} catch (e) { }
|
||||
await fetchTickets();
|
||||
|
||||
// 实时刷新:后台新增/更新工单时静默刷新列表;断线重连后补拉一次
|
||||
openSocket();
|
||||
stopTicketWatch = onTicketChanged(() => fetchTickets(true));
|
||||
stopReconnectWatch = onReconnect(() => fetchTickets(true));
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopTicketWatch && stopTicketWatch();
|
||||
stopReconnectWatch && stopReconnectWatch();
|
||||
closeSocket();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user