docs(sdk): 添加 Android SDK 文档和 API 设计规范
- 新增 Android SDK 使用文档,包含模块结构、集成方式和快速开始指南 - 添加 SDK API 重设计规范,统一初始化和登录接口设计 - 补充安全设计规范,完善 UserSig 鉴权和敏感数据处理方案 - 创建平台 REST API 规范,定义服务端到服务端的调用接口 - 添加离线推送架构设计,集成各大厂商推送服务与 IM 联动方案
这个提交包含在:
父节点
9c1dc4fbd7
当前提交
fb82e4b8b6
@ -39,13 +39,13 @@ dependencies {
|
|||||||
|
|
||||||
### 2. 初始化
|
### 2. 初始化
|
||||||
|
|
||||||
只需传入 `appId`,服务器地址由 SDK 内置,**无需传 `serverUrl`**。
|
只需传入 `appKey`,服务器地址由 SDK 内置,**无需传 `serverUrl`**。
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// Application.onCreate()
|
// Application.onCreate()
|
||||||
XuqmSDK.initialize(
|
XuqmSDK.initialize(
|
||||||
context = this,
|
context = this,
|
||||||
appId = "your_app_id", // 在租户平台创建应用后获得
|
appKey = "your_app_key", // 在租户平台创建应用后获得
|
||||||
logLevel = LogLevel.WARN, // 可选,DEBUG 开启详细日志
|
logLevel = LogLevel.WARN, // 可选,DEBUG 开启详细日志
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
1. 访问 [XuqmGroup 控制台](https://dev.xuqinmin.com)
|
1. 访问 [XuqmGroup 控制台](https://dev.xuqinmin.com)
|
||||||
2. 注册租户账号,创建应用
|
2. 注册租户账号,创建应用
|
||||||
3. 记录 `appId`(即 `appKey`)
|
3. 记录 `appKey`
|
||||||
|
|
||||||
## 2. 选择你的平台
|
## 2. 选择你的平台
|
||||||
|
|
||||||
@ -25,7 +25,7 @@
|
|||||||
```
|
```
|
||||||
API 地址:https://dev.xuqinmin.com
|
API 地址:https://dev.xuqinmin.com
|
||||||
WS 地址:wss://dev.xuqinmin.com/ws/im
|
WS 地址:wss://dev.xuqinmin.com/ws/im
|
||||||
演示 AppId:ak_demo_chat
|
演示 AppKey:ak_demo_chat
|
||||||
演示用户:demo_alice / demo_bob
|
演示用户:demo_alice / demo_bob
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -37,8 +37,9 @@ WS 地址:wss://dev.xuqinmin.com/ws/im
|
|||||||
|
|
||||||
```
|
```
|
||||||
你的业务服务端
|
你的业务服务端
|
||||||
→ 持有 appId/appSecret
|
→ 持有 appKey/appSecret
|
||||||
→ 调用 POST /api/im/auth/login?appId=&userId=&nickname= 换取 IM Token
|
→ 调用 IM 登录接口换取 IM Token
|
||||||
|
→ 平台内部协议字段由 SDK 和后端自动处理,业务方无需感知
|
||||||
→ 返回 Token 给客户端
|
→ 返回 Token 给客户端
|
||||||
|
|
||||||
客户端 SDK
|
客户端 SDK
|
||||||
|
|||||||
@ -44,11 +44,7 @@ import { XuqmSDK } from '@xuqm/harmony-sdk'
|
|||||||
export default class EntryAbility extends UIAbility {
|
export default class EntryAbility extends UIAbility {
|
||||||
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
|
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
|
||||||
XuqmSDK.init({
|
XuqmSDK.init({
|
||||||
appId: 'your_app_id',
|
appKey: 'your_app_key',
|
||||||
appKey: 'your_app_id',
|
|
||||||
appSecret: 'your_app_secret',
|
|
||||||
apiBaseUrl: 'https://dev.xuqinmin.com',
|
|
||||||
imWsUrl: 'wss://dev.xuqinmin.com/ws/im',
|
|
||||||
debug: true,
|
debug: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -113,8 +109,8 @@ const sent = await ImSDK.sendMessage({ toId: group.id, chatType: 'GROUP', msgTyp
|
|||||||
import { UpdateSDK } from '@xuqm/harmony-sdk'
|
import { UpdateSDK } from '@xuqm/harmony-sdk'
|
||||||
|
|
||||||
const appInfo = await UpdateSDK.checkAppUpdate(1)
|
const appInfo = await UpdateSDK.checkAppUpdate(1)
|
||||||
if (appInfo?.forceUpdate) {
|
if (appInfo?.marketUrl) {
|
||||||
// 跳转应用市场
|
await UpdateSDK.openAppMarket(getContext(this), appInfo.marketUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
const bundle = await UpdateSDK.checkRNUpdate('home', '1.0.0')
|
const bundle = await UpdateSDK.checkRNUpdate('home', '1.0.0')
|
||||||
@ -124,6 +120,8 @@ if (bundle) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Harmony 的整包更新只提供应用市场跳转,不提供本地安装包下载。
|
||||||
|
|
||||||
## ArkUI 示例
|
## ArkUI 示例
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@ -52,10 +52,8 @@ dependencies: [
|
|||||||
import XuqmCore
|
import XuqmCore
|
||||||
|
|
||||||
XuqmSDK.shared.initialize(
|
XuqmSDK.shared.initialize(
|
||||||
appKey: "your_app_id",
|
appKey: "your_app_key",
|
||||||
appSecret: "your_app_secret",
|
appSecret: "your_app_secret",
|
||||||
apiBaseUrl: "https://dev.xuqinmin.com",
|
|
||||||
imWsUrl: "wss://dev.xuqinmin.com/ws/im",
|
|
||||||
debug: false
|
debug: false
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -65,7 +63,7 @@ XuqmSDK.shared.initialize(
|
|||||||
```swift
|
```swift
|
||||||
import XuqmIM
|
import XuqmIM
|
||||||
|
|
||||||
// 登录(appId 已在 init 时指定)
|
// 登录(appKey 已在 init 时指定)
|
||||||
try await ImSDK.shared.login(userId: "user_001", nickname: "张三")
|
try await ImSDK.shared.login(userId: "user_001", nickname: "张三")
|
||||||
|
|
||||||
// 监听事件
|
// 监听事件
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**包名**:`@xuqm/rn-sdk` · **版本**:0.3.x(v0.4.0 规划中,将引入 UserSig 鉴权)
|
**包名**:`@xuqm/rn-sdk` · **版本**:0.3.x(v0.4.0 规划中,将引入 UserSig 鉴权)
|
||||||
|
|
||||||
> **注意**:v0.4.0 将是 Breaking 版本。`initialize()` 将移除 `serverUrl` 参数,`login()` 将改为 UserSig 鉴权模式,详见[迁移指南](#迁移指南-v03x--v04x)。
|
> **注意**:v0.4.0 将是 Breaking 版本。`initialize()` 将只保留 `appKey`,`login()` 将改为 UserSig 鉴权模式,详见[迁移指南](#迁移指南-v03x--v04x)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -38,14 +38,14 @@ yarn add @xuqm/rn-sdk
|
|||||||
|
|
||||||
### 1. 初始化
|
### 1. 初始化
|
||||||
|
|
||||||
初始化只需传入 `appId`,服务器地址由 SDK 内置,**不需要传 `serverUrl`**。
|
初始化只需传入 `appKey`,平台地址由 SDK 内置,开发者无需额外配置。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { XuqmSDK } from '@xuqm/rn-sdk'
|
import { XuqmSDK } from '@xuqm/rn-sdk'
|
||||||
|
|
||||||
// App 入口(如 App.tsx 的顶层)
|
// App 入口(如 App.tsx 的顶层)
|
||||||
await XuqmSDK.initialize({
|
await XuqmSDK.initialize({
|
||||||
appId: 'your_app_id', // 在租户平台创建应用后获得
|
appKey: 'your_app_key', // 在租户平台创建应用后获得
|
||||||
logLevel: __DEV__ ? 'debug' : 'warn', // 可选
|
logLevel: __DEV__ ? 'debug' : 'warn', // 可选
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@ -247,14 +247,14 @@ v0.4.0 预计改动(**Breaking Changes**):
|
|||||||
|
|
||||||
```diff
|
```diff
|
||||||
// 初始化
|
// 初始化
|
||||||
- await XuqmSDK.initialize({ appId: 'xxx', serverUrl: 'https://...' })
|
- await XuqmSDK.initialize({ appKey: 'xxx', /* 平台地址参数已移除 */ })
|
||||||
+ await XuqmSDK.initialize({ appId: 'xxx' }) // serverUrl 内置,无需传入
|
+ await XuqmSDK.initialize({ appKey: 'xxx' }) // 平台地址内置,无需传入
|
||||||
|
|
||||||
// IM 登录(改为 UserSig 鉴权,密码不再传入 SDK)
|
// IM 登录(改为 UserSig 鉴权,密码不再传入 SDK)
|
||||||
- await ImSDK.login(userId, nickname, avatar, dbName)
|
- await ImSDK.login(userId, nickname, avatar, dbName)
|
||||||
+ const userSig = await yourServer.getUserSig(userId) // 您的服务端签发
|
+ const userSig = await yourServer.getUserSig(userId) // 您的服务端签发
|
||||||
+ await ImSDK.login(userId, userSig, { nickname, avatar })
|
+ await ImSDK.login(userId, userSig, { nickname, avatar })
|
||||||
// dbName 自动由 appId + userId 派生,无需传入
|
// dbName 自动由 appKey + userId 派生,无需传入
|
||||||
|
|
||||||
// loginWithToken 废弃
|
// loginWithToken 废弃
|
||||||
- await ImSDK.loginWithToken(userId, token, dbName)
|
- await ImSDK.loginWithToken(userId, token, dbName)
|
||||||
@ -267,14 +267,14 @@ UserSig 生成方式见[安全设计文档](../../design/02-security-design.md)
|
|||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
**Q: 如何获取 appId?**
|
**Q: 如何获取 appKey?**
|
||||||
在 [租户平台](https://dev.xuqinmin.com) 注册账号后,创建应用即可获得 AppId。
|
在 [租户平台](https://dev.xuqinmin.com) 注册账号后,创建应用即可获得 AppKey。
|
||||||
|
|
||||||
**Q: 为什么不需要传 serverUrl?**
|
**Q: 为什么不需要传平台地址参数?**
|
||||||
XuqmGroup 是托管平台,服务地址统一管理,与腾讯云 IM 等平台的设计一致,开发者只需关心业务逻辑。
|
XuqmGroup 是托管平台,服务地址统一管理,与腾讯云 IM 等平台的设计一致,开发者只需关心业务逻辑。
|
||||||
|
|
||||||
**Q: UserSig 是什么?(v0.4.0)**
|
**Q: UserSig 是什么?(v0.4.0)**
|
||||||
UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证,有效期可配置。AppSecret 绝不下发到客户端。
|
UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证,有效期可配置。AppSecret 绝不下发到客户端。
|
||||||
|
|
||||||
**Q: 本地消息存储在哪里?**
|
**Q: 本地消息存储在哪里?**
|
||||||
使用 WatermelonDB(SQLite),按 `appId + userId` 自动隔离,多账号切换安全。
|
使用 WatermelonDB(SQLite),按 `appKey + userId` 自动隔离,多账号切换安全。
|
||||||
|
|||||||
@ -35,11 +35,7 @@ import App from './App.vue'
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createXuqm({
|
app.use(createXuqm({
|
||||||
appId: 'your_app_id',
|
appKey: 'your_app_key',
|
||||||
appKey: 'your_app_id',
|
|
||||||
appSecret: 'your_app_secret',
|
|
||||||
apiBaseUrl: 'https://dev.xuqinmin.com',
|
|
||||||
imWsUrl: 'wss://dev.xuqinmin.com/ws/im',
|
|
||||||
debug: import.meta.env.DEV,
|
debug: import.meta.env.DEV,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
1
tenant-platform/components.d.ts
vendored
1
tenant-platform/components.d.ts
vendored
@ -21,6 +21,7 @@ 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']
|
||||||
|
|||||||
@ -103,25 +103,6 @@ export interface OperationLog {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
|
||||||
id: string
|
|
||||||
appId: string
|
|
||||||
fromUserId: string
|
|
||||||
toUserId: string
|
|
||||||
remark?: string | null
|
|
||||||
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
|
|
||||||
createdAt: number
|
|
||||||
reviewedAt?: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlacklistEntry {
|
|
||||||
id: string
|
|
||||||
appId: string
|
|
||||||
userId: string
|
|
||||||
blockedUserId: string
|
|
||||||
createdAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KeywordFilter {
|
export interface KeywordFilter {
|
||||||
id: string
|
id: string
|
||||||
appId: string
|
appId: string
|
||||||
@ -227,76 +208,6 @@ export const imAdminApi = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
listFriendRequests(appId: string) {
|
|
||||||
return imClient.get<{ data: FriendRequest[] }>('/api/im/admin/friend-requests', {
|
|
||||||
params: { appId },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
sendFriendRequest(appId: string, toUserId: string, remark?: string) {
|
|
||||||
return imClient.post<{ data: FriendRequest }>(
|
|
||||||
'/api/im/friend-requests',
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
appId,
|
|
||||||
toUserId,
|
|
||||||
...(remark ? { remark } : {}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
createFriendRequest(appId: string, fromUserId: string, toUserId: string, remark?: string) {
|
|
||||||
return imClient.post<{ data: FriendRequest }>(
|
|
||||||
'/api/im/admin/friend-requests',
|
|
||||||
{
|
|
||||||
fromUserId,
|
|
||||||
toUserId,
|
|
||||||
...(remark ? { remark } : {}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params: { appId },
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
acceptFriendRequest(appId: string, requestId: string) {
|
|
||||||
return imClient.post<{ data: FriendRequest }>(
|
|
||||||
`/api/im/admin/friend-requests/${encodeURIComponent(requestId)}/accept`,
|
|
||||||
null,
|
|
||||||
{ params: { appId } },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
rejectFriendRequest(appId: string, requestId: string) {
|
|
||||||
return imClient.post<{ data: FriendRequest }>(
|
|
||||||
`/api/im/admin/friend-requests/${encodeURIComponent(requestId)}/reject`,
|
|
||||||
null,
|
|
||||||
{ params: { appId } },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
listBlacklist(appId: string) {
|
|
||||||
return imClient.get<{ data: BlacklistEntry[] }>('/api/im/admin/blacklist', {
|
|
||||||
params: { appId },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
addBlacklist(appId: string, userId: string, blockedUserId: string) {
|
|
||||||
return imClient.post<{ data: BlacklistEntry }>(
|
|
||||||
'/api/im/admin/blacklist',
|
|
||||||
{ userId, blockedUserId },
|
|
||||||
{ params: { appId } },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
removeBlacklist(appId: string, userId: string, blockedUserId: string) {
|
|
||||||
return imClient.delete<{ data: null }>('/api/im/admin/blacklist', {
|
|
||||||
params: { appId, userId, blockedUserId },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
listKeywordFilters(appId: string) {
|
listKeywordFilters(appId: string) {
|
||||||
return imClient.get<{ data: KeywordFilter[] }>('/api/im/admin/keyword-filters', {
|
return imClient.get<{ data: KeywordFilter[] }>('/api/im/admin/keyword-filters', {
|
||||||
params: { appId },
|
params: { appId },
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export interface StoreConfig {
|
|||||||
export interface AppVersion {
|
export interface AppVersion {
|
||||||
id: string
|
id: string
|
||||||
appId: string
|
appId: string
|
||||||
platform: 'ANDROID' | 'IOS'
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||||
versionName: string
|
versionName: string
|
||||||
versionCode: number
|
versionCode: number
|
||||||
packageName?: string
|
packageName?: string
|
||||||
@ -65,7 +65,7 @@ export interface AppVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppPackageInspectResult {
|
export interface AppPackageInspectResult {
|
||||||
platform: 'ANDROID' | 'IOS'
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||||
packageName?: string
|
packageName?: string
|
||||||
versionName?: string
|
versionName?: string
|
||||||
versionCode?: number
|
versionCode?: number
|
||||||
@ -77,10 +77,11 @@ export interface RnBundle {
|
|||||||
id: string
|
id: string
|
||||||
appId: string
|
appId: string
|
||||||
moduleId: string
|
moduleId: string
|
||||||
platform: 'ANDROID' | 'IOS'
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||||
version: string
|
version: string
|
||||||
md5: string
|
md5: string
|
||||||
minCommonVersion?: string
|
minCommonVersion?: string
|
||||||
|
packageName?: string
|
||||||
note?: string
|
note?: string
|
||||||
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
|
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
|
||||||
grayEnabled: boolean
|
grayEnabled: boolean
|
||||||
@ -90,30 +91,34 @@ export interface RnBundle {
|
|||||||
|
|
||||||
export interface RnBundleInspectResult {
|
export interface RnBundleInspectResult {
|
||||||
moduleId?: string
|
moduleId?: string
|
||||||
platform?: 'ANDROID' | 'IOS'
|
platform?: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||||
version?: string
|
version?: string
|
||||||
minCommonVersion?: string
|
minCommonVersion?: string
|
||||||
|
packageName?: string
|
||||||
fileName?: string
|
fileName?: string
|
||||||
detected: boolean
|
detected: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnifiedAppUploadItem {
|
export interface UnifiedAppUploadItem {
|
||||||
fileKey: string
|
fileKey: string
|
||||||
platform: 'ANDROID' | 'IOS'
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||||
versionName: string
|
versionName: string
|
||||||
versionCode: number
|
versionCode: number
|
||||||
changeLog?: string
|
changeLog?: string
|
||||||
forceUpdate: boolean
|
forceUpdate: boolean
|
||||||
|
packageName?: string
|
||||||
appStoreUrl?: string
|
appStoreUrl?: string
|
||||||
marketUrl?: string
|
marketUrl?: string
|
||||||
|
publishImmediately: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnifiedRnUploadItem {
|
export interface UnifiedRnUploadItem {
|
||||||
fileKey: string
|
fileKey: string
|
||||||
moduleId: string
|
moduleId: string
|
||||||
platform: 'ANDROID' | 'IOS'
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||||
version: string
|
version: string
|
||||||
minCommonVersion?: string
|
minCommonVersion?: string
|
||||||
|
packageName?: string
|
||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +128,7 @@ export interface UnifiedReleaseManifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const updateAdminApi = {
|
export const updateAdminApi = {
|
||||||
listAppVersions(appId: string, platform: 'ANDROID' | 'IOS') {
|
listAppVersions(appId: string, platform: 'ANDROID' | 'IOS' | 'HARMONY') {
|
||||||
return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', {
|
return updateClient.get<{ data: AppVersion[] }>('/api/v1/updates/app/list', {
|
||||||
params: { appId, platform },
|
params: { appId, platform },
|
||||||
})
|
})
|
||||||
|
|||||||
@ -53,6 +53,10 @@ const router = createRouter({
|
|||||||
path: 'apps/:appId/update',
|
path: 'apps/:appId/update',
|
||||||
component: () => import('@/views/update/VersionManagementView.vue'),
|
component: () => import('@/views/update/VersionManagementView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'apps/:appId/update-guide',
|
||||||
|
component: () => import('@/views/update/StoreGuideView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'accounts',
|
path: 'accounts',
|
||||||
component: () => import('@/views/accounts/SubAccountView.vue'),
|
component: () => import('@/views/accounts/SubAccountView.vue'),
|
||||||
|
|||||||
@ -117,86 +117,6 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="好友申请" name="friendRequests">
|
|
||||||
<div class="toolbar toolbar-space-between">
|
|
||||||
<div class="toolbar-group">
|
|
||||||
<el-button type="primary" @click="openFriendRequestDialog">发起申请</el-button>
|
|
||||||
<el-button @click="loadFriendRequests" :loading="loadingFriendRequests">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
<el-select v-model="friendRequestStatusFilter" style="width: 140px">
|
|
||||||
<el-option label="全部" value="ALL" />
|
|
||||||
<el-option label="待处理" value="PENDING" />
|
|
||||||
<el-option label="已同意" value="ACCEPTED" />
|
|
||||||
<el-option label="已拒绝" value="REJECTED" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="filteredFriendRequests" v-loading="loadingFriendRequests" border stripe>
|
|
||||||
<el-table-column prop="fromUserId" label="申请人" width="160" />
|
|
||||||
<el-table-column prop="toUserId" label="接收人" width="160" />
|
|
||||||
<el-table-column prop="remark" label="备注" min-width="220" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="status" label="状态" width="110">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="friendRequestTagType(row.status)" size="small">
|
|
||||||
{{ friendRequestStatusLabel(row.status) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="createdAt" label="申请时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="reviewedAt" label="处理时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.reviewedAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="170" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
:disabled="row.status !== 'PENDING'"
|
|
||||||
@click="approveFriend(row)"
|
|
||||||
>
|
|
||||||
同意
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
link
|
|
||||||
type="danger"
|
|
||||||
size="small"
|
|
||||||
:disabled="row.status !== 'PENDING'"
|
|
||||||
@click="declineFriend(row)"
|
|
||||||
>
|
|
||||||
拒绝
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="黑名单" name="blacklist">
|
|
||||||
<div class="toolbar toolbar-space-between">
|
|
||||||
<div class="toolbar-group">
|
|
||||||
<el-button type="primary" @click="openBlacklistDialog">新增黑名单</el-button>
|
|
||||||
<el-button @click="loadBlacklist" :loading="loadingBlacklist">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="blacklist" v-loading="loadingBlacklist" border stripe>
|
|
||||||
<el-table-column prop="userId" label="拉黑人" width="160" />
|
|
||||||
<el-table-column prop="blockedUserId" label="被拉黑人" width="160" />
|
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button link type="danger" size="small" @click="removeBlacklist(row)">
|
|
||||||
移除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="内容治理" name="governance">
|
<el-tab-pane label="内容治理" name="governance">
|
||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
@ -719,50 +639,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="showSendFriendRequest"
|
|
||||||
title="发起好友申请"
|
|
||||||
width="520px"
|
|
||||||
@closed="resetSendFriendRequestForm"
|
|
||||||
>
|
|
||||||
<el-form :model="sendFriendRequestForm" label-width="88px">
|
|
||||||
<el-form-item label="接收人ID">
|
|
||||||
<el-input v-model="sendFriendRequestForm.toUserId" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注">
|
|
||||||
<el-input v-model="sendFriendRequestForm.remark" type="textarea" :rows="3" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showSendFriendRequest = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submittingSendFriendRequest" @click="submitFriendRequest">
|
|
||||||
发送
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="showBlacklistDialog"
|
|
||||||
title="新增黑名单"
|
|
||||||
width="520px"
|
|
||||||
@closed="resetBlacklistForm"
|
|
||||||
>
|
|
||||||
<el-form :model="blacklistForm" label-width="96px">
|
|
||||||
<el-form-item label="拉黑人ID">
|
|
||||||
<el-input v-model="blacklistForm.userId" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="被拉黑人ID">
|
|
||||||
<el-input v-model="blacklistForm.blockedUserId" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showBlacklistDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submittingBlacklist" @click="submitBlacklist">
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="showKeywordFilterDialog"
|
v-model="showKeywordFilterDialog"
|
||||||
:title="editingKeywordFilterId ? '编辑过滤规则' : '新增过滤规则'"
|
:title="editingKeywordFilterId ? '编辑过滤规则' : '新增过滤规则'"
|
||||||
@ -803,9 +679,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
imAdminApi,
|
imAdminApi,
|
||||||
type BlacklistEntry,
|
|
||||||
type GlobalMute,
|
type GlobalMute,
|
||||||
type FriendRequest,
|
|
||||||
type GroupJoinRequest,
|
type GroupJoinRequest,
|
||||||
type ImGroup,
|
type ImGroup,
|
||||||
type ImMessage,
|
type ImMessage,
|
||||||
@ -867,14 +741,9 @@ const webhookEvents = [
|
|||||||
{ event: 'message.revoked', payload: 'ImMessageEntity', description: '消息撤回后触发。' },
|
{ event: 'message.revoked', payload: 'ImMessageEntity', description: '消息撤回后触发。' },
|
||||||
{ event: 'message.edited', payload: 'ImMessageEntity', description: '文本消息编辑后触发。' },
|
{ event: 'message.edited', payload: 'ImMessageEntity', description: '文本消息编辑后触发。' },
|
||||||
{ event: 'message.read', payload: 'MessageReadCallbackPayload', description: '已读回执同步后触发。' },
|
{ event: 'message.read', payload: 'MessageReadCallbackPayload', description: '已读回执同步后触发。' },
|
||||||
{ event: 'friend.request.sent', payload: 'FriendRequestCallbackPayload', description: '好友申请创建后触发。' },
|
|
||||||
{ event: 'friend.request.accepted', payload: 'FriendRequestCallbackPayload', description: '好友申请通过后触发。' },
|
|
||||||
{ event: 'friend.request.rejected', payload: 'FriendRequestCallbackPayload', description: '好友申请拒绝后触发。' },
|
|
||||||
{ event: 'group.join.request.sent', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请创建后触发。' },
|
{ event: 'group.join.request.sent', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请创建后触发。' },
|
||||||
{ event: 'group.join.request.accepted', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请通过后触发。' },
|
{ event: 'group.join.request.accepted', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请通过后触发。' },
|
||||||
{ event: 'group.join.request.rejected', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请拒绝后触发。' },
|
{ event: 'group.join.request.rejected', payload: 'GroupJoinRequestCallbackPayload', description: '入群申请拒绝后触发。' },
|
||||||
{ event: 'blacklist.added', payload: 'BlacklistCallbackPayload', description: '黑名单记录新增后触发。' },
|
|
||||||
{ event: 'blacklist.removed', payload: 'BlacklistCallbackPayload', description: '黑名单记录移除后触发。' },
|
|
||||||
]
|
]
|
||||||
const loadingWebhooks = ref(false)
|
const loadingWebhooks = ref(false)
|
||||||
const showWebhookGuideDialog = ref(false)
|
const showWebhookGuideDialog = ref(false)
|
||||||
@ -883,19 +752,6 @@ const submittingWebhook = ref(false)
|
|||||||
const editingWebhookId = ref<string | null>(null)
|
const editingWebhookId = ref<string | null>(null)
|
||||||
const webhookForm = ref({ url: '', secret: '', enabled: true })
|
const webhookForm = ref({ url: '', secret: '', enabled: true })
|
||||||
|
|
||||||
const friendRequests = ref<FriendRequest[]>([])
|
|
||||||
const loadingFriendRequests = ref(false)
|
|
||||||
const friendRequestStatusFilter = ref<'ALL' | 'PENDING' | 'ACCEPTED' | 'REJECTED'>('ALL')
|
|
||||||
const showSendFriendRequest = ref(false)
|
|
||||||
const submittingSendFriendRequest = ref(false)
|
|
||||||
const sendFriendRequestForm = ref({ toUserId: '', remark: '' })
|
|
||||||
|
|
||||||
const blacklist = ref<BlacklistEntry[]>([])
|
|
||||||
const loadingBlacklist = ref(false)
|
|
||||||
const showBlacklistDialog = ref(false)
|
|
||||||
const submittingBlacklist = ref(false)
|
|
||||||
const blacklistForm = ref({ userId: '', blockedUserId: '' })
|
|
||||||
|
|
||||||
const keywordFilters = ref<KeywordFilter[]>([])
|
const keywordFilters = ref<KeywordFilter[]>([])
|
||||||
const loadingKeywordFilters = ref(false)
|
const loadingKeywordFilters = ref(false)
|
||||||
const showKeywordFilterDialog = ref(false)
|
const showKeywordFilterDialog = ref(false)
|
||||||
@ -980,13 +836,6 @@ const statCards = computed(() => [
|
|||||||
{ label: '今日消息', value: stats.value?.todayMessages ?? '-' },
|
{ label: '今日消息', value: stats.value?.todayMessages ?? '-' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const filteredFriendRequests = computed(() => {
|
|
||||||
if (friendRequestStatusFilter.value === 'ALL') {
|
|
||||||
return friendRequests.value
|
|
||||||
}
|
|
||||||
return friendRequests.value.filter(item => item.status === friendRequestStatusFilter.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const globalMuteEnabled = computed({
|
const globalMuteEnabled = computed({
|
||||||
get: () => globalMute.value?.enabled ?? false,
|
get: () => globalMute.value?.enabled ?? false,
|
||||||
set: (enabled: boolean) => {
|
set: (enabled: boolean) => {
|
||||||
@ -1145,19 +994,6 @@ function resetEditGroupForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSendFriendRequestForm() {
|
|
||||||
sendFriendRequestForm.value = { toUserId: '', remark: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetBlacklistForm() {
|
|
||||||
blacklistForm.value = { userId: '', blockedUserId: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
function openBlacklistDialog() {
|
|
||||||
resetBlacklistForm()
|
|
||||||
showBlacklistDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetKeywordFilterForm() {
|
function resetKeywordFilterForm() {
|
||||||
editingKeywordFilterId.value = null
|
editingKeywordFilterId.value = null
|
||||||
keywordFilterForm.value = {
|
keywordFilterForm.value = {
|
||||||
@ -1307,30 +1143,6 @@ async function loadWebhooks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFriendRequests() {
|
|
||||||
loadingFriendRequests.value = true
|
|
||||||
try {
|
|
||||||
const res = await imAdminApi.listFriendRequests(appKey.value)
|
|
||||||
friendRequests.value = res.data.data
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('加载好友申请失败')
|
|
||||||
} finally {
|
|
||||||
loadingFriendRequests.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadBlacklist() {
|
|
||||||
loadingBlacklist.value = true
|
|
||||||
try {
|
|
||||||
const res = await imAdminApi.listBlacklist(appKey.value)
|
|
||||||
blacklist.value = res.data.data
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('加载黑名单失败')
|
|
||||||
} finally {
|
|
||||||
loadingBlacklist.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadKeywordFilters() {
|
async function loadKeywordFilters() {
|
||||||
loadingKeywordFilters.value = true
|
loadingKeywordFilters.value = true
|
||||||
try {
|
try {
|
||||||
@ -1492,11 +1304,6 @@ async function muteManagedGroupMember(user: ImUser) {
|
|||||||
ElMessage.success('成员已禁言')
|
ElMessage.success('成员已禁言')
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFriendRequestDialog() {
|
|
||||||
resetSendFriendRequestForm()
|
|
||||||
showSendFriendRequest.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleGlobalMute() {
|
async function toggleGlobalMute() {
|
||||||
savingGlobalMute.value = true
|
savingGlobalMute.value = true
|
||||||
try {
|
try {
|
||||||
@ -1511,40 +1318,6 @@ async function toggleGlobalMute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitBlacklist() {
|
|
||||||
if (!blacklistForm.value.userId.trim() || !blacklistForm.value.blockedUserId.trim()) {
|
|
||||||
ElMessage.warning('请填写双方用户ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
submittingBlacklist.value = true
|
|
||||||
try {
|
|
||||||
await imAdminApi.addBlacklist(
|
|
||||||
appKey.value,
|
|
||||||
blacklistForm.value.userId.trim(),
|
|
||||||
blacklistForm.value.blockedUserId.trim(),
|
|
||||||
)
|
|
||||||
ElMessage.success('黑名单已添加')
|
|
||||||
showBlacklistDialog.value = false
|
|
||||||
resetBlacklistForm()
|
|
||||||
loadBlacklist()
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('添加黑名单失败')
|
|
||||||
} finally {
|
|
||||||
submittingBlacklist.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeBlacklist(row: BlacklistEntry) {
|
|
||||||
await ElMessageBox.confirm(`确认移除 ${row.userId} -> ${row.blockedUserId} 的黑名单?`, '移除黑名单', {
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonText: '确认',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
})
|
|
||||||
await imAdminApi.removeBlacklist(appKey.value, row.userId, row.blockedUserId)
|
|
||||||
ElMessage.success('黑名单已移除')
|
|
||||||
loadBlacklist()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitKeywordFilter() {
|
async function submitKeywordFilter() {
|
||||||
if (!keywordFilterForm.value.pattern.trim()) {
|
if (!keywordFilterForm.value.pattern.trim()) {
|
||||||
ElMessage.warning('请填写命中词')
|
ElMessage.warning('请填写命中词')
|
||||||
@ -1586,29 +1359,6 @@ async function deleteKeywordFilter(row: KeywordFilter) {
|
|||||||
loadKeywordFilters()
|
loadKeywordFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitFriendRequest() {
|
|
||||||
if (!sendFriendRequestForm.value.toUserId.trim()) {
|
|
||||||
ElMessage.warning('请填写接收人ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
submittingSendFriendRequest.value = true
|
|
||||||
try {
|
|
||||||
await imAdminApi.sendFriendRequest(
|
|
||||||
appKey.value,
|
|
||||||
sendFriendRequestForm.value.toUserId.trim(),
|
|
||||||
sendFriendRequestForm.value.remark.trim() || undefined,
|
|
||||||
)
|
|
||||||
ElMessage.success('好友申请已发送')
|
|
||||||
showSendFriendRequest.value = false
|
|
||||||
resetSendFriendRequestForm()
|
|
||||||
loadFriendRequests()
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('发送失败')
|
|
||||||
} finally {
|
|
||||||
submittingSendFriendRequest.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchMessages() {
|
async function searchMessages() {
|
||||||
await loadMessages(0)
|
await loadMessages(0)
|
||||||
}
|
}
|
||||||
@ -1692,28 +1442,6 @@ async function revokeMessage(message: ImMessage) {
|
|||||||
loadMessages(historyPage.value)
|
loadMessages(historyPage.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function approveFriend(request: FriendRequest) {
|
|
||||||
await ElMessageBox.confirm(`确认同意好友申请 ${request.fromUserId} -> ${request.toUserId}?`, '同意好友申请', {
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonText: '确认',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
})
|
|
||||||
await imAdminApi.acceptFriendRequest(appKey.value, request.id)
|
|
||||||
ElMessage.success('已同意好友申请')
|
|
||||||
loadFriendRequests()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function declineFriend(request: FriendRequest) {
|
|
||||||
await ElMessageBox.confirm(`确认拒绝好友申请 ${request.fromUserId} -> ${request.toUserId}?`, '拒绝好友申请', {
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonText: '确认',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
})
|
|
||||||
await imAdminApi.rejectFriendRequest(appKey.value, request.id)
|
|
||||||
ElMessage.success('已拒绝好友申请')
|
|
||||||
loadFriendRequests()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function approveGroupJoin(request: GroupJoinRequest) {
|
async function approveGroupJoin(request: GroupJoinRequest) {
|
||||||
const groupId = managedGroup.value?.id || groupRequestGroupId.value.trim()
|
const groupId = managedGroup.value?.id || groupRequestGroupId.value.trim()
|
||||||
if (!groupId) {
|
if (!groupId) {
|
||||||
@ -1872,10 +1600,6 @@ async function submitEditGroup() {
|
|||||||
function handleTabChange(tab: string) {
|
function handleTabChange(tab: string) {
|
||||||
if (tab === 'groups' && groups.value.length === 0) {
|
if (tab === 'groups' && groups.value.length === 0) {
|
||||||
loadGroups()
|
loadGroups()
|
||||||
} else if (tab === 'friendRequests' && friendRequests.value.length === 0) {
|
|
||||||
loadFriendRequests()
|
|
||||||
} else if (tab === 'blacklist' && blacklist.value.length === 0) {
|
|
||||||
loadBlacklist()
|
|
||||||
} else if (tab === 'governance') {
|
} else if (tab === 'governance') {
|
||||||
if (keywordFilters.value.length === 0) {
|
if (keywordFilters.value.length === 0) {
|
||||||
loadKeywordFilters()
|
loadKeywordFilters()
|
||||||
|
|||||||
@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<div class="store-guide-page">
|
||||||
|
<el-page-header @back="$router.back()" :content="`应用配置指引 — ${appId}`" />
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
title="这里是应用商店配置的独立指引页。建议先完成凭据配置,再回到版本管理页提交审核。Harmony 应用仅跳转应用市场,不做本地安装。"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin: 16px 0;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col v-for="store in STORE_GUIDES" :key="store.type" :xs="24" :md="12" :lg="8" style="margin-bottom: 16px;">
|
||||||
|
<el-card shadow="hover" class="guide-card">
|
||||||
|
<div class="guide-card-title">
|
||||||
|
<span>{{ store.label }}</span>
|
||||||
|
<el-tag size="small" type="success" v-if="store.enabled">已接入</el-tag>
|
||||||
|
</div>
|
||||||
|
<p class="guide-subtitle">{{ store.subtitle }}</p>
|
||||||
|
<el-link :href="store.url" target="_blank" type="primary">{{ store.urlLabel }}</el-link>
|
||||||
|
<el-steps direction="vertical" :active="store.steps.length" finish-status="success" style="margin: 16px 0;">
|
||||||
|
<el-step v-for="step in store.steps" :key="step.title" :title="step.title" :description="step.description" />
|
||||||
|
</el-steps>
|
||||||
|
<p class="guide-hint">{{ store.hint }}</p>
|
||||||
|
<img v-if="store.image" :src="store.image" :alt="store.label + ' 配置截图'" class="guide-image" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import huaweiGuideImage from '@/assets/update-store/huawei/01.png'
|
||||||
|
import miGuideImage from '@/assets/update-store/mi/01.png'
|
||||||
|
import oppoGuideImage from '@/assets/update-store/oppo/01.png'
|
||||||
|
import vivoGuideImage from '@/assets/update-store/vivo/01.png'
|
||||||
|
import honorGuideImage from '@/assets/update-store/honor/01.png'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const appId = route.params.appId as string
|
||||||
|
|
||||||
|
const STORE_GUIDES = [
|
||||||
|
{
|
||||||
|
type: 'HUAWEI',
|
||||||
|
label: '华为应用市场',
|
||||||
|
subtitle: 'AppGallery Connect Connect API 凭据',
|
||||||
|
url: 'https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-Guides/agcapi-getstarted-0000001111845114',
|
||||||
|
urlLabel: '查看华为官方文档',
|
||||||
|
steps: [
|
||||||
|
{ title: '创建应用', description: '在 AppGallery Connect 中打开目标应用。' },
|
||||||
|
{ title: '创建 Connect API 凭据', description: '保存 Client ID 和 Client Secret。' },
|
||||||
|
{ title: '回到版本管理页保存', description: '配置凭据后即可提交审核。' },
|
||||||
|
],
|
||||||
|
hint: '适合上传 APK 并提交审核;支持回调跟踪审核状态。',
|
||||||
|
image: huaweiGuideImage,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'MI',
|
||||||
|
label: '小米应用商店',
|
||||||
|
subtitle: '使用自动发布接口的账号与私钥',
|
||||||
|
url: 'https://dev.mi.com/distribute/doc/details?pId=1134',
|
||||||
|
urlLabel: '查看小米官方文档',
|
||||||
|
steps: [
|
||||||
|
{ title: '进入应用管理', description: '定位到目标应用。' },
|
||||||
|
{ title: '准备自动发布接口密钥', description: '记录用户名和 RSA 私钥。' },
|
||||||
|
{ title: '保存到租户平台', description: '完成后可提交审核。' },
|
||||||
|
],
|
||||||
|
hint: '字段按 username / privateKey 保存。',
|
||||||
|
image: miGuideImage,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'OPPO',
|
||||||
|
label: 'OPPO 软件商店',
|
||||||
|
subtitle: '我的 API 服务端应用凭据',
|
||||||
|
url: 'https://open.oppomobile.com/new/developmentDoc/info?id=11119',
|
||||||
|
urlLabel: '查看 OPPO 官方文档',
|
||||||
|
steps: [
|
||||||
|
{ title: '进入我的 API', description: '确认当前应用可创建服务端应用。' },
|
||||||
|
{ title: '创建 client_id / client_secret', description: '完成服务端凭据申请。' },
|
||||||
|
{ title: '保存并提交审核', description: '配置完成后返回版本管理页。' },
|
||||||
|
],
|
||||||
|
hint: '与后端 submitToOppo 的字段一致。',
|
||||||
|
image: oppoGuideImage,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'VIVO',
|
||||||
|
label: 'vivo 应用商店',
|
||||||
|
subtitle: 'API 管理中的 access_key / access_secret',
|
||||||
|
url: 'https://dev.vivo.com.cn/documentCenter/doc/326',
|
||||||
|
urlLabel: '查看 vivo 官方文档',
|
||||||
|
steps: [
|
||||||
|
{ title: '进入 API 管理', description: '找到当前应用对应入口。' },
|
||||||
|
{ title: '复制 access key 和 secret', description: '按服务端要求保存。' },
|
||||||
|
{ title: '保存后提交审核', description: '审核状态会通过 Webhook 回传。' },
|
||||||
|
],
|
||||||
|
hint: '注意请求频率限制,状态拉取需做节流。',
|
||||||
|
image: vivoGuideImage,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'HONOR',
|
||||||
|
label: '荣耀应用市场',
|
||||||
|
subtitle: '管理中心 API 凭据',
|
||||||
|
url: 'https://developer.honor.com/cn',
|
||||||
|
urlLabel: '查看荣耀官方文档',
|
||||||
|
steps: [
|
||||||
|
{ title: '进入管理中心', description: '打开荣耀开发者后台。' },
|
||||||
|
{ title: '申请服务端凭据', description: '保存 Client ID / Client Secret。' },
|
||||||
|
{ title: '提交审核', description: '与华为同类流程接入。' },
|
||||||
|
],
|
||||||
|
hint: 'Harmony 这条线仅跳转应用市场,不提供本地安装包。',
|
||||||
|
image: honorGuideImage,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'APP_STORE',
|
||||||
|
label: 'Apple App Store',
|
||||||
|
subtitle: 'App Store Connect API Key',
|
||||||
|
url: 'https://developer.apple.com/documentation/appstoreconnectapi',
|
||||||
|
urlLabel: '查看 Apple 官方文档',
|
||||||
|
steps: [
|
||||||
|
{ title: '创建 API Key', description: '保存 Team ID / Key ID / p8 私钥。' },
|
||||||
|
{ title: '补充 Bundle ID', description: '回到版本管理页保存包名与链接。' },
|
||||||
|
{ title: '提交审核', description: '支持审核后自动发布。' },
|
||||||
|
],
|
||||||
|
hint: 'iOS 可填写 App Store 链接。',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'GOOGLE_PLAY',
|
||||||
|
label: 'Google Play',
|
||||||
|
subtitle: '服务账号 JSON 凭据',
|
||||||
|
url: 'https://developer.android.com/google/play/developer-api',
|
||||||
|
urlLabel: '查看 Google Play 官方文档',
|
||||||
|
steps: [
|
||||||
|
{ title: '创建服务账号', description: '从 Google Cloud 获取 JSON。' },
|
||||||
|
{ title: '绑定 Play 权限', description: '授予对应应用的发布权限。' },
|
||||||
|
{ title: '保存服务账号 JSON', description: '回到版本管理页保存凭据。' },
|
||||||
|
],
|
||||||
|
hint: '适合由服务端或脚本自动提交审核。',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.guide-card { min-height: 520px; }
|
||||||
|
.guide-card-title { display: flex; justify-content: space-between; align-items: center; font-weight: 600; margin-bottom: 8px; }
|
||||||
|
.guide-subtitle { margin: 0 0 8px; color: var(--el-text-color-secondary); }
|
||||||
|
.guide-hint { color: var(--el-text-color-secondary); font-size: 13px; margin-top: 8px; }
|
||||||
|
.guide-image { width: 100%; border-radius: 12px; margin-top: 12px; border: 1px solid var(--el-border-color-light); }
|
||||||
|
</style>
|
||||||
@ -10,8 +10,10 @@
|
|||||||
<el-radio-group v-model="appPlatform" @change="loadAppVersions" style="margin-right:12px">
|
<el-radio-group v-model="appPlatform" @change="loadAppVersions" style="margin-right:12px">
|
||||||
<el-radio-button value="ANDROID">Android</el-radio-button>
|
<el-radio-button value="ANDROID">Android</el-radio-button>
|
||||||
<el-radio-button value="IOS">iOS</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-radio-group>
|
||||||
<el-button type="primary" @click="showUploadApp = true">上传新版本</el-button>
|
<el-button type="primary" @click="showUploadApp = true">上传新版本</el-button>
|
||||||
|
<el-button @click="goToStoreGuide">应用配置指引</el-button>
|
||||||
<el-button @click="loadAppVersions" :loading="loadingApp">刷新</el-button>
|
<el-button @click="loadAppVersions" :loading="loadingApp">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -281,25 +283,34 @@
|
|||||||
<el-select v-model="appUploadForm.platform">
|
<el-select v-model="appUploadForm.platform">
|
||||||
<el-option value="ANDROID" label="Android" />
|
<el-option value="ANDROID" label="Android" />
|
||||||
<el-option value="IOS" label="iOS" />
|
<el-option value="IOS" label="iOS" />
|
||||||
|
<el-option value="HARMONY" label="Harmony" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="包名 / Bundle ID">
|
<el-form-item label="包名 / Bundle ID">
|
||||||
<el-input v-model="appUploadForm.packageName" placeholder="选择文件后可自动填充" />
|
<el-input v-model="appUploadForm.packageName" placeholder="选择文件后可自动填充,或手动填写 Harmony bundleName" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="版本名称"><el-input v-model="appUploadForm.versionName" placeholder="选择文件后可自动填充" /></el-form-item>
|
<el-form-item label="版本名称"><el-input v-model="appUploadForm.versionName" placeholder="选择文件后可自动填充" /></el-form-item>
|
||||||
<el-form-item label="版本码"><el-input-number v-model="appUploadForm.versionCode" :min="1" /></el-form-item>
|
<el-form-item label="版本码"><el-input-number v-model="appUploadForm.versionCode" :min="1" /></el-form-item>
|
||||||
<el-form-item label="强制更新"><el-switch v-model="appUploadForm.forceUpdate" /></el-form-item>
|
<el-form-item label="强制更新"><el-switch v-model="appUploadForm.forceUpdate" /></el-form-item>
|
||||||
<el-form-item label="更新说明"><el-input v-model="appUploadForm.changeLog" type="textarea" :rows="3" /></el-form-item>
|
<el-form-item label="更新说明"><el-input v-model="appUploadForm.changeLog" type="textarea" :rows="3" /></el-form-item>
|
||||||
<el-form-item label="包文件">
|
<el-form-item v-if="appUploadForm.platform !== 'HARMONY'" label="包文件">
|
||||||
<el-upload :auto-upload="false" :limit="1" :on-change="onAppPackageChange" accept=".apk,.ipa">
|
<el-upload :auto-upload="false" :limit="1" :on-change="onAppPackageChange" accept=".apk,.ipa">
|
||||||
<el-button>选择文件</el-button>
|
<el-button>选择文件</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-alert
|
<el-alert
|
||||||
|
v-if="appUploadForm.platform === 'HARMONY'"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="Harmony 应用更新只提交市场配置与版本信息,不需要本地安装包。"
|
||||||
|
/>
|
||||||
|
<el-alert
|
||||||
|
v-if="appUploadForm.platform !== 'HARMONY'"
|
||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
title="选中 APK 后会自动读取包名、版本名和版本码;iOS 包若能解析到 Info.plist,也会自动填充。"
|
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="定时发布">
|
||||||
@ -315,19 +326,16 @@
|
|||||||
<el-form-item label="Webhook 通知">
|
<el-form-item label="Webhook 通知">
|
||||||
<el-input v-model="appUploadForm.webhookUrl" placeholder="审核状态变更时回调此 URL" />
|
<el-input v-model="appUploadForm.webhookUrl" placeholder="审核状态变更时回调此 URL" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="自动提交市场">
|
|
||||||
<el-switch v-model="appUploadForm.autoSubmitStore" />
|
|
||||||
<span class="form-tip">上传后立即让服务端提交已配置的应用商店</span>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="appUploadForm.autoSubmitStore" label="目标市场">
|
|
||||||
<el-checkbox-group v-model="appUploadForm.storeTargets">
|
|
||||||
<el-checkbox v-for="s in enabledStores" :key="s.type" :value="s.type">{{ s.label }}</el-checkbox>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="审核后自动发布">
|
<el-form-item label="审核后自动发布">
|
||||||
<el-switch v-model="appUploadForm.autoPublishAfterReview" :disabled="!!appUploadForm.scheduledPublishAt" />
|
<el-switch v-model="appUploadForm.autoPublishAfterReview" :disabled="!!appUploadForm.scheduledPublishAt" />
|
||||||
<span class="form-tip">与定时发布互斥</span>
|
<span class="form-tip">与定时发布互斥</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="appUploadForm.platform === 'IOS'" label="App Store 链接">
|
||||||
|
<el-input v-model="appUploadForm.appStoreUrl" placeholder="可填写 App Store 链接,便于审核跳转" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="appUploadForm.platform === 'HARMONY'" label="应用市场链接">
|
||||||
|
<el-input v-model="appUploadForm.marketUrl" placeholder="Harmony 应用市场详情页链接" />
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showUploadApp = false">取消</el-button>
|
<el-button @click="showUploadApp = false">取消</el-button>
|
||||||
@ -347,17 +355,21 @@
|
|||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
title="推荐文件名格式:moduleId__ANDROID__1.0.0__1.0.0.bundle,系统会按命名自动识别模块、平台、版本和最低 Common 版本。"
|
title="推荐文件名格式:moduleId__ANDROID__1.0.0__1.0.0__com.example.app.bundle,系统会按命名自动识别模块、平台、版本、最低 Common 版本和包名。"
|
||||||
/>
|
/>
|
||||||
<el-form-item label="模块ID"><el-input v-model="rnUploadForm.moduleId" placeholder="可由文件名自动识别" /></el-form-item>
|
<el-form-item label="模块ID"><el-input v-model="rnUploadForm.moduleId" placeholder="可由文件名自动识别" /></el-form-item>
|
||||||
<el-form-item label="平台">
|
<el-form-item label="平台">
|
||||||
<el-select v-model="rnUploadForm.platform">
|
<el-select v-model="rnUploadForm.platform">
|
||||||
<el-option value="ANDROID" label="Android" />
|
<el-option value="ANDROID" label="Android" />
|
||||||
<el-option value="IOS" label="iOS" />
|
<el-option value="IOS" label="iOS" />
|
||||||
|
<el-option value="HARMONY" label="Harmony" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="版本"><el-input v-model="rnUploadForm.version" placeholder="可由文件名自动识别" /></el-form-item>
|
<el-form-item label="版本"><el-input v-model="rnUploadForm.version" placeholder="可由文件名自动识别" /></el-form-item>
|
||||||
<el-form-item label="最低 Common 版本"><el-input v-model="rnUploadForm.minCommonVersion" placeholder="可由文件名自动识别" /></el-form-item>
|
<el-form-item label="最低 Common 版本"><el-input v-model="rnUploadForm.minCommonVersion" placeholder="可由文件名自动识别" /></el-form-item>
|
||||||
|
<el-form-item label="包名 / Bundle">
|
||||||
|
<el-input v-model="rnUploadForm.packageName" placeholder="可选,建议与应用包名一致" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item>
|
<el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -371,7 +383,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
updateAdminApi,
|
updateAdminApi,
|
||||||
@ -389,11 +401,12 @@ 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'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const appId = route.params.appId as string
|
const appId = route.params.appId as string
|
||||||
|
|
||||||
const activeTab = ref('app')
|
const activeTab = ref('app')
|
||||||
const appPlatform = ref<'ANDROID' | 'IOS'>('ANDROID')
|
const appPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY'>('ANDROID')
|
||||||
const rnPlatform = ref<'ANDROID' | 'IOS' | ''>('')
|
const rnPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY' | ''>('')
|
||||||
const rnModuleFilter = ref('')
|
const rnModuleFilter = ref('')
|
||||||
|
|
||||||
const appVersions = ref<AppVersion[]>([])
|
const appVersions = ref<AppVersion[]>([])
|
||||||
@ -687,7 +700,7 @@ async function submitGray() {
|
|||||||
const showUploadApp = ref(false)
|
const showUploadApp = ref(false)
|
||||||
const uploadingApp = ref(false)
|
const uploadingApp = ref(false)
|
||||||
const appUploadForm = ref({
|
const appUploadForm = ref({
|
||||||
platform: 'ANDROID' as 'ANDROID' | 'IOS',
|
platform: 'ANDROID' as 'ANDROID' | 'IOS' | 'HARMONY',
|
||||||
packageName: '',
|
packageName: '',
|
||||||
versionName: '',
|
versionName: '',
|
||||||
versionCode: 1,
|
versionCode: 1,
|
||||||
@ -696,9 +709,9 @@ const appUploadForm = ref({
|
|||||||
file: null as File | null,
|
file: null as File | null,
|
||||||
scheduledPublishAt: '',
|
scheduledPublishAt: '',
|
||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
autoSubmitStore: false,
|
|
||||||
storeTargets: [] as StoreType[],
|
|
||||||
autoPublishAfterReview: false,
|
autoPublishAfterReview: false,
|
||||||
|
appStoreUrl: '',
|
||||||
|
marketUrl: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
||||||
@ -712,7 +725,20 @@ async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
|||||||
const res = await updateAdminApi.inspectAppPackage(formData)
|
const res = await updateAdminApi.inspectAppPackage(formData)
|
||||||
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 = inspected.packageName
|
if (inspected.packageName && appUploadForm.value.packageName && appUploadForm.value.packageName !== inspected.packageName) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`检测到文件包名为 ${inspected.packageName},当前填写为 ${appUploadForm.value.packageName}。是否强制使用文件识别结果?`,
|
||||||
|
'包名不一致',
|
||||||
|
{ type: 'warning', confirmButtonText: '使用文件包名', cancelButtonText: '保留当前填写' },
|
||||||
|
)
|
||||||
|
appUploadForm.value.packageName = inspected.packageName
|
||||||
|
} catch {
|
||||||
|
// keep current manual input
|
||||||
|
}
|
||||||
|
} else if (inspected.packageName) {
|
||||||
|
appUploadForm.value.packageName = inspected.packageName
|
||||||
|
}
|
||||||
if (inspected.versionName) appUploadForm.value.versionName = inspected.versionName
|
if (inspected.versionName) appUploadForm.value.versionName = inspected.versionName
|
||||||
if (typeof inspected.versionCode === 'number') appUploadForm.value.versionCode = inspected.versionCode
|
if (typeof inspected.versionCode === 'number') appUploadForm.value.versionCode = inspected.versionCode
|
||||||
} catch {
|
} catch {
|
||||||
@ -722,7 +748,9 @@ async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
|||||||
|
|
||||||
async function submitAppUpload() {
|
async function submitAppUpload() {
|
||||||
const f = appUploadForm.value
|
const f = appUploadForm.value
|
||||||
if (!f.file) return ElMessage.warning('请先选择应用包文件')
|
if (f.platform !== 'HARMONY' && !f.file) 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('请填写版本信息')
|
if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')
|
||||||
if (f.scheduledPublishAt && f.autoPublishAfterReview) {
|
if (f.scheduledPublishAt && f.autoPublishAfterReview) {
|
||||||
f.autoPublishAfterReview = false
|
f.autoPublishAfterReview = false
|
||||||
@ -741,13 +769,11 @@ async function submitAppUpload() {
|
|||||||
if (f.changeLog) fd.append('changeLog', f.changeLog)
|
if (f.changeLog) fd.append('changeLog', f.changeLog)
|
||||||
if (f.scheduledPublishAt) fd.append('scheduledPublishAt', f.scheduledPublishAt)
|
if (f.scheduledPublishAt) fd.append('scheduledPublishAt', f.scheduledPublishAt)
|
||||||
if (f.webhookUrl) fd.append('webhookUrl', f.webhookUrl)
|
if (f.webhookUrl) fd.append('webhookUrl', f.webhookUrl)
|
||||||
if (f.storeTargets.length) fd.append('storeSubmitTargets', JSON.stringify(f.storeTargets))
|
if (f.appStoreUrl) fd.append('appStoreUrl', f.appStoreUrl)
|
||||||
|
if (f.marketUrl) fd.append('marketUrl', f.marketUrl)
|
||||||
fd.append('autoPublishAfterReview', String(f.autoPublishAfterReview))
|
fd.append('autoPublishAfterReview', String(f.autoPublishAfterReview))
|
||||||
fd.append('apkFile', f.file)
|
if (f.file) fd.append('apkFile', f.file)
|
||||||
const resp = await updateAdminApi.uploadAppVersion(fd)
|
await updateAdminApi.uploadAppVersion(fd)
|
||||||
if (f.autoSubmitStore && f.storeTargets.length) {
|
|
||||||
await updateAdminApi.executeSubmitToStores(resp.data.data.id, f.storeTargets)
|
|
||||||
}
|
|
||||||
ElMessage.success('上传成功')
|
ElMessage.success('上传成功')
|
||||||
showUploadApp.value = false
|
showUploadApp.value = false
|
||||||
await loadAppVersions()
|
await loadAppVersions()
|
||||||
@ -760,9 +786,10 @@ const showUploadRn = ref(false)
|
|||||||
const uploadingRn = ref(false)
|
const uploadingRn = ref(false)
|
||||||
const rnUploadForm = ref({
|
const rnUploadForm = ref({
|
||||||
moduleId: '',
|
moduleId: '',
|
||||||
platform: 'ANDROID' as 'ANDROID' | 'IOS',
|
platform: 'ANDROID' as 'ANDROID' | 'IOS' | 'HARMONY',
|
||||||
version: '',
|
version: '',
|
||||||
minCommonVersion: '',
|
minCommonVersion: '',
|
||||||
|
packageName: '',
|
||||||
note: '',
|
note: '',
|
||||||
file: null as File | null,
|
file: null as File | null,
|
||||||
})
|
})
|
||||||
@ -773,9 +800,10 @@ function parseRnBundleName(fileName: string): RnBundleInspectResult | null {
|
|||||||
if (parts.length >= 4) {
|
if (parts.length >= 4) {
|
||||||
return {
|
return {
|
||||||
moduleId: parts[0],
|
moduleId: parts[0],
|
||||||
platform: parts[1].toUpperCase() as 'ANDROID' | 'IOS',
|
platform: parts[1].toUpperCase() as 'ANDROID' | 'IOS' | 'HARMONY',
|
||||||
version: parts[2],
|
version: parts[2],
|
||||||
minCommonVersion: parts[3],
|
minCommonVersion: parts[3],
|
||||||
|
packageName: parts[4],
|
||||||
fileName,
|
fileName,
|
||||||
detected: true,
|
detected: true,
|
||||||
}
|
}
|
||||||
@ -794,6 +822,7 @@ async function onRnBundleChange(uploadFile: { raw?: File } | null) {
|
|||||||
if (local.platform) rnUploadForm.value.platform = local.platform
|
if (local.platform) rnUploadForm.value.platform = local.platform
|
||||||
if (local.version) rnUploadForm.value.version = local.version
|
if (local.version) rnUploadForm.value.version = local.version
|
||||||
if (local.minCommonVersion) rnUploadForm.value.minCommonVersion = local.minCommonVersion
|
if (local.minCommonVersion) rnUploadForm.value.minCommonVersion = local.minCommonVersion
|
||||||
|
if (local.packageName) rnUploadForm.value.packageName = local.packageName
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -806,6 +835,7 @@ async function onRnBundleChange(uploadFile: { raw?: File } | null) {
|
|||||||
if (inspected.platform) rnUploadForm.value.platform = inspected.platform
|
if (inspected.platform) rnUploadForm.value.platform = inspected.platform
|
||||||
if (inspected.version) rnUploadForm.value.version = inspected.version
|
if (inspected.version) rnUploadForm.value.version = inspected.version
|
||||||
if (inspected.minCommonVersion) rnUploadForm.value.minCommonVersion = inspected.minCommonVersion
|
if (inspected.minCommonVersion) rnUploadForm.value.minCommonVersion = inspected.minCommonVersion
|
||||||
|
if (inspected.packageName) rnUploadForm.value.packageName = inspected.packageName
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.warning('已选择文件,但未能从文件名识别出 RN Bundle 元数据,请补全后上传')
|
ElMessage.warning('已选择文件,但未能从文件名识别出 RN Bundle 元数据,请补全后上传')
|
||||||
}
|
}
|
||||||
@ -822,6 +852,7 @@ async function submitRnUpload() {
|
|||||||
fd.append('platform', f.platform)
|
fd.append('platform', f.platform)
|
||||||
fd.append('version', f.version)
|
fd.append('version', f.version)
|
||||||
if (f.minCommonVersion) fd.append('minCommonVersion', f.minCommonVersion)
|
if (f.minCommonVersion) fd.append('minCommonVersion', f.minCommonVersion)
|
||||||
|
if (f.packageName) fd.append('packageName', f.packageName)
|
||||||
if (f.note) fd.append('note', f.note)
|
if (f.note) fd.append('note', f.note)
|
||||||
fd.append('bundle', f.file)
|
fd.append('bundle', f.file)
|
||||||
await updateAdminApi.uploadRnBundle(fd)
|
await updateAdminApi.uploadRnBundle(fd)
|
||||||
@ -915,6 +946,10 @@ function parseStoreReview(json?: string): { store: string; state: string }[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToStoreGuide() {
|
||||||
|
router.push(`/apps/${appId}/update-guide`)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAppVersions()
|
loadAppVersions()
|
||||||
loadRnBundles()
|
loadRnBundles()
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户