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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from "vue";
|
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import {
|
import {
|
||||||
@@ -226,6 +226,7 @@ import { ticketApi, userApi } from "@/api/ticket";
|
|||||||
import { resetRouter } from "@/router";
|
import { resetRouter } from "@/router";
|
||||||
import { formatUrl, formatDate } from "@/utils/format";
|
import { formatUrl, formatDate } from "@/utils/format";
|
||||||
import SearchInput from "@/components/common/SearchInput.vue";
|
import SearchInput from "@/components/common/SearchInput.vue";
|
||||||
|
import { openSocket, closeSocket, onTicketChanged, onReconnect } from "@/utils/socket";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -258,8 +259,9 @@ const currentUser = computed(() => {
|
|||||||
|
|
||||||
|
|
||||||
// 获取工单信息
|
// 获取工单信息
|
||||||
const fetchTickets = async () => {
|
// silent: 静默刷新(WS 推送触发),不显示 loading 遮罩
|
||||||
loading.value = true; // 显示加载中状态
|
const fetchTickets = async (silent = false) => {
|
||||||
|
if (!silent) loading.value = true; // 显示加载中状态
|
||||||
// 优化:不再获取所有工单,而是根据当前视图的状态和用户ID进行筛选
|
// 优化:不再获取所有工单,而是根据当前视图的状态和用户ID进行筛选
|
||||||
try {
|
try {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
@@ -304,7 +306,7 @@ const fetchTickets = async () => {
|
|||||||
allTickets.value = [];
|
allTickets.value = [];
|
||||||
ElMessage.error("获取列表失败");
|
ElMessage.error("获取列表失败");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
if (!silent) loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -446,6 +448,9 @@ const handleLogout = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let stopTicketWatch = null;
|
||||||
|
let stopReconnectWatch = null;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await ticketApi.getUserStaffs();
|
const res = await ticketApi.getUserStaffs();
|
||||||
@@ -455,6 +460,17 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
await fetchTickets();
|
await fetchTickets();
|
||||||
|
|
||||||
|
// 实时刷新:后台新增/更新工单时静默刷新列表;断线重连后补拉一次
|
||||||
|
openSocket();
|
||||||
|
stopTicketWatch = onTicketChanged(() => fetchTickets(true));
|
||||||
|
stopReconnectWatch = onReconnect(() => fetchTickets(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopTicketWatch && stopTicketWatch();
|
||||||
|
stopReconnectWatch && stopReconnectWatch();
|
||||||
|
closeSocket();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user