u
This commit is contained in:
1 parent
07e0001b87
commit
46cbb2f50d
543 files changed
+32576
-4231
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import type { CompanyQuery } from '@/types/base'
|
||||
import type { Area, Industry } from '@/types/company'
|
||||
import type { PostTag } from '@/types/post'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
export const useCommonStore = defineStore('common', () => {
|
||||
const areaOptions = reactive<Area[]>([])
|
||||
const industryOptions = reactive<Industry[]>([])
|
||||
const postTagOptions = reactive<PostTag[]>([]) // 原始列表,不含“全部”
|
||||
|
||||
const companyQuery = ref<CompanyQuery>({
|
||||
industries: [],
|
||||
areas: [],
|
||||
name: '',
|
||||
})
|
||||
|
||||
// 计算属性:带“全部”选项
|
||||
const postTagOptionsWithAll = computed(() => [
|
||||
{ id: 0, name: '全部', icon: 'fluent-color:apps-28' },
|
||||
...postTagOptions,
|
||||
])
|
||||
|
||||
function setAreaOptions(options: Area[]) {
|
||||
areaOptions.splice(0, areaOptions.length, ...options)
|
||||
}
|
||||
|
||||
function setIndustryOptions(options: Industry[]) {
|
||||
industryOptions.splice(0, industryOptions.length, ...options)
|
||||
}
|
||||
|
||||
function setPostTagOptions(options: PostTag[]) {
|
||||
postTagOptions.splice(0, postTagOptions.length, ...options)
|
||||
}
|
||||
|
||||
return {
|
||||
areaOptions,
|
||||
industryOptions,
|
||||
companyQuery,
|
||||
postTagOptions, // 原始数据
|
||||
postTagOptionsWithAll, // 带“全部”,响应式
|
||||
setAreaOptions,
|
||||
setIndustryOptions,
|
||||
setPostTagOptions,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useCountStore = defineStore('count', () => {
|
||||
const imCount = ref(0)
|
||||
|
||||
function incrementImCount(amount: number = 1) {
|
||||
imCount.value += amount
|
||||
}
|
||||
|
||||
function decrementImCount(amount: number = 1) {
|
||||
imCount.value = Math.max(imCount.value - amount, 0)
|
||||
}
|
||||
|
||||
function setImCount(value: number) {
|
||||
imCount.value = value
|
||||
}
|
||||
|
||||
return {
|
||||
imCount,
|
||||
incrementImCount,
|
||||
decrementImCount,
|
||||
setImCount,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
|
||||
export function setupPinia(app: App) {
|
||||
const store = createPinia()
|
||||
store.use(createPersistedState())
|
||||
app.use(store)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface IUser {
|
||||
id: number
|
||||
role: number
|
||||
name: string
|
||||
position: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
interface ICompany {
|
||||
id: number
|
||||
logo: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
'user',
|
||||
() => {
|
||||
const user = ref<IUser>({
|
||||
id: 0,
|
||||
role: 0,
|
||||
name: '',
|
||||
position: '',
|
||||
avatar: '',
|
||||
})
|
||||
|
||||
const company = ref<ICompany>({
|
||||
id: 0,
|
||||
logo: '',
|
||||
name: '',
|
||||
})
|
||||
|
||||
const token = ref('')
|
||||
|
||||
// ===== setters =====
|
||||
const setUser = (val: IUser) => {
|
||||
user.value = val
|
||||
}
|
||||
|
||||
const setCompany = (val: ICompany) => {
|
||||
company.value = val
|
||||
}
|
||||
|
||||
const setToken = (val: string) => {
|
||||
token.value = val
|
||||
}
|
||||
|
||||
// 一次性设置(登录时用)
|
||||
const setLoginInfo = (data: {
|
||||
user: IUser
|
||||
company: ICompany
|
||||
token: string
|
||||
}) => {
|
||||
user.value = data.user
|
||||
company.value = data.company
|
||||
token.value = data.token
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
user.value = {
|
||||
id: 0,
|
||||
role: 0,
|
||||
name: '',
|
||||
position: '',
|
||||
avatar: '',
|
||||
}
|
||||
company.value = {
|
||||
id: 0,
|
||||
logo: '',
|
||||
name: '',
|
||||
}
|
||||
token.value = ''
|
||||
}
|
||||
|
||||
// ===== computed =====
|
||||
const isLogined = computed(() => {
|
||||
return !!(token.value && user.value.id && company.value.id)
|
||||
})
|
||||
|
||||
return {
|
||||
user,
|
||||
company,
|
||||
token,
|
||||
setUser,
|
||||
setCompany,
|
||||
setToken,
|
||||
setLoginInfo,
|
||||
clear,
|
||||
isLogined,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,292 @@
|
||||
import type { MessageRequest, MessageResponse } from '@/types/im'
|
||||
import { readonly, ref } from 'vue'
|
||||
import { MessageType } from '@/types/im'
|
||||
import { useUserStore } from './user'
|
||||
|
||||
interface UseWebSocketOptions {
|
||||
url: string
|
||||
heartbeatInterval?: number
|
||||
pongTimeout?: number
|
||||
reconnectInterval?: number
|
||||
maxReconnectTimes?: number
|
||||
}
|
||||
|
||||
/* ================== 单例 ================== */
|
||||
let singleton: ReturnType<typeof createWebSocket> | null = null
|
||||
|
||||
function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
const userStore = useUserStore()
|
||||
/* ================== 配置 ================== */
|
||||
const config: Required<UseWebSocketOptions> = {
|
||||
url:
|
||||
options?.url
|
||||
|| import.meta.env.VITE_WEBSOCKET_BASEURL
|
||||
|| 'ws://127.0.0.1:8090/ws',
|
||||
|
||||
heartbeatInterval: options?.heartbeatInterval ?? 15_000,
|
||||
pongTimeout: options?.pongTimeout ?? 5_000,
|
||||
reconnectInterval: options?.reconnectInterval ?? 3_000,
|
||||
maxReconnectTimes: options?.maxReconnectTimes ?? 5,
|
||||
}
|
||||
|
||||
/* ================== 状态 ================== */
|
||||
let ws: WebSocket | null = null
|
||||
|
||||
const connected = ref(false)
|
||||
const authenticated = ref(false)
|
||||
|
||||
let cachedToken: string | null = userStore.token
|
||||
|
||||
let heartbeatTimer: number | null = null
|
||||
let pongTimeoutTimer: number | null = null
|
||||
let reconnectTimer: number | null = null
|
||||
|
||||
let reconnectTimes = 0
|
||||
let manualClose = false
|
||||
|
||||
/* ================== 外部消息回调 ================== */
|
||||
const messageHandlers: Array<(msg: MessageResponse) => void> = []
|
||||
|
||||
/* ================== 连接管理 ================== */
|
||||
|
||||
function connect() {
|
||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
||||
console.log('[WebSocket] 已存在连接或正在连接,跳过')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[WebSocket] 开始建立连接')
|
||||
manualClose = false
|
||||
|
||||
ws = new WebSocket(config.url)
|
||||
|
||||
ws.onopen = () => {
|
||||
connected.value = true
|
||||
reconnectTimes = 0
|
||||
|
||||
console.log('[WebSocket] 连接成功')
|
||||
|
||||
startHeartbeat()
|
||||
|
||||
// 自动重新登录
|
||||
if (cachedToken) {
|
||||
console.log('[WebSocket] 自动重新发送 LOGIN')
|
||||
login(cachedToken)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = (evt) => {
|
||||
const msg: MessageResponse = JSON.parse(evt.data)
|
||||
|
||||
console.log('===收到消息===')
|
||||
console.log(msg)
|
||||
|
||||
handleMessage(msg)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.warn('[WebSocket] 连接关闭')
|
||||
|
||||
connected.value = false
|
||||
authenticated.value = false
|
||||
|
||||
stopHeartbeat()
|
||||
stopPongTimeout()
|
||||
|
||||
ws = null
|
||||
|
||||
if (!manualClose) {
|
||||
tryReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('[WebSocket] 连接错误', err)
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
console.log('[WebSocket] 主动关闭连接')
|
||||
|
||||
manualClose = true
|
||||
stopHeartbeat()
|
||||
stopPongTimeout()
|
||||
clearReconnectTimer()
|
||||
|
||||
ws?.close()
|
||||
ws = null
|
||||
}
|
||||
|
||||
/* ================== 重连 ================== */
|
||||
|
||||
function tryReconnect() {
|
||||
if (reconnectTimer || reconnectTimes >= config.maxReconnectTimes)
|
||||
return
|
||||
|
||||
reconnectTimes++
|
||||
console.warn(`[WebSocket] 第 ${reconnectTimes} 次重连中...`)
|
||||
|
||||
reconnectTimer = window.setTimeout(() => {
|
||||
reconnectTimer = null
|
||||
connect()
|
||||
}, config.reconnectInterval)
|
||||
}
|
||||
|
||||
function clearReconnectTimer() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== 心跳 ================== */
|
||||
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat()
|
||||
|
||||
heartbeatTimer = window.setTimeout(() => {
|
||||
sendRaw(MessageType.PING)
|
||||
startPongTimeout()
|
||||
startHeartbeat()
|
||||
}, config.heartbeatInterval)
|
||||
}
|
||||
|
||||
function stopHeartbeat() {
|
||||
if (heartbeatTimer) {
|
||||
clearTimeout(heartbeatTimer)
|
||||
heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function startPongTimeout() {
|
||||
stopPongTimeout()
|
||||
|
||||
pongTimeoutTimer = window.setTimeout(() => {
|
||||
console.error('[WebSocket] 心跳超时,断开连接')
|
||||
ws?.close()
|
||||
}, config.pongTimeout)
|
||||
}
|
||||
|
||||
function stopPongTimeout() {
|
||||
if (pongTimeoutTimer) {
|
||||
clearTimeout(pongTimeoutTimer)
|
||||
pongTimeoutTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== 发送 ================== */
|
||||
|
||||
function sendRaw(type: MessageType, data?: any, to?: any) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('[WebSocket] 发送失败,未连接')
|
||||
return
|
||||
}
|
||||
|
||||
const msg: MessageRequest = {
|
||||
type,
|
||||
from: userStore.user.id,
|
||||
to: to ?? null,
|
||||
timestamp: Date.now(),
|
||||
data: data ?? null,
|
||||
}
|
||||
|
||||
console.log('===发送消息===')
|
||||
console.log(msg)
|
||||
|
||||
ws.send(JSON.stringify(msg))
|
||||
}
|
||||
|
||||
function send(type: MessageType, data?: any, to?: any) {
|
||||
if (!authenticated.value && type !== MessageType.LOGIN) {
|
||||
console.warn('[WebSocket] 未 LOGIN,禁止发送业务消息')
|
||||
return
|
||||
}
|
||||
sendRaw(type, data, to)
|
||||
}
|
||||
|
||||
/* ================== 登录相关 ================== */
|
||||
function login(token: string) {
|
||||
console.log('触发login:', authenticated.value)
|
||||
|
||||
if (!connected.value || authenticated.value)
|
||||
return
|
||||
cachedToken = token
|
||||
sendRaw(MessageType.LOGIN, { token })
|
||||
}
|
||||
|
||||
function logout() {
|
||||
cachedToken = null
|
||||
authenticated.value = false
|
||||
|
||||
sendRaw(MessageType.LOGOUT)
|
||||
}
|
||||
|
||||
/* ================== 消息处理(核心修改点) ================== */
|
||||
|
||||
function handleMessage(msg: MessageResponse) {
|
||||
switch (msg.type) {
|
||||
case MessageType.PONG:
|
||||
stopPongTimeout()
|
||||
return
|
||||
|
||||
case MessageType.LOGIN:
|
||||
authenticated.value = true
|
||||
console.log('[WebSocket] LOGIN 成功')
|
||||
return
|
||||
|
||||
case MessageType.LOGOUT:
|
||||
authenticated.value = false
|
||||
console.log('[WebSocket] 已 LOGOUT')
|
||||
return
|
||||
|
||||
default:
|
||||
// 所有业务消息交给外部
|
||||
messageHandlers.forEach(cb => cb(msg))
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== 对外订阅 ================== */
|
||||
|
||||
function onMessage(handler: (msg: MessageResponse) => void) {
|
||||
messageHandlers.push(handler)
|
||||
}
|
||||
|
||||
/* ================== 业务 API ================== */
|
||||
|
||||
function sendPrivate(toUserId: number, content: any) {
|
||||
send(MessageType.PRIVATE, content, toUserId)
|
||||
}
|
||||
|
||||
function sendGroup(groupId: number, content: any) {
|
||||
send(MessageType.GROUP, content, groupId)
|
||||
}
|
||||
|
||||
/* ================== 初始化 ================== */
|
||||
|
||||
connect()
|
||||
|
||||
return {
|
||||
connect,
|
||||
close,
|
||||
|
||||
login,
|
||||
logout,
|
||||
|
||||
sendPrivate,
|
||||
sendGroup,
|
||||
|
||||
onMessage,
|
||||
|
||||
connected: readonly(connected),
|
||||
authenticated: readonly(authenticated),
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== 对外 ================== */
|
||||
|
||||
export function useWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
if (!singleton) {
|
||||
singleton = createWebSocket(options)
|
||||
}
|
||||
return singleton
|
||||
}
|
||||
Reference in new issue
Block a user