比较提交
2 次代码提交
6b68dd4f58
...
2107d0b191
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
2107d0b191 | ||
|
|
255be6987b |
@ -40,6 +40,7 @@ export default defineConfig({
|
|||||||
{ text: '快速开始', link: '/guide/quickstart' },
|
{ text: '快速开始', link: '/guide/quickstart' },
|
||||||
{ text: '平台概念', link: '/guide/concepts' },
|
{ text: '平台概念', link: '/guide/concepts' },
|
||||||
{ text: '接入流程', link: '/guide/flow' },
|
{ text: '接入流程', link: '/guide/flow' },
|
||||||
|
{ text: '应用商店监测', link: '/guide/store-monitoring' },
|
||||||
],
|
],
|
||||||
'/android/': [
|
'/android/': [
|
||||||
{ text: '概览', link: '/android/' },
|
{ text: '概览', link: '/android/' },
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
# iOS 与 HarmonyOS 应用商店监测
|
||||||
|
|
||||||
|
租户平台的 Update 服务可以监测 App Store Connect 和华为 AppGallery Connect
|
||||||
|
中的正式应用版本。当前能力是只读监测,不会代替开发者上传 IPA/HAP、提交审核、
|
||||||
|
立即发布或撤回审核。
|
||||||
|
|
||||||
|
## 能监测什么
|
||||||
|
|
||||||
|
- 当前已上架版本
|
||||||
|
- 等待审核和审核中版本
|
||||||
|
- 审核通过、等待开发者发布的版本
|
||||||
|
- 审核拒绝版本
|
||||||
|
- 中国大陆地区是否实际可下载
|
||||||
|
- 官方后台直接提交、并非通过 Xuqm 创建的外部版本
|
||||||
|
|
||||||
|
“审核通过”不代表“已上架”。只有中国大陆用户已经能够从对应应用商店下载时,
|
||||||
|
租户平台才会显示“已上架”。
|
||||||
|
|
||||||
|
## 配置监测
|
||||||
|
|
||||||
|
1. 进入目标应用的 **Update 服务 > 应用商店监测**。
|
||||||
|
2. 选择 Apple App Store 或 HarmonyOS 应用市场。
|
||||||
|
3. 填写 Bundle ID/包名和商店链接。
|
||||||
|
4. 新增只读监测凭据。
|
||||||
|
5. 保存后点击 **立即同步** 验证连接和读取权限。
|
||||||
|
|
||||||
|
凭据保存成功后不会再次显示原文。更换凭据时直接提交一份新凭据即可。
|
||||||
|
|
||||||
|
## 通知
|
||||||
|
|
||||||
|
应用商店通知默认关闭。可以按应用选择:
|
||||||
|
|
||||||
|
- 邮件
|
||||||
|
- 签名 Webhook
|
||||||
|
|
||||||
|
没有启用通知时,平台仍会同步商店状态并保留事件历史。Update 服务不依赖 IM。
|
||||||
|
|
||||||
|
## 配置已上架版本为更新
|
||||||
|
|
||||||
|
从商店发现的版本不会自动成为客户端更新。确认版本已经在中国大陆上架后:
|
||||||
|
|
||||||
|
1. 在商店版本记录中点击 **创建 Xuqm 更新草稿**。
|
||||||
|
2. 平台创建一条“跳转应用商店”类型的更新草稿。
|
||||||
|
3. 配置更新说明、是否强制更新、灰度范围和发布时间。
|
||||||
|
4. 人工确认发布。
|
||||||
|
|
||||||
|
同一个商店版本只能关联一条有效更新记录。审核中、待发布、部分地区可用或状态
|
||||||
|
未知的版本不能创建客户端更新。
|
||||||
|
|
||||||
|
## 常见状态
|
||||||
|
|
||||||
|
| 状态 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 等待审核 | 已准备提交或等待官方开始审核 |
|
||||||
|
| 审核中 | 官方正在审核 |
|
||||||
|
| 审核拒绝 | 需要开发者在官方后台处理 |
|
||||||
|
| 审核通过待发布 | 已通过,但尚未向用户发布 |
|
||||||
|
| 商店处理中 | 已进入分发处理,暂未确认可下载 |
|
||||||
|
| 已上架 | 中国大陆地区已经实际可下载 |
|
||||||
|
| 同步失败 | 本次无法读取官方状态,历史已验证状态不会被覆盖 |
|
||||||
|
|
||||||
|
## 当前边界
|
||||||
|
|
||||||
|
iOS/HarmonyOS 的构建上传、自动提审和审核后立即/定时发布正在进行正式设计,
|
||||||
|
当前页面不会显示无法执行的操作。Android 应用商店发布能力按 Android 文档执行。
|
||||||
@ -64,7 +64,7 @@ SHA-256 和最终依赖闭包校验。宿主不能分别下载后自行拼接版
|
|||||||
进入 buz 前自动检查并安装:
|
进入 buz 前自动检查并安装:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
await UpdateSDK.checkAndInstallPlugin('home', {
|
await UpdateSDK.checkAndInstall('home', {
|
||||||
onProgress(moduleId, progress) {
|
onProgress(moduleId, progress) {
|
||||||
console.log(moduleId, progress.percent)
|
console.log(moduleId, progress.percent)
|
||||||
},
|
},
|
||||||
@ -74,13 +74,13 @@ await UpdateSDK.checkAndInstallPlugin('home', {
|
|||||||
需要由宿主展示确认弹窗时,检查与安装分开调用:
|
需要由宿主展示确认弹窗时,检查与安装分开调用:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const plan = await UpdateSDK.checkPluginRelease('home')
|
const plan = await UpdateSDK.check('home')
|
||||||
if (plan && (await showPluginUpdateDialog(plan))) {
|
if (plan && (await showPluginUpdateDialog(plan))) {
|
||||||
await UpdateSDK.installPluginRelease(plan)
|
await UpdateSDK.install(plan)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`checkPluginRelease` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
|
`check` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
|
||||||
记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
|
记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
|
||||||
管理,宿主不得另建一套缓存版本表。
|
管理,宿主不得另建一套缓存版本表。
|
||||||
|
|
||||||
|
|||||||
@ -126,6 +126,105 @@ export interface StoreConfig {
|
|||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StoreMonitoringType = 'APP_STORE' | 'HARMONY_APP'
|
||||||
|
export type StoreMonitoringPlatform = 'IOS' | 'HARMONY'
|
||||||
|
export type StoreVersionState =
|
||||||
|
| 'DISCOVERED'
|
||||||
|
| 'PREPARING'
|
||||||
|
| 'SUBMITTED'
|
||||||
|
| 'WAITING_FOR_REVIEW'
|
||||||
|
| 'IN_REVIEW'
|
||||||
|
| 'REJECTED'
|
||||||
|
| 'APPROVED_PENDING_RELEASE'
|
||||||
|
| 'RELEASE_SCHEDULED'
|
||||||
|
| 'RELEASE_REQUESTED'
|
||||||
|
| 'PROCESSING_DISTRIBUTION'
|
||||||
|
| 'PARTIALLY_AVAILABLE'
|
||||||
|
| 'AVAILABLE'
|
||||||
|
| 'WITHDRAWN'
|
||||||
|
| 'REMOVED'
|
||||||
|
| 'UNKNOWN'
|
||||||
|
| 'SYNC_FAILED'
|
||||||
|
|
||||||
|
export interface StoreMonitoringBinding {
|
||||||
|
id: string
|
||||||
|
appKey: string
|
||||||
|
platform: StoreMonitoringPlatform
|
||||||
|
storeType: StoreMonitoringType
|
||||||
|
packageIdentifier: string
|
||||||
|
officialAppId?: string
|
||||||
|
marketUrl?: string
|
||||||
|
targetRegion: 'CHN'
|
||||||
|
enabled: boolean
|
||||||
|
monitorCredentialConfigured: boolean
|
||||||
|
lastSyncStatus?: string
|
||||||
|
lastSyncError?: string
|
||||||
|
lastSyncedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreMonitoringBindingInput {
|
||||||
|
packageIdentifier: string
|
||||||
|
marketUrl?: string
|
||||||
|
enabled: boolean
|
||||||
|
credentialDisplayName?: string
|
||||||
|
credential?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreMonitoringVersion {
|
||||||
|
id: string
|
||||||
|
officialVersionId: string
|
||||||
|
versionName: string
|
||||||
|
buildVersion: string
|
||||||
|
source: 'XUQM_PLATFORM' | 'EXTERNAL_STORE'
|
||||||
|
state: StoreVersionState
|
||||||
|
rawState?: string
|
||||||
|
chinaAvailable: boolean
|
||||||
|
releaseSequence?: number
|
||||||
|
stateOccurredAt?: string
|
||||||
|
lastSyncedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreMonitoringEvent {
|
||||||
|
id: string
|
||||||
|
eventType: string
|
||||||
|
source: 'WEBHOOK' | 'POLL' | 'MANUAL' | 'IMPORT'
|
||||||
|
previousState?: StoreVersionState
|
||||||
|
currentState?: StoreVersionState
|
||||||
|
rawState?: string
|
||||||
|
occurredAt?: string
|
||||||
|
receivedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreUpdateDraft {
|
||||||
|
storeVersionId: string
|
||||||
|
appVersionId: string
|
||||||
|
platform: StoreMonitoringPlatform
|
||||||
|
versionName: string
|
||||||
|
buildVersion: string
|
||||||
|
releaseSequence: number
|
||||||
|
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
|
||||||
|
storeUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreNotificationPolicy {
|
||||||
|
emailEnabled: boolean
|
||||||
|
emailRecipients: string[]
|
||||||
|
webhookEnabled: boolean
|
||||||
|
webhookUrl?: string
|
||||||
|
webhookSecretConfigured: boolean
|
||||||
|
eventTypes: string[]
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreNotificationPolicyInput {
|
||||||
|
emailEnabled: boolean
|
||||||
|
emailRecipients: string[]
|
||||||
|
webhookEnabled: boolean
|
||||||
|
webhookUrl?: string
|
||||||
|
webhookSecret?: string
|
||||||
|
eventTypes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppVersion {
|
export interface AppVersion {
|
||||||
id: string
|
id: string
|
||||||
appKey: string
|
appKey: string
|
||||||
@ -351,6 +450,72 @@ export const updateAdminApi = {
|
|||||||
return updateClient.delete(`/api/v1/updates/store/configs/${storeType}`, { params: { appKey } })
|
return updateClient.delete(`/api/v1/updates/store/configs/${storeType}`, { params: { appKey } })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── iOS / Harmony official store read-only monitoring ──────────────────
|
||||||
|
|
||||||
|
listStoreMonitoringBindings(appKey: string) {
|
||||||
|
return updateClient.get<{ data: StoreMonitoringBinding[] }>(
|
||||||
|
'/api/v1/updates/store-monitoring/bindings',
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
saveStoreMonitoringBinding(
|
||||||
|
appKey: string,
|
||||||
|
storeType: StoreMonitoringType,
|
||||||
|
body: StoreMonitoringBindingInput,
|
||||||
|
) {
|
||||||
|
return updateClient.put<{ data: StoreMonitoringBinding }>(
|
||||||
|
`/api/v1/updates/store-monitoring/bindings/${storeType}`,
|
||||||
|
body,
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
synchronizeStoreMonitoring(appKey: string, storeType: StoreMonitoringType) {
|
||||||
|
return updateClient.post<{ data: StoreMonitoringVersion[] }>(
|
||||||
|
`/api/v1/updates/store-monitoring/bindings/${storeType}/sync`,
|
||||||
|
null,
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
listStoreMonitoringVersions(appKey: string, bindingId: string) {
|
||||||
|
return updateClient.get<{ data: StoreMonitoringVersion[] }>(
|
||||||
|
`/api/v1/updates/store-monitoring/bindings/${bindingId}/versions`,
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
listStoreMonitoringEvents(appKey: string, bindingId: string) {
|
||||||
|
return updateClient.get<{ data: StoreMonitoringEvent[] }>(
|
||||||
|
`/api/v1/updates/store-monitoring/bindings/${bindingId}/events`,
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
createStoreUpdateDraft(appKey: string, bindingId: string, storeVersionId: string) {
|
||||||
|
return updateClient.post<{ data: StoreUpdateDraft }>(
|
||||||
|
`/api/v1/updates/store-monitoring/bindings/${bindingId}/versions/${storeVersionId}/update-draft`,
|
||||||
|
null,
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
getStoreNotificationPolicy(appKey: string) {
|
||||||
|
return updateClient.get<{ data: StoreNotificationPolicy }>(
|
||||||
|
'/api/v1/updates/store-monitoring/notification-policy',
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
saveStoreNotificationPolicy(appKey: string, body: StoreNotificationPolicyInput) {
|
||||||
|
return updateClient.put<{ data: StoreNotificationPolicy }>(
|
||||||
|
'/api/v1/updates/store-monitoring/notification-policy',
|
||||||
|
body,
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
executeSubmitToStores(
|
executeSubmitToStores(
|
||||||
versionId: string,
|
versionId: string,
|
||||||
storeTypes: StoreType[],
|
storeTypes: StoreType[],
|
||||||
|
|||||||
@ -0,0 +1,816 @@
|
|||||||
|
<template>
|
||||||
|
<section class="store-monitoring">
|
||||||
|
<el-alert
|
||||||
|
title="这里只监测 Apple App Store 与鸿蒙应用市场的官方状态,不会上传安装包、提交审核或执行发布。当前仅判断中国大陆(CHN)是否可下载。"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<h3>商店绑定</h3>
|
||||||
|
<p>监测凭据加密保存,页面只展示是否已配置,不会回显密钥正文。</p>
|
||||||
|
</div>
|
||||||
|
<el-button :loading="loadingBindings" @click="loadBindings">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="binding-grid">
|
||||||
|
<el-card
|
||||||
|
v-for="definition in STORE_DEFINITIONS"
|
||||||
|
:key="definition.storeType"
|
||||||
|
shadow="never"
|
||||||
|
class="binding-card"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div>
|
||||||
|
<strong>{{ definition.label }}</strong>
|
||||||
|
<div class="card-subtitle">{{ definition.platformLabel }} · 中国大陆</div>
|
||||||
|
</div>
|
||||||
|
<el-tag :type="bindingFor(definition.storeType)?.enabled ? 'success' : 'info'">
|
||||||
|
{{ bindingFor(definition.storeType)?.enabled ? '监测中' : '未启用' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="bindingFor(definition.storeType)">
|
||||||
|
<el-descriptions :column="1" size="small" border>
|
||||||
|
<el-descriptions-item label="应用标识">
|
||||||
|
{{ bindingFor(definition.storeType)?.packageIdentifier }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="官方应用 ID">
|
||||||
|
{{ bindingFor(definition.storeType)?.officialAppId || '同步后识别' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="监测凭据">
|
||||||
|
<el-tag
|
||||||
|
:type="bindingFor(definition.storeType)?.monitorCredentialConfigured ? 'success' : 'danger'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ bindingFor(definition.storeType)?.monitorCredentialConfigured ? '已配置' : '未配置' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最近同步">
|
||||||
|
{{ displayTime(bindingFor(definition.storeType)?.lastSyncedAt) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="bindingFor(definition.storeType)?.lastSyncStatus === 'FAILED'"
|
||||||
|
:title="bindingFor(definition.storeType)?.lastSyncError || '最近一次同步失败'"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
class="sync-error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<el-empty v-else description="尚未绑定" :image-size="56" />
|
||||||
|
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button @click="openBindingDialog(definition.storeType)">
|
||||||
|
{{ bindingFor(definition.storeType) ? '修改绑定' : '创建绑定' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:disabled="!bindingFor(definition.storeType)?.enabled"
|
||||||
|
:loading="syncingStore === definition.storeType"
|
||||||
|
@click="synchronize(definition.storeType)"
|
||||||
|
>
|
||||||
|
手动同步
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
:disabled="!bindingFor(definition.storeType)"
|
||||||
|
@click="selectBinding(definition.storeType)"
|
||||||
|
>
|
||||||
|
查看监测结果
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="selectedBinding">
|
||||||
|
<div class="section-heading result-heading">
|
||||||
|
<div>
|
||||||
|
<h3>{{ storeLabel(selectedBinding.storeType) }}监测结果</h3>
|
||||||
|
<p>“已在中国大陆上架”只在官方库存确认 CHN 可下载时显示。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-tabs v-model="resultTab" @tab-change="handleResultTabChange">
|
||||||
|
<el-tab-pane label="版本状态" name="versions">
|
||||||
|
<el-table :data="versions" v-loading="loadingVersions" border stripe>
|
||||||
|
<el-table-column prop="versionName" label="版本" min-width="110" />
|
||||||
|
<el-table-column prop="buildVersion" label="构建号" min-width="110">
|
||||||
|
<template #default="{ row }">{{ row.buildVersion || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" min-width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="stateTagType(row.state)">{{ stateLabel(row.state) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="中国大陆" min-width="145">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.chinaAvailable ? 'success' : 'info'">
|
||||||
|
{{ row.chinaAvailable ? '已可下载' : '尚不可下载' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="rawState" label="官方原始状态" min-width="170">
|
||||||
|
<template #default="{ row }">{{ row.rawState || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态时间" min-width="170">
|
||||||
|
<template #default="{ row }">{{ displayTime(row.stateOccurredAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最近同步" min-width="170">
|
||||||
|
<template #default="{ row }">{{ displayTime(row.lastSyncedAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Xuqm 更新草稿" min-width="245" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="draftResults[row.id]" class="draft-result">
|
||||||
|
<el-tag
|
||||||
|
:type="draftResults[row.id].publishStatus === 'DRAFT' ? 'success' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ draftResults[row.id].publishStatus }}
|
||||||
|
</el-tag>
|
||||||
|
<span>AppVersion ID:{{ draftResults[row.id].appVersionId }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
v-else-if="canCreateUpdateDraft(row)"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:loading="creatingDraftVersionId === row.id"
|
||||||
|
:disabled="creatingDraftVersionId === row.id"
|
||||||
|
@click="createUpdateDraft(row)"
|
||||||
|
>
|
||||||
|
创建 Xuqm 更新草稿
|
||||||
|
</el-button>
|
||||||
|
<span v-else class="ineligible-draft">上架且 CHN 可下载后可创建</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="!loadingVersions && versions.length === 0" description="暂无官方版本记录" />
|
||||||
|
<el-alert
|
||||||
|
title="此操作在尚未关联时只创建 Xuqm 版本管理中的 DRAFT 草稿,不会向 Apple 或鸿蒙应用市场提交审核,也不会发布更新。"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="draft-hint"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="事件时间线" name="events">
|
||||||
|
<div v-loading="loadingEvents" class="timeline-wrap">
|
||||||
|
<el-timeline v-if="events.length">
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="event in events"
|
||||||
|
:key="event.id"
|
||||||
|
:timestamp="displayTime(event.occurredAt || event.receivedAt)"
|
||||||
|
placement="top"
|
||||||
|
:type="stateTagType(event.currentState)"
|
||||||
|
>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div class="event-title">{{ eventLabel(event.eventType) }}</div>
|
||||||
|
<div class="event-state">
|
||||||
|
<template v-if="event.previousState">
|
||||||
|
{{ stateLabel(event.previousState) }} →
|
||||||
|
</template>
|
||||||
|
{{ stateLabel(event.currentState) }}
|
||||||
|
</div>
|
||||||
|
<div class="event-meta">
|
||||||
|
来源:{{ sourceLabel(event.source) }}
|
||||||
|
<span v-if="event.rawState"> · 官方状态:{{ event.rawState }}</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
<el-empty v-else-if="!loadingEvents" description="暂无状态事件" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<h3>状态通知</h3>
|
||||||
|
<p>邮件和 Webhook 默认关闭;即使通知关闭,商店状态事件仍会保留在时间线中。</p>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="savingNotification"
|
||||||
|
@click="saveNotificationPolicy"
|
||||||
|
>
|
||||||
|
保存通知策略
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
v-loading="loadingNotification"
|
||||||
|
:model="notificationForm"
|
||||||
|
label-width="150px"
|
||||||
|
class="notification-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="邮件通知">
|
||||||
|
<el-switch v-model="notificationForm.emailEnabled" />
|
||||||
|
<span class="form-tip">默认关闭</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="notificationForm.emailEnabled" label="收件人">
|
||||||
|
<el-select
|
||||||
|
v-model="notificationForm.emailRecipients"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="输入邮箱后按回车,可添加多个"
|
||||||
|
class="full-width"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Webhook 通知">
|
||||||
|
<el-switch v-model="notificationForm.webhookEnabled" />
|
||||||
|
<span class="form-tip">默认关闭,启用后请求包含事件 ID 与签名</span>
|
||||||
|
</el-form-item>
|
||||||
|
<template v-if="notificationForm.webhookEnabled">
|
||||||
|
<el-form-item label="Webhook 地址">
|
||||||
|
<el-input
|
||||||
|
v-model.trim="notificationForm.webhookUrl"
|
||||||
|
placeholder="https://example.com/hooks/xuqm-store"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="签名密钥">
|
||||||
|
<el-input
|
||||||
|
v-model="notificationForm.webhookSecret"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="new-password"
|
||||||
|
:placeholder="notificationForm.webhookSecretConfigured ? '已配置;留空表示不变' : '首次启用必须填写'"
|
||||||
|
/>
|
||||||
|
<span class="form-tip">密钥只写入、不回显。</span>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<el-form-item label="通知事件">
|
||||||
|
<el-checkbox-group v-model="notificationForm.eventTypes">
|
||||||
|
<el-checkbox value="STORE_VERSION_DISCOVERED">发现新版本</el-checkbox>
|
||||||
|
<el-checkbox value="STORE_STATE_CHANGED">版本状态变化</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
<div class="event-tip">不选择表示订阅全部商店状态事件。</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="bindingDialogVisible"
|
||||||
|
:title="`${bindingFor(bindingForm.storeType) ? '修改' : '创建'}${storeLabel(bindingForm.storeType)}监测绑定`"
|
||||||
|
width="620px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
v-if="bindingFor(bindingForm.storeType)?.monitorCredentialConfigured"
|
||||||
|
title="监测凭据已经配置。密钥字段全部留空时保留现有凭据;填写后将整体替换。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="dialog-alert"
|
||||||
|
/>
|
||||||
|
<el-form :model="bindingForm" label-width="140px">
|
||||||
|
<el-form-item label="应用标识" required>
|
||||||
|
<el-input
|
||||||
|
v-model.trim="bindingForm.packageIdentifier"
|
||||||
|
:placeholder="bindingForm.storeType === 'APP_STORE' ? 'iOS Bundle ID' : '鸿蒙 Bundle Name'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="应用市场地址">
|
||||||
|
<el-input v-model.trim="bindingForm.marketUrl" placeholder="可选,只用于记录跳转地址" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用定时监测">
|
||||||
|
<el-switch v-model="bindingForm.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="凭据名称">
|
||||||
|
<el-input v-model.trim="bindingForm.credentialDisplayName" placeholder="例如:生产只读监测凭据" />
|
||||||
|
</el-form-item>
|
||||||
|
<template v-if="bindingForm.storeType === 'APP_STORE'">
|
||||||
|
<el-form-item label="Issuer ID">
|
||||||
|
<el-input v-model.trim="bindingForm.issuerId" autocomplete="off" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Key ID">
|
||||||
|
<el-input v-model.trim="bindingForm.keyId" autocomplete="off" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label=".p8 私钥">
|
||||||
|
<el-input
|
||||||
|
v-model="bindingForm.privateKey"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="粘贴包含 BEGIN/END 标记的完整 PEM 内容"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item label="Client ID">
|
||||||
|
<el-input v-model.trim="bindingForm.clientId" autocomplete="off" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Client Secret">
|
||||||
|
<el-input
|
||||||
|
v-model="bindingForm.clientSecret"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="bindingDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="savingBinding" @click="saveBinding">保存绑定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
updateAdminApi,
|
||||||
|
type StoreMonitoringBinding,
|
||||||
|
type StoreMonitoringEvent,
|
||||||
|
type StoreMonitoringType,
|
||||||
|
type StoreMonitoringVersion,
|
||||||
|
type StoreUpdateDraft,
|
||||||
|
type StoreVersionState,
|
||||||
|
} from '@/api/update'
|
||||||
|
import { formatTime } from '@/utils/date'
|
||||||
|
|
||||||
|
const props = defineProps<{ appKey: string }>()
|
||||||
|
|
||||||
|
const STORE_DEFINITIONS: ReadonlyArray<{
|
||||||
|
storeType: StoreMonitoringType
|
||||||
|
label: string
|
||||||
|
platformLabel: string
|
||||||
|
}> = [
|
||||||
|
{ storeType: 'APP_STORE', label: 'Apple App Store', platformLabel: 'iOS' },
|
||||||
|
{ storeType: 'HARMONY_APP', label: '鸿蒙应用市场', platformLabel: 'HarmonyOS' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const bindings = ref<StoreMonitoringBinding[]>([])
|
||||||
|
const loadingBindings = ref(false)
|
||||||
|
const syncingStore = ref<StoreMonitoringType>()
|
||||||
|
const selectedBinding = ref<StoreMonitoringBinding>()
|
||||||
|
const versions = ref<StoreMonitoringVersion[]>([])
|
||||||
|
const events = ref<StoreMonitoringEvent[]>([])
|
||||||
|
const resultTab = ref<'versions' | 'events'>('versions')
|
||||||
|
const loadingVersions = ref(false)
|
||||||
|
const loadingEvents = ref(false)
|
||||||
|
const creatingDraftVersionId = ref<string>()
|
||||||
|
const draftResults = reactive<Record<string, StoreUpdateDraft>>({})
|
||||||
|
|
||||||
|
const bindingDialogVisible = ref(false)
|
||||||
|
const savingBinding = ref(false)
|
||||||
|
const bindingForm = reactive({
|
||||||
|
storeType: 'APP_STORE' as StoreMonitoringType,
|
||||||
|
packageIdentifier: '',
|
||||||
|
marketUrl: '',
|
||||||
|
enabled: true,
|
||||||
|
credentialDisplayName: '',
|
||||||
|
issuerId: '',
|
||||||
|
keyId: '',
|
||||||
|
privateKey: '',
|
||||||
|
clientId: '',
|
||||||
|
clientSecret: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadingNotification = ref(false)
|
||||||
|
const savingNotification = ref(false)
|
||||||
|
const notificationForm = reactive({
|
||||||
|
emailEnabled: false,
|
||||||
|
emailRecipients: [] as string[],
|
||||||
|
webhookEnabled: false,
|
||||||
|
webhookUrl: '',
|
||||||
|
webhookSecret: '',
|
||||||
|
webhookSecretConfigured: false,
|
||||||
|
eventTypes: [] as string[],
|
||||||
|
})
|
||||||
|
|
||||||
|
function bindingFor(storeType: StoreMonitoringType) {
|
||||||
|
return bindings.value.find(binding => binding.storeType === storeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeLabel(storeType?: StoreMonitoringType) {
|
||||||
|
return STORE_DEFINITIONS.find(item => item.storeType === storeType)?.label ?? '应用商店'
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayTime(value?: string) {
|
||||||
|
return value ? formatTime(value) : '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateLabel(state?: StoreVersionState) {
|
||||||
|
const labels: Partial<Record<StoreVersionState, string>> = {
|
||||||
|
DISCOVERED: '已发现',
|
||||||
|
PREPARING: '准备中',
|
||||||
|
SUBMITTED: '已提交',
|
||||||
|
WAITING_FOR_REVIEW: '等待审核',
|
||||||
|
IN_REVIEW: '审核中',
|
||||||
|
REJECTED: '审核被拒',
|
||||||
|
APPROVED_PENDING_RELEASE: '已审核待发布',
|
||||||
|
RELEASE_SCHEDULED: '已安排发布',
|
||||||
|
RELEASE_REQUESTED: '已请求发布',
|
||||||
|
PROCESSING_DISTRIBUTION: '分发处理中',
|
||||||
|
PARTIALLY_AVAILABLE: '部分地区可用',
|
||||||
|
AVAILABLE: '已上架',
|
||||||
|
WITHDRAWN: '已撤回',
|
||||||
|
REMOVED: '已下架',
|
||||||
|
UNKNOWN: '未知',
|
||||||
|
SYNC_FAILED: '同步失败',
|
||||||
|
}
|
||||||
|
return state ? labels[state] ?? state : '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateTagType(state?: StoreVersionState) {
|
||||||
|
if (state === 'AVAILABLE') return 'success'
|
||||||
|
if (state === 'REJECTED' || state === 'REMOVED' || state === 'SYNC_FAILED') return 'danger'
|
||||||
|
if (state === 'IN_REVIEW' || state === 'WAITING_FOR_REVIEW' || state === 'APPROVED_PENDING_RELEASE') {
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLabel(source: StoreMonitoringEvent['source']) {
|
||||||
|
if (source === 'WEBHOOK') return '官方回调'
|
||||||
|
if (source === 'MANUAL') return '人工操作'
|
||||||
|
if (source === 'IMPORT') return '历史迁移'
|
||||||
|
return '定时轮询'
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventLabel(eventType: string) {
|
||||||
|
if (eventType === 'STORE_VERSION_DISCOVERED') return '发现商店版本'
|
||||||
|
if (eventType === 'STORE_STATE_CHANGED') return '商店状态变化'
|
||||||
|
return eventType
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBindings() {
|
||||||
|
if (!props.appKey) return
|
||||||
|
loadingBindings.value = true
|
||||||
|
try {
|
||||||
|
const response = await updateAdminApi.listStoreMonitoringBindings(props.appKey)
|
||||||
|
bindings.value = response.data.data
|
||||||
|
if (selectedBinding.value) {
|
||||||
|
selectedBinding.value = bindingFor(selectedBinding.value.storeType)
|
||||||
|
if (selectedBinding.value) await loadVersions()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loadingBindings.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCredentialFields() {
|
||||||
|
bindingForm.issuerId = ''
|
||||||
|
bindingForm.keyId = ''
|
||||||
|
bindingForm.privateKey = ''
|
||||||
|
bindingForm.clientId = ''
|
||||||
|
bindingForm.clientSecret = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBindingDialog(storeType: StoreMonitoringType) {
|
||||||
|
const binding = bindingFor(storeType)
|
||||||
|
bindingForm.storeType = storeType
|
||||||
|
bindingForm.packageIdentifier = binding?.packageIdentifier ?? ''
|
||||||
|
bindingForm.marketUrl = binding?.marketUrl ?? ''
|
||||||
|
bindingForm.enabled = binding?.enabled ?? true
|
||||||
|
bindingForm.credentialDisplayName = ''
|
||||||
|
clearCredentialFields()
|
||||||
|
bindingDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCredential() {
|
||||||
|
if (bindingForm.storeType === 'APP_STORE') {
|
||||||
|
const anyFilled = bindingForm.issuerId || bindingForm.keyId || bindingForm.privateKey
|
||||||
|
if (!anyFilled) return undefined
|
||||||
|
if (!bindingForm.issuerId || !bindingForm.keyId || !bindingForm.privateKey) {
|
||||||
|
throw new Error('替换 App Store 凭据时必须同时填写 Issuer ID、Key ID 和 .p8 私钥')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
issuerId: bindingForm.issuerId,
|
||||||
|
keyId: bindingForm.keyId,
|
||||||
|
privateKey: bindingForm.privateKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const anyFilled = bindingForm.clientId || bindingForm.clientSecret
|
||||||
|
if (!anyFilled) return undefined
|
||||||
|
if (!bindingForm.clientId || !bindingForm.clientSecret) {
|
||||||
|
throw new Error('替换鸿蒙监测凭据时必须同时填写 Client ID 和 Client Secret')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
clientId: bindingForm.clientId,
|
||||||
|
clientSecret: bindingForm.clientSecret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBinding() {
|
||||||
|
if (!bindingForm.packageIdentifier) {
|
||||||
|
ElMessage.warning('请填写应用标识')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
savingBinding.value = true
|
||||||
|
try {
|
||||||
|
const credential = buildCredential()
|
||||||
|
if (!bindingFor(bindingForm.storeType) && !credential) {
|
||||||
|
ElMessage.warning('首次绑定必须填写完整的监测凭据')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await updateAdminApi.saveStoreMonitoringBinding(props.appKey, bindingForm.storeType, {
|
||||||
|
packageIdentifier: bindingForm.packageIdentifier,
|
||||||
|
marketUrl: bindingForm.marketUrl || undefined,
|
||||||
|
enabled: bindingForm.enabled,
|
||||||
|
credentialDisplayName: bindingForm.credentialDisplayName || undefined,
|
||||||
|
...(credential ? { credential } : {}),
|
||||||
|
})
|
||||||
|
bindingDialogVisible.value = false
|
||||||
|
clearCredentialFields()
|
||||||
|
await loadBindings()
|
||||||
|
ElMessage.success('商店监测绑定已保存')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && !('response' in error)) ElMessage.error(error.message)
|
||||||
|
} finally {
|
||||||
|
savingBinding.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function synchronize(storeType: StoreMonitoringType) {
|
||||||
|
syncingStore.value = storeType
|
||||||
|
try {
|
||||||
|
await updateAdminApi.synchronizeStoreMonitoring(props.appKey, storeType)
|
||||||
|
await loadBindings()
|
||||||
|
await selectBinding(storeType)
|
||||||
|
ElMessage.success('已完成官方商店状态同步')
|
||||||
|
} finally {
|
||||||
|
syncingStore.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectBinding(storeType: StoreMonitoringType) {
|
||||||
|
selectedBinding.value = bindingFor(storeType)
|
||||||
|
resultTab.value = 'versions'
|
||||||
|
await loadVersions()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadVersions() {
|
||||||
|
if (!selectedBinding.value) return
|
||||||
|
loadingVersions.value = true
|
||||||
|
try {
|
||||||
|
const response = await updateAdminApi.listStoreMonitoringVersions(
|
||||||
|
props.appKey,
|
||||||
|
selectedBinding.value.id,
|
||||||
|
)
|
||||||
|
versions.value = response.data.data
|
||||||
|
} finally {
|
||||||
|
loadingVersions.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEvents() {
|
||||||
|
if (!selectedBinding.value) return
|
||||||
|
loadingEvents.value = true
|
||||||
|
try {
|
||||||
|
const response = await updateAdminApi.listStoreMonitoringEvents(
|
||||||
|
props.appKey,
|
||||||
|
selectedBinding.value.id,
|
||||||
|
)
|
||||||
|
events.value = response.data.data
|
||||||
|
} finally {
|
||||||
|
loadingEvents.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canCreateUpdateDraft(version: StoreMonitoringVersion) {
|
||||||
|
return version.state === 'AVAILABLE' && version.chinaAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUpdateDraft(version: StoreMonitoringVersion) {
|
||||||
|
if (!selectedBinding.value || !canCreateUpdateDraft(version) || draftResults[version.id]) return
|
||||||
|
creatingDraftVersionId.value = version.id
|
||||||
|
try {
|
||||||
|
const response = await updateAdminApi.createStoreUpdateDraft(
|
||||||
|
props.appKey,
|
||||||
|
selectedBinding.value.id,
|
||||||
|
version.id,
|
||||||
|
)
|
||||||
|
draftResults[version.id] = response.data.data
|
||||||
|
const result = response.data.data
|
||||||
|
ElMessage.success(
|
||||||
|
result.publishStatus === 'DRAFT'
|
||||||
|
? `已创建 Xuqm 更新草稿:${result.appVersionId}`
|
||||||
|
: `该商店版本已关联 Xuqm 版本:${result.appVersionId}`,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
creatingDraftVersionId.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResultTabChange(name: string | number) {
|
||||||
|
if (name === 'events') void loadEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadNotificationPolicy() {
|
||||||
|
if (!props.appKey) return
|
||||||
|
loadingNotification.value = true
|
||||||
|
try {
|
||||||
|
const response = await updateAdminApi.getStoreNotificationPolicy(props.appKey)
|
||||||
|
const policy = response.data.data
|
||||||
|
notificationForm.emailEnabled = policy.emailEnabled
|
||||||
|
notificationForm.emailRecipients = [...policy.emailRecipients]
|
||||||
|
notificationForm.webhookEnabled = policy.webhookEnabled
|
||||||
|
notificationForm.webhookUrl = policy.webhookUrl ?? ''
|
||||||
|
notificationForm.webhookSecret = ''
|
||||||
|
notificationForm.webhookSecretConfigured = policy.webhookSecretConfigured
|
||||||
|
notificationForm.eventTypes = [...policy.eventTypes]
|
||||||
|
} finally {
|
||||||
|
loadingNotification.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNotificationPolicy() {
|
||||||
|
if (notificationForm.emailEnabled && notificationForm.emailRecipients.length === 0) {
|
||||||
|
ElMessage.warning('启用邮件通知时至少添加一个收件人')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (notificationForm.webhookEnabled && !notificationForm.webhookUrl) {
|
||||||
|
ElMessage.warning('启用 Webhook 通知时必须填写地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
notificationForm.webhookEnabled &&
|
||||||
|
!notificationForm.webhookSecretConfigured &&
|
||||||
|
!notificationForm.webhookSecret
|
||||||
|
) {
|
||||||
|
ElMessage.warning('首次启用 Webhook 通知时必须填写签名密钥')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
savingNotification.value = true
|
||||||
|
try {
|
||||||
|
const response = await updateAdminApi.saveStoreNotificationPolicy(props.appKey, {
|
||||||
|
emailEnabled: notificationForm.emailEnabled,
|
||||||
|
emailRecipients: notificationForm.emailRecipients,
|
||||||
|
webhookEnabled: notificationForm.webhookEnabled,
|
||||||
|
webhookUrl: notificationForm.webhookUrl || undefined,
|
||||||
|
webhookSecret: notificationForm.webhookSecret || undefined,
|
||||||
|
eventTypes: notificationForm.eventTypes,
|
||||||
|
})
|
||||||
|
notificationForm.webhookSecret = ''
|
||||||
|
notificationForm.webhookSecretConfigured = response.data.data.webhookSecretConfigured
|
||||||
|
notificationForm.eventTypes = [...response.data.data.eventTypes]
|
||||||
|
ElMessage.success('通知策略已保存')
|
||||||
|
} finally {
|
||||||
|
savingNotification.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initialize() {
|
||||||
|
await Promise.all([loadBindings(), loadNotificationPolicy()])
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.appKey, () => {
|
||||||
|
selectedBinding.value = undefined
|
||||||
|
versions.value = []
|
||||||
|
events.value = []
|
||||||
|
void initialize()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => void initialize())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.store-monitoring {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 22px 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading p {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binding-card {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle,
|
||||||
|
.event-meta,
|
||||||
|
.form-tip,
|
||||||
|
.event-tip {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-error,
|
||||||
|
.dialog-alert {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-heading {
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-wrap {
|
||||||
|
min-height: 140px;
|
||||||
|
padding: 12px 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-state {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-meta {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ineligible-draft {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-hint {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-form {
|
||||||
|
max-width: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-tip {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.binding-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -156,6 +156,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="应用商店监测" name="store-monitoring">
|
||||||
|
<StoreMonitoringPanel :app-key="appKey" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- RN Bundles -->
|
<!-- RN Bundles -->
|
||||||
<el-tab-pane label="RN Bundle 热更新" name="rn">
|
<el-tab-pane label="RN Bundle 热更新" name="rn">
|
||||||
<div class="toolbar responsive-toolbar">
|
<div class="toolbar responsive-toolbar">
|
||||||
@ -218,9 +222,9 @@
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- App Store Config -->
|
<!-- App Store Config -->
|
||||||
<el-tab-pane label="应用商店配置" name="store">
|
<el-tab-pane label="Android 应用商店配置" name="store">
|
||||||
<el-alert
|
<el-alert
|
||||||
title="应用商店配置按渠道分别维护,App Store 和鸿蒙只填写跳转页,不再填写密钥。审核通知单独配置一次,所有市场共享。"
|
title="这里只维护 Android 应用商店上传配置。iOS、HarmonyOS 的只读状态、跳转地址和通知统一在“应用商店监测”中维护。"
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
show-icon
|
||||||
:closable="false"
|
:closable="false"
|
||||||
@ -239,7 +243,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="store-grid">
|
<div class="store-grid">
|
||||||
<el-card
|
<el-card
|
||||||
v-for="store in STORE_DEFS"
|
v-for="store in ANDROID_STORE_DEFS"
|
||||||
:key="store.type"
|
:key="store.type"
|
||||||
class="store-card"
|
class="store-card"
|
||||||
shadow="hover"
|
shadow="hover"
|
||||||
@ -274,14 +278,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="应用配置指引" name="guide">
|
<el-tab-pane label="应用配置指引" name="guide">
|
||||||
<!-- 平台选择 -->
|
|
||||||
<div class="guide-platform-tabs">
|
|
||||||
<el-radio-group v-model="guidePlatform" size="default">
|
|
||||||
<el-radio-button value="android">🤖 Android</el-radio-button>
|
|
||||||
<el-radio-button value="ios">🍎 iOS</el-radio-button>
|
|
||||||
<el-radio-button value="harmony">🔷 鸿蒙</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
<!-- 商店列表 -->
|
<!-- 商店列表 -->
|
||||||
<div class="guide-store-list">
|
<div class="guide-store-list">
|
||||||
<el-collapse v-model="guideExpandedStores">
|
<el-collapse v-model="guideExpandedStores">
|
||||||
@ -1010,6 +1006,7 @@ import miGuideImage from '@/assets/update-store/mi/01.png'
|
|||||||
import oppoGuideImage from '@/assets/update-store/oppo/01.png'
|
import oppoGuideImage from '@/assets/update-store/oppo/01.png'
|
||||||
import vivoGuideImage from '@/assets/update-store/vivo/01.png'
|
import vivoGuideImage from '@/assets/update-store/vivo/01.png'
|
||||||
import honorGuideImage from '@/assets/update-store/honor/01.png'
|
import honorGuideImage from '@/assets/update-store/honor/01.png'
|
||||||
|
import StoreMonitoringPanel from './StoreMonitoringPanel.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -1411,32 +1408,11 @@ const STORE_DEFS: StoreDef[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const GUIDE_GROUPS = computed(() => [
|
const ANDROID_STORE_DEFS = STORE_DEFS.filter(
|
||||||
{
|
store => ['HUAWEI', 'MI', 'OPPO', 'VIVO', 'HONOR', 'GOOGLE_PLAY'].includes(store.type),
|
||||||
label: 'Android 应用商店',
|
)
|
||||||
stores: STORE_DEFS.filter(s => ['HUAWEI', 'MI', 'OPPO', 'VIVO', 'HONOR', 'GOOGLE_PLAY'].includes(s.type)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'iOS / 鸿蒙',
|
|
||||||
stores: STORE_DEFS.filter(s => ['APP_STORE', 'HARMONY_APP'].includes(s.type)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '通知配置',
|
|
||||||
stores: STORE_DEFS.filter(s => s.type === 'REVIEW_WEBHOOK'),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const guidePlatform = ref('android')
|
|
||||||
const guideExpandedStores = ref<string[]>([])
|
const guideExpandedStores = ref<string[]>([])
|
||||||
const guideFilteredStores = computed(() => {
|
const guideFilteredStores = computed(() => ANDROID_STORE_DEFS)
|
||||||
const map: Record<string, string[]> = {
|
|
||||||
android: ['HUAWEI', 'MI', 'OPPO', 'VIVO', 'HONOR', 'GOOGLE_PLAY'],
|
|
||||||
ios: ['APP_STORE'],
|
|
||||||
harmony: ['HARMONY_APP'],
|
|
||||||
}
|
|
||||||
const types = map[guidePlatform.value] ?? []
|
|
||||||
return STORE_DEFS.filter(s => types.includes(s.type))
|
|
||||||
})
|
|
||||||
|
|
||||||
function getStoreConfig(type: StoreType): StoreConfig | undefined {
|
function getStoreConfig(type: StoreType): StoreConfig | undefined {
|
||||||
return storeConfigs.value.find(c => c.storeType === type)
|
return storeConfigs.value.find(c => c.storeType === type)
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户