feat(update): 重构灰度发布系统并新增标签管理功能
- 灰度模式简化为 PERCENT 和 MEMBERS 两种 - 新增成员标签系统,支持标签 CRUD 和按标签选择发版 - 成员同步保留已有标签,手动成员不受同步影响 - 支持标签 + 额外成员组合选择:groupNames + extraMemberIds - 发布时回调集成方获取成员列表,支持 AppSecret 签名验证 - 从 IM 服务导入成员功能 - 修复 isInGrayRelease() 中的 String.contains() 误匹配 bug - 移除 IM_PUSH_USERS、CUSTOMER_SYNC、CUSTOMER_CALLBACK 模式 - 更新前端界面,优化灰度成员选择体验 - 添加发布配置和操作日志等相关数据库表结构
这个提交包含在:
父节点
cbc29cf255
当前提交
f116b63369
@ -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 StoreReviewState = 'PENDING' | 'SUBMITTING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED'
|
||||||
export type PublishMode = 'MANUAL' | 'NOW' | 'SCHEDULED' | 'AUTO_REVIEW'
|
export type PublishMode = 'MANUAL' | 'NOW' | 'SCHEDULED' | 'AUTO_REVIEW'
|
||||||
export type GrayMode = 'PERCENT' | 'MEMBERS'
|
export type GrayMode = 'PERCENT' | 'MEMBERS'
|
||||||
export type GraySelectionSource = 'LOCAL' | 'CALLBACK'
|
|
||||||
|
|
||||||
export interface PublishConfig {
|
export interface PublishConfig {
|
||||||
id: string
|
id: string
|
||||||
@ -107,15 +106,15 @@ export interface OperationLog {
|
|||||||
|
|
||||||
export interface GrayMember {
|
export interface GrayMember {
|
||||||
userId: string
|
userId: string
|
||||||
name?: string
|
name: string
|
||||||
groupName?: string
|
source: 'SYNC' | 'MANUAL'
|
||||||
extraJson?: string
|
active: boolean
|
||||||
updatedAt?: string
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrayMemberGroup {
|
export interface GrayTag {
|
||||||
groupName: string
|
tagName: string
|
||||||
members: GrayMember[]
|
memberCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreConfig {
|
export interface StoreConfig {
|
||||||
@ -264,8 +263,8 @@ export const updateAdminApi = {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
grayMode: GrayMode
|
grayMode: GrayMode
|
||||||
percent?: number
|
percent?: number
|
||||||
memberIds?: string[]
|
groupNames?: string[]
|
||||||
selectionSource?: GraySelectionSource
|
extraMemberIds?: string[]
|
||||||
}) {
|
}) {
|
||||||
return updateClient.post(`/api/v1/updates/app/${id}/gray`, body)
|
return updateClient.post(`/api/v1/updates/app/${id}/gray`, body)
|
||||||
},
|
},
|
||||||
@ -298,8 +297,8 @@ export const updateAdminApi = {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
grayMode: GrayMode
|
grayMode: GrayMode
|
||||||
percent?: number
|
percent?: number
|
||||||
memberIds?: string[]
|
groupNames?: string[]
|
||||||
selectionSource?: GraySelectionSource
|
extraMemberIds?: string[]
|
||||||
}) {
|
}) {
|
||||||
return updateClient.post(`/api/v1/rn/${id}/gray`, body)
|
return updateClient.post(`/api/v1/rn/${id}/gray`, body)
|
||||||
},
|
},
|
||||||
@ -404,15 +403,47 @@ export const updateAdminApi = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
listGrayMembers(appKey: string, keyword?: string, groupName?: string) {
|
listGrayMembers(appKey: string) {
|
||||||
return updateClient.get<{ data: GrayMemberGroup[] }>('/api/v1/updates/gray/members', {
|
return updateClient.get<{ data: GrayMember[] }>('/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, {
|
|
||||||
params: { appKey },
|
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 } })
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -335,26 +335,11 @@
|
|||||||
<el-slider v-model="publishConfigForm.defaultGrayPercent" :min="1" :max="100" show-input :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
<el-slider v-model="publishConfigForm.defaultGrayPercent" :min="1" :max="100" show-input :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<template v-if="publishConfigForm.grayMode === 'MEMBERS'">
|
<template v-if="publishConfigForm.grayMode === 'MEMBERS'">
|
||||||
<el-form-item label="成员选择回调">
|
<el-form-item label="成员同步地址">
|
||||||
<el-input v-model="publishConfigForm.graySelectCallbackUrl" placeholder="选择成员时调用的回调地址" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
<el-input v-model="publishConfigForm.graySyncUrl" placeholder="集成方成员列表接口地址(留空则使用 AppSecret 签名)" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="成员选择密钥">
|
<el-form-item label="发布时回调地址">
|
||||||
<el-input v-model="publishConfigForm.graySelectCallbackSecret" type="password" show-password placeholder="可选,用于成员选择回调验签" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
<el-input v-model="publishConfigForm.publishCallbackUrl" placeholder="发版灰度时回调集成方获取成员列表(可选)" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="成员目录同步回调">
|
|
||||||
<el-input v-model="publishConfigForm.grayDirectorySyncCallbackUrl" placeholder="同步所有成员时调用的回调地址" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="成员目录密钥">
|
|
||||||
<el-input v-model="publishConfigForm.grayDirectorySyncCallbackSecret" type="password" show-password placeholder="可选,用于成员同步回调验签" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="成员选择方式">
|
|
||||||
<el-radio-group v-model="publishConfigForm.graySelectionSource" :disabled="publishConfigForm.allowAnonymousUpdateCheck">
|
|
||||||
<el-radio-button value="LOCAL" :disabled="!hasGrayDirectorySyncCallback">同步后本地选择</el-radio-button>
|
|
||||||
<el-radio-button value="CALLBACK" :disabled="!hasGraySelectCallback">回调直接返回成员</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="syncGrayMembers" :loading="loadingGrayMembers" :disabled="!hasGrayDirectorySyncCallback || publishConfigForm.allowAnonymousUpdateCheck">同步成员</el-button>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -417,7 +402,7 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- Gray Release Dialog -->
|
<!-- Gray Release Dialog -->
|
||||||
<el-dialog v-model="showGray" title="灰度发布配置" :width="dialogWidth">
|
<el-dialog v-model="showGray" title="灰度发布配置" width="600px">
|
||||||
<el-form label-width="110px">
|
<el-form label-width="110px">
|
||||||
<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">
|
||||||
@ -429,51 +414,47 @@
|
|||||||
<el-form-item label="灰度比例" v-if="grayForm.enabled && grayForm.grayMode === 'PERCENT'">
|
<el-form-item label="灰度比例" v-if="grayForm.enabled && grayForm.grayMode === 'PERCENT'">
|
||||||
<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>
|
||||||
<template v-else-if="grayForm.enabled && grayForm.grayMode === 'MEMBERS'">
|
<template v-if="grayForm.enabled && grayForm.grayMode === 'MEMBERS'">
|
||||||
<el-form-item label="成员来源">
|
<!-- 按标签选择 -->
|
||||||
<el-radio-group v-model="grayForm.selectionSource">
|
<el-form-item label="按标签选择">
|
||||||
<el-radio-button value="LOCAL">同步后本地选择</el-radio-button>
|
<div style="width:100%">
|
||||||
<el-radio-button value="CALLBACK">回调直接返回成员</el-radio-button>
|
<el-checkbox-group v-model="grayForm.groupNames">
|
||||||
</el-radio-group>
|
<div v-for="tag in grayTags" :key="tag.tagName" class="gray-tag-item">
|
||||||
|
<el-checkbox :value="tag.tagName">
|
||||||
|
{{ tag.tagName }} <el-tag size="small" type="info">{{ tag.memberCount }}人</el-tag>
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</el-checkbox-group>
|
||||||
|
<el-empty v-if="!grayTags.length" description="暂无标签,请先在成员管理中创建" :image-size="40" />
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="成员搜索" v-if="grayForm.selectionSource === 'LOCAL'">
|
<!-- 额外指定成员 -->
|
||||||
|
<el-form-item label="追加指定成员">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="grayMemberKeyword"
|
v-model="grayExtraInput"
|
||||||
placeholder="搜索 userId / name / 分组"
|
placeholder="输入 userId,多个用逗号分隔"
|
||||||
clearable
|
clearable
|
||||||
style="width: 320px"
|
@blur="addExtraMembers"
|
||||||
@change="loadGrayMembers"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
<el-button style="margin-left: 8px" @click="syncGrayMembers" :loading="loadingGrayMembers">同步成员</el-button>
|
<div v-if="grayForm.extraMemberIds.length" class="gray-extra-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="uid in grayForm.extraMemberIds"
|
||||||
|
:key="uid"
|
||||||
|
closable
|
||||||
|
@close="removeExtraMember(uid)"
|
||||||
|
size="small"
|
||||||
|
>{{ uid }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<!-- 汇总 -->
|
||||||
|
<el-form-item label="灰度人数">
|
||||||
|
<span class="gray-summary">
|
||||||
|
标签 {{ grayTagMemberCount }} 人 + 指定 {{ grayForm.extraMemberIds.length }} 人
|
||||||
|
<template v-if="grayOverlapCount > 0">(去重 {{ grayOverlapCount }} 人)</template>
|
||||||
|
= <strong>{{ grayTotalCount }}</strong> 人
|
||||||
|
</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-alert
|
|
||||||
v-if="grayForm.selectionSource === 'CALLBACK'"
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
title="选择成员时将调用发布配置里的成员选择回调,由你的系统直接返回可参与灰度的成员列表。"
|
|
||||||
/>
|
|
||||||
<div v-if="grayForm.selectionSource === 'LOCAL'" class="gray-member-groups">
|
|
||||||
<el-empty v-if="!grayMembers.length" description="暂无成员数据,请先同步" />
|
|
||||||
<el-collapse v-else>
|
|
||||||
<el-collapse-item v-for="group in grayMembers" :key="group.groupName" :name="group.groupName">
|
|
||||||
<template #title>
|
|
||||||
<div class="gray-group-title">
|
|
||||||
<span>{{ group.groupName }}</span>
|
|
||||||
<el-tag size="small">{{ group.members.length }}</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-checkbox-group v-model="grayMemberIds">
|
|
||||||
<div v-for="member in group.members" :key="member.userId" class="gray-member-row">
|
|
||||||
<el-checkbox :value="member.userId">
|
|
||||||
<span class="gray-member-id">{{ member.userId }}</span>
|
|
||||||
<span class="gray-member-name">{{ member.name || '未命名' }}</span>
|
|
||||||
</el-checkbox>
|
|
||||||
</div>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -1076,18 +1057,11 @@ const publishConfigForm = ref({
|
|||||||
allowAnonymousUpdateCheck: false,
|
allowAnonymousUpdateCheck: false,
|
||||||
defaultGrayPercent: 0,
|
defaultGrayPercent: 0,
|
||||||
grayMode: 'PERCENT' as GrayMode,
|
grayMode: 'PERCENT' as GrayMode,
|
||||||
graySelectionSource: 'LOCAL' as GraySelectionSource,
|
graySyncUrl: '',
|
||||||
graySelectCallbackUrl: '',
|
publishCallbackUrl: '',
|
||||||
graySelectCallbackSecret: '',
|
enableRealtimeNotification: false,
|
||||||
grayDirectorySyncCallbackUrl: '',
|
|
||||||
grayDirectorySyncCallbackSecret: '',
|
|
||||||
parallelStoreUpload: true,
|
parallelStoreUpload: true,
|
||||||
})
|
})
|
||||||
const grayMembers = ref<GrayMemberGroup[]>([])
|
|
||||||
const loadingGrayMembers = ref(false)
|
|
||||||
const grayMemberKeyword = ref('')
|
|
||||||
const grayMemberGroupFilter = ref('')
|
|
||||||
const grayMemberIds = ref<string[]>([])
|
|
||||||
const appPackageInspecting = ref(false)
|
const appPackageInspecting = ref(false)
|
||||||
const appPackageUploadProgress = ref(0)
|
const appPackageUploadProgress = ref(0)
|
||||||
const appVersionUploadProgress = ref(0)
|
const appVersionUploadProgress = ref(0)
|
||||||
@ -1110,9 +1084,6 @@ let storeReviewReloadTimer: ReturnType<typeof setTimeout> | null = null
|
|||||||
let storeReviewAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
let storeReviewAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let storeReviewDialogPollTimer: ReturnType<typeof setInterval> | null = null
|
let storeReviewDialogPollTimer: ReturnType<typeof setInterval> | 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))
|
const allowAnonymousUpdateCheck = computed(() => Boolean(publishConfigForm.value.allowAnonymousUpdateCheck))
|
||||||
|
|
||||||
type FieldDef = {
|
type FieldDef = {
|
||||||
@ -1774,13 +1745,48 @@ async function confirmSubmitToStores() {
|
|||||||
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 grayTags = ref<GrayTag[]>([])
|
||||||
|
const grayExtraInput = ref('')
|
||||||
const grayForm = ref({
|
const grayForm = ref({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
grayMode: 'PERCENT' as GrayMode,
|
grayMode: 'PERCENT' as GrayMode,
|
||||||
percent: 10,
|
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') {
|
function openGrayDialog(row: { id: string }, type: 'app' | 'rn') {
|
||||||
if (allowAnonymousUpdateCheck.value) {
|
if (allowAnonymousUpdateCheck.value) {
|
||||||
ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用')
|
ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用')
|
||||||
@ -1789,49 +1795,22 @@ function openGrayDialog(row: { id: string }, type: 'app' | 'rn') {
|
|||||||
grayTarget.value = { id: row.id, type }
|
grayTarget.value = { id: row.id, type }
|
||||||
grayForm.value = {
|
grayForm.value = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
grayMode: hasAnyGrayCallback.value ? publishConfigForm.value.grayMode : 'PERCENT',
|
grayMode: 'PERCENT',
|
||||||
percent: publishConfigForm.value.defaultGrayPercent || 10,
|
percent: publishConfigForm.value.defaultGrayPercent || 10,
|
||||||
selectionSource: hasGrayDirectorySyncCallback.value
|
groupNames: [],
|
||||||
? publishConfigForm.value.graySelectionSource
|
extraMemberIds: [],
|
||||||
: (hasGraySelectCallback.value ? 'CALLBACK' : 'LOCAL'),
|
|
||||||
}
|
}
|
||||||
grayMemberIds.value = []
|
grayExtraInput.value = ''
|
||||||
grayMemberKeyword.value = ''
|
|
||||||
grayMemberGroupFilter.value = ''
|
|
||||||
grayMembers.value = []
|
|
||||||
showGray.value = true
|
showGray.value = true
|
||||||
if (grayForm.value.selectionSource === 'LOCAL' && hasGrayDirectorySyncCallback.value) {
|
loadGrayTags()
|
||||||
loadGrayMembers()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadGrayMembers() {
|
async function loadGrayTags() {
|
||||||
if (!showGray.value || !hasGrayDirectorySyncCallback.value) return
|
|
||||||
loadingGrayMembers.value = true
|
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.listGrayMembers(appKey.value, grayMemberKeyword.value || undefined, grayMemberGroupFilter.value || undefined)
|
const res = await updateAdminApi.listGrayTags(appKey.value)
|
||||||
grayMembers.value = res.data.data
|
grayTags.value = res.data.data
|
||||||
} catch {
|
} catch {
|
||||||
grayMembers.value = []
|
grayTags.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1841,16 +1820,9 @@ async function submitGray() {
|
|||||||
ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用')
|
ElMessage.warning('当前应用开启了免登录检查更新,灰度发布已禁用')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'LOCAL' && !hasGrayDirectorySyncCallback.value) {
|
if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS'
|
||||||
ElMessage.warning('未配置成员目录同步回调,无法选择本地成员')
|
&& !grayForm.value.groupNames.length && !grayForm.value.extraMemberIds.length) {
|
||||||
return
|
ElMessage.warning('请至少选择一个标签或指定一个成员')
|
||||||
}
|
|
||||||
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('请选择至少一个灰度成员')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
submittingGray.value = true
|
submittingGray.value = true
|
||||||
@ -1860,17 +1832,17 @@ async function submitGray() {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
grayMode: GrayMode
|
grayMode: GrayMode
|
||||||
percent?: number
|
percent?: number
|
||||||
memberIds?: string[]
|
groupNames?: string[]
|
||||||
selectionSource?: GraySelectionSource
|
extraMemberIds?: string[]
|
||||||
} = {
|
} = {
|
||||||
enabled: grayForm.value.enabled,
|
enabled: grayForm.value.enabled,
|
||||||
grayMode: grayForm.value.grayMode,
|
grayMode: grayForm.value.grayMode,
|
||||||
selectionSource: grayForm.value.selectionSource,
|
|
||||||
}
|
}
|
||||||
if (grayForm.value.grayMode === 'PERCENT') {
|
if (grayForm.value.grayMode === 'PERCENT') {
|
||||||
payload.percent = grayForm.value.percent
|
payload.percent = grayForm.value.percent
|
||||||
} else {
|
} else {
|
||||||
payload.memberIds = grayMemberIds.value
|
payload.groupNames = grayForm.value.groupNames
|
||||||
|
payload.extraMemberIds = grayForm.value.extraMemberIds
|
||||||
}
|
}
|
||||||
if (type === 'app') {
|
if (type === 'app') {
|
||||||
await updateAdminApi.grayAppVersion(id, payload)
|
await updateAdminApi.grayAppVersion(id, payload)
|
||||||
@ -2791,27 +2763,28 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gray-member-groups {
|
.gray-tag-item {
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gray-group-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gray-member-row {
|
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
.gray-tag-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gray-member-id {
|
.gray-extra-tags {
|
||||||
font-weight: 600;
|
display: flex;
|
||||||
margin-right: 8px;
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gray-member-name {
|
.gray-summary {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.gray-summary strong {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-progress-block {
|
.upload-progress-block {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户