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') },
|
||||||
|
},
|
||||||
|
})
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户