Files
wenzi/frontend/index.ts

492 lines
13 KiB
TypeScript
Raw Normal View History

/**
* 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 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[]> {
return this.requestData('/api/v1/activities')
}
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<string> {
const params = new URLSearchParams({
activityId: activityId.toString(),
userId: userId.toString(),
...(template && { template }),
})
return this.requestData(`/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 }