chore: initial commit
这个提交包含在:
当前提交
115e093ce2
10
.gitignore
vendored
普通文件
10
.gitignore
vendored
普通文件
@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.class
|
||||
target/
|
||||
build/
|
||||
.gradle/
|
||||
*.iml
|
||||
.idea/
|
||||
*.log
|
||||
35
package.json
普通文件
35
package.json
普通文件
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@xuqm/vue3-sdk",
|
||||
"version": "0.1.0",
|
||||
"description": "XuqmGroup Vue3 SDK — IM & platform integration for web",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
|
||||
},
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.es.js",
|
||||
"require": "./dist/index.cjs.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vue": "^3.5.13"
|
||||
}
|
||||
}
|
||||
32
src/core/http.ts
普通文件
32
src/core/http.ts
普通文件
@ -0,0 +1,32 @@
|
||||
import type { ApiResponse } from '../types'
|
||||
|
||||
let _baseUrl = ''
|
||||
let _tokenGetter: (() => string | null) = () => null
|
||||
|
||||
export function configureHttp(baseUrl: string, tokenGetter: () => string | null) {
|
||||
_baseUrl = baseUrl.replace(/\/$/, '')
|
||||
_tokenGetter = tokenGetter
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const token = _tokenGetter()
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(`${_baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
const json: ApiResponse<T> = await res.json()
|
||||
if (json.code !== 200) throw new Error(json.message)
|
||||
return json.data
|
||||
}
|
||||
|
||||
export const http = {
|
||||
get: <T>(path: string) => request<T>('GET', path),
|
||||
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
|
||||
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
|
||||
delete: <T>(path: string) => request<T>('DELETE', path),
|
||||
}
|
||||
24
src/core/sdk.ts
普通文件
24
src/core/sdk.ts
普通文件
@ -0,0 +1,24 @@
|
||||
import type { SDKConfig } from '../types'
|
||||
import { configureHttp } from './http'
|
||||
|
||||
let _config: SDKConfig | null = null
|
||||
let _token: string | null = null
|
||||
|
||||
export function init(config: SDKConfig) {
|
||||
_config = config
|
||||
configureHttp(config.apiBaseUrl, () => _token)
|
||||
if (config.debug) console.log('[XuqmSDK] initialized', config.appKey)
|
||||
}
|
||||
|
||||
export function setToken(token: string | null) {
|
||||
_token = token
|
||||
}
|
||||
|
||||
export function getToken(): string | null {
|
||||
return _token
|
||||
}
|
||||
|
||||
export function getConfig(): SDKConfig {
|
||||
if (!_config) throw new Error('XuqmSDK not initialized. Call init() first.')
|
||||
return _config
|
||||
}
|
||||
112
src/im/ImClient.ts
普通文件
112
src/im/ImClient.ts
普通文件
@ -0,0 +1,112 @@
|
||||
import type { ImMessage, SendMessageParams, ImEventMap } from '../types'
|
||||
import { getConfig, getToken } from '../core/sdk'
|
||||
|
||||
type EventListener<K extends keyof ImEventMap> = ImEventMap[K]
|
||||
|
||||
const MAX_RECONNECT_DELAY = 30_000
|
||||
|
||||
export class ImClient {
|
||||
private ws: WebSocket | null = null
|
||||
private reconnectDelay = 3_000
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private destroyed = false
|
||||
private listeners: { [K in keyof ImEventMap]?: Set<EventListener<K>> } = {}
|
||||
|
||||
on<K extends keyof ImEventMap>(event: K, handler: EventListener<K>): this {
|
||||
if (!this.listeners[event]) {
|
||||
(this.listeners[event] as Set<EventListener<K>>) = new Set()
|
||||
}
|
||||
(this.listeners[event] as Set<EventListener<K>>).add(handler)
|
||||
return this
|
||||
}
|
||||
|
||||
off<K extends keyof ImEventMap>(event: K, handler: EventListener<K>): this {
|
||||
(this.listeners[event] as Set<EventListener<K>> | undefined)?.delete(handler)
|
||||
return this
|
||||
}
|
||||
|
||||
private emit<K extends keyof ImEventMap>(event: K, ...args: Parameters<ImEventMap[K]>): void {
|
||||
(this.listeners[event] as Set<(...a: unknown[]) => void> | undefined)?.forEach((h) =>
|
||||
h(...(args as unknown[]))
|
||||
)
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (this.destroyed) return
|
||||
const config = getConfig()
|
||||
const token = getToken()
|
||||
const url = `${config.imBaseUrl}/ws/im?token=${token ?? ''}`
|
||||
|
||||
this.ws = new WebSocket(url)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.reconnectDelay = 3_000
|
||||
if (config.debug) console.log('[ImClient] connected')
|
||||
this.emit('connected')
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const frame = JSON.parse(event.data as string)
|
||||
if (frame.type === 'MESSAGE') {
|
||||
this.emit('message', frame.payload as ImMessage)
|
||||
} else if (frame.type === 'REVOKE') {
|
||||
this.emit('revoke', frame.payload as { msgId: string; operatorId: string })
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed frames
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
this.emit('disconnected', event.code, event.reason)
|
||||
if (!this.destroyed) this.scheduleReconnect()
|
||||
}
|
||||
|
||||
this.ws.onerror = (event) => {
|
||||
this.emit('error', event)
|
||||
}
|
||||
}
|
||||
|
||||
send(params: SendMessageParams): void {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket not connected')
|
||||
}
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
destination: '/app/chat.send',
|
||||
payload: params,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
revoke(msgId: string): void {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket not connected')
|
||||
}
|
||||
this.ws.send(
|
||||
JSON.stringify({
|
||||
destination: '/app/chat.revoke',
|
||||
payload: { msgId },
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.destroyed = true
|
||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
|
||||
this.ws?.close()
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.destroyed) return
|
||||
if (getConfig().debug) {
|
||||
console.log(`[ImClient] reconnect in ${this.reconnectDelay}ms`)
|
||||
}
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.connect()
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY)
|
||||
}, this.reconnectDelay)
|
||||
}
|
||||
}
|
||||
38
src/im/useIm.ts
普通文件
38
src/im/useIm.ts
普通文件
@ -0,0 +1,38 @@
|
||||
import { ref, shallowRef, onUnmounted } from 'vue'
|
||||
import { ImClient } from './ImClient'
|
||||
import type { ImMessage, SendMessageParams } from '../types'
|
||||
|
||||
export function useIm() {
|
||||
const client = shallowRef<ImClient | null>(null)
|
||||
const messages = ref<ImMessage[]>([])
|
||||
const connected = ref(false)
|
||||
const error = ref<Event | null>(null)
|
||||
|
||||
function connect() {
|
||||
const im = new ImClient()
|
||||
im.on('connected', () => { connected.value = true })
|
||||
im.on('disconnected', () => { connected.value = false })
|
||||
im.on('message', (msg) => { messages.value = [...messages.value, msg] })
|
||||
im.on('error', (e) => { error.value = e })
|
||||
im.connect()
|
||||
client.value = im
|
||||
}
|
||||
|
||||
function send(params: SendMessageParams) {
|
||||
client.value?.send(params)
|
||||
}
|
||||
|
||||
function revoke(msgId: string) {
|
||||
client.value?.revoke(msgId)
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
client.value?.disconnect()
|
||||
client.value = null
|
||||
connected.value = false
|
||||
}
|
||||
|
||||
onUnmounted(disconnect)
|
||||
|
||||
return { connect, send, revoke, disconnect, messages, connected, error }
|
||||
}
|
||||
13
src/index.ts
普通文件
13
src/index.ts
普通文件
@ -0,0 +1,13 @@
|
||||
export { init, setToken, getToken, getConfig } from './core/sdk'
|
||||
export { http } from './core/http'
|
||||
export { ImClient } from './im/ImClient'
|
||||
export { useIm } from './im/useIm'
|
||||
export type {
|
||||
SDKConfig,
|
||||
MsgType,
|
||||
ChatType,
|
||||
ImMessage,
|
||||
SendMessageParams,
|
||||
ImEventMap,
|
||||
ApiResponse,
|
||||
} from './types'
|
||||
59
src/types/index.ts
普通文件
59
src/types/index.ts
普通文件
@ -0,0 +1,59 @@
|
||||
export interface SDKConfig {
|
||||
appKey: string
|
||||
appSecret: string
|
||||
apiBaseUrl: string
|
||||
imBaseUrl: string
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
export type MsgType =
|
||||
| 'TEXT'
|
||||
| 'IMAGE'
|
||||
| 'VIDEO'
|
||||
| 'AUDIO'
|
||||
| 'FILE'
|
||||
| 'CUSTOM'
|
||||
| 'LOCATION'
|
||||
| 'NOTIFY'
|
||||
| 'RICH_TEXT'
|
||||
| 'CALL_AUDIO'
|
||||
| 'CALL_VIDEO'
|
||||
| 'REVOKED'
|
||||
| 'FORWARD'
|
||||
|
||||
export type ChatType = 'SINGLE' | 'GROUP'
|
||||
|
||||
export interface ImMessage {
|
||||
id: string
|
||||
fromId: string
|
||||
toId: string
|
||||
chatType: ChatType
|
||||
msgType: MsgType
|
||||
content: string
|
||||
extra?: string
|
||||
revoked: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface SendMessageParams {
|
||||
toId: string
|
||||
chatType: ChatType
|
||||
msgType: MsgType
|
||||
content: string
|
||||
extra?: string
|
||||
}
|
||||
|
||||
export interface ImEventMap {
|
||||
message: (msg: ImMessage) => void
|
||||
revoke: (data: { msgId: string; operatorId: string }) => void
|
||||
connected: () => void
|
||||
disconnected: (code: number, reason: string) => void
|
||||
error: (err: Event) => void
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
code: number
|
||||
status: string
|
||||
data: T
|
||||
message: string
|
||||
}
|
||||
20
tsconfig.json
普通文件
20
tsconfig.json
普通文件
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
28
vite.config.ts
普通文件
28
vite.config.ts
普通文件
@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import dts from 'vite-plugin-dts'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
dts({ include: ['src/**/*.ts', 'src/**/*.vue'], rollupTypes: true }),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'XuqmVue3SDK',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format}.js`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue'],
|
||||
output: {
|
||||
globals: { vue: 'Vue' },
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: { '@': resolve(__dirname, 'src') },
|
||||
},
|
||||
})
|
||||
正在加载...
在新工单中引用
屏蔽一个用户