From c84a27f4ec4844ffe1810211862c0666bf4d3d84 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Wed, 29 Apr 2026 00:36:41 +0800 Subject: [PATCH] feat(tenant-platform): add app store config tab, store review status, enhanced release dialogs Co-Authored-By: Claude Sonnet 4.6 --- tenant-platform/src/api/im.ts | 150 ++++ tenant-platform/src/api/update.ts | 50 ++ .../src/views/im/ImManagementView.vue | 502 ++++++++++- .../views/update/VersionManagementView.vue | 802 ++++++++++++------ 4 files changed, 1258 insertions(+), 246 deletions(-) diff --git a/tenant-platform/src/api/im.ts b/tenant-platform/src/api/im.ts index 3fa724d..bd8b626 100644 --- a/tenant-platform/src/api/im.ts +++ b/tenant-platform/src/api/im.ts @@ -92,6 +92,54 @@ export interface ImStats { todayMessages: number } +export interface OperationLog { + id: string + appId: string + operatorId: string + action: string + resourceType: string + resourceId?: string | null + detail?: string | null + createdAt: number +} + +export interface FriendRequest { + id: string + appId: string + fromUserId: string + toUserId: string + remark?: string | null + status: 'PENDING' | 'ACCEPTED' | 'REJECTED' + createdAt: number + reviewedAt?: number | null +} + +export interface WebhookConfig { + id: string + appId: string + url: string + secret?: string | null + enabled: boolean + createdAt: number +} + +export interface WebhookConfigForm { + url: string + secret?: string + enabled?: boolean +} + +export interface GroupJoinRequest { + id: string + appId: string + groupId: string + requesterId: string + remark?: string | null + status: 'PENDING' | 'ACCEPTED' | 'REJECTED' + createdAt: number + reviewedAt?: number | null +} + export const imAdminApi = { listUsers(appId: string, page = 0, size = 20) { return imClient.get<{ data: PagedResult }>( @@ -111,6 +159,108 @@ export const imAdminApi = { return imClient.get<{ data: ImStats }>('/api/im/admin/stats', { params: { appId } }) }, + getOperationLogs(appId: string, page = 0, size = 20) { + return imClient.get<{ data: PagedResult }>('/api/im/admin/operation-logs', { + params: { appId, page, size }, + }) + }, + + listWebhooks(appId: string) { + return imClient.get<{ data: WebhookConfig[] }>('/api/im/admin/webhooks', { + params: { appId }, + }) + }, + + createWebhook(appId: string, form: WebhookConfigForm) { + return imClient.post<{ data: WebhookConfig }>('/api/im/admin/webhooks', form, { + params: { appId }, + }) + }, + + updateWebhook(appId: string, webhookId: string, form: WebhookConfigForm) { + return imClient.put<{ data: WebhookConfig }>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}`, form, { + params: { appId }, + }) + }, + + deleteWebhook(appId: string, webhookId: string) { + return imClient.delete<{ data: null }>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}`, { + params: { appId }, + }) + }, + + listFriendRequests(appId: string, direction: 'incoming' | 'outgoing' = 'incoming') { + return imClient.get<{ data: FriendRequest[] }>('/api/im/friend-requests', { + params: { appId, direction }, + }) + }, + + sendFriendRequest(appId: string, toUserId: string, remark?: string) { + return imClient.post<{ data: FriendRequest }>( + '/api/im/friend-requests', + null, + { + params: { + appId, + toUserId, + ...(remark ? { remark } : {}), + }, + }, + ) + }, + + acceptFriendRequest(appId: string, requestId: string) { + return imClient.post<{ data: FriendRequest }>( + `/api/im/friend-requests/${encodeURIComponent(requestId)}/accept`, + null, + { params: { appId } }, + ) + }, + + rejectFriendRequest(appId: string, requestId: string) { + return imClient.post<{ data: FriendRequest }>( + `/api/im/friend-requests/${encodeURIComponent(requestId)}/reject`, + null, + { params: { appId } }, + ) + }, + + listGroupJoinRequests(appId: string, groupId: string) { + return imClient.get<{ data: GroupJoinRequest[] }>( + `/api/im/groups/${encodeURIComponent(groupId)}/join-requests`, + { params: { appId } }, + ) + }, + + sendGroupJoinRequest(appId: string, groupId: string, remark?: string) { + return imClient.post<{ data: GroupJoinRequest }>( + `/api/im/groups/${encodeURIComponent(groupId)}/join-requests`, + null, + { + params: { + appId, + ...(remark ? { remark } : {}), + }, + }, + ) + }, + + acceptGroupJoinRequest(appId: string, groupId: string, requestId: string) { + return imClient.post<{ data: GroupJoinRequest }>( + `/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/accept`, + null, + { params: { appId } }, + ) + }, + + rejectGroupJoinRequest(appId: string, groupId: string, requestId: string) { + return imClient.post<{ data: GroupJoinRequest }>( + `/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/reject`, + null, + { params: { appId } }, + ) + }, + getMessages( appId: string, userA: string, diff --git a/tenant-platform/src/api/update.ts b/tenant-platform/src/api/update.ts index 1773179..a20b121 100644 --- a/tenant-platform/src/api/update.ts +++ b/tenant-platform/src/api/update.ts @@ -29,12 +29,25 @@ updateClient.interceptors.request.use((config) => { return config }) +export type StoreType = 'HUAWEI' | 'MI' | 'OPPO' | 'VIVO' | 'HONOR' | 'APP_STORE' | 'GOOGLE_PLAY' +export type StoreReviewState = 'PENDING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED' + +export interface StoreConfig { + id: string + appId: string + storeType: StoreType + configJson?: string + enabled: boolean + updatedAt: string +} + export interface AppVersion { id: string appId: string platform: 'ANDROID' | 'IOS' versionName: string versionCode: number + packageName?: string downloadUrl?: string changeLog?: string forceUpdate: boolean @@ -43,6 +56,11 @@ export interface AppVersion { grayPercent: number appStoreUrl?: string marketUrl?: string + scheduledPublishAt?: string + autoPublishAfterReview: boolean + webhookUrl?: string + storeSubmitTargets?: string + storeReviewStatus?: string createdAt: string } @@ -140,4 +158,36 @@ export const updateAdminApi = { headers: { 'Content-Type': 'multipart/form-data' }, }) }, + + // ── Store config ──────────────────────────────────────────────────────── + + getStoreConfigs(appId: string) { + return updateClient.get<{ data: StoreConfig[] }>('/api/v1/updates/store/configs', { params: { appId } }) + }, + + saveStoreConfig(appId: string, storeType: StoreType, configJson: string, enabled: boolean) { + return updateClient.put<{ data: StoreConfig }>( + `/api/v1/updates/store/configs/${storeType}`, + { configJson, enabled }, + { params: { appId } }, + ) + }, + + deleteStoreConfig(appId: string, storeType: StoreType) { + return updateClient.delete(`/api/v1/updates/store/configs/${storeType}`, { params: { appId } }) + }, + + executeSubmitToStores(versionId: string, storeTypes: StoreType[]) { + return updateClient.post<{ data: AppVersion }>( + `/api/v1/updates/store/app/${versionId}/execute-submit`, + { storeTypes }, + ) + }, + + updateStoreReview(versionId: string, storeType: StoreType, state: StoreReviewState) { + return updateClient.post<{ data: AppVersion }>( + `/api/v1/updates/store/app/${versionId}/review`, + { storeType, state }, + ) + }, } diff --git a/tenant-platform/src/views/im/ImManagementView.vue b/tenant-platform/src/views/im/ImManagementView.vue index 410d0b6..401769a 100644 --- a/tenant-platform/src/views/im/ImManagementView.vue +++ b/tenant-platform/src/views/im/ImManagementView.vue @@ -170,6 +170,148 @@ @current-change="handleHistoryPageChange" /> + + +
+
+ + + + + 刷新 +
+ 发送好友申请 +
+ + + + + + + + + + + + + + + + + + +
+ + +
+
+ + 刷新 +
+ 发起入群申请 +
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+ 刷新 +
+ + + + + + + + + + + + + +
+ + +
+ 新增回调 + 刷新 +
+ + + + + + + + + + + + + + + + +
@@ -243,6 +385,59 @@ 创建 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -251,7 +446,17 @@ import { computed, onMounted, ref } from 'vue' import { useRoute } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { Search } from '@element-plus/icons-vue' -import { imAdminApi, type ImGroup, type ImMessage, type ImStats, type ImUser } from '@/api/im' +import { + imAdminApi, + type FriendRequest, + type GroupJoinRequest, + type ImGroup, + type ImMessage, + type ImStats, + type ImUser, + type OperationLog, + type WebhookConfig, +} from '@/api/im' const route = useRoute() const appId = route.params.appId as string @@ -285,6 +490,33 @@ const historyPage = ref(0) const historyPageSize = 20 const historyTotal = ref(0) +const operationLogs = ref([]) +const loadingLogs = ref(false) +const operationLogPage = ref(0) +const operationLogPageSize = 20 +const operationLogTotal = ref(0) + +const webhooks = ref([]) +const loadingWebhooks = ref(false) +const showWebhookDialog = ref(false) +const submittingWebhook = ref(false) +const editingWebhookId = ref(null) +const webhookForm = ref({ url: '', secret: '', enabled: true }) + +const friendRequests = ref([]) +const loadingFriendRequests = ref(false) +const friendRequestDirection = ref<'incoming' | 'outgoing'>('incoming') +const showSendFriendRequest = ref(false) +const submittingSendFriendRequest = ref(false) +const sendFriendRequestForm = ref({ toUserId: '', remark: '' }) + +const groupJoinRequests = ref([]) +const loadingGroupRequests = ref(false) +const groupRequestGroupId = ref('') +const showSendGroupJoinRequest = ref(false) +const submittingSendGroupJoinRequest = ref(false) +const sendGroupJoinRequestForm = ref({ groupId: '', remark: '' }) + const historyForm = ref({ userA: '', userB: '', @@ -325,6 +557,22 @@ function formatContent(content: string) { return content.length > 120 ? `${content.slice(0, 120)}...` : content } +function friendRequestStatusLabel(status: string) { + return { PENDING: '待处理', ACCEPTED: '已同意', REJECTED: '已拒绝' }[status] ?? status +} + +function friendRequestTagType(status: string) { + return { PENDING: 'warning', ACCEPTED: 'success', REJECTED: 'danger' }[status] ?? 'info' +} + +function groupRequestStatusLabel(status: string) { + return { PENDING: '待处理', ACCEPTED: '已同意', REJECTED: '已拒绝' }[status] ?? status +} + +function groupRequestTagType(status: string) { + return { PENDING: 'warning', ACCEPTED: 'success', REJECTED: 'danger' }[status] ?? 'info' +} + function parseIdList(value: string) { try { const parsed = JSON.parse(value) @@ -380,6 +628,19 @@ function resetCreateGroupForm() { selectedMembers.value = [] } +function resetSendFriendRequestForm() { + sendFriendRequestForm.value = { toUserId: '', remark: '' } +} + +function resetSendGroupJoinRequestForm() { + sendGroupJoinRequestForm.value = { groupId: '', remark: '' } +} + +function resetWebhookForm() { + editingWebhookId.value = null + webhookForm.value = { url: '', secret: '', enabled: true } +} + function resetMessageSearch() { historyForm.value = { userA: '', @@ -453,10 +714,120 @@ async function loadMessages(page = historyPage.value) { } } +async function loadOperationLogs(page = operationLogPage.value) { + loadingLogs.value = true + try { + const res = await imAdminApi.getOperationLogs(appId, page, operationLogPageSize) + operationLogs.value = res.data.data.content + operationLogTotal.value = res.data.data.totalElements + operationLogPage.value = page + } catch { + ElMessage.error('加载操作日志失败') + } finally { + loadingLogs.value = false + } +} + +async function loadWebhooks() { + loadingWebhooks.value = true + try { + const res = await imAdminApi.listWebhooks(appId) + webhooks.value = res.data.data + } catch { + ElMessage.error('加载回调配置失败') + } finally { + loadingWebhooks.value = false + } +} + +async function loadFriendRequests() { + loadingFriendRequests.value = true + try { + const res = await imAdminApi.listFriendRequests(appId, friendRequestDirection.value) + friendRequests.value = res.data.data + } catch { + ElMessage.error('加载好友申请失败') + } finally { + loadingFriendRequests.value = false + } +} + +async function loadGroupJoinRequests() { + const groupId = groupRequestGroupId.value.trim() + if (!groupId) { + ElMessage.warning('请先输入群组ID') + return + } + loadingGroupRequests.value = true + try { + const res = await imAdminApi.listGroupJoinRequests(appId, groupId) + groupJoinRequests.value = res.data.data + } catch { + ElMessage.error('加载入群申请失败') + } finally { + loadingGroupRequests.value = false + } +} + async function searchMessages() { await loadMessages(0) } +function openCreateWebhookDialog() { + resetWebhookForm() + showWebhookDialog.value = true +} + +function openEditWebhookDialog(row: WebhookConfig) { + editingWebhookId.value = row.id + webhookForm.value = { + url: row.url, + secret: '', + enabled: row.enabled, + } + showWebhookDialog.value = true +} + +async function submitWebhookForm() { + if (!webhookForm.value.url.trim()) { + ElMessage.warning('请填写回调地址') + return + } + submittingWebhook.value = true + try { + const payload = { + url: webhookForm.value.url.trim(), + secret: webhookForm.value.secret.trim() || undefined, + enabled: webhookForm.value.enabled, + } + if (editingWebhookId.value) { + await imAdminApi.updateWebhook(appId, editingWebhookId.value, payload) + ElMessage.success('回调配置已更新') + } else { + await imAdminApi.createWebhook(appId, payload) + ElMessage.success('回调配置已创建') + } + showWebhookDialog.value = false + resetWebhookForm() + loadWebhooks() + } catch { + ElMessage.error('保存回调配置失败') + } finally { + submittingWebhook.value = false + } +} + +async function deleteWebhook(row: WebhookConfig) { + await ElMessageBox.confirm(`确认删除回调配置 ${row.url}?`, '删除回调配置', { + type: 'warning', + confirmButtonText: '确认', + cancelButtonText: '取消', + }) + await imAdminApi.deleteWebhook(appId, row.id) + ElMessage.success('回调配置已删除') + loadWebhooks() +} + async function toggleUserStatus(user: ImUser) { const nextStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE' const action = nextStatus === 'BANNED' ? '封禁' : '解封' @@ -481,6 +852,108 @@ async function revokeMessage(message: ImMessage) { loadMessages(historyPage.value) } +async function approveFriend(request: FriendRequest) { + await ElMessageBox.confirm(`确认同意好友申请 ${request.fromUserId} -> ${request.toUserId}?`, '同意好友申请', { + type: 'warning', + confirmButtonText: '确认', + cancelButtonText: '取消', + }) + await imAdminApi.acceptFriendRequest(appId, request.id) + ElMessage.success('已同意好友申请') + loadFriendRequests() +} + +async function declineFriend(request: FriendRequest) { + await ElMessageBox.confirm(`确认拒绝好友申请 ${request.fromUserId} -> ${request.toUserId}?`, '拒绝好友申请', { + type: 'warning', + confirmButtonText: '确认', + cancelButtonText: '取消', + }) + await imAdminApi.rejectFriendRequest(appId, request.id) + ElMessage.success('已拒绝好友申请') + loadFriendRequests() +} + +async function submitSendFriendRequest() { + if (!sendFriendRequestForm.value.toUserId.trim()) { + ElMessage.warning('请填写接收人ID') + return + } + submittingSendFriendRequest.value = true + try { + await imAdminApi.sendFriendRequest( + appId, + sendFriendRequestForm.value.toUserId.trim(), + sendFriendRequestForm.value.remark.trim() || undefined, + ) + ElMessage.success('好友申请已发送') + showSendFriendRequest.value = false + resetSendFriendRequestForm() + loadFriendRequests() + } catch { + ElMessage.error('发送失败') + } finally { + submittingSendFriendRequest.value = false + } +} + +async function approveGroupJoin(request: GroupJoinRequest) { + const groupId = groupRequestGroupId.value.trim() + if (!groupId) { + ElMessage.warning('请先输入群组ID') + return + } + await ElMessageBox.confirm(`确认同意群 ${groupId} 的入群申请 ${request.requesterId}?`, '同意入群申请', { + type: 'warning', + confirmButtonText: '确认', + cancelButtonText: '取消', + }) + await imAdminApi.acceptGroupJoinRequest(appId, groupId, request.id) + ElMessage.success('已同意入群申请') + loadGroupJoinRequests() +} + +async function declineGroupJoin(request: GroupJoinRequest) { + const groupId = groupRequestGroupId.value.trim() + if (!groupId) { + ElMessage.warning('请先输入群组ID') + return + } + await ElMessageBox.confirm(`确认拒绝群 ${groupId} 的入群申请 ${request.requesterId}?`, '拒绝入群申请', { + type: 'warning', + confirmButtonText: '确认', + cancelButtonText: '取消', + }) + await imAdminApi.rejectGroupJoinRequest(appId, groupId, request.id) + ElMessage.success('已拒绝入群申请') + loadGroupJoinRequests() +} + +async function submitSendGroupJoinRequest() { + const groupId = sendGroupJoinRequestForm.value.groupId.trim() + if (!groupId) { + ElMessage.warning('请填写群组ID') + return + } + submittingSendGroupJoinRequest.value = true + try { + await imAdminApi.sendGroupJoinRequest( + appId, + groupId, + sendGroupJoinRequestForm.value.remark.trim() || undefined, + ) + ElMessage.success('入群申请已发送') + showSendGroupJoinRequest.value = false + resetSendGroupJoinRequestForm() + groupRequestGroupId.value = groupId + loadGroupJoinRequests() + } catch { + ElMessage.error('发送失败') + } finally { + submittingSendGroupJoinRequest.value = false + } +} + async function dismissGroup(group: ImGroup) { await ElMessageBox.confirm(`确认解散群组 ${group.name}?`, '解散群组', { type: 'warning', @@ -535,6 +1008,14 @@ async function submitCreateGroup() { function handleTabChange(tab: string) { if (tab === 'groups' && groups.value.length === 0) { loadGroups() + } else if (tab === 'friend-requests' && friendRequests.value.length === 0) { + loadFriendRequests() + } else if (tab === 'group-requests' && groupJoinRequests.value.length === 0 && groupRequestGroupId.value.trim()) { + loadGroupJoinRequests() + } else if (tab === 'logs' && operationLogs.value.length === 0) { + loadOperationLogs() + } else if (tab === 'webhooks' && webhooks.value.length === 0) { + loadWebhooks() } } @@ -547,9 +1028,14 @@ function handleHistoryPageChange(page: number) { loadMessages(page - 1) } +function handleOperationLogPageChange(page: number) { + loadOperationLogs(page - 1) +} + onMounted(() => { loadStats() loadUsers() + loadFriendRequests() }) @@ -577,6 +1063,20 @@ onMounted(() => { margin-bottom: 12px; } +.toolbar-space-between { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.toolbar-group { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + .search-results { width: 100%; margin-top: 4px; diff --git a/tenant-platform/src/views/update/VersionManagementView.vue b/tenant-platform/src/views/update/VersionManagementView.vue index 37d9ca1..1de0b42 100644 --- a/tenant-platform/src/views/update/VersionManagementView.vue +++ b/tenant-platform/src/views/update/VersionManagementView.vue @@ -19,9 +19,9 @@ - - - + + + - + + + + - + - + @@ -92,36 +110,60 @@ - + - + + + + +
+ +
+ {{ store.label }} + +
+
+ 已配置 + 未配置 +
+ +
+
+
- - - + @@ -132,28 +174,125 @@ + + +
+

+ 版本 {{ submitStoreVersion.versionName }} + 将由服务端自动提交至以下已配置且启用的市场: +

+ +
+ {{ store.label }} + 已配置 +
+
+ +
+ +
+ + + + + + + + + + + + - - + + + 基础信息 + - - + + 选择文件 + 发版配置 + + + + + + + + + 上传后立即让服务端提交已配置的应用商店 + + + + {{ s.label }} + + + + +