docs(sdk): 添加 React Native SDK 文档和 Android/HarmonyOS 发版脚本
- 新增 XuqmGroup React Native SDK 使用文档,包含安装、初始化、HTTP客户端、IM模块、推送模块、版本管理等功能说明 - 添加 Android Gradle 发版任务脚本,支持构建发布 APK 并上传到更新服务 - 添加 HarmonyOS hvigorw 发版任务脚本,支持 HAP 包构建和上传功能 - 实现多平台版本检查、自动重连、灰度发布等发版流程自动化 - 集成商店提交、定时发布、Webhook 回调等发布后处理功能
这个提交包含在:
父节点
5c93a14941
当前提交
470521c3a8
@ -1,8 +1,8 @@
|
|||||||
# React Native SDK 接入指南
|
# React Native SDK 接入指南
|
||||||
|
|
||||||
**包名**:`@xuqm/rn-sdk` · **版本**:0.3.x(v0.4.0 规划中,将引入 UserSig 鉴权)
|
**包名**:`@xuqm/rn-sdk` · **版本**:0.3.x(内部基础包,业务方不直接引用)
|
||||||
|
|
||||||
> **注意**:v0.4.0 将是 Breaking 版本。`initialize()` 将只保留 `appKey`,`login()` 将改为 UserSig 鉴权模式,详见[迁移指南](#迁移指南-v03x--v04x)。
|
> **注意**:`rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
| 包 | 功能 |
|
| 包 | 功能 |
|
||||||
|----|------|
|
|----|------|
|
||||||
| `@xuqm/rn-common` | 初始化、网络、设备信息(必须) |
|
| `@xuqm/rn-common` | 初始化、网络、设备信息,可独立使用 |
|
||||||
| `@xuqm/rn-im` | 单聊、群聊、消息收发、本地 DB(WatermelonDB)|
|
| `@xuqm/rn-im` | 单聊、群聊、消息收发、本地 DB(WatermelonDB)|
|
||||||
| `@xuqm/rn-push` | 推送设备 Token 上报 |
|
| `@xuqm/rn-push` | 推送设备 Token 上报 |
|
||||||
| `@xuqm/rn-update` | App 版本检查、RN Bundle 热更新 |
|
| `@xuqm/rn-update` | App 版本检查、RN Bundle 热更新 |
|
||||||
| `@xuqm/rn-sdk` | 以上所有模块的 meta 包(推荐直接使用) |
|
| `@xuqm/rn-sdk` | 内部基础包,随 IM / Push / Update 自动安装,不建议业务方直接引用 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -26,12 +26,20 @@
|
|||||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm/
|
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm/
|
||||||
```
|
```
|
||||||
|
|
||||||
安装 meta 包(包含全部功能):
|
只使用基础能力时,直接安装 `rn-common`,不会带入 IM / Push / Update:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn add @xuqm/rn-sdk
|
yarn add @xuqm/rn-common
|
||||||
```
|
```
|
||||||
|
|
||||||
|
按需安装模块时,`rn-im` / `rn-push` / `rn-update` 都会自动带上 `rn-common` 和 `rn-sdk`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @xuqm/rn-common @xuqm/rn-im
|
||||||
|
```
|
||||||
|
|
||||||
|
`rn-sdk` 不作为业务方直接安装入口;它会随着 `rn-im` / `rn-push` / `rn-update` 自动进入依赖树。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速接入(当前 v0.3.x)
|
## 快速接入(当前 v0.3.x)
|
||||||
@ -41,7 +49,7 @@ yarn add @xuqm/rn-sdk
|
|||||||
初始化只需传入 `appKey`,平台地址由 SDK 内置,开发者无需额外配置。
|
初始化只需传入 `appKey`,平台地址由 SDK 内置,开发者无需额外配置。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { XuqmSDK } from '@xuqm/rn-sdk'
|
import { XuqmSDK } from '@xuqm/rn-common'
|
||||||
|
|
||||||
// App 入口(如 App.tsx 的顶层)
|
// App 入口(如 App.tsx 的顶层)
|
||||||
await XuqmSDK.initialize({
|
await XuqmSDK.initialize({
|
||||||
@ -55,7 +63,7 @@ await XuqmSDK.initialize({
|
|||||||
### 2. IM 登录
|
### 2. IM 登录
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { ImSDK } from '@xuqm/rn-sdk'
|
import { ImSDK } from '@xuqm/rn-im'
|
||||||
|
|
||||||
// 登录(userId + 用户信息)
|
// 登录(userId + 用户信息)
|
||||||
// v0.3.x:传入用户信息,本地 DB 按 userId 自动隔离
|
// v0.3.x:传入用户信息,本地 DB 按 userId 自动隔离
|
||||||
@ -150,8 +158,8 @@ await ImSDK.removeFriend('user_002')
|
|||||||
### 7. 消息搜索(本地)
|
### 7. 消息搜索(本地)
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { ImSDK } from '@xuqm/rn-sdk'
|
import { ImSDK } from '@xuqm/rn-im'
|
||||||
import type { MessageSearchParams } from '@xuqm/rn-sdk'
|
import type { MessageSearchParams } from '@xuqm/rn-im'
|
||||||
|
|
||||||
const params: MessageSearchParams = {
|
const params: MessageSearchParams = {
|
||||||
keyword: '会议', // 关键词搜索
|
keyword: '会议', // 关键词搜索
|
||||||
@ -166,7 +174,7 @@ const results = await ImSDK.searchMessages(params)
|
|||||||
### 8. 推送 SDK
|
### 8. 推送 SDK
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { PushSDK } from '@xuqm/rn-sdk'
|
import { PushSDK } from '@xuqm/rn-push'
|
||||||
|
|
||||||
// 初始化并注册设备(登录后调用)
|
// 初始化并注册设备(登录后调用)
|
||||||
await PushSDK.initialize({ userId: 'user_001' })
|
await PushSDK.initialize({ userId: 'user_001' })
|
||||||
@ -175,7 +183,7 @@ await PushSDK.initialize({ userId: 'user_001' })
|
|||||||
### 9. 版本更新 SDK
|
### 9. 版本更新 SDK
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { UpdateSDK } from '@xuqm/rn-sdk'
|
import { UpdateSDK } from '@xuqm/rn-update'
|
||||||
|
|
||||||
// 检查 App 原生更新
|
// 检查 App 原生更新
|
||||||
const appUpdate = await UpdateSDK.checkAppUpdate()
|
const appUpdate = await UpdateSDK.checkAppUpdate()
|
||||||
|
|||||||
1
tenant-platform/components.d.ts
vendored
1
tenant-platform/components.d.ts
vendored
@ -21,7 +21,6 @@ declare module 'vue' {
|
|||||||
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']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
|
||||||
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']
|
||||||
|
|||||||
@ -41,6 +41,21 @@ export interface ImServiceConfig {
|
|||||||
multiClientConversationDeleteSync: boolean
|
multiClientConversationDeleteSync: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateServiceConfig {
|
||||||
|
defaultStoreTargets?: string[]
|
||||||
|
defaultPublishMode?: 'MANUAL' | 'NOW' | 'SCHEDULED' | 'AUTO_REVIEW'
|
||||||
|
defaultPublishImmediately?: boolean
|
||||||
|
defaultScheduledPublishAt?: string
|
||||||
|
defaultAutoPublishAfterReview?: boolean
|
||||||
|
defaultWebhookUrl?: string
|
||||||
|
defaultForceUpdate?: boolean
|
||||||
|
defaultGrayEnabled?: boolean
|
||||||
|
defaultGrayPercent?: number
|
||||||
|
defaultPackageName?: string
|
||||||
|
defaultAppStoreUrl?: string
|
||||||
|
defaultMarketUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const appApi = {
|
export const appApi = {
|
||||||
list: () => client.get<{ data: App[] }>('/apps'),
|
list: () => client.get<{ data: App[] }>('/apps'),
|
||||||
|
|
||||||
@ -55,6 +70,11 @@ export const appApi = {
|
|||||||
getServices: (appId: string) =>
|
getServices: (appId: string) =>
|
||||||
client.get<{ data: FeatureService[] }>(`/apps/${appId}/services`),
|
client.get<{ data: FeatureService[] }>(`/apps/${appId}/services`),
|
||||||
|
|
||||||
|
getService: (appId: string, platform: string, serviceType: string) =>
|
||||||
|
client.get<{ data: FeatureService }>(`/apps/${appId}/services/item`, {
|
||||||
|
params: { platform, serviceType },
|
||||||
|
}),
|
||||||
|
|
||||||
toggleService: (appId: string, platform: string, serviceType: string, enable: boolean) =>
|
toggleService: (appId: string, platform: string, serviceType: string, enable: boolean) =>
|
||||||
client.post<{ data: FeatureService }>(`/apps/${appId}/services/toggle`, null, {
|
client.post<{ data: FeatureService }>(`/apps/${appId}/services/toggle`, null, {
|
||||||
params: { platform, serviceType, enable },
|
params: { platform, serviceType, enable },
|
||||||
@ -64,7 +84,7 @@ export const appApi = {
|
|||||||
appId: string,
|
appId: string,
|
||||||
platform: string,
|
platform: string,
|
||||||
serviceType: string,
|
serviceType: string,
|
||||||
config: Partial<ImServiceConfig>,
|
config: Partial<ImServiceConfig> & Partial<UpdateServiceConfig>,
|
||||||
) =>
|
) =>
|
||||||
client.put<{ data: FeatureService }>(`/apps/${appId}/services/config`, config, {
|
client.put<{ data: FeatureService }>(`/apps/${appId}/services/config`, config, {
|
||||||
params: { platform, serviceType },
|
params: { platform, serviceType },
|
||||||
|
|||||||
53
tenant-platform/src/api/file.ts
普通文件
53
tenant-platform/src/api/file.ts
普通文件
@ -0,0 +1,53 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const fileClient = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_FILE_SERVICE_URL ?? 'http://192.168.116.9:8086',
|
||||||
|
timeout: 30000,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
fileClient.interceptors.request.use((config) => {
|
||||||
|
console.debug('[tenant-platform][FILE] request', {
|
||||||
|
method: config.method?.toUpperCase(),
|
||||||
|
url: config.baseURL ? `${config.baseURL}${config.url ?? ''}` : config.url,
|
||||||
|
params: config.params,
|
||||||
|
})
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
fileClient.interceptors.response.use((res) => {
|
||||||
|
console.debug('[tenant-platform][FILE] response', {
|
||||||
|
status: res.status,
|
||||||
|
url: res.config.url,
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileClient.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface FileUploadResult {
|
||||||
|
url: string
|
||||||
|
thumbnailUrl?: string
|
||||||
|
hash: string
|
||||||
|
size: number
|
||||||
|
originalName: string
|
||||||
|
mimeType?: string
|
||||||
|
ext?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileApi = {
|
||||||
|
uploadFile(file: File, thumbnail?: File | null) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
if (thumbnail) {
|
||||||
|
formData.append('thumbnail', thumbnail)
|
||||||
|
}
|
||||||
|
return fileClient.post<{ data: FileUploadResult }>('/api/file/upload', formData)
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -24,8 +24,18 @@ if (import.meta.env.DEV) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateClient.interceptors.request.use((config) => {
|
updateClient.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('token')
|
const url = config.url ?? ''
|
||||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
const skipAuth = (
|
||||||
|
url.startsWith('/api/v1/updates/app/check') ||
|
||||||
|
url.startsWith('/api/v1/updates/app/inspect') ||
|
||||||
|
url.startsWith('/api/v1/rn/update/check') ||
|
||||||
|
url.startsWith('/api/v1/rn/inspect') ||
|
||||||
|
url.startsWith('/api/v1/rn/files/')
|
||||||
|
)
|
||||||
|
if (!skipAuth) {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -147,14 +157,12 @@ export const updateAdminApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
uploadAppVersion(formData: FormData) {
|
uploadAppVersion(formData: FormData) {
|
||||||
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData, {
|
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData)
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
inspectAppPackage(formData: FormData) {
|
inspectAppPackage(apkUrl: string) {
|
||||||
return updateClient.post<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', formData, {
|
return updateClient.get<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
params: { apkUrl },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -177,21 +185,15 @@ export const updateAdminApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
uploadRnBundle(formData: FormData) {
|
uploadRnBundle(formData: FormData) {
|
||||||
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, {
|
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData)
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
inspectRnBundle(formData: FormData) {
|
inspectRnBundle(formData: FormData) {
|
||||||
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, {
|
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData)
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadUnifiedRelease(formData: FormData) {
|
uploadUnifiedRelease(formData: FormData) {
|
||||||
return updateClient.post('/api/v1/updates/unified/upload', formData, {
|
return updateClient.post('/api/v1/updates/unified/upload', formData)
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Store config ────────────────────────────────────────────────────────
|
// ── Store config ────────────────────────────────────────────────────────
|
||||||
|
|||||||
1
tenant-platform/src/env.d.ts
vendored
1
tenant-platform/src/env.d.ts
vendored
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_BASE_URL: string
|
readonly VITE_API_BASE_URL: string
|
||||||
|
readonly VITE_FILE_SERVICE_URL: string
|
||||||
readonly BASE_URL: string
|
readonly BASE_URL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -167,6 +167,94 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Release Defaults -->
|
||||||
|
<el-tab-pane label="发版默认配置" name="release-config">
|
||||||
|
<el-alert
|
||||||
|
title="这里配置的是当前租户应用的更新默认值,脚本运行时会自动读取并优先使用这些设置。它们不会替代每次发版时的最终确认。"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom:16px"
|
||||||
|
/>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-radio-group v-model="releaseConfigPlatform" @change="loadReleaseConfig" style="margin-right:12px">
|
||||||
|
<el-radio-button value="ANDROID">Android</el-radio-button>
|
||||||
|
<el-radio-button value="IOS">iOS</el-radio-button>
|
||||||
|
<el-radio-button value="HARMONY">Harmony</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-button @click="loadReleaseConfig" :loading="loadingReleaseConfig">刷新</el-button>
|
||||||
|
<el-button type="primary" @click="saveReleaseConfig" :loading="savingReleaseConfig">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form :model="releaseConfigForm" label-width="150px" class="release-config-form">
|
||||||
|
<el-form-item label="默认发布模式">
|
||||||
|
<el-select v-model="releaseConfigForm.defaultPublishMode" style="width:240px">
|
||||||
|
<el-option value="MANUAL" label="手动发布" />
|
||||||
|
<el-option value="NOW" label="上传即发布" />
|
||||||
|
<el-option value="SCHEDULED" label="定时发布" />
|
||||||
|
<el-option value="AUTO_REVIEW" label="审核通过后自动发布" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认计划发布时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="releaseConfigForm.defaultScheduledPublishAt"
|
||||||
|
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="默认审核后自动发布">
|
||||||
|
<el-switch v-model="releaseConfigForm.defaultAutoPublishAfterReview" :disabled="!!releaseConfigForm.defaultScheduledPublishAt" />
|
||||||
|
<span class="form-tip">与默认定时发布互斥</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认立即发布">
|
||||||
|
<el-switch v-model="releaseConfigForm.defaultPublishImmediately" :disabled="!!releaseConfigForm.defaultScheduledPublishAt" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认 Webhook URL">
|
||||||
|
<el-input v-model="releaseConfigForm.defaultWebhookUrl" placeholder="审核状态变更时回调的 URL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认强制更新">
|
||||||
|
<el-switch v-model="releaseConfigForm.defaultForceUpdate" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认灰度开关">
|
||||||
|
<el-switch v-model="releaseConfigForm.defaultGrayEnabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="releaseConfigForm.defaultGrayEnabled" label="默认灰度比例">
|
||||||
|
<el-slider v-model="releaseConfigForm.defaultGrayPercent" :min="1" :max="100" show-input />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认包名 / Bundle ID">
|
||||||
|
<el-input v-model="releaseConfigForm.defaultPackageName" placeholder="脚本可自动读取,也可在这里预置" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="releaseConfigPlatform === 'IOS'" label="默认 App Store 链接">
|
||||||
|
<el-input v-model="releaseConfigForm.defaultAppStoreUrl" placeholder="iOS 默认跳转链接" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="releaseConfigPlatform === 'HARMONY'" label="默认应用市场链接">
|
||||||
|
<el-input v-model="releaseConfigForm.defaultMarketUrl" placeholder="Harmony 默认市场链接" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="默认市场提交目标">
|
||||||
|
<el-checkbox-group
|
||||||
|
v-model="releaseConfigForm.defaultStoreTargets"
|
||||||
|
:disabled="releaseStoreDefs.length === 0"
|
||||||
|
>
|
||||||
|
<div v-if="releaseStoreDefs.length" class="store-checkbox-grid">
|
||||||
|
<div v-for="store in releaseStoreDefs" :key="store.type" class="release-store-checkbox-row">
|
||||||
|
<el-checkbox :value="store.type">{{ store.label }}</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-alert
|
||||||
|
v-else
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="当前平台没有需要默认勾选的市场目标,仍可手动在发版时选择。"
|
||||||
|
/>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@ -310,7 +398,7 @@
|
|||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
title="选中 APK / IPA 后会自动读取包名、版本名和版本码;若识别到的包名与当前填写不一致,会提示你确认。"
|
title="选中 APK / IPA 后会先上传到文件服务,再读取包名、版本名和版本码;若识别到的包名与当前填写不一致,会提示你确认。"
|
||||||
/>
|
/>
|
||||||
<el-divider content-position="left">发版配置</el-divider>
|
<el-divider content-position="left">发版配置</el-divider>
|
||||||
<el-form-item label="定时发布">
|
<el-form-item label="定时发布">
|
||||||
@ -385,6 +473,8 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { appApi, type UpdateServiceConfig } from '@/api/app'
|
||||||
|
import { fileApi } from '@/api/file'
|
||||||
import {
|
import {
|
||||||
updateAdminApi,
|
updateAdminApi,
|
||||||
type AppPackageInspectResult,
|
type AppPackageInspectResult,
|
||||||
@ -414,6 +504,33 @@ const loadingApp = ref(false)
|
|||||||
const rnBundles = ref<RnBundle[]>([])
|
const rnBundles = ref<RnBundle[]>([])
|
||||||
const loadingRn = ref(false)
|
const loadingRn = ref(false)
|
||||||
const storeConfigs = ref<StoreConfig[]>([])
|
const storeConfigs = ref<StoreConfig[]>([])
|
||||||
|
const releaseConfigPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY'>('ANDROID')
|
||||||
|
const loadingReleaseConfig = ref(false)
|
||||||
|
const savingReleaseConfig = ref(false)
|
||||||
|
const releaseStoreDefs = computed(() => {
|
||||||
|
if (releaseConfigPlatform.value === 'ANDROID') {
|
||||||
|
return STORE_DEFS.filter(store => store.type !== 'APP_STORE')
|
||||||
|
}
|
||||||
|
if (releaseConfigPlatform.value === 'IOS') {
|
||||||
|
return STORE_DEFS.filter(store => store.type === 'APP_STORE')
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
const releaseConfigForm = ref<UpdateServiceConfig>({
|
||||||
|
defaultStoreTargets: [],
|
||||||
|
defaultPublishMode: 'MANUAL',
|
||||||
|
defaultPublishImmediately: false,
|
||||||
|
defaultScheduledPublishAt: '',
|
||||||
|
defaultAutoPublishAfterReview: false,
|
||||||
|
defaultWebhookUrl: '',
|
||||||
|
defaultForceUpdate: false,
|
||||||
|
defaultGrayEnabled: false,
|
||||||
|
defaultGrayPercent: 0,
|
||||||
|
defaultPackageName: '',
|
||||||
|
defaultAppStoreUrl: '',
|
||||||
|
defaultMarketUrl: '',
|
||||||
|
})
|
||||||
|
|
||||||
type FieldDef = { key: string; label: string; type?: 'password' | 'textarea'; placeholder?: string }
|
type FieldDef = { key: string; label: string; type?: 'password' | 'textarea'; placeholder?: string }
|
||||||
type GuideStep = { title: string; description: string }
|
type GuideStep = { title: string; description: string }
|
||||||
@ -641,6 +758,66 @@ async function removeStoreConfig(type: StoreType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeReleaseConfig(raw: Partial<UpdateServiceConfig> | null | undefined): UpdateServiceConfig {
|
||||||
|
return {
|
||||||
|
defaultStoreTargets: raw?.defaultStoreTargets ?? [],
|
||||||
|
defaultPublishMode: raw?.defaultPublishMode ?? 'MANUAL',
|
||||||
|
defaultPublishImmediately: raw?.defaultPublishImmediately ?? false,
|
||||||
|
defaultScheduledPublishAt: raw?.defaultScheduledPublishAt ?? '',
|
||||||
|
defaultAutoPublishAfterReview: raw?.defaultAutoPublishAfterReview ?? false,
|
||||||
|
defaultWebhookUrl: raw?.defaultWebhookUrl ?? '',
|
||||||
|
defaultForceUpdate: raw?.defaultForceUpdate ?? false,
|
||||||
|
defaultGrayEnabled: raw?.defaultGrayEnabled ?? false,
|
||||||
|
defaultGrayPercent: raw?.defaultGrayPercent ?? 0,
|
||||||
|
defaultPackageName: raw?.defaultPackageName ?? '',
|
||||||
|
defaultAppStoreUrl: raw?.defaultAppStoreUrl ?? '',
|
||||||
|
defaultMarketUrl: raw?.defaultMarketUrl ?? '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseReleaseConfig(config?: string | null): UpdateServiceConfig {
|
||||||
|
if (!config) return normalizeReleaseConfig({})
|
||||||
|
try {
|
||||||
|
return normalizeReleaseConfig(JSON.parse(config) as Partial<UpdateServiceConfig>)
|
||||||
|
} catch {
|
||||||
|
return normalizeReleaseConfig({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadReleaseConfig() {
|
||||||
|
loadingReleaseConfig.value = true
|
||||||
|
try {
|
||||||
|
const res = await appApi.getService(appId, releaseConfigPlatform.value, 'UPDATE')
|
||||||
|
releaseConfigForm.value = parseReleaseConfig(res.data.data.config)
|
||||||
|
const cfg = releaseConfigForm.value
|
||||||
|
if (cfg.defaultScheduledPublishAt) {
|
||||||
|
cfg.defaultPublishImmediately = false
|
||||||
|
cfg.defaultAutoPublishAfterReview = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
releaseConfigForm.value = normalizeReleaseConfig({})
|
||||||
|
} finally {
|
||||||
|
loadingReleaseConfig.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveReleaseConfig() {
|
||||||
|
savingReleaseConfig.value = true
|
||||||
|
try {
|
||||||
|
const cfg = normalizeReleaseConfig(releaseConfigForm.value)
|
||||||
|
if (cfg.defaultScheduledPublishAt) {
|
||||||
|
cfg.defaultPublishImmediately = false
|
||||||
|
cfg.defaultAutoPublishAfterReview = false
|
||||||
|
}
|
||||||
|
await appApi.updateServiceConfig(appId, releaseConfigPlatform.value, 'UPDATE', cfg)
|
||||||
|
ElMessage.success('发版默认配置已保存')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('保存失败')
|
||||||
|
} finally {
|
||||||
|
savingReleaseConfig.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const showSubmitStore = ref(false)
|
const showSubmitStore = ref(false)
|
||||||
const submittingToStores = ref(false)
|
const submittingToStores = ref(false)
|
||||||
const submitStoreVersion = ref<AppVersion | null>(null)
|
const submitStoreVersion = ref<AppVersion | null>(null)
|
||||||
@ -707,6 +884,7 @@ const appUploadForm = ref({
|
|||||||
forceUpdate: false,
|
forceUpdate: false,
|
||||||
changeLog: '',
|
changeLog: '',
|
||||||
file: null as File | null,
|
file: null as File | null,
|
||||||
|
fileUrl: '',
|
||||||
scheduledPublishAt: '',
|
scheduledPublishAt: '',
|
||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
autoPublishAfterReview: false,
|
autoPublishAfterReview: false,
|
||||||
@ -717,12 +895,14 @@ const appUploadForm = ref({
|
|||||||
async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
||||||
const file = uploadFile?.raw ?? null
|
const file = uploadFile?.raw ?? null
|
||||||
appUploadForm.value.file = file
|
appUploadForm.value.file = file
|
||||||
|
appUploadForm.value.fileUrl = ''
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('apkFile', file)
|
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.inspectAppPackage(formData)
|
const uploaded = await fileApi.uploadFile(file)
|
||||||
|
const fileInfo = uploaded.data.data
|
||||||
|
appUploadForm.value.fileUrl = fileInfo.url
|
||||||
|
const res = await updateAdminApi.inspectAppPackage(fileInfo.url)
|
||||||
const inspected = res.data.data as AppPackageInspectResult
|
const inspected = res.data.data as AppPackageInspectResult
|
||||||
if (inspected.platform) appUploadForm.value.platform = inspected.platform
|
if (inspected.platform) appUploadForm.value.platform = inspected.platform
|
||||||
if (inspected.packageName && appUploadForm.value.packageName && appUploadForm.value.packageName !== inspected.packageName) {
|
if (inspected.packageName && appUploadForm.value.packageName && appUploadForm.value.packageName !== inspected.packageName) {
|
||||||
@ -748,7 +928,7 @@ async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
|||||||
|
|
||||||
async function submitAppUpload() {
|
async function submitAppUpload() {
|
||||||
const f = appUploadForm.value
|
const f = appUploadForm.value
|
||||||
if (f.platform !== 'HARMONY' && !f.file) return ElMessage.warning('请先选择应用包文件')
|
if (f.platform !== 'HARMONY' && !f.fileUrl) return ElMessage.warning('请先选择应用包文件')
|
||||||
if (f.platform === 'HARMONY' && !f.marketUrl) return ElMessage.warning('Harmony 版本请填写应用市场链接')
|
if (f.platform === 'HARMONY' && !f.marketUrl) return ElMessage.warning('Harmony 版本请填写应用市场链接')
|
||||||
if (f.platform === 'HARMONY' && !f.packageName) return ElMessage.warning('Harmony 版本请填写 bundleName / 包名')
|
if (f.platform === 'HARMONY' && !f.packageName) return ElMessage.warning('Harmony 版本请填写 bundleName / 包名')
|
||||||
if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')
|
if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')
|
||||||
@ -772,7 +952,7 @@ async function submitAppUpload() {
|
|||||||
if (f.appStoreUrl) fd.append('appStoreUrl', f.appStoreUrl)
|
if (f.appStoreUrl) fd.append('appStoreUrl', f.appStoreUrl)
|
||||||
if (f.marketUrl) fd.append('marketUrl', f.marketUrl)
|
if (f.marketUrl) fd.append('marketUrl', f.marketUrl)
|
||||||
fd.append('autoPublishAfterReview', String(f.autoPublishAfterReview))
|
fd.append('autoPublishAfterReview', String(f.autoPublishAfterReview))
|
||||||
if (f.file) fd.append('apkFile', f.file)
|
if (f.fileUrl) fd.append('apkUrl', f.fileUrl)
|
||||||
await updateAdminApi.uploadAppVersion(fd)
|
await updateAdminApi.uploadAppVersion(fd)
|
||||||
ElMessage.success('上传成功')
|
ElMessage.success('上传成功')
|
||||||
showUploadApp.value = false
|
showUploadApp.value = false
|
||||||
@ -954,6 +1134,7 @@ onMounted(() => {
|
|||||||
loadAppVersions()
|
loadAppVersions()
|
||||||
loadRnBundles()
|
loadRnBundles()
|
||||||
loadStoreConfigs()
|
loadStoreConfigs()
|
||||||
|
loadReleaseConfig()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -1019,4 +1200,20 @@ onMounted(() => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.release-config-form {
|
||||||
|
max-width: 920px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-checkbox-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 8px 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-store-checkbox-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户