chore: initial commit

这个提交包含在:
XuqmGroup 2026-04-21 22:07:29 +08:00
当前提交 115e093ce2
共有 11 个文件被更改,包括 372 次插入0 次删除

10
.gitignore vendored 普通文件
查看文件

@ -0,0 +1,10 @@
node_modules/
dist/
.DS_Store
*.class
target/
build/
.gradle/
*.iml
.idea/
*.log

1
.npmrc 普通文件
查看文件

@ -0,0 +1 @@
registry=https://nexus.xuqinmin.com/repository/npm-hosted/

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 普通文件
查看文件

@ -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 普通文件
查看文件

@ -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 普通文件
查看文件

@ -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 普通文件
查看文件

@ -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 普通文件
查看文件

@ -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 普通文件
查看文件

@ -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 普通文件
查看文件

@ -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 普通文件
查看文件

@ -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') },
},
})