比较提交
没有共同的提交。c84a27f4ec4844ffe1810211862c0666bf4d3d84 和 867227fc51bf699693cb5912014a0625a3dd533d 的历史完全不同。
c84a27f4ec
...
867227fc51
1
tenant-platform/components.d.ts
vendored
1
tenant-platform/components.d.ts
vendored
@ -17,7 +17,6 @@ declare module 'vue' {
|
|||||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
|||||||
@ -92,54 +92,6 @@ export interface ImStats {
|
|||||||
todayMessages: number
|
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 = {
|
export const imAdminApi = {
|
||||||
listUsers(appId: string, page = 0, size = 20) {
|
listUsers(appId: string, page = 0, size = 20) {
|
||||||
return imClient.get<{ data: PagedResult<ImUser> }>(
|
return imClient.get<{ data: PagedResult<ImUser> }>(
|
||||||
@ -159,108 +111,6 @@ export const imAdminApi = {
|
|||||||
return imClient.get<{ data: ImStats }>('/api/im/admin/stats', { params: { appId } })
|
return imClient.get<{ data: ImStats }>('/api/im/admin/stats', { params: { appId } })
|
||||||
},
|
},
|
||||||
|
|
||||||
getOperationLogs(appId: string, page = 0, size = 20) {
|
|
||||||
return imClient.get<{ data: PagedResult<OperationLog> }>('/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(
|
getMessages(
|
||||||
appId: string,
|
appId: string,
|
||||||
userA: string,
|
userA: string,
|
||||||
|
|||||||
@ -29,25 +29,12 @@ updateClient.interceptors.request.use((config) => {
|
|||||||
return 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 {
|
export interface AppVersion {
|
||||||
id: string
|
id: string
|
||||||
appId: string
|
appId: string
|
||||||
platform: 'ANDROID' | 'IOS'
|
platform: 'ANDROID' | 'IOS'
|
||||||
versionName: string
|
versionName: string
|
||||||
versionCode: number
|
versionCode: number
|
||||||
packageName?: string
|
|
||||||
downloadUrl?: string
|
downloadUrl?: string
|
||||||
changeLog?: string
|
changeLog?: string
|
||||||
forceUpdate: boolean
|
forceUpdate: boolean
|
||||||
@ -56,11 +43,6 @@ export interface AppVersion {
|
|||||||
grayPercent: number
|
grayPercent: number
|
||||||
appStoreUrl?: string
|
appStoreUrl?: string
|
||||||
marketUrl?: string
|
marketUrl?: string
|
||||||
scheduledPublishAt?: string
|
|
||||||
autoPublishAfterReview: boolean
|
|
||||||
webhookUrl?: string
|
|
||||||
storeSubmitTargets?: string
|
|
||||||
storeReviewStatus?: string
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,31 +61,6 @@ export interface RnBundle {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnifiedAppUploadItem {
|
|
||||||
fileKey: string
|
|
||||||
platform: 'ANDROID' | 'IOS'
|
|
||||||
versionName: string
|
|
||||||
versionCode: number
|
|
||||||
changeLog?: string
|
|
||||||
forceUpdate: boolean
|
|
||||||
appStoreUrl?: string
|
|
||||||
marketUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UnifiedRnUploadItem {
|
|
||||||
fileKey: string
|
|
||||||
moduleId: string
|
|
||||||
platform: 'ANDROID' | 'IOS'
|
|
||||||
version: string
|
|
||||||
minCommonVersion?: string
|
|
||||||
note?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UnifiedReleaseManifest {
|
|
||||||
appVersions: UnifiedAppUploadItem[]
|
|
||||||
rnBundles: UnifiedRnUploadItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateAdminApi = {
|
export const updateAdminApi = {
|
||||||
listAppVersions(appId: string, platform: 'ANDROID' | 'IOS') {
|
listAppVersions(appId: string, platform: 'ANDROID' | 'IOS') {
|
||||||
return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', {
|
return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', {
|
||||||
@ -152,42 +109,4 @@ export const updateAdminApi = {
|
|||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadUnifiedRelease(formData: FormData) {
|
|
||||||
return updateClient.post('/api/v1/updates/unified/upload', formData, {
|
|
||||||
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 },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,148 +170,6 @@
|
|||||||
@current-change="handleHistoryPageChange"
|
@current-change="handleHistoryPageChange"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="好友申请" name="friend-requests">
|
|
||||||
<div class="toolbar toolbar-space-between">
|
|
||||||
<div class="toolbar-group">
|
|
||||||
<el-select v-model="friendRequestDirection" style="width:140px" @change="loadFriendRequests">
|
|
||||||
<el-option label="收到的申请" value="incoming" />
|
|
||||||
<el-option label="发出的申请" value="outgoing" />
|
|
||||||
</el-select>
|
|
||||||
<el-button @click="loadFriendRequests" :loading="loadingFriendRequests">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
<el-button type="primary" @click="showSendFriendRequest = true">发送好友申请</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="friendRequests" v-loading="loadingFriendRequests" border stripe>
|
|
||||||
<el-table-column prop="createdAt" label="时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="fromUserId" label="申请人" width="160" />
|
|
||||||
<el-table-column prop="toUserId" label="接收人" width="160" />
|
|
||||||
<el-table-column prop="remark" label="申请信息" min-width="220" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="status" label="状态" width="110">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="friendRequestTagType(row.status)" size="small">{{ friendRequestStatusLabel(row.status) }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="reviewedAt" label="处理时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.reviewedAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="friendRequestDirection === 'incoming' && row.status === 'PENDING'">
|
|
||||||
<el-button link type="success" size="small" @click="approveFriend(row)">同意</el-button>
|
|
||||||
<el-button link type="danger" size="small" @click="declineFriend(row)">拒绝</el-button>
|
|
||||||
</template>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="入群申请" name="group-requests">
|
|
||||||
<div class="toolbar toolbar-space-between">
|
|
||||||
<div class="toolbar-group">
|
|
||||||
<el-input v-model="groupRequestGroupId" placeholder="群组ID" clearable style="width:260px" />
|
|
||||||
<el-button @click="loadGroupJoinRequests" :loading="loadingGroupRequests">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
<el-button type="primary" @click="showSendGroupJoinRequest = true">发起入群申请</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-alert
|
|
||||||
v-if="!groupRequestGroupId.trim()"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
title="请输入群组ID后刷新,才能查看该群的入群申请。"
|
|
||||||
style="margin-bottom:12px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-table :data="groupJoinRequests" v-loading="loadingGroupRequests" border stripe>
|
|
||||||
<el-table-column prop="createdAt" label="时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="requesterId" label="申请人" width="160" />
|
|
||||||
<el-table-column prop="groupId" label="群组ID" width="180" />
|
|
||||||
<el-table-column prop="remark" label="申请信息" min-width="220" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="status" label="状态" width="110">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="groupRequestTagType(row.status)" size="small">{{ groupRequestStatusLabel(row.status) }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="reviewedAt" label="处理时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.reviewedAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row.status === 'PENDING'">
|
|
||||||
<el-button link type="success" size="small" @click="approveGroupJoin(row)">同意</el-button>
|
|
||||||
<el-button link type="danger" size="small" @click="declineGroupJoin(row)">拒绝</el-button>
|
|
||||||
</template>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="操作日志" name="logs">
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-button @click="loadOperationLogs" :loading="loadingLogs">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="operationLogs" v-loading="loadingLogs" border stripe>
|
|
||||||
<el-table-column prop="createdAt" label="时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="operatorId" label="操作者" width="160" />
|
|
||||||
<el-table-column prop="action" label="动作" width="180" />
|
|
||||||
<el-table-column prop="resourceType" label="资源类型" width="120" />
|
|
||||||
<el-table-column prop="resourceId" label="资源ID" width="220" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="detail" label="详情" min-width="220" show-overflow-tooltip />
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-pagination
|
|
||||||
v-if="operationLogTotal > operationLogPageSize"
|
|
||||||
style="margin-top:16px"
|
|
||||||
layout="prev, pager, next"
|
|
||||||
:total="operationLogTotal"
|
|
||||||
:page-size="operationLogPageSize"
|
|
||||||
:current-page="operationLogPage + 1"
|
|
||||||
@current-change="handleOperationLogPageChange"
|
|
||||||
/>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="回调配置" name="webhooks">
|
|
||||||
<div class="toolbar toolbar-space-between">
|
|
||||||
<el-button type="primary" @click="openCreateWebhookDialog">新增回调</el-button>
|
|
||||||
<el-button @click="loadWebhooks" :loading="loadingWebhooks">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="webhooks" v-loading="loadingWebhooks" border stripe>
|
|
||||||
<el-table-column prop="url" label="回调地址" min-width="260" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="secret" label="密钥" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.secret ? '******' : '-' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="enabled" label="启用" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
|
||||||
{{ row.enabled ? '启用' : '停用' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button link type="primary" size="small" @click="openEditWebhookDialog(row)">编辑</el-button>
|
|
||||||
<el-button link type="danger" size="small" @click="deleteWebhook(row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@ -385,59 +243,6 @@
|
|||||||
<el-button type="primary" :loading="submittingCreateGroup" @click="submitCreateGroup">创建</el-button>
|
<el-button type="primary" :loading="submittingCreateGroup" @click="submitCreateGroup">创建</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="showSendFriendRequest" title="发送好友申请" width="420px" @closed="resetSendFriendRequestForm">
|
|
||||||
<el-form :model="sendFriendRequestForm" label-width="88px">
|
|
||||||
<el-form-item label="接收人ID">
|
|
||||||
<el-input v-model="sendFriendRequestForm.toUserId" placeholder="对方用户ID" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="申请信息">
|
|
||||||
<el-input v-model="sendFriendRequestForm.remark" type="textarea" :rows="3" placeholder="例如:我是 XXX,想和你加好友" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showSendFriendRequest = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submittingSendFriendRequest" @click="submitSendFriendRequest">发送</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="showSendGroupJoinRequest" title="发起入群申请" width="420px" @closed="resetSendGroupJoinRequestForm">
|
|
||||||
<el-form :model="sendGroupJoinRequestForm" label-width="88px">
|
|
||||||
<el-form-item label="群组ID">
|
|
||||||
<el-input v-model="sendGroupJoinRequestForm.groupId" placeholder="目标群组ID" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="申请信息">
|
|
||||||
<el-input v-model="sendGroupJoinRequestForm.remark" type="textarea" :rows="3" placeholder="例如:希望加入此群进行沟通" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showSendGroupJoinRequest = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submittingSendGroupJoinRequest" @click="submitSendGroupJoinRequest">发送</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="showWebhookDialog"
|
|
||||||
:title="editingWebhookId ? '编辑回调配置' : '新增回调配置'"
|
|
||||||
width="520px"
|
|
||||||
@closed="resetWebhookForm"
|
|
||||||
>
|
|
||||||
<el-form :model="webhookForm" label-width="90px">
|
|
||||||
<el-form-item label="回调地址">
|
|
||||||
<el-input v-model="webhookForm.url" placeholder="https://example.com/webhook" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="回调密钥">
|
|
||||||
<el-input v-model="webhookForm.secret" placeholder="留空则不修改" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="启用">
|
|
||||||
<el-switch v-model="webhookForm.enabled" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showWebhookDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submittingWebhook" @click="submitWebhookForm">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -446,17 +251,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import {
|
import { imAdminApi, type ImGroup, type ImMessage, type ImStats, type ImUser } from '@/api/im'
|
||||||
imAdminApi,
|
|
||||||
type FriendRequest,
|
|
||||||
type GroupJoinRequest,
|
|
||||||
type ImGroup,
|
|
||||||
type ImMessage,
|
|
||||||
type ImStats,
|
|
||||||
type ImUser,
|
|
||||||
type OperationLog,
|
|
||||||
type WebhookConfig,
|
|
||||||
} from '@/api/im'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const appId = route.params.appId as string
|
const appId = route.params.appId as string
|
||||||
@ -490,33 +285,6 @@ const historyPage = ref(0)
|
|||||||
const historyPageSize = 20
|
const historyPageSize = 20
|
||||||
const historyTotal = ref(0)
|
const historyTotal = ref(0)
|
||||||
|
|
||||||
const operationLogs = ref<OperationLog[]>([])
|
|
||||||
const loadingLogs = ref(false)
|
|
||||||
const operationLogPage = ref(0)
|
|
||||||
const operationLogPageSize = 20
|
|
||||||
const operationLogTotal = ref(0)
|
|
||||||
|
|
||||||
const webhooks = ref<WebhookConfig[]>([])
|
|
||||||
const loadingWebhooks = ref(false)
|
|
||||||
const showWebhookDialog = ref(false)
|
|
||||||
const submittingWebhook = ref(false)
|
|
||||||
const editingWebhookId = ref<string | null>(null)
|
|
||||||
const webhookForm = ref({ url: '', secret: '', enabled: true })
|
|
||||||
|
|
||||||
const friendRequests = ref<FriendRequest[]>([])
|
|
||||||
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<GroupJoinRequest[]>([])
|
|
||||||
const loadingGroupRequests = ref(false)
|
|
||||||
const groupRequestGroupId = ref('')
|
|
||||||
const showSendGroupJoinRequest = ref(false)
|
|
||||||
const submittingSendGroupJoinRequest = ref(false)
|
|
||||||
const sendGroupJoinRequestForm = ref({ groupId: '', remark: '' })
|
|
||||||
|
|
||||||
const historyForm = ref({
|
const historyForm = ref({
|
||||||
userA: '',
|
userA: '',
|
||||||
userB: '',
|
userB: '',
|
||||||
@ -557,22 +325,6 @@ function formatContent(content: string) {
|
|||||||
return content.length > 120 ? `${content.slice(0, 120)}...` : content
|
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) {
|
function parseIdList(value: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(value)
|
const parsed = JSON.parse(value)
|
||||||
@ -628,19 +380,6 @@ function resetCreateGroupForm() {
|
|||||||
selectedMembers.value = []
|
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() {
|
function resetMessageSearch() {
|
||||||
historyForm.value = {
|
historyForm.value = {
|
||||||
userA: '',
|
userA: '',
|
||||||
@ -714,120 +453,10 @@ 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() {
|
async function searchMessages() {
|
||||||
await loadMessages(0)
|
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) {
|
async function toggleUserStatus(user: ImUser) {
|
||||||
const nextStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE'
|
const nextStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE'
|
||||||
const action = nextStatus === 'BANNED' ? '封禁' : '解封'
|
const action = nextStatus === 'BANNED' ? '封禁' : '解封'
|
||||||
@ -852,108 +481,6 @@ async function revokeMessage(message: ImMessage) {
|
|||||||
loadMessages(historyPage.value)
|
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) {
|
async function dismissGroup(group: ImGroup) {
|
||||||
await ElMessageBox.confirm(`确认解散群组 ${group.name}?`, '解散群组', {
|
await ElMessageBox.confirm(`确认解散群组 ${group.name}?`, '解散群组', {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@ -1008,14 +535,6 @@ async function submitCreateGroup() {
|
|||||||
function handleTabChange(tab: string) {
|
function handleTabChange(tab: string) {
|
||||||
if (tab === 'groups' && groups.value.length === 0) {
|
if (tab === 'groups' && groups.value.length === 0) {
|
||||||
loadGroups()
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1028,14 +547,9 @@ function handleHistoryPageChange(page: number) {
|
|||||||
loadMessages(page - 1)
|
loadMessages(page - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOperationLogPageChange(page: number) {
|
|
||||||
loadOperationLogs(page - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadStats()
|
loadStats()
|
||||||
loadUsers()
|
loadUsers()
|
||||||
loadFriendRequests()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -1063,20 +577,6 @@ onMounted(() => {
|
|||||||
margin-bottom: 12px;
|
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 {
|
.search-results {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
|||||||
@ -3,9 +3,6 @@
|
|||||||
<el-page-header @back="$router.back()" :content="`版本管理 — ${appId}`" style="margin-bottom:20px" />
|
<el-page-header @back="$router.back()" :content="`版本管理 — ${appId}`" style="margin-bottom:20px" />
|
||||||
|
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="toolbar" style="margin-bottom: 16px">
|
|
||||||
<el-button type="primary" @click="showUnifiedUpload = true">一键上传</el-button>
|
|
||||||
</div>
|
|
||||||
<el-tabs v-model="activeTab">
|
<el-tabs v-model="activeTab">
|
||||||
<!-- App Versions -->
|
<!-- App Versions -->
|
||||||
<el-tab-pane label="App 整包版本" name="app">
|
<el-tab-pane label="App 整包版本" name="app">
|
||||||
@ -19,9 +16,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="appVersions" v-loading="loadingApp" border stripe>
|
<el-table :data="appVersions" v-loading="loadingApp" border stripe>
|
||||||
<el-table-column prop="versionName" label="版本名" width="110" />
|
<el-table-column prop="versionName" label="版本名" width="120" />
|
||||||
<el-table-column prop="versionCode" label="版本码" width="90" />
|
<el-table-column prop="versionCode" label="版本码" width="100" />
|
||||||
<el-table-column label="状态" width="140">
|
<el-table-column label="状态" width="120">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
|
<el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
|
||||||
<el-tag v-if="row.grayEnabled" type="warning" size="small" style="margin-left:4px">
|
<el-tag v-if="row.grayEnabled" type="warning" size="small" style="margin-left:4px">
|
||||||
@ -29,21 +26,7 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="应用商店" width="220" show-overflow-tooltip>
|
<el-table-column prop="forceUpdate" label="强制更新" width="100">
|
||||||
<template #default="{row}">
|
|
||||||
<template v-if="parseStoreReview(row.storeReviewStatus).length">
|
|
||||||
<el-tag
|
|
||||||
v-for="item in parseStoreReview(row.storeReviewStatus)"
|
|
||||||
:key="item.store"
|
|
||||||
:type="reviewTagType(item.state)"
|
|
||||||
size="small"
|
|
||||||
style="margin:2px"
|
|
||||||
>{{ storeLabel(item.store) }} · {{ reviewLabel(item.state) }}</el-tag>
|
|
||||||
</template>
|
|
||||||
<span v-else class="text-muted">—</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="forceUpdate" label="强制" width="70">
|
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<el-tag :type="row.forceUpdate ? 'danger' : 'info'" size="small">
|
<el-tag :type="row.forceUpdate ? 'danger' : 'info'" size="small">
|
||||||
{{ row.forceUpdate ? '是' : '否' }}
|
{{ row.forceUpdate ? '是' : '否' }}
|
||||||
@ -51,10 +34,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="changeLog" label="更新说明" show-overflow-tooltip />
|
<el-table-column prop="changeLog" label="更新说明" show-overflow-tooltip />
|
||||||
<el-table-column prop="createdAt" label="上传时间" width="160">
|
<el-table-column prop="createdAt" label="上传时间" width="180">
|
||||||
<template #default="{row}">{{ formatTime(row.createdAt) }}</template>
|
<template #default="{row}">{{ formatTime(row.createdAt) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="220" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.publishStatus === 'DRAFT'"
|
v-if="row.publishStatus === 'DRAFT'"
|
||||||
@ -68,10 +51,6 @@
|
|||||||
v-if="row.publishStatus === 'PUBLISHED'"
|
v-if="row.publishStatus === 'PUBLISHED'"
|
||||||
link type="danger" size="small"
|
link type="danger" size="small"
|
||||||
@click="unpublishApp(row.id)">下架</el-button>
|
@click="unpublishApp(row.id)">下架</el-button>
|
||||||
<el-button
|
|
||||||
v-if="row.downloadUrl && row.publishStatus !== 'DEPRECATED'"
|
|
||||||
link type="primary" size="small"
|
|
||||||
@click="openSubmitStoreDialog(row)">提交市场</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -110,60 +89,36 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="minCommonVersion" label="最低 Common 版本" width="160" />
|
<el-table-column prop="minCommonVersion" label="最低 Common 版本" width="160" />
|
||||||
<el-table-column prop="note" label="说明" show-overflow-tooltip />
|
<el-table-column prop="note" label="说明" show-overflow-tooltip />
|
||||||
<el-table-column prop="createdAt" label="上传时间" width="160">
|
<el-table-column prop="createdAt" label="上传时间" width="180">
|
||||||
<template #default="{row}">{{ formatTime(row.createdAt) }}</template>
|
<template #default="{row}">{{ formatTime(row.createdAt) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="160" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<el-button v-if="row.publishStatus === 'DRAFT'" link type="success" size="small" @click="publishRn(row.id)">发布</el-button>
|
<el-button
|
||||||
<el-button v-if="row.publishStatus === 'PUBLISHED'" link type="warning" size="small" @click="openGrayDialog(row, 'rn')">灰度</el-button>
|
v-if="row.publishStatus === 'DRAFT'"
|
||||||
<el-button v-if="row.publishStatus === 'PUBLISHED'" link type="danger" size="small" @click="unpublishRn(row.id)">下架</el-button>
|
link type="success" size="small"
|
||||||
|
@click="publishRn(row.id)">发布</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.publishStatus === 'PUBLISHED'"
|
||||||
|
link type="warning" size="small"
|
||||||
|
@click="openGrayDialog(row, 'rn')">灰度</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="row.publishStatus === 'PUBLISHED'"
|
||||||
|
link type="danger" size="small"
|
||||||
|
@click="unpublishRn(row.id)">下架</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- App Store Config -->
|
|
||||||
<el-tab-pane label="应用商店配置" name="store">
|
|
||||||
<div class="store-grid">
|
|
||||||
<el-card
|
|
||||||
v-for="store in STORE_DEFS"
|
|
||||||
:key="store.type"
|
|
||||||
class="store-card"
|
|
||||||
shadow="hover"
|
|
||||||
>
|
|
||||||
<div class="store-card-header">
|
|
||||||
<span class="store-card-name">{{ store.label }}</span>
|
|
||||||
<el-switch
|
|
||||||
:model-value="isStoreEnabled(store.type)"
|
|
||||||
:disabled="!getStoreConfig(store.type)"
|
|
||||||
@change="toggleStore(store.type, $event as boolean)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="store-card-status">
|
|
||||||
<el-tag v-if="getStoreConfig(store.type)" type="success" size="small">已配置</el-tag>
|
|
||||||
<el-tag v-else type="info" size="small">未配置</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="store-card-footer">
|
|
||||||
<el-button size="small" @click="openStoreConfigDialog(store)">
|
|
||||||
{{ getStoreConfig(store.type) ? '编辑凭据' : '配置凭据' }}
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="getStoreConfig(store.type)"
|
|
||||||
size="small" type="danger"
|
|
||||||
@click="removeStoreConfig(store.type)"
|
|
||||||
>删除</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- Gray Release Dialog -->
|
<!-- Gray Release Dialog -->
|
||||||
<el-dialog v-model="showGray" title="灰度发布配置" width="400px">
|
<el-dialog v-model="showGray" title="灰度发布配置" width="400px">
|
||||||
<el-form label-width="90px">
|
<el-form label-width="90px">
|
||||||
<el-form-item label="开启灰度"><el-switch v-model="grayForm.enabled" /></el-form-item>
|
<el-form-item label="开启灰度">
|
||||||
|
<el-switch v-model="grayForm.enabled" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="灰度比例" v-if="grayForm.enabled">
|
<el-form-item label="灰度比例" v-if="grayForm.enabled">
|
||||||
<el-slider v-model="grayForm.percent" :min="1" :max="100" show-input />
|
<el-slider v-model="grayForm.percent" :min="1" :max="100" show-input />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -174,125 +129,28 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- Submit to Store Dialog -->
|
|
||||||
<el-dialog v-model="showSubmitStore" title="提交应用市场" width="480px">
|
|
||||||
<div v-if="submitStoreVersion">
|
|
||||||
<p style="margin-bottom:12px">
|
|
||||||
版本 <strong>{{ submitStoreVersion.versionName }}</strong>
|
|
||||||
将由服务端自动提交至以下已配置且启用的市场:
|
|
||||||
</p>
|
|
||||||
<el-checkbox-group v-model="selectedStores">
|
|
||||||
<div v-for="store in enabledStores" :key="store.type" class="store-checkbox-row">
|
|
||||||
<el-checkbox :value="store.type">{{ store.label }}</el-checkbox>
|
|
||||||
<el-tag size="small" type="success" style="margin-left:8px">已配置</el-tag>
|
|
||||||
</div>
|
|
||||||
</el-checkbox-group>
|
|
||||||
<el-alert
|
|
||||||
v-if="!enabledStores.length"
|
|
||||||
type="warning"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
title="当前应用暂未配置任何应用商店凭据,请先在「应用商店配置」标签页中配置。"
|
|
||||||
style="margin-top:12px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showSubmitStore = false">取消</el-button>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
:disabled="!selectedStores.length"
|
|
||||||
@click="confirmSubmitToStores"
|
|
||||||
:loading="submittingToStores"
|
|
||||||
>提交审核</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- Store Credential Config Dialog -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="showStoreConfig"
|
|
||||||
:title="`配置 ${currentStoreDef?.label} 凭据`"
|
|
||||||
width="520px"
|
|
||||||
>
|
|
||||||
<el-form v-if="currentStoreDef" :model="storeConfigForm" label-width="160px">
|
|
||||||
<el-form-item label="启用">
|
|
||||||
<el-switch v-model="storeConfigForm.enabled" />
|
|
||||||
</el-form-item>
|
|
||||||
<template v-for="field in currentStoreDef.fields" :key="field.key">
|
|
||||||
<el-form-item :label="field.label">
|
|
||||||
<el-input
|
|
||||||
v-if="field.type === 'password'"
|
|
||||||
v-model="storeConfigForm.values[field.key]"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
/>
|
|
||||||
<el-input
|
|
||||||
v-else-if="field.type === 'textarea'"
|
|
||||||
v-model="storeConfigForm.values[field.key]"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
/>
|
|
||||||
<el-input
|
|
||||||
v-else
|
|
||||||
v-model="storeConfigForm.values[field.key]"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showStoreConfig = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="saveStoreConfig" :loading="savingStoreConfig">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- Upload App Version Dialog -->
|
<!-- Upload App Version Dialog -->
|
||||||
<el-dialog v-model="showUploadApp" title="上传 App 版本" width="540px">
|
<el-dialog v-model="showUploadApp" title="上传 App 版本" width="480px">
|
||||||
<el-form :model="appUploadForm" label-width="120px">
|
<el-form :model="appUploadForm" label-width="100px">
|
||||||
<el-divider content-position="left">基础信息</el-divider>
|
|
||||||
<el-form-item label="平台">
|
<el-form-item label="平台">
|
||||||
<el-select v-model="appUploadForm.platform">
|
<el-select v-model="appUploadForm.platform">
|
||||||
<el-option value="ANDROID" label="Android" />
|
<el-option value="ANDROID" label="Android" />
|
||||||
<el-option value="IOS" label="iOS" />
|
<el-option value="IOS" label="iOS" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="包名"><el-input v-model="appUploadForm.packageName" placeholder="com.example.app" /></el-form-item>
|
|
||||||
<el-form-item label="版本名称"><el-input v-model="appUploadForm.versionName" /></el-form-item>
|
<el-form-item label="版本名称"><el-input v-model="appUploadForm.versionName" /></el-form-item>
|
||||||
<el-form-item label="版本码"><el-input-number v-model="appUploadForm.versionCode" :min="1" /></el-form-item>
|
<el-form-item label="版本码"><el-input-number v-model="appUploadForm.versionCode" :min="1" /></el-form-item>
|
||||||
<el-form-item label="强制更新"><el-switch v-model="appUploadForm.forceUpdate" /></el-form-item>
|
<el-form-item label="强制更新"><el-switch v-model="appUploadForm.forceUpdate" /></el-form-item>
|
||||||
<el-form-item label="更新说明"><el-input v-model="appUploadForm.changeLog" type="textarea" :rows="3" /></el-form-item>
|
<el-form-item label="更新说明"><el-input v-model="appUploadForm.changeLog" type="textarea" :rows="3" /></el-form-item>
|
||||||
<el-form-item label="包文件">
|
<el-form-item label="APK 文件">
|
||||||
<el-upload :auto-upload="false" :limit="1" :on-change="f => appUploadForm.file = f.raw ?? null" accept=".apk,.ipa">
|
<el-upload
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:on-change="f => appUploadForm.file = f.raw ?? null"
|
||||||
|
accept=".apk,.ipa">
|
||||||
<el-button>选择文件</el-button>
|
<el-button>选择文件</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-divider content-position="left">发版配置</el-divider>
|
|
||||||
<el-form-item label="定时发布">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="appUploadForm.scheduledPublishAt"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="留空则手动发布"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Webhook 通知">
|
|
||||||
<el-input v-model="appUploadForm.webhookUrl" placeholder="审核状态变更时回调此 URL" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="自动提交市场">
|
|
||||||
<el-switch v-model="appUploadForm.autoSubmitStore" />
|
|
||||||
<span class="form-tip">上传后立即让服务端提交已配置的应用商店</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="appUploadForm.autoSubmitStore" label="目标市场">
|
|
||||||
<el-checkbox-group v-model="appUploadForm.storeTargets">
|
|
||||||
<el-checkbox v-for="s in enabledStores" :key="s.type" :value="s.type">{{ s.label }}</el-checkbox>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="审核后自动发布">
|
|
||||||
<el-switch v-model="appUploadForm.autoPublishAfterReview" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showUploadApp = false">取消</el-button>
|
<el-button @click="showUploadApp = false">取消</el-button>
|
||||||
@ -302,7 +160,7 @@
|
|||||||
|
|
||||||
<!-- Upload RN Bundle Dialog -->
|
<!-- Upload RN Bundle Dialog -->
|
||||||
<el-dialog v-model="showUploadRn" title="上传 RN Bundle" width="480px">
|
<el-dialog v-model="showUploadRn" title="上传 RN Bundle" width="480px">
|
||||||
<el-form :model="rnUploadForm" label-width="120px">
|
<el-form :model="rnUploadForm" label-width="100px">
|
||||||
<el-form-item label="模块ID"><el-input v-model="rnUploadForm.moduleId" /></el-form-item>
|
<el-form-item label="模块ID"><el-input v-model="rnUploadForm.moduleId" /></el-form-item>
|
||||||
<el-form-item label="平台">
|
<el-form-item label="平台">
|
||||||
<el-select v-model="rnUploadForm.platform">
|
<el-select v-model="rnUploadForm.platform">
|
||||||
@ -314,7 +172,11 @@
|
|||||||
<el-form-item label="最低 Common 版本"><el-input v-model="rnUploadForm.minCommonVersion" placeholder="如 1.0.0" /></el-form-item>
|
<el-form-item label="最低 Common 版本"><el-input v-model="rnUploadForm.minCommonVersion" placeholder="如 1.0.0" /></el-form-item>
|
||||||
<el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item>
|
<el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item>
|
||||||
<el-form-item label="Bundle 文件">
|
<el-form-item label="Bundle 文件">
|
||||||
<el-upload :auto-upload="false" :limit="1" :on-change="f => rnUploadForm.file = f.raw ?? null" accept=".bundle,.js">
|
<el-upload
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:on-change="f => rnUploadForm.file = f.raw ?? null"
|
||||||
|
accept=".bundle,.js">
|
||||||
<el-button>选择文件</el-button>
|
<el-button>选择文件</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -324,306 +186,113 @@
|
|||||||
<el-button type="primary" @click="submitRnUpload" :loading="uploadingRn">上传</el-button>
|
<el-button type="primary" @click="submitRnUpload" :loading="uploadingRn">上传</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- Unified Release Dialog -->
|
|
||||||
<el-dialog v-model="showUnifiedUpload" title="一键上传" width="960px">
|
|
||||||
<el-form label-width="120px">
|
|
||||||
<el-divider content-position="left">Android / iOS 整包</el-divider>
|
|
||||||
<div class="unified-grid">
|
|
||||||
<div v-for="item in unifiedAppForms" :key="item.platform" class="unified-block">
|
|
||||||
<div class="unified-block-title">{{ item.platform === 'ANDROID' ? 'Android' : 'iOS' }}</div>
|
|
||||||
<el-form-item label="启用"><el-switch v-model="item.enabled" /></el-form-item>
|
|
||||||
<template v-if="item.enabled">
|
|
||||||
<el-form-item label="包名"><el-input v-model="item.packageName" placeholder="com.example.app" /></el-form-item>
|
|
||||||
<el-form-item label="版本名称"><el-input v-model="item.versionName" /></el-form-item>
|
|
||||||
<el-form-item label="版本码"><el-input-number v-model="item.versionCode" :min="1" /></el-form-item>
|
|
||||||
<el-form-item label="强制更新"><el-switch v-model="item.forceUpdate" /></el-form-item>
|
|
||||||
<el-form-item label="更新说明"><el-input v-model="item.changeLog" type="textarea" :rows="2" /></el-form-item>
|
|
||||||
<el-form-item label="包文件">
|
|
||||||
<el-upload :auto-upload="false" :limit="1" :on-change="f => item.file = f.raw ?? null"
|
|
||||||
:accept="item.platform === 'ANDROID' ? '.apk' : '.ipa'">
|
|
||||||
<el-button>选择文件</el-button>
|
|
||||||
</el-upload>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-divider content-position="left">发版配置</el-divider>
|
|
||||||
<el-row :gutter="16">
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="定时发布">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="unifiedReleaseOptions.scheduledPublishAt"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="留空手动发布"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
|
||||||
clearable
|
|
||||||
style="width:100%"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="Webhook URL">
|
|
||||||
<el-input v-model="unifiedReleaseOptions.webhookUrl" placeholder="审核结果回调" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="16">
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="自动提交市场">
|
|
||||||
<el-switch v-model="unifiedReleaseOptions.autoSubmitStore" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="审核后自动发布">
|
|
||||||
<el-switch v-model="unifiedReleaseOptions.autoPublishAfterReview" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-form-item v-if="unifiedReleaseOptions.autoSubmitStore" label="目标市场">
|
|
||||||
<el-checkbox-group v-model="unifiedReleaseOptions.storeTargets">
|
|
||||||
<el-checkbox v-for="s in enabledStores" :key="s.type" :value="s.type">{{ s.label }}</el-checkbox>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-divider content-position="left">
|
|
||||||
Bundle 插件
|
|
||||||
<el-button link type="primary" @click="addUnifiedBundle" style="margin-left:8px">新增插件</el-button>
|
|
||||||
</el-divider>
|
|
||||||
<div v-for="(item, index) in unifiedBundleForms" :key="item.key" class="unified-bundle-row">
|
|
||||||
<div class="unified-bundle-head">
|
|
||||||
<div>插件 {{ index + 1 }}</div>
|
|
||||||
<el-button link type="danger" @click="removeUnifiedBundle(index)" :disabled="unifiedBundleForms.length === 1">删除</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="unified-grid">
|
|
||||||
<el-form-item label="模块ID"><el-input v-model="item.moduleId" /></el-form-item>
|
|
||||||
<el-form-item label="平台">
|
|
||||||
<el-select v-model="item.platform">
|
|
||||||
<el-option value="ANDROID" label="Android" />
|
|
||||||
<el-option value="IOS" label="iOS" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="版本"><el-input v-model="item.version" /></el-form-item>
|
|
||||||
<el-form-item label="最低 Common 版本"><el-input v-model="item.minCommonVersion" /></el-form-item>
|
|
||||||
<el-form-item label="说明"><el-input v-model="item.note" type="textarea" :rows="2" /></el-form-item>
|
|
||||||
<el-form-item label="Bundle 文件">
|
|
||||||
<el-upload :auto-upload="false" :limit="1" :on-change="f => item.file = f.raw ?? null" accept=".bundle,.js">
|
|
||||||
<el-button>选择文件</el-button>
|
|
||||||
</el-upload>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showUnifiedUpload = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitUnifiedUpload" :loading="uploadingUnified">上传</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import {
|
import { updateAdminApi, type AppVersion, type RnBundle } from '@/api/update'
|
||||||
updateAdminApi,
|
|
||||||
type AppVersion,
|
|
||||||
type RnBundle,
|
|
||||||
type StoreConfig,
|
|
||||||
type StoreType,
|
|
||||||
type StoreReviewState,
|
|
||||||
} from '@/api/update'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const appId = route.params.appId as string
|
const appId = route.params.appId as string
|
||||||
|
|
||||||
const activeTab = ref('app')
|
const activeTab = ref('app')
|
||||||
const appPlatform = ref<'ANDROID' | 'IOS'>('ANDROID')
|
const appPlatform = ref<'ANDROID' | 'IOS'>('ANDROID')
|
||||||
const rnPlatform = ref<'ANDROID' | 'IOS' | ''>('')
|
const rnPlatform = ref('')
|
||||||
const rnModuleFilter = ref('')
|
const rnModuleFilter = ref('')
|
||||||
|
|
||||||
const appVersions = ref<AppVersion[]>([])
|
const appVersions = ref<AppVersion[]>([])
|
||||||
const loadingApp = ref(false)
|
const loadingApp = ref(false)
|
||||||
const rnBundles = ref<RnBundle[]>([])
|
const rnBundles = ref<RnBundle[]>([])
|
||||||
const loadingRn = ref(false)
|
const loadingRn = ref(false)
|
||||||
const storeConfigs = ref<StoreConfig[]>([])
|
|
||||||
|
|
||||||
// ── Store definitions ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
type FieldDef = { key: string; label: string; type?: 'password' | 'textarea'; placeholder?: string }
|
|
||||||
|
|
||||||
const STORE_DEFS: { type: StoreType; label: string; fields: FieldDef[] }[] = [
|
|
||||||
{
|
|
||||||
type: 'HUAWEI',
|
|
||||||
label: '华为应用市场',
|
|
||||||
fields: [
|
|
||||||
{ key: 'clientId', label: 'Client ID', placeholder: 'AppGallery Connect Client ID' },
|
|
||||||
{ key: 'clientSecret', label: 'Client Secret', type: 'password', placeholder: 'AppGallery Connect Client Secret' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'MI',
|
|
||||||
label: '小米应用商店',
|
|
||||||
fields: [
|
|
||||||
{ key: 'username', label: '用户名' },
|
|
||||||
{ key: 'privateKey', label: 'RSA 私钥', type: 'textarea', placeholder: '-----BEGIN PRIVATE KEY-----\n...' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'OPPO',
|
|
||||||
label: 'OPPO 软件商店',
|
|
||||||
fields: [
|
|
||||||
{ key: 'clientId', label: 'Client ID' },
|
|
||||||
{ key: 'clientSecret', label: 'Client Secret', type: 'password' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'VIVO',
|
|
||||||
label: 'vivo 应用商店',
|
|
||||||
fields: [
|
|
||||||
{ key: 'accessKey', label: 'Access Key' },
|
|
||||||
{ key: 'accessSecret', label: 'Access Secret', type: 'password' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'HONOR',
|
|
||||||
label: '荣耀应用市场',
|
|
||||||
fields: [
|
|
||||||
{ key: 'clientId', label: 'Client ID', placeholder: 'AppGallery Connect Client ID' },
|
|
||||||
{ key: 'clientSecret', label: 'Client Secret', type: 'password' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'APP_STORE',
|
|
||||||
label: 'Apple App Store',
|
|
||||||
fields: [
|
|
||||||
{ key: 'teamId', label: 'Team ID' },
|
|
||||||
{ key: 'keyId', label: 'Key ID' },
|
|
||||||
{ key: 'privateKey', label: 'P8 私钥内容', type: 'textarea', placeholder: '-----BEGIN PRIVATE KEY-----\n...' },
|
|
||||||
{ key: 'bundleId', label: 'Bundle ID', placeholder: 'com.example.app' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'GOOGLE_PLAY',
|
|
||||||
label: 'Google Play',
|
|
||||||
fields: [
|
|
||||||
{ key: 'serviceAccountJson', label: '服务账号 JSON', type: 'textarea', placeholder: '{ "type": "service_account", ... }' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// ── Store config helpers ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function getStoreConfig(type: StoreType): StoreConfig | undefined {
|
|
||||||
return storeConfigs.value.find(c => c.storeType === type)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStoreEnabled(type: StoreType): boolean {
|
|
||||||
return getStoreConfig(type)?.enabled ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabledStores = computed(() =>
|
|
||||||
STORE_DEFS.filter(s => isStoreEnabled(s.type))
|
|
||||||
)
|
|
||||||
|
|
||||||
async function toggleStore(type: StoreType, enabled: boolean) {
|
|
||||||
const cfg = getStoreConfig(type)
|
|
||||||
if (!cfg) return
|
|
||||||
try {
|
|
||||||
await updateAdminApi.saveStoreConfig(appId, type, cfg.configJson ?? '{}', enabled)
|
|
||||||
await loadStoreConfigs()
|
|
||||||
} catch { ElMessage.error('操作失败') }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStoreConfigs() {
|
|
||||||
try {
|
|
||||||
const res = await updateAdminApi.getStoreConfigs(appId)
|
|
||||||
storeConfigs.value = res.data.data
|
|
||||||
} catch { /* silent */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Store config dialog ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const showStoreConfig = ref(false)
|
|
||||||
const savingStoreConfig = ref(false)
|
|
||||||
const currentStoreDef = ref<typeof STORE_DEFS[0] | null>(null)
|
|
||||||
const storeConfigForm = ref<{ enabled: boolean; values: Record<string, string> }>({
|
|
||||||
enabled: true, values: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
function openStoreConfigDialog(store: typeof STORE_DEFS[0]) {
|
|
||||||
currentStoreDef.value = store
|
|
||||||
const existing = getStoreConfig(store.type)
|
|
||||||
let values: Record<string, string> = {}
|
|
||||||
if (existing?.configJson) {
|
|
||||||
try { values = JSON.parse(existing.configJson) } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
storeConfigForm.value = { enabled: existing?.enabled ?? true, values }
|
|
||||||
showStoreConfig.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveStoreConfig() {
|
|
||||||
if (!currentStoreDef.value) return
|
|
||||||
savingStoreConfig.value = true
|
|
||||||
try {
|
|
||||||
await updateAdminApi.saveStoreConfig(
|
|
||||||
appId,
|
|
||||||
currentStoreDef.value.type,
|
|
||||||
JSON.stringify(storeConfigForm.value.values),
|
|
||||||
storeConfigForm.value.enabled,
|
|
||||||
)
|
|
||||||
ElMessage.success('凭据已保存')
|
|
||||||
showStoreConfig.value = false
|
|
||||||
await loadStoreConfigs()
|
|
||||||
} catch { ElMessage.error('保存失败') }
|
|
||||||
finally { savingStoreConfig.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeStoreConfig(type: StoreType) {
|
|
||||||
await ElMessageBox.confirm('确认删除此应用商店凭据?', '提示', { type: 'warning' })
|
|
||||||
try {
|
|
||||||
await updateAdminApi.deleteStoreConfig(appId, type)
|
|
||||||
ElMessage.success('已删除')
|
|
||||||
await loadStoreConfigs()
|
|
||||||
} catch { ElMessage.error('删除失败') }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Submit to stores ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const showSubmitStore = ref(false)
|
|
||||||
const submittingToStores = ref(false)
|
|
||||||
const submitStoreVersion = ref<AppVersion | null>(null)
|
|
||||||
const selectedStores = ref<StoreType[]>([])
|
|
||||||
|
|
||||||
function openSubmitStoreDialog(row: AppVersion) {
|
|
||||||
submitStoreVersion.value = row
|
|
||||||
selectedStores.value = enabledStores.value.map(s => s.type)
|
|
||||||
showSubmitStore.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmSubmitToStores() {
|
|
||||||
if (!submitStoreVersion.value || !selectedStores.value.length) return
|
|
||||||
submittingToStores.value = true
|
|
||||||
try {
|
|
||||||
await updateAdminApi.executeSubmitToStores(submitStoreVersion.value.id, selectedStores.value)
|
|
||||||
ElMessage.success('已提交,服务端正在向应用市场上传,审核状态将通过 Webhook 或刷新页面查看')
|
|
||||||
showSubmitStore.value = false
|
|
||||||
loadAppVersions()
|
|
||||||
} catch { ElMessage.error('提交失败') }
|
|
||||||
finally { submittingToStores.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Gray release ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const showGray = ref(false)
|
const showGray = ref(false)
|
||||||
const submittingGray = ref(false)
|
const submittingGray = ref(false)
|
||||||
const grayTarget = ref<{ id: string; type: 'app' | 'rn' } | null>(null)
|
const grayTarget = ref<{ id: string; type: 'app' | 'rn' } | null>(null)
|
||||||
const grayForm = ref({ enabled: true, percent: 10 })
|
const grayForm = ref({ enabled: true, percent: 10 })
|
||||||
|
|
||||||
|
const showUploadApp = ref(false)
|
||||||
|
const uploadingApp = ref(false)
|
||||||
|
const appUploadForm = ref({
|
||||||
|
platform: 'ANDROID' as 'ANDROID' | 'IOS',
|
||||||
|
versionName: '',
|
||||||
|
versionCode: 1,
|
||||||
|
forceUpdate: false,
|
||||||
|
changeLog: '',
|
||||||
|
file: null as File | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const showUploadRn = ref(false)
|
||||||
|
const uploadingRn = ref(false)
|
||||||
|
const rnUploadForm = ref({
|
||||||
|
moduleId: '',
|
||||||
|
platform: 'ANDROID' as 'ANDROID' | 'IOS',
|
||||||
|
version: '',
|
||||||
|
minCommonVersion: '',
|
||||||
|
note: '',
|
||||||
|
file: null as File | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatTime(t: string) {
|
||||||
|
return t ? new Date(t).toLocaleString('zh-CN') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(row: { publishStatus: string }) {
|
||||||
|
return { DRAFT: '草稿', PUBLISHED: '已发布', DEPRECATED: '已下架' }[row.publishStatus] ?? row.publishStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTagType(row: { publishStatus: string }) {
|
||||||
|
return { DRAFT: '', PUBLISHED: 'success', DEPRECATED: 'info' }[row.publishStatus] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAppVersions() {
|
||||||
|
loadingApp.value = true
|
||||||
|
try {
|
||||||
|
const res = await updateAdminApi.listAppVersions(appId, appPlatform.value)
|
||||||
|
appVersions.value = res.data.data
|
||||||
|
} catch { ElMessage.error('加载失败') }
|
||||||
|
finally { loadingApp.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRnBundles() {
|
||||||
|
loadingRn.value = true
|
||||||
|
try {
|
||||||
|
const res = await updateAdminApi.listRnBundles(
|
||||||
|
appId,
|
||||||
|
rnModuleFilter.value || undefined,
|
||||||
|
rnPlatform.value || undefined,
|
||||||
|
)
|
||||||
|
rnBundles.value = res.data.data
|
||||||
|
} catch { ElMessage.error('加载失败') }
|
||||||
|
finally { loadingRn.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishApp(id: string) {
|
||||||
|
await updateAdminApi.publishAppVersion(id)
|
||||||
|
ElMessage.success('已发布')
|
||||||
|
loadAppVersions()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unpublishApp(id: string) {
|
||||||
|
await updateAdminApi.unpublishAppVersion(id)
|
||||||
|
ElMessage.success('已下架')
|
||||||
|
loadAppVersions()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishRn(id: string) {
|
||||||
|
await updateAdminApi.publishRnBundle(id)
|
||||||
|
ElMessage.success('已发布')
|
||||||
|
loadRnBundles()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unpublishRn(id: string) {
|
||||||
|
await updateAdminApi.unpublishRnBundle(id)
|
||||||
|
ElMessage.success('已下架')
|
||||||
|
loadRnBundles()
|
||||||
|
}
|
||||||
|
|
||||||
function openGrayDialog(row: { id: string }, type: 'app' | 'rn') {
|
function openGrayDialog(row: { id: string }, type: 'app' | 'rn') {
|
||||||
grayTarget.value = { id: row.id, type }
|
grayTarget.value = { id: row.id, type }
|
||||||
grayForm.value = { enabled: true, percent: 10 }
|
grayForm.value = { enabled: true, percent: 10 }
|
||||||
@ -647,25 +316,6 @@ async function submitGray() {
|
|||||||
} finally { submittingGray.value = false }
|
} finally { submittingGray.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Upload app ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const showUploadApp = ref(false)
|
|
||||||
const uploadingApp = ref(false)
|
|
||||||
const appUploadForm = ref({
|
|
||||||
platform: 'ANDROID' as 'ANDROID' | 'IOS',
|
|
||||||
packageName: '',
|
|
||||||
versionName: '',
|
|
||||||
versionCode: 1,
|
|
||||||
forceUpdate: false,
|
|
||||||
changeLog: '',
|
|
||||||
file: null as File | null,
|
|
||||||
scheduledPublishAt: '',
|
|
||||||
webhookUrl: '',
|
|
||||||
autoSubmitStore: false,
|
|
||||||
storeTargets: [] as StoreType[],
|
|
||||||
autoPublishAfterReview: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function submitAppUpload() {
|
async function submitAppUpload() {
|
||||||
const f = appUploadForm.value
|
const f = appUploadForm.value
|
||||||
if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')
|
if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')
|
||||||
@ -677,212 +327,41 @@ async function submitAppUpload() {
|
|||||||
fd.append('versionName', f.versionName)
|
fd.append('versionName', f.versionName)
|
||||||
fd.append('versionCode', String(f.versionCode))
|
fd.append('versionCode', String(f.versionCode))
|
||||||
fd.append('forceUpdate', String(f.forceUpdate))
|
fd.append('forceUpdate', String(f.forceUpdate))
|
||||||
if (f.packageName) fd.append('packageName', f.packageName)
|
|
||||||
if (f.changeLog) fd.append('changeLog', f.changeLog)
|
if (f.changeLog) fd.append('changeLog', f.changeLog)
|
||||||
if (f.scheduledPublishAt) fd.append('scheduledPublishAt', f.scheduledPublishAt)
|
|
||||||
if (f.webhookUrl) fd.append('webhookUrl', f.webhookUrl)
|
|
||||||
if (f.storeTargets.length) fd.append('storeSubmitTargets', JSON.stringify(f.storeTargets))
|
|
||||||
fd.append('autoPublishAfterReview', String(f.autoPublishAfterReview))
|
|
||||||
if (f.file) fd.append('apkFile', f.file)
|
if (f.file) fd.append('apkFile', f.file)
|
||||||
const resp = await updateAdminApi.uploadAppVersion(fd)
|
await updateAdminApi.uploadAppVersion(fd)
|
||||||
if (f.autoSubmitStore && f.storeTargets.length) {
|
ElMessage.success('上传成功,版本已创建为草稿')
|
||||||
const versionId = resp.data.data.id
|
|
||||||
await updateAdminApi.executeSubmitToStores(versionId, f.storeTargets)
|
|
||||||
}
|
|
||||||
ElMessage.success('上传成功')
|
|
||||||
showUploadApp.value = false
|
showUploadApp.value = false
|
||||||
loadAppVersions()
|
loadAppVersions()
|
||||||
} finally { uploadingApp.value = false }
|
} finally { uploadingApp.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Upload RN bundle ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const showUploadRn = ref(false)
|
|
||||||
const uploadingRn = ref(false)
|
|
||||||
const rnUploadForm = ref({
|
|
||||||
moduleId: '', platform: 'ANDROID' as 'ANDROID' | 'IOS',
|
|
||||||
version: '', minCommonVersion: '', note: '', file: null as File | null,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function submitRnUpload() {
|
async function submitRnUpload() {
|
||||||
const f = rnUploadForm.value
|
const f = rnUploadForm.value
|
||||||
if (!f.moduleId || !f.version || !f.file) return ElMessage.warning('请填写模块ID、版本和 Bundle 文件')
|
if (!f.moduleId || !f.version || !f.file) return ElMessage.warning('请填写模块ID、版本和 Bundle 文件')
|
||||||
uploadingRn.value = true
|
uploadingRn.value = true
|
||||||
try {
|
try {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append('appId', appId); fd.append('moduleId', f.moduleId)
|
fd.append('appId', appId)
|
||||||
fd.append('platform', f.platform); fd.append('version', f.version)
|
fd.append('moduleId', f.moduleId)
|
||||||
|
fd.append('platform', f.platform)
|
||||||
|
fd.append('version', f.version)
|
||||||
if (f.minCommonVersion) fd.append('minCommonVersion', f.minCommonVersion)
|
if (f.minCommonVersion) fd.append('minCommonVersion', f.minCommonVersion)
|
||||||
if (f.note) fd.append('note', f.note)
|
if (f.note) fd.append('note', f.note)
|
||||||
fd.append('bundle', f.file)
|
fd.append('bundle', f.file)
|
||||||
await updateAdminApi.uploadRnBundle(fd)
|
await updateAdminApi.uploadRnBundle(fd)
|
||||||
ElMessage.success('Bundle 上传成功')
|
ElMessage.success('上传成功,Bundle 已创建为草稿')
|
||||||
showUploadRn.value = false
|
showUploadRn.value = false
|
||||||
loadRnBundles()
|
loadRnBundles()
|
||||||
} finally { uploadingRn.value = false }
|
} finally { uploadingRn.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Unified upload ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const showUnifiedUpload = ref(false)
|
|
||||||
const uploadingUnified = ref(false)
|
|
||||||
const unifiedAppForms = ref([
|
|
||||||
{ key: 'ANDROID', enabled: true, platform: 'ANDROID' as const, packageName: '', versionName: '', versionCode: 1, forceUpdate: false, changeLog: '', file: null as File | null },
|
|
||||||
{ key: 'IOS', enabled: true, platform: 'IOS' as const, packageName: '', versionName: '', versionCode: 1, forceUpdate: false, changeLog: '', file: null as File | null },
|
|
||||||
])
|
|
||||||
const unifiedBundleForms = ref([
|
|
||||||
{ key: 'bundle-0', moduleId: '', platform: 'ANDROID' as 'ANDROID' | 'IOS', version: '', minCommonVersion: '', note: '', file: null as File | null },
|
|
||||||
])
|
|
||||||
const unifiedReleaseOptions = ref({
|
|
||||||
scheduledPublishAt: '',
|
|
||||||
webhookUrl: '',
|
|
||||||
autoSubmitStore: false,
|
|
||||||
storeTargets: [] as StoreType[],
|
|
||||||
autoPublishAfterReview: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
function addUnifiedBundle() {
|
|
||||||
unifiedBundleForms.value.push({ key: `bundle-${Date.now()}`, moduleId: '', platform: 'ANDROID', version: '', minCommonVersion: '', note: '', file: null })
|
|
||||||
}
|
|
||||||
function removeUnifiedBundle(index: number) {
|
|
||||||
unifiedBundleForms.value.splice(index, 1)
|
|
||||||
if (!unifiedBundleForms.value.length) addUnifiedBundle()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitUnifiedUpload() {
|
|
||||||
const appItems = unifiedAppForms.value
|
|
||||||
.filter(i => i.enabled && i.file)
|
|
||||||
.map(i => ({ fileKey: i.key, platform: i.platform, versionName: i.versionName, versionCode: i.versionCode, changeLog: i.changeLog || undefined, forceUpdate: i.forceUpdate, packageName: i.packageName || undefined }))
|
|
||||||
const bundleItems = unifiedBundleForms.value
|
|
||||||
.filter(i => i.file)
|
|
||||||
.map(i => ({ fileKey: i.key, moduleId: i.moduleId, platform: i.platform, version: i.version, minCommonVersion: i.minCommonVersion || undefined, note: i.note || undefined }))
|
|
||||||
|
|
||||||
if (!appItems.length && !bundleItems.length) return ElMessage.warning('请至少选择一个包或 Bundle 文件')
|
|
||||||
for (const i of unifiedAppForms.value) {
|
|
||||||
if (i.enabled && i.file && (!i.versionName || !i.versionCode)) return ElMessage.warning('请填写整包版本信息')
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = unifiedReleaseOptions.value
|
|
||||||
uploadingUnified.value = true
|
|
||||||
try {
|
|
||||||
const fd = new FormData()
|
|
||||||
fd.append('appId', appId)
|
|
||||||
fd.append('manifest', JSON.stringify({ appVersions: appItems, rnBundles: bundleItems }))
|
|
||||||
if (opts.scheduledPublishAt) fd.append('scheduledPublishAt', opts.scheduledPublishAt)
|
|
||||||
if (opts.webhookUrl) fd.append('webhookUrl', opts.webhookUrl)
|
|
||||||
if (opts.storeTargets.length) fd.append('storeSubmitTargets', JSON.stringify(opts.storeTargets))
|
|
||||||
fd.append('autoPublishAfterReview', String(opts.autoPublishAfterReview))
|
|
||||||
for (const i of unifiedAppForms.value) if (i.enabled && i.file) fd.append(i.key, i.file)
|
|
||||||
for (const i of unifiedBundleForms.value) if (i.file) fd.append(i.key, i.file)
|
|
||||||
|
|
||||||
const resp = await updateAdminApi.uploadUnifiedRelease(fd)
|
|
||||||
|
|
||||||
// If auto-submit to stores, trigger for each uploaded app version
|
|
||||||
if (opts.autoSubmitStore && opts.storeTargets.length) {
|
|
||||||
const versionIds: string[] = (resp.data as any)?.data?.appVersionIds ?? []
|
|
||||||
for (const vid of versionIds) {
|
|
||||||
await updateAdminApi.executeSubmitToStores(vid, opts.storeTargets).catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success('一键上传成功')
|
|
||||||
showUnifiedUpload.value = false
|
|
||||||
loadAppVersions(); loadRnBundles()
|
|
||||||
} finally { uploadingUnified.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Version list actions ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function loadAppVersions() {
|
|
||||||
loadingApp.value = true
|
|
||||||
try {
|
|
||||||
const res = await updateAdminApi.listAppVersions(appId, appPlatform.value)
|
|
||||||
appVersions.value = res.data.data
|
|
||||||
} catch { ElMessage.error('加载失败') }
|
|
||||||
finally { loadingApp.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRnBundles() {
|
|
||||||
loadingRn.value = true
|
|
||||||
try {
|
|
||||||
const res = await updateAdminApi.listRnBundles(appId, rnModuleFilter.value || undefined, rnPlatform.value || undefined)
|
|
||||||
rnBundles.value = res.data.data
|
|
||||||
} catch { ElMessage.error('加载失败') }
|
|
||||||
finally { loadingRn.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function publishApp(id: string) { await updateAdminApi.publishAppVersion(id); ElMessage.success('已发布'); loadAppVersions() }
|
|
||||||
async function unpublishApp(id: string) { await updateAdminApi.unpublishAppVersion(id); ElMessage.success('已下架'); loadAppVersions() }
|
|
||||||
async function publishRn(id: string) { await updateAdminApi.publishRnBundle(id); ElMessage.success('已发布'); loadRnBundles() }
|
|
||||||
async function unpublishRn(id: string) { await updateAdminApi.unpublishRnBundle(id); ElMessage.success('已下架'); loadRnBundles() }
|
|
||||||
|
|
||||||
// ── Formatting helpers ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function formatTime(t: string) { return t ? new Date(t).toLocaleString('zh-CN') : '-' }
|
|
||||||
function statusLabel(row: { publishStatus: string }) { return { DRAFT: '草稿', PUBLISHED: '已发布', DEPRECATED: '已下架' }[row.publishStatus] ?? row.publishStatus }
|
|
||||||
function statusTagType(row: { publishStatus: string }) { return { DRAFT: '', PUBLISHED: 'success', DEPRECATED: 'info' }[row.publishStatus] ?? '' }
|
|
||||||
function storeLabel(type: string) { return STORE_DEFS.find(s => s.type === type)?.label ?? type }
|
|
||||||
function reviewLabel(state: string): string { return { PENDING: '待提交', UNDER_REVIEW: '审核中', APPROVED: '已通过', REJECTED: '已拒绝' }[state] ?? state }
|
|
||||||
function reviewTagType(state: string): string { return { PENDING: 'info', UNDER_REVIEW: 'warning', APPROVED: 'success', REJECTED: 'danger' }[state] ?? '' }
|
|
||||||
|
|
||||||
function parseStoreReview(json?: string): { store: string; state: string }[] {
|
|
||||||
if (!json) return []
|
|
||||||
try {
|
|
||||||
const m = JSON.parse(json) as Record<string, string>
|
|
||||||
return Object.entries(m).map(([store, state]) => ({ store, state }))
|
|
||||||
} catch { return [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAppVersions()
|
loadAppVersions()
|
||||||
loadRnBundles()
|
loadRnBundles()
|
||||||
loadStoreConfigs()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
|
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||||
.text-muted { color: var(--el-text-color-placeholder); font-size: 12px; }
|
|
||||||
.form-tip { font-size: 12px; color: var(--el-text-color-secondary); margin-left: 8px; }
|
|
||||||
|
|
||||||
/* Store config grid */
|
|
||||||
.store-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.store-card { cursor: default; }
|
|
||||||
.store-card-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.store-card-name { font-weight: 600; font-size: 14px; }
|
|
||||||
.store-card-status { margin-bottom: 12px; }
|
|
||||||
.store-card-footer { display: flex; gap: 8px; }
|
|
||||||
|
|
||||||
/* Submit store checkbox */
|
|
||||||
.store-checkbox-row { padding: 6px 0; }
|
|
||||||
|
|
||||||
/* Unified upload */
|
|
||||||
.unified-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.unified-block, .unified-bundle-row {
|
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
background: var(--el-bg-color-page);
|
|
||||||
}
|
|
||||||
.unified-block-title, .unified-bundle-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.unified-bundle-row + .unified-bundle-row { margin-top: 12px; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户