507 lines
14 KiB
TypeScript
507 lines
14 KiB
TypeScript
/**
|
|
* Mosquito Vue 3 增强版插件
|
|
* 包含全局配置、错误处理、加载状态管理
|
|
*/
|
|
|
|
import { inject, type App, type Plugin } from 'vue'
|
|
import MosquitoShareButton from './components/MosquitoShareButton.vue'
|
|
import MosquitoPosterCard from './components/MosquitoPosterCard.vue'
|
|
import MosquitoLeaderboard from './components/MosquitoLeaderboard.vue'
|
|
import './style.css'
|
|
|
|
// 全局配置接口
|
|
export interface MosquitoConfig {
|
|
baseUrl: string
|
|
apiKey: string
|
|
userToken?: string
|
|
timeout?: number
|
|
retryCount?: number
|
|
enableLogging?: boolean
|
|
defaultTheme?: 'light' | 'dark'
|
|
locale?: string
|
|
}
|
|
|
|
// 默认配置
|
|
const defaultConfig: MosquitoConfig = {
|
|
baseUrl: '',
|
|
apiKey: '',
|
|
userToken: '',
|
|
timeout: 10000,
|
|
retryCount: 3,
|
|
enableLogging: false,
|
|
defaultTheme: 'light',
|
|
locale: 'zh-CN'
|
|
}
|
|
|
|
// 全局配置实例
|
|
let globalConfig = { ...defaultConfig }
|
|
|
|
// API错误类
|
|
export class MosquitoError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public code?: string,
|
|
public statusCode?: number,
|
|
public details?: any
|
|
) {
|
|
super(message)
|
|
this.name = 'MosquitoError'
|
|
}
|
|
}
|
|
|
|
export interface ShortenResponse {
|
|
code: string
|
|
path: string
|
|
originalUrl: string
|
|
trackingId?: string
|
|
}
|
|
|
|
export interface ApiResponse<T> {
|
|
code: number
|
|
message: string
|
|
data: T
|
|
meta?: {
|
|
pagination?: {
|
|
page: number
|
|
size: number
|
|
total: number
|
|
totalPages: number
|
|
hasNext: boolean
|
|
hasPrevious: boolean
|
|
}
|
|
extra?: Record<string, unknown>
|
|
}
|
|
error?: {
|
|
message?: string
|
|
details?: any
|
|
code?: string
|
|
}
|
|
timestamp?: string
|
|
traceId?: string
|
|
}
|
|
|
|
// 加载状态管理
|
|
export class LoadingManager {
|
|
private static loadingStates = new Map<string, boolean>()
|
|
private static callbacks = new Map<string, ((loading: boolean) => void)[]>()
|
|
|
|
static setLoading(key: string, loading: boolean) {
|
|
this.loadingStates.set(key, loading)
|
|
|
|
const callbacks = this.callbacks.get(key) || []
|
|
callbacks.forEach(callback => callback(loading))
|
|
}
|
|
|
|
static isLoading(key: string): boolean {
|
|
return this.loadingStates.get(key) || false
|
|
}
|
|
|
|
static onLoadingChange(key: string, callback: (loading: boolean) => void) {
|
|
if (!this.callbacks.has(key)) {
|
|
this.callbacks.set(key, [])
|
|
}
|
|
|
|
this.callbacks.get(key)!.push(callback)
|
|
|
|
// 返回清理函数
|
|
return () => {
|
|
const callbacks = this.callbacks.get(key)
|
|
if (callbacks) {
|
|
const index = callbacks.indexOf(callback)
|
|
if (index > -1) {
|
|
callbacks.splice(index, 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 增强的API客户端
|
|
export class EnhancedApiClient {
|
|
private config: MosquitoConfig
|
|
|
|
constructor(config: MosquitoConfig) {
|
|
this.config = config
|
|
}
|
|
|
|
private async request<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<ApiResponse<T>> {
|
|
const url = `${this.config.baseUrl}${endpoint}`
|
|
const controller = new AbortController()
|
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout)
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': this.config.apiKey,
|
|
...(this.config.userToken ? { Authorization: `Bearer ${this.config.userToken}` } : {}),
|
|
...options.headers,
|
|
},
|
|
signal: controller.signal,
|
|
})
|
|
|
|
clearTimeout(timeoutId)
|
|
|
|
const payload = await response.json().catch(() => ({}))
|
|
|
|
if (!response.ok) {
|
|
const message = payload.message || payload.error?.message || `HTTP ${response.status}: ${response.statusText}`
|
|
throw new MosquitoError(
|
|
message,
|
|
payload.error?.code || payload.code,
|
|
response.status,
|
|
payload.error?.details || payload.error || payload.details
|
|
)
|
|
}
|
|
|
|
if (typeof payload?.code === 'number' && payload.code >= 400) {
|
|
throw new MosquitoError(
|
|
payload.message || '请求失败',
|
|
payload.error?.code || payload.code,
|
|
response.status,
|
|
payload.error?.details || payload.error || payload.details
|
|
)
|
|
}
|
|
|
|
return payload as ApiResponse<T>
|
|
} catch (error) {
|
|
clearTimeout(timeoutId)
|
|
|
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
throw new MosquitoError('请求超时', 'TIMEOUT', 408)
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private async requestData<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
const response = await this.request<T>(endpoint, options)
|
|
return response.data
|
|
}
|
|
|
|
async getActivity(id: number): Promise<any> {
|
|
return this.requestData(`/api/v1/activities/${id}`)
|
|
}
|
|
|
|
async getActivities(): Promise<any[]> {
|
|
const response = await this.requestData<any>('/api/v1/activities')
|
|
// 兼容分页响应 (content 字段) 和数组响应
|
|
if (response && typeof response === 'object' && 'content' in response) {
|
|
return response.content || []
|
|
}
|
|
if (Array.isArray(response)) {
|
|
return response
|
|
}
|
|
return []
|
|
}
|
|
|
|
async createActivity(data: any): Promise<any> {
|
|
return this.requestData('/api/v1/activities', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
})
|
|
}
|
|
|
|
async getActivityStats(activityId: number): Promise<any> {
|
|
return this.requestData(`/api/v1/activities/${activityId}/stats`)
|
|
}
|
|
|
|
async getShareUrl(activityId: number, userId: number, template?: string): Promise<ShortenResponse> {
|
|
const params = new URLSearchParams({
|
|
activityId: activityId.toString(),
|
|
userId: userId.toString(),
|
|
...(template && { template }),
|
|
})
|
|
|
|
return this.requestData<ShortenResponse>(`/api/v1/me/share-url?${params}`)
|
|
}
|
|
|
|
async getPosterImage(activityId: number, userId: number, template?: string): Promise<Blob> {
|
|
const params = new URLSearchParams({
|
|
activityId: activityId.toString(),
|
|
userId: userId.toString(),
|
|
...(template && { template }),
|
|
})
|
|
|
|
const response = await fetch(`${this.config.baseUrl}/api/v1/me/poster/image?${params}`, {
|
|
headers: {
|
|
'X-API-Key': this.config.apiKey,
|
|
...(this.config.userToken ? { Authorization: `Bearer ${this.config.userToken}` } : {}),
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new MosquitoError('获取海报失败', 'POSTER_ERROR', response.status)
|
|
}
|
|
|
|
return response.blob()
|
|
}
|
|
|
|
async getLeaderboard(
|
|
activityId: number,
|
|
page: number = 0,
|
|
size: number = 20
|
|
): Promise<any> {
|
|
const params = new URLSearchParams({
|
|
activityId: activityId.toString(),
|
|
page: page.toString(),
|
|
size: size.toString(),
|
|
})
|
|
|
|
return this.request(`/api/v1/activities/${activityId}/leaderboard?${params}`)
|
|
}
|
|
|
|
async getShareMetrics(activityId: number): Promise<any> {
|
|
const params = new URLSearchParams({
|
|
activityId: activityId.toString(),
|
|
})
|
|
|
|
return this.requestData(`/api/v1/share/metrics?${params}`)
|
|
}
|
|
|
|
async getRewards(activityId: number, userId: number, page: number = 0, size: number = 20): Promise<any[]> {
|
|
const params = new URLSearchParams({
|
|
activityId: activityId.toString(),
|
|
userId: userId.toString(),
|
|
page: page.toString(),
|
|
size: size.toString(),
|
|
})
|
|
|
|
return this.requestData(`/api/v1/me/rewards?${params}`)
|
|
}
|
|
|
|
async exportLeaderboardCsv(activityId: number): Promise<string> {
|
|
const response = await fetch(`${this.config.baseUrl}/api/v1/activities/${activityId}/leaderboard/export`, {
|
|
headers: {
|
|
'X-API-Key': this.config.apiKey,
|
|
...(this.config.userToken ? { Authorization: `Bearer ${this.config.userToken}` } : {}),
|
|
},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new MosquitoError('导出排行榜失败', 'EXPORT_ERROR', response.status)
|
|
}
|
|
|
|
return response.text()
|
|
}
|
|
}
|
|
|
|
// Vue 插件
|
|
const MosquitoEnhancedPlugin: Plugin = {
|
|
install(app: App, options: MosquitoConfig) {
|
|
// 合并配置
|
|
globalConfig = { ...globalConfig, ...options }
|
|
|
|
// 注册全局属性
|
|
app.config.globalProperties.$mosquito = {
|
|
config: globalConfig,
|
|
apiClient: new EnhancedApiClient(globalConfig),
|
|
loadingManager: LoadingManager,
|
|
}
|
|
|
|
// 注册全局组件
|
|
app.component('MosquitoShareButton', MosquitoShareButton)
|
|
app.component('MosquitoPosterCard', MosquitoPosterCard)
|
|
app.component('MosquitoLeaderboard', MosquitoLeaderboard)
|
|
|
|
// 提供组合式API
|
|
app.provide('mosquito', {
|
|
config: globalConfig,
|
|
apiClient: new EnhancedApiClient(globalConfig),
|
|
loadingManager: LoadingManager,
|
|
})
|
|
},
|
|
}
|
|
|
|
// 组合式API
|
|
export function useMosquito() {
|
|
const mosquito = inject('mosquito') as any
|
|
|
|
if (!mosquito) {
|
|
throw new Error('Mosquito plugin is not installed. Please install it with app.use(MosquitoEnhancedPlugin, config)')
|
|
}
|
|
|
|
const getShareUrl = async (activityId: number, userId: number, template?: string) => {
|
|
const loadingKey = `share-url-${activityId}-${userId}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getShareUrl(activityId, userId, template)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取分享URL失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const getPosterImage = async (activityId: number, userId: number, template?: string) => {
|
|
const loadingKey = `poster-image-${activityId}-${userId}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getPosterImage(activityId, userId, template)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取海报失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const getLeaderboard = async (activityId: number, page: number = 0, size: number = 20) => {
|
|
const loadingKey = `leaderboard-${activityId}-${page}-${size}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getLeaderboard(activityId, page, size)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取排行榜失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const exportLeaderboardCsv = async (activityId: number) => {
|
|
const loadingKey = `export-csv-${activityId}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.exportLeaderboardCsv(activityId)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('导出排行榜失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const getActivity = async (id: number) => {
|
|
const loadingKey = `activity-${id}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getActivity(id)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取活动信息失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const getActivities = async () => {
|
|
const loadingKey = 'activities'
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getActivities()
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取活动列表失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const createActivity = async (data: any) => {
|
|
const loadingKey = 'create-activity'
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.createActivity(data)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('创建活动失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const getActivityStats = async (activityId: number) => {
|
|
const loadingKey = `activity-stats-${activityId}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getActivityStats(activityId)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取活动统计失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const getShareMetrics = async (activityId: number) => {
|
|
const loadingKey = `share-metrics-${activityId}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getShareMetrics(activityId)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取分享指标失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
const getRewards = async (activityId: number, userId: number, page: number = 0, size: number = 20) => {
|
|
const loadingKey = `rewards-${activityId}-${userId}-${page}-${size}`
|
|
LoadingManager.setLoading(loadingKey, true)
|
|
|
|
try {
|
|
return await mosquito.apiClient.getRewards(activityId, userId, page, size)
|
|
} catch (error) {
|
|
if (globalConfig.enableLogging) {
|
|
console.error('获取奖励失败:', error)
|
|
}
|
|
throw error
|
|
} finally {
|
|
LoadingManager.setLoading(loadingKey, false)
|
|
}
|
|
}
|
|
|
|
return {
|
|
config: mosquito.config,
|
|
getShareUrl,
|
|
getPosterImage,
|
|
getLeaderboard,
|
|
exportLeaderboardCsv,
|
|
getActivity,
|
|
getActivities,
|
|
createActivity,
|
|
getActivityStats,
|
|
getShareMetrics,
|
|
getRewards,
|
|
loadingManager: LoadingManager,
|
|
}
|
|
}
|
|
|
|
export default MosquitoEnhancedPlugin
|
|
export { MosquitoEnhancedPlugin }
|