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 接入指南
|
||||
|
||||
**包名**:`@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-push` | 推送设备 Token 上报 |
|
||||
| `@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/
|
||||
```
|
||||
|
||||
安装 meta 包(包含全部功能):
|
||||
只使用基础能力时,直接安装 `rn-common`,不会带入 IM / Push / Update:
|
||||
|
||||
```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)
|
||||
@ -41,7 +49,7 @@ yarn add @xuqm/rn-sdk
|
||||
初始化只需传入 `appKey`,平台地址由 SDK 内置,开发者无需额外配置。
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-sdk'
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
// App 入口(如 App.tsx 的顶层)
|
||||
await XuqmSDK.initialize({
|
||||
@ -55,7 +63,7 @@ await XuqmSDK.initialize({
|
||||
### 2. IM 登录
|
||||
|
||||
```ts
|
||||
import { ImSDK } from '@xuqm/rn-sdk'
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
|
||||
// 登录(userId + 用户信息)
|
||||
// v0.3.x:传入用户信息,本地 DB 按 userId 自动隔离
|
||||
@ -150,8 +158,8 @@ await ImSDK.removeFriend('user_002')
|
||||
### 7. 消息搜索(本地)
|
||||
|
||||
```ts
|
||||
import { ImSDK } from '@xuqm/rn-sdk'
|
||||
import type { MessageSearchParams } from '@xuqm/rn-sdk'
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
import type { MessageSearchParams } from '@xuqm/rn-im'
|
||||
|
||||
const params: MessageSearchParams = {
|
||||
keyword: '会议', // 关键词搜索
|
||||
@ -166,7 +174,7 @@ const results = await ImSDK.searchMessages(params)
|
||||
### 8. 推送 SDK
|
||||
|
||||
```ts
|
||||
import { PushSDK } from '@xuqm/rn-sdk'
|
||||
import { PushSDK } from '@xuqm/rn-push'
|
||||
|
||||
// 初始化并注册设备(登录后调用)
|
||||
await PushSDK.initialize({ userId: 'user_001' })
|
||||
@ -175,7 +183,7 @@ await PushSDK.initialize({ userId: 'user_001' })
|
||||
### 9. 版本更新 SDK
|
||||
|
||||
```ts
|
||||
import { UpdateSDK } from '@xuqm/rn-sdk'
|
||||
import { UpdateSDK } from '@xuqm/rn-update'
|
||||
|
||||
// 检查 App 原生更新
|
||||
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']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
|
||||
@ -41,6 +41,21 @@ export interface ImServiceConfig {
|
||||
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 = {
|
||||
list: () => client.get<{ data: App[] }>('/apps'),
|
||||
|
||||
@ -55,6 +70,11 @@ export const appApi = {
|
||||
getServices: (appId: string) =>
|
||||
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) =>
|
||||
client.post<{ data: FeatureService }>(`/apps/${appId}/services/toggle`, null, {
|
||||
params: { platform, serviceType, enable },
|
||||
@ -64,7 +84,7 @@ export const appApi = {
|
||||
appId: string,
|
||||
platform: string,
|
||||
serviceType: string,
|
||||
config: Partial<ImServiceConfig>,
|
||||
config: Partial<ImServiceConfig> & Partial<UpdateServiceConfig>,
|
||||
) =>
|
||||
client.put<{ data: FeatureService }>(`/apps/${appId}/services/config`, config, {
|
||||
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) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
const url = config.url ?? ''
|
||||
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
|
||||
})
|
||||
|
||||
@ -147,14 +157,12 @@ export const updateAdminApi = {
|
||||
},
|
||||
|
||||
uploadAppVersion(formData: FormData) {
|
||||
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData)
|
||||
},
|
||||
|
||||
inspectAppPackage(formData: FormData) {
|
||||
return updateClient.post<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
inspectAppPackage(apkUrl: string) {
|
||||
return updateClient.get<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', {
|
||||
params: { apkUrl },
|
||||
})
|
||||
},
|
||||
|
||||
@ -177,21 +185,15 @@ export const updateAdminApi = {
|
||||
},
|
||||
|
||||
uploadRnBundle(formData: FormData) {
|
||||
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData)
|
||||
},
|
||||
|
||||
inspectRnBundle(formData: FormData) {
|
||||
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData)
|
||||
},
|
||||
|
||||
uploadUnifiedRelease(formData: FormData) {
|
||||
return updateClient.post('/api/v1/updates/unified/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return updateClient.post('/api/v1/updates/unified/upload', formData)
|
||||
},
|
||||
|
||||
// ── Store config ────────────────────────────────────────────────────────
|
||||
|
||||
1
tenant-platform/src/env.d.ts
vendored
1
tenant-platform/src/env.d.ts
vendored
@ -2,6 +2,7 @@
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string
|
||||
readonly VITE_FILE_SERVICE_URL: string
|
||||
readonly BASE_URL: string
|
||||
}
|
||||
|
||||
|
||||
@ -167,6 +167,94 @@
|
||||
</el-card>
|
||||
</div>
|
||||
</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-card>
|
||||
|
||||
@ -310,7 +398,7 @@
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="选中 APK / IPA 后会自动读取包名、版本名和版本码;若识别到的包名与当前填写不一致,会提示你确认。"
|
||||
title="选中 APK / IPA 后会先上传到文件服务,再读取包名、版本名和版本码;若识别到的包名与当前填写不一致,会提示你确认。"
|
||||
/>
|
||||
<el-divider content-position="left">发版配置</el-divider>
|
||||
<el-form-item label="定时发布">
|
||||
@ -385,6 +473,8 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { appApi, type UpdateServiceConfig } from '@/api/app'
|
||||
import { fileApi } from '@/api/file'
|
||||
import {
|
||||
updateAdminApi,
|
||||
type AppPackageInspectResult,
|
||||
@ -414,6 +504,33 @@ const loadingApp = ref(false)
|
||||
const rnBundles = ref<RnBundle[]>([])
|
||||
const loadingRn = ref(false)
|
||||
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 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 submittingToStores = ref(false)
|
||||
const submitStoreVersion = ref<AppVersion | null>(null)
|
||||
@ -707,6 +884,7 @@ const appUploadForm = ref({
|
||||
forceUpdate: false,
|
||||
changeLog: '',
|
||||
file: null as File | null,
|
||||
fileUrl: '',
|
||||
scheduledPublishAt: '',
|
||||
webhookUrl: '',
|
||||
autoPublishAfterReview: false,
|
||||
@ -717,12 +895,14 @@ const appUploadForm = ref({
|
||||
async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
||||
const file = uploadFile?.raw ?? null
|
||||
appUploadForm.value.file = file
|
||||
appUploadForm.value.fileUrl = ''
|
||||
if (!file) return
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('apkFile', file)
|
||||
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
|
||||
if (inspected.platform) appUploadForm.value.platform = inspected.platform
|
||||
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() {
|
||||
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.packageName) return ElMessage.warning('Harmony 版本请填写 bundleName / 包名')
|
||||
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.marketUrl) fd.append('marketUrl', f.marketUrl)
|
||||
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)
|
||||
ElMessage.success('上传成功')
|
||||
showUploadApp.value = false
|
||||
@ -954,6 +1134,7 @@ onMounted(() => {
|
||||
loadAppVersions()
|
||||
loadRnBundles()
|
||||
loadStoreConfigs()
|
||||
loadReleaseConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1019,4 +1200,20 @@ onMounted(() => {
|
||||
border-radius: 8px;
|
||||
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>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户