diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/tenant-platform/src/api/im.ts b/tenant-platform/src/api/im.ts new file mode 100644 index 0000000..7c2a434 --- /dev/null +++ b/tenant-platform/src/api/im.ts @@ -0,0 +1,60 @@ +import axios from 'axios' + +const imClient = axios.create({ + baseURL: 'https://sentry.xuqinmin.com', + timeout: 15000, +}) + +imClient.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +export interface ImUser { + id: string + appId: string + userId: string + nickname: string + avatar?: string + status: 'ACTIVE' | 'BANNED' + gender: 'UNKNOWN' | 'MALE' | 'FEMALE' + createdAt: string +} + +export interface ImGroup { + id: string + appId: string + name: string + creatorId: string + memberIds: string[] + adminIds: string[] + createdAt: string +} + +export interface ImStats { + totalMessages: number + totalUsers: number + totalGroups: number + todayMessages: number +} + +export const imAdminApi = { + listUsers(appId: string, page = 0, size = 20) { + return imClient.get<{ data: { content: ImUser[]; totalElements: number; totalPages: number } }>( + '/api/im/admin/users', { params: { appId, page, size } }, + ) + }, + + updateUserStatus(appId: string, userId: string, status: 'ACTIVE' | 'BANNED') { + return imClient.put(`/api/im/admin/users/${userId}/status`, { status }, { params: { appId } }) + }, + + listGroups(appId: string) { + return imClient.get<{ data: ImGroup[] }>('/api/im/admin/groups', { params: { appId } }) + }, + + getStats(appId: string) { + return imClient.get<{ data: ImStats }>('/api/im/admin/stats', { params: { appId } }) + }, +} diff --git a/tenant-platform/src/api/update.ts b/tenant-platform/src/api/update.ts new file mode 100644 index 0000000..8bd236e --- /dev/null +++ b/tenant-platform/src/api/update.ts @@ -0,0 +1,94 @@ +import axios from 'axios' + +const updateClient = axios.create({ + baseURL: 'https://sentry.xuqinmin.com', + timeout: 30000, +}) + +updateClient.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +export interface AppVersion { + id: string + appId: string + platform: 'ANDROID' | 'IOS' + versionName: string + versionCode: number + downloadUrl?: string + changeLog?: string + forceUpdate: boolean + publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' + grayEnabled: boolean + grayPercent: number + appStoreUrl?: string + marketUrl?: string + createdAt: string +} + +export interface RnBundle { + id: string + appId: string + moduleId: string + platform: 'ANDROID' | 'IOS' + version: string + md5: string + minCommonVersion?: string + note?: string + publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED' + grayEnabled: boolean + grayPercent: number + createdAt: string +} + +export const updateAdminApi = { + listAppVersions(appId: string, platform: 'ANDROID' | 'IOS') { + return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', { + params: { appId, platform }, + }) + }, + + publishAppVersion(id: string) { + return updateClient.post(`/api/v1/updates/app/${id}/publish`) + }, + + unpublishAppVersion(id: string) { + return updateClient.post(`/api/v1/updates/app/${id}/unpublish`) + }, + + grayAppVersion(id: string, enabled: boolean, percent: number) { + return updateClient.post(`/api/v1/updates/app/${id}/gray`, { enabled, percent }) + }, + + uploadAppVersion(formData: FormData) { + return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + }, + + listRnBundles(appId: string, moduleId?: string, platform?: string) { + return updateClient.get<{ data: RnBundle[] }>('/api/v1/rn/list', { + params: { appId, ...(moduleId && { moduleId }), ...(platform && { platform }) }, + }) + }, + + publishRnBundle(id: string) { + return updateClient.post(`/api/v1/rn/${id}/publish`) + }, + + unpublishRnBundle(id: string) { + return updateClient.post(`/api/v1/rn/${id}/unpublish`) + }, + + grayRnBundle(id: string, enabled: boolean, percent: number) { + return updateClient.post(`/api/v1/rn/${id}/gray`, { enabled, percent }) + }, + + uploadRnBundle(formData: FormData) { + return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + }, +} diff --git a/tenant-platform/src/router/index.ts b/tenant-platform/src/router/index.ts index 765e4c2..304fd98 100644 --- a/tenant-platform/src/router/index.ts +++ b/tenant-platform/src/router/index.ts @@ -37,6 +37,14 @@ const router = createRouter({ path: 'apps/:id', component: () => import('@/views/apps/AppDetailView.vue'), }, + { + path: 'apps/:appId/im', + component: () => import('@/views/im/ImManagementView.vue'), + }, + { + path: 'apps/:appId/update', + component: () => import('@/views/update/VersionManagementView.vue'), + }, { path: 'accounts', component: () => import('@/views/accounts/SubAccountView.vue'), diff --git a/tenant-platform/src/views/apps/AppDetailView.vue b/tenant-platform/src/views/apps/AppDetailView.vue index a6e1398..001cb9d 100644 --- a/tenant-platform/src/views/apps/AppDetailView.vue +++ b/tenant-platform/src/views/apps/AppDetailView.vue @@ -49,6 +49,20 @@ 重新生成 +
+ + 即时通讯管理 → + + + 版本管理 → + +
diff --git a/tenant-platform/src/views/im/ImManagementView.vue b/tenant-platform/src/views/im/ImManagementView.vue new file mode 100644 index 0000000..2a73555 --- /dev/null +++ b/tenant-platform/src/views/im/ImManagementView.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/tenant-platform/src/views/update/VersionManagementView.vue b/tenant-platform/src/views/update/VersionManagementView.vue new file mode 100644 index 0000000..a914147 --- /dev/null +++ b/tenant-platform/src/views/update/VersionManagementView.vue @@ -0,0 +1,367 @@ + + + + +