diff --git a/tenant-platform/src/api/update.ts b/tenant-platform/src/api/update.ts index 6de4276..c645895 100644 --- a/tenant-platform/src/api/update.ts +++ b/tenant-platform/src/api/update.ts @@ -83,7 +83,6 @@ export type StoreType = 'HUAWEI' | 'MI' | 'OPPO' | 'VIVO' | 'HONOR' | 'APP_STORE export type StoreReviewState = 'PENDING' | 'SUBMITTING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED' export type PublishMode = 'MANUAL' | 'NOW' | 'SCHEDULED' | 'AUTO_REVIEW' export type GrayMode = 'PERCENT' | 'MEMBERS' -export type GraySelectionSource = 'LOCAL' | 'CALLBACK' export interface PublishConfig { id: string @@ -107,15 +106,15 @@ export interface OperationLog { export interface GrayMember { userId: string - name?: string - groupName?: string - extraJson?: string - updatedAt?: string + name: string + source: 'SYNC' | 'MANUAL' + active: boolean + tags: string[] } -export interface GrayMemberGroup { - groupName: string - members: GrayMember[] +export interface GrayTag { + tagName: string + memberCount: number } export interface StoreConfig { @@ -264,8 +263,8 @@ export const updateAdminApi = { enabled: boolean grayMode: GrayMode percent?: number - memberIds?: string[] - selectionSource?: GraySelectionSource + groupNames?: string[] + extraMemberIds?: string[] }) { return updateClient.post(`/api/v1/updates/app/${id}/gray`, body) }, @@ -298,8 +297,8 @@ export const updateAdminApi = { enabled: boolean grayMode: GrayMode percent?: number - memberIds?: string[] - selectionSource?: GraySelectionSource + groupNames?: string[] + extraMemberIds?: string[] }) { return updateClient.post(`/api/v1/rn/${id}/gray`, body) }, @@ -404,15 +403,47 @@ export const updateAdminApi = { }) }, - listGrayMembers(appKey: string, keyword?: string, groupName?: string) { - return updateClient.get<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members', { - params: { appKey, ...(keyword && { keyword }), ...(groupName && { groupName }) }, - }) - }, - - syncGrayMembers(appKey: string) { - return updateClient.post<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members/sync', null, { + listGrayMembers(appKey: string) { + return updateClient.get<{ data: GrayMember[] }>('/api/v1/updates/gray/members', { params: { appKey }, }) }, + + addGrayMembers(appKey: string, userIds: string[], name?: string) { + return updateClient.post('/api/v1/updates/gray/members', { appKey, userIds, name }) + }, + + deleteGrayMember(appKey: string, userId: string) { + return updateClient.delete(`/api/v1/updates/gray/members/${userId}`, { params: { appKey } }) + }, + + syncGrayMembers(appKey: string, members: { userId: string; name?: string }[]) { + return updateClient.post<{ data: { added: number; updated: number; removed: number } }>( + '/api/v1/updates/gray/members/sync', { appKey, members }) + }, + + importGrayMembersFromIm(appKey: string) { + return updateClient.post<{ data: { added: number; updated: number; removed: number } }>( + '/api/v1/updates/gray/members/import-im', null, { params: { appKey } }) + }, + + listGrayTags(appKey: string) { + return updateClient.get<{ data: GrayTag[] }>('/api/v1/updates/gray/tags', { params: { appKey } }) + }, + + createGrayTag(appKey: string, tagName: string, userIds: string[]) { + return updateClient.post('/api/v1/updates/gray/tags', { appKey, tagName, userIds }) + }, + + deleteGrayTag(appKey: string, tagName: string) { + return updateClient.delete(`/api/v1/updates/gray/tags/${tagName}`, { params: { appKey } }) + }, + + addMembersToTag(appKey: string, tagName: string, userIds: string[]) { + return updateClient.post(`/api/v1/updates/gray/tags/${tagName}/members`, { appKey, userIds }) + }, + + removeMembersFromTag(appKey: string, tagName: string, userIds: string[]) { + return updateClient.delete(`/api/v1/updates/gray/tags/${tagName}/members`, { data: { appKey, userIds } }) + }, } diff --git a/tenant-platform/src/views/update/VersionManagementView.vue b/tenant-platform/src/views/update/VersionManagementView.vue index 48753aa..cdd651e 100644 --- a/tenant-platform/src/views/update/VersionManagementView.vue +++ b/tenant-platform/src/views/update/VersionManagementView.vue @@ -335,26 +335,11 @@ - - + + - - - - - - - - - - - - 同步后本地选择 - 回调直接返回成员 - - - - 同步成员 + + @@ -417,7 +402,7 @@ - + @@ -429,51 +414,47 @@ - - - - 同步后本地选择 - 回调直接返回成员 - + + + + + + + + {{ tag.tagName }} {{ tag.memberCount }}人 + + + + + - + + - 同步成员 + + {{ uid }} + + + + + + 标签 {{ grayTagMemberCount }} 人 + 指定 {{ grayForm.extraMemberIds.length }} 人 + (去重 {{ grayOverlapCount }} 人) + = {{ grayTotalCount }} 人 + - - - - - - - - {{ group.groupName }} - {{ group.members.length }} - - - - - - {{ member.userId }} - {{ member.name || '未命名' }} - - - - - - @@ -1076,18 +1057,11 @@ const publishConfigForm = ref({ allowAnonymousUpdateCheck: false, defaultGrayPercent: 0, grayMode: 'PERCENT' as GrayMode, - graySelectionSource: 'LOCAL' as GraySelectionSource, - graySelectCallbackUrl: '', - graySelectCallbackSecret: '', - grayDirectorySyncCallbackUrl: '', - grayDirectorySyncCallbackSecret: '', + graySyncUrl: '', + publishCallbackUrl: '', + enableRealtimeNotification: false, parallelStoreUpload: true, }) -const grayMembers = ref([]) -const loadingGrayMembers = ref(false) -const grayMemberKeyword = ref('') -const grayMemberGroupFilter = ref('') -const grayMemberIds = ref([]) const appPackageInspecting = ref(false) const appPackageUploadProgress = ref(0) const appVersionUploadProgress = ref(0) @@ -1110,9 +1084,6 @@ let storeReviewReloadTimer: ReturnType | null = null let storeReviewAutoRefreshTimer: ReturnType | null = null let storeReviewDialogPollTimer: ReturnType | null = null -const hasGraySelectCallback = computed(() => Boolean(publishConfigForm.value.graySelectCallbackUrl.trim())) -const hasGrayDirectorySyncCallback = computed(() => Boolean(publishConfigForm.value.grayDirectorySyncCallbackUrl.trim())) -const hasAnyGrayCallback = computed(() => hasGraySelectCallback.value || hasGrayDirectorySyncCallback.value) const allowAnonymousUpdateCheck = computed(() => Boolean(publishConfigForm.value.allowAnonymousUpdateCheck)) type FieldDef = { @@ -1774,13 +1745,48 @@ async function confirmSubmitToStores() { const showGray = ref(false) const submittingGray = ref(false) const grayTarget = ref<{ id: string; type: 'app' | 'rn' } | null>(null) +const grayTags = ref([]) +const grayExtraInput = ref('') const grayForm = ref({ enabled: true, grayMode: 'PERCENT' as GrayMode, percent: 10, - selectionSource: 'LOCAL' as GraySelectionSource, + groupNames: [] as string[], + extraMemberIds: [] as string[], }) +const grayTagMemberCount = computed(() => { + const selected = new Set(grayForm.value.groupNames) + return grayTags.value + .filter(t => selected.has(t.tagName)) + .reduce((sum, t) => sum + t.memberCount, 0) +}) + +const grayOverlapCount = computed(() => { + // 简化:无法精确计算标签与额外成员的重叠,显示为 0 + return 0 +}) + +const grayTotalCount = computed(() => { + return grayTagMemberCount.value + grayForm.value.extraMemberIds.length - grayOverlapCount.value +}) + +function addExtraMembers() { + const input = grayExtraInput.value.trim() + if (!input) return + const ids = input.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean) + for (const id of ids) { + if (!grayForm.value.extraMemberIds.includes(id)) { + grayForm.value.extraMemberIds.push(id) + } + } + grayExtraInput.value = '' +} + +function removeExtraMember(uid: string) { + grayForm.value.extraMemberIds = grayForm.value.extraMemberIds.filter(id => id !== uid) +} + function openGrayDialog(row: { id: string }, type: 'app' | 'rn') { if (allowAnonymousUpdateCheck.value) { ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用') @@ -1789,49 +1795,22 @@ function openGrayDialog(row: { id: string }, type: 'app' | 'rn') { grayTarget.value = { id: row.id, type } grayForm.value = { enabled: true, - grayMode: hasAnyGrayCallback.value ? publishConfigForm.value.grayMode : 'PERCENT', + grayMode: 'PERCENT', percent: publishConfigForm.value.defaultGrayPercent || 10, - selectionSource: hasGrayDirectorySyncCallback.value - ? publishConfigForm.value.graySelectionSource - : (hasGraySelectCallback.value ? 'CALLBACK' : 'LOCAL'), + groupNames: [], + extraMemberIds: [], } - grayMemberIds.value = [] - grayMemberKeyword.value = '' - grayMemberGroupFilter.value = '' - grayMembers.value = [] + grayExtraInput.value = '' showGray.value = true - if (grayForm.value.selectionSource === 'LOCAL' && hasGrayDirectorySyncCallback.value) { - loadGrayMembers() - } + loadGrayTags() } -async function loadGrayMembers() { - if (!showGray.value || !hasGrayDirectorySyncCallback.value) return - loadingGrayMembers.value = true +async function loadGrayTags() { try { - const res = await updateAdminApi.listGrayMembers(appKey.value, grayMemberKeyword.value || undefined, grayMemberGroupFilter.value || undefined) - grayMembers.value = res.data.data + const res = await updateAdminApi.listGrayTags(appKey.value) + grayTags.value = res.data.data } catch { - grayMembers.value = [] - } finally { - loadingGrayMembers.value = false - } -} - -async function syncGrayMembers() { - if (!hasGrayDirectorySyncCallback.value) { - ElMessage.warning('未配置成员目录同步回调,无法同步成员') - return - } - loadingGrayMembers.value = true - try { - const res = await updateAdminApi.syncGrayMembers(appKey.value) - grayMembers.value = res.data.data - ElMessage.success('成员已同步') - } catch { - ElMessage.error('同步失败') - } finally { - loadingGrayMembers.value = false + grayTags.value = [] } } @@ -1841,16 +1820,9 @@ async function submitGray() { ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用') return } - if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'LOCAL' && !hasGrayDirectorySyncCallback.value) { - ElMessage.warning('未配置成员目录同步回调,无法选择本地成员') - return - } - if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'CALLBACK' && !hasGraySelectCallback.value) { - ElMessage.warning('未配置成员选择回调,无法使用回调成员选择') - return - } - if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'LOCAL' && !grayMemberIds.value.length) { - ElMessage.warning('请选择至少一个灰度成员') + if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' + && !grayForm.value.groupNames.length && !grayForm.value.extraMemberIds.length) { + ElMessage.warning('请至少选择一个标签或指定一个成员') return } submittingGray.value = true @@ -1860,17 +1832,17 @@ async function submitGray() { enabled: boolean grayMode: GrayMode percent?: number - memberIds?: string[] - selectionSource?: GraySelectionSource + groupNames?: string[] + extraMemberIds?: string[] } = { enabled: grayForm.value.enabled, grayMode: grayForm.value.grayMode, - selectionSource: grayForm.value.selectionSource, } if (grayForm.value.grayMode === 'PERCENT') { payload.percent = grayForm.value.percent } else { - payload.memberIds = grayMemberIds.value + payload.groupNames = grayForm.value.groupNames + payload.extraMemberIds = grayForm.value.extraMemberIds } if (type === 'app') { await updateAdminApi.grayAppVersion(id, payload) @@ -2791,27 +2763,28 @@ onBeforeUnmount(() => { color: var(--el-text-color-secondary); } -.gray-member-groups { - width: 100%; -} - -.gray-group-title { - display: flex; - align-items: center; - gap: 8px; -} - -.gray-member-row { +.gray-tag-item { padding: 6px 0; + border-bottom: 1px solid var(--el-border-color-lighter); +} +.gray-tag-item:last-child { + border-bottom: none; } -.gray-member-id { - font-weight: 600; - margin-right: 8px; +.gray-extra-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; } -.gray-member-name { +.gray-summary { color: var(--el-text-color-secondary); + font-size: 13px; +} +.gray-summary strong { + color: var(--el-color-primary); + font-size: 16px; } .upload-progress-block {