feat(deploy): 添加生产环境部署配置和联调环境切换功能
- 新增 .env.production.example 配置文件,包含所有微服务的数据库和Redis配置 - 添加 compose.production.yaml Docker Compose部署文件,定义web和各服务容器 - 实现Android SDK环境切换功能,支持外部服务和本地联调模式切换 - 添加推送注册状态管理和接收开关设置界面 - 集成演示服务的应用密钥客户端和认证服务实现 - 完善文档说明各SDK模块的集成和使用方法
这个提交包含在:
父节点
c2ff993e05
当前提交
92eb1ed539
1
Jenkinsfile
vendored
1
Jenkinsfile
vendored
@ -2,6 +2,7 @@ pipeline {
|
|||||||
agent any
|
agent any
|
||||||
|
|
||||||
parameters {
|
parameters {
|
||||||
|
string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名')
|
||||||
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag')
|
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag')
|
||||||
booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署')
|
booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
server: {
|
server: {
|
||||||
port: 5174,
|
port: 5174,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { target: 'http://192.168.116.9:8081', changeOrigin: true },
|
'/api': { target: 'http://127.0.0.1:8081', changeOrigin: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
1
tenant-platform/components.d.ts
vendored
1
tenant-platform/components.d.ts
vendored
@ -42,6 +42,7 @@ declare module 'vue' {
|
|||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
|||||||
@ -95,10 +95,17 @@ 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) =>
|
getService: async (appId: string, platform: string, serviceType: string) => {
|
||||||
client.get<{ data: FeatureService }>(`/apps/${appId}/services/item`, {
|
const res = await client.get<{ data: FeatureService[] }>(`/apps/${appId}/services`)
|
||||||
params: { platform, serviceType },
|
const service = res.data.data.find(item => item.platform === platform && item.serviceType === serviceType)
|
||||||
}),
|
if (!service) {
|
||||||
|
throw new Error('服务不存在')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
data: { data: service },
|
||||||
|
} as typeof res & { data: { data: FeatureService } }
|
||||||
|
},
|
||||||
|
|
||||||
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, {
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import client from './client'
|
||||||
|
|
||||||
|
export interface DashboardStats {
|
||||||
|
appCount: number
|
||||||
|
serviceCount: number
|
||||||
|
subAccountCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardApi = {
|
||||||
|
stats: () => client.get<{ data: DashboardStats }>('/dashboard/stats'),
|
||||||
|
}
|
||||||
@ -1,7 +1,12 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import router from '@/router'
|
||||||
|
import { isJwtExpired } from '@/utils/jwt'
|
||||||
|
|
||||||
|
export type UploadProgressHandler = (percent: number) => void
|
||||||
|
|
||||||
const fileClient = axios.create({
|
const fileClient = axios.create({
|
||||||
baseURL: import.meta.env.VITE_FILE_SERVICE_URL ?? 'http://192.168.116.9:8086',
|
baseURL: import.meta.env.VITE_FILE_SERVICE_URL ?? '',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -25,12 +30,44 @@ if (import.meta.env.DEV) {
|
|||||||
|
|
||||||
fileClient.interceptors.request.use((config) => {
|
fileClient.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (token) {
|
if (token && !isJwtExpired(token)) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
} else if (token && isJwtExpired(token)) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录'))
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('登录已失效,请重新登录'))
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fileClient.interceptors.response.use(
|
||||||
|
(res) => res,
|
||||||
|
(error) => {
|
||||||
|
const status = error.response?.status
|
||||||
|
if (status === 401) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
ElMessage.error('登录已失效,请重新登录')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
if (status === 403) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录'))
|
||||||
|
}
|
||||||
|
ElMessage.error(error.response?.data?.message ?? '登录已失效,请重新登录')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
const msg = error.response?.data?.message ?? '文件请求失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export interface FileUploadResult {
|
export interface FileUploadResult {
|
||||||
url: string
|
url: string
|
||||||
thumbnailUrl?: string
|
thumbnailUrl?: string
|
||||||
@ -42,12 +79,19 @@ export interface FileUploadResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fileApi = {
|
export const fileApi = {
|
||||||
uploadFile(file: File, thumbnail?: File | null) {
|
uploadFile(file: File, thumbnail?: File | null, onProgress?: UploadProgressHandler) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
formData.append('thumbnail', thumbnail)
|
formData.append('thumbnail', thumbnail)
|
||||||
}
|
}
|
||||||
return fileClient.post<{ data: FileUploadResult }>('/api/file/upload', formData)
|
return fileClient.post<{ data: FileUploadResult }>('/api/file/upload', formData, {
|
||||||
|
onUploadProgress: (event) => {
|
||||||
|
if (!onProgress) return
|
||||||
|
const total = event.total ?? 0
|
||||||
|
if (!total) return
|
||||||
|
onProgress(Math.min(100, Math.round((event.loaded * 100) / total)))
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import router from '@/router'
|
||||||
|
import { isJwtExpired } from '@/utils/jwt'
|
||||||
|
|
||||||
const imClient = axios.create({
|
const imClient = axios.create({
|
||||||
baseURL: import.meta.env.VITE_IM_API_BASE_URL ?? 'http://192.168.116.9:8082',
|
baseURL: import.meta.env.VITE_IM_API_BASE_URL ?? '',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -25,10 +28,44 @@ if (import.meta.env.DEV) {
|
|||||||
|
|
||||||
imClient.interceptors.request.use((config) => {
|
imClient.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
if (token && !isJwtExpired(token)) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
} else if (token && isJwtExpired(token)) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录'))
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('登录已失效,请重新登录'))
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
|
imClient.interceptors.response.use(
|
||||||
|
(res) => res,
|
||||||
|
(error) => {
|
||||||
|
const status = error.response?.status
|
||||||
|
if (status === 401) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
ElMessage.error('登录已失效,请重新登录')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
if (status === 403) {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
if (router.currentRoute.value.path !== '/login') {
|
||||||
|
router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录'))
|
||||||
|
}
|
||||||
|
ElMessage.error(error.response?.data?.message ?? '登录已失效,请重新登录')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
const msg = error.response?.data?.message ?? 'IM 请求失败'
|
||||||
|
ElMessage.error(msg)
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export interface ImUser {
|
export interface ImUser {
|
||||||
id: string
|
id: string
|
||||||
appId: string
|
appId: string
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import axios from 'axios'
|
import axios, { type AxiosProgressEvent } from 'axios'
|
||||||
import { isJwtExpired } from '@/utils/jwt'
|
import { isJwtExpired } from '@/utils/jwt'
|
||||||
|
|
||||||
const updateClient = axios.create({
|
const updateClient = axios.create({
|
||||||
@ -6,6 +6,19 @@ const updateClient = axios.create({
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type UploadProgressHandler = (percent: number) => void
|
||||||
|
|
||||||
|
function uploadProgressConfig(onProgress?: UploadProgressHandler) {
|
||||||
|
if (!onProgress) return {}
|
||||||
|
return {
|
||||||
|
onUploadProgress: (event: AxiosProgressEvent) => {
|
||||||
|
const total = event.total ?? 0
|
||||||
|
if (!total) return
|
||||||
|
onProgress(Math.min(100, Math.round((event.loaded * 100) / total)))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
updateClient.interceptors.request.use((config) => {
|
updateClient.interceptors.request.use((config) => {
|
||||||
console.debug('[tenant-platform][UPDATE] request', {
|
console.debug('[tenant-platform][UPDATE] request', {
|
||||||
@ -232,8 +245,8 @@ export const updateAdminApi = {
|
|||||||
return updateClient.post(`/api/v1/updates/app/${id}/gray`, body)
|
return updateClient.post(`/api/v1/updates/app/${id}/gray`, body)
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadAppVersion(formData: FormData) {
|
uploadAppVersion(formData: FormData, onProgress?: UploadProgressHandler) {
|
||||||
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData)
|
return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData, uploadProgressConfig(onProgress))
|
||||||
},
|
},
|
||||||
|
|
||||||
inspectAppPackage(apkUrl: string) {
|
inspectAppPackage(apkUrl: string) {
|
||||||
@ -266,16 +279,16 @@ export const updateAdminApi = {
|
|||||||
return updateClient.post(`/api/v1/rn/${id}/gray`, body)
|
return updateClient.post(`/api/v1/rn/${id}/gray`, body)
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadRnBundle(formData: FormData) {
|
uploadRnBundle(formData: FormData, onProgress?: UploadProgressHandler) {
|
||||||
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData)
|
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, uploadProgressConfig(onProgress))
|
||||||
},
|
},
|
||||||
|
|
||||||
inspectRnBundle(formData: FormData) {
|
inspectRnBundle(formData: FormData, onProgress?: UploadProgressHandler) {
|
||||||
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData)
|
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, uploadProgressConfig(onProgress))
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadUnifiedRelease(formData: FormData) {
|
uploadUnifiedRelease(formData: FormData, onProgress?: UploadProgressHandler) {
|
||||||
return updateClient.post('/api/v1/updates/unified/upload', formData)
|
return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress))
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Store config ────────────────────────────────────────────────────────
|
// ── Store config ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -45,25 +45,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { appApi } from '@/api/app'
|
import { dashboardApi } from '@/api/dashboard'
|
||||||
import { accountApi } from '@/api/account'
|
|
||||||
|
|
||||||
const stats = ref({ appCount: 0, serviceCount: 0, subAccountCount: 0 })
|
const stats = ref({ appCount: 0, serviceCount: 0, subAccountCount: 0 })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const [appsRes, accountsRes] = await Promise.all([
|
const res = await dashboardApi.stats()
|
||||||
appApi.list(), accountApi.list(),
|
stats.value = res.data.data
|
||||||
])
|
|
||||||
stats.value.appCount = appsRes.data.data.length
|
|
||||||
stats.value.subAccountCount = accountsRes.data.data.length
|
|
||||||
|
|
||||||
let serviceCount = 0
|
|
||||||
for (const app of appsRes.data.data) {
|
|
||||||
const svcRes = await appApi.getServices(app.id)
|
|
||||||
serviceCount += svcRes.data.data.filter(s => s.enabled).length
|
|
||||||
}
|
|
||||||
stats.value.serviceCount = serviceCount
|
|
||||||
} catch {}
|
} catch {}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,26 +3,35 @@
|
|||||||
<h2 style="margin-bottom: 24px">操作日志</h2>
|
<h2 style="margin-bottom: 24px">操作日志</h2>
|
||||||
|
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
|
<el-tabs v-model="activeSource">
|
||||||
|
<el-tab-pane label="租户平台" name="TENANT">
|
||||||
<div class="toolbar responsive-toolbar">
|
<div class="toolbar responsive-toolbar">
|
||||||
<el-select v-model="filters.moduleType" placeholder="模块" style="width: 180px" clearable @change="handleModuleChange">
|
<el-select
|
||||||
|
v-model="tenantFilters.moduleType"
|
||||||
|
placeholder="模块"
|
||||||
|
style="width: 180px"
|
||||||
|
clearable
|
||||||
|
@change="handleTenantModuleChange"
|
||||||
|
>
|
||||||
<el-option label="全部模块" value="" />
|
<el-option label="全部模块" value="" />
|
||||||
|
<el-option label="控制台" value="CONSOLE" />
|
||||||
<el-option label="应用管理" value="APP" />
|
<el-option label="应用管理" value="APP" />
|
||||||
<el-option label="子账号管理" value="SUB_ACCOUNT" />
|
<el-option label="子账号管理" value="SUB_ACCOUNT" />
|
||||||
<el-option label="服务管理" value="SERVICE" />
|
<el-option label="服务管理" value="SERVICE" />
|
||||||
<el-option label="密钥验证" value="APP_SECRET" />
|
<el-option label="密钥验证" value="APP_SECRET" />
|
||||||
<el-option label="邮箱验证" value="EMAIL_VERIFY" />
|
<el-option label="邮箱验证" value="EMAIL_VERIFY" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button :loading="loading" @click="loadLogs">刷新</el-button>
|
<el-button :loading="tenantLoading" @click="loadTenantLogs">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<el-table :data="logs" v-loading="loading" border stripe>
|
<el-table :data="tenantLogs" v-loading="tenantLoading" border stripe>
|
||||||
<el-table-column prop="createdAt" label="时间" width="180">
|
<el-table-column prop="createdAt" label="时间" width="180">
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="moduleType" label="模块" width="140">
|
<el-table-column prop="moduleType" label="模块" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag size="small" effect="plain">{{ moduleLabel(row.moduleType) }}</el-tag>
|
<el-tag size="small" effect="plain">{{ tenantModuleLabel(row.moduleType) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="action" label="动作" width="180" />
|
<el-table-column prop="action" label="动作" width="180" />
|
||||||
@ -40,61 +49,160 @@
|
|||||||
<el-pagination
|
<el-pagination
|
||||||
style="margin-top: 16px"
|
style="margin-top: 16px"
|
||||||
layout="total, prev, pager, next"
|
layout="total, prev, pager, next"
|
||||||
:total="total"
|
:total="tenantTotal"
|
||||||
:page-size="pageSize"
|
:page-size="tenantPageSize"
|
||||||
:current-page="page + 1"
|
:current-page="tenantPage + 1"
|
||||||
@current-change="handlePageChange"
|
@current-change="handleTenantPageChange"
|
||||||
/>
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="版本管理" name="UPDATE">
|
||||||
|
<div class="toolbar responsive-toolbar">
|
||||||
|
<el-select
|
||||||
|
v-model="updateAppId"
|
||||||
|
placeholder="选择应用"
|
||||||
|
style="width: 320px"
|
||||||
|
filterable
|
||||||
|
@change="handleUpdateAppChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="app in apps"
|
||||||
|
:key="app.id"
|
||||||
|
:label="`${app.name} · ${app.packageName}`"
|
||||||
|
:value="app.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input-number v-model="updateLimit" :min="20" :max="200" :step="10" controls-position="right" />
|
||||||
|
<el-button :loading="updateLoading" @click="loadUpdateLogs">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<el-table :data="updateLogs" v-loading="updateLoading" border stripe>
|
||||||
|
<el-table-column prop="createdAt" label="时间" width="180">
|
||||||
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="resourceType" label="资源类型" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" effect="plain">{{ updateResourceLabel(row.resourceType) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="action" label="动作" width="160">
|
||||||
|
<template #default="{ row }">{{ updateActionLabel(row.action) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="resourceId" label="资源ID" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="operator" label="操作人" width="180" />
|
||||||
|
<el-table-column label="原因 / 详情" min-width="320">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="detail">{{ row.reason || formatDetail(row.detailJson) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { appApi, type App } from '@/api/app'
|
||||||
|
import { updateAdminApi, type OperationLog as UpdateOperationLog } from '@/api/update'
|
||||||
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
|
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
|
||||||
|
|
||||||
const loading = ref(false)
|
const activeSource = ref<'TENANT' | 'UPDATE'>('TENANT')
|
||||||
const logs = ref<TenantOperationLog[]>([])
|
|
||||||
const total = ref(0)
|
|
||||||
const page = ref(0)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
|
|
||||||
const filters = reactive({
|
const tenantLoading = ref(false)
|
||||||
|
const tenantLogs = ref<TenantOperationLog[]>([])
|
||||||
|
const tenantTotal = ref(0)
|
||||||
|
const tenantPage = ref(0)
|
||||||
|
const tenantPageSize = ref(20)
|
||||||
|
const tenantFilters = reactive({
|
||||||
moduleType: '',
|
moduleType: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
const updateLoading = ref(false)
|
||||||
loadLogs()
|
const updateLogs = ref<UpdateOperationLog[]>([])
|
||||||
|
const updateLimit = ref(100)
|
||||||
|
const apps = ref<App[]>([])
|
||||||
|
const updateAppId = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadApps(),
|
||||||
|
loadTenantLogs(),
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadLogs() {
|
watch(activeSource, async (value) => {
|
||||||
loading.value = true
|
if (value === 'UPDATE' && !apps.value.length) {
|
||||||
|
await loadApps()
|
||||||
|
}
|
||||||
|
if (value === 'UPDATE') {
|
||||||
|
await loadUpdateLogs()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadApps() {
|
||||||
|
if (apps.value.length) return
|
||||||
|
try {
|
||||||
|
const res = await appApi.list()
|
||||||
|
apps.value = res.data.data
|
||||||
|
if (!updateAppId.value && apps.value.length) {
|
||||||
|
updateAppId.value = apps.value[0].id
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore; empty state will be shown in the selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTenantLogs() {
|
||||||
|
tenantLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await operationLogApi.list({
|
const res = await operationLogApi.list({
|
||||||
moduleType: filters.moduleType || undefined,
|
moduleType: tenantFilters.moduleType || undefined,
|
||||||
page: page.value,
|
page: tenantPage.value,
|
||||||
size: pageSize.value,
|
size: tenantPageSize.value,
|
||||||
})
|
})
|
||||||
const data = res.data.data
|
const data = res.data.data
|
||||||
logs.value = data.content ?? []
|
tenantLogs.value = data.content ?? []
|
||||||
total.value = data.totalElements ?? 0
|
tenantTotal.value = data.totalElements ?? 0
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
tenantLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageChange(nextPage: number) {
|
async function loadUpdateLogs() {
|
||||||
page.value = nextPage - 1
|
if (!updateAppId.value) {
|
||||||
loadLogs()
|
updateLogs.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await updateAdminApi.listOperationLogs(updateAppId.value, updateLimit.value)
|
||||||
|
updateLogs.value = res.data.data ?? []
|
||||||
|
} finally {
|
||||||
|
updateLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModuleChange() {
|
function handleTenantPageChange(nextPage: number) {
|
||||||
page.value = 0
|
tenantPage.value = nextPage - 1
|
||||||
loadLogs()
|
loadTenantLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
function moduleLabel(moduleType: string) {
|
function handleTenantModuleChange() {
|
||||||
|
tenantPage.value = 0
|
||||||
|
loadTenantLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateAppChange() {
|
||||||
|
loadUpdateLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
function tenantModuleLabel(moduleType: string) {
|
||||||
return {
|
return {
|
||||||
|
CONSOLE: '控制台',
|
||||||
APP: '应用管理',
|
APP: '应用管理',
|
||||||
SUB_ACCOUNT: '子账号管理',
|
SUB_ACCOUNT: '子账号管理',
|
||||||
SERVICE: '服务管理',
|
SERVICE: '服务管理',
|
||||||
@ -103,11 +211,45 @@ function moduleLabel(moduleType: string) {
|
|||||||
}[moduleType] ?? moduleType
|
}[moduleType] ?? moduleType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateResourceLabel(resourceType: string) {
|
||||||
|
return {
|
||||||
|
APP_VERSION: '应用版本',
|
||||||
|
RN_BUNDLE: 'RN 包',
|
||||||
|
STORE_CONFIG: '应用市场',
|
||||||
|
PUBLISH_CONFIG: '发布配置',
|
||||||
|
GRAY_MEMBER: '灰度成员',
|
||||||
|
STORE_SUBMIT: '市场提交流程',
|
||||||
|
SERVICE: '服务配置',
|
||||||
|
}[resourceType] ?? resourceType
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActionLabel(action: string) {
|
||||||
|
return {
|
||||||
|
UPLOAD: '上传',
|
||||||
|
PUBLISH: '发布',
|
||||||
|
REPUBLISH: '重新发布',
|
||||||
|
SCHEDULE_PUBLISH: '定时发布',
|
||||||
|
UPDATE_FORCE: '修改强更',
|
||||||
|
UNPUBLISH: '下架',
|
||||||
|
GRAY_UPDATE: '灰度调整',
|
||||||
|
STORE_SUBMIT: '提交市场',
|
||||||
|
CREATE_STORE_CONFIG: '创建配置',
|
||||||
|
UPDATE_STORE_CONFIG: '更新配置',
|
||||||
|
DELETE_STORE_CONFIG: '删除配置',
|
||||||
|
AUTO_PUBLISH: '自动发布',
|
||||||
|
REQUEST_SERVICE_ACTIVATION: '申请开通',
|
||||||
|
UPDATE_SERVICE_CONFIG: '更新服务配置',
|
||||||
|
DISABLE_SERVICE: '停用服务',
|
||||||
|
VIEW_DASHBOARD: '查看控制台',
|
||||||
|
}[action] ?? action
|
||||||
|
}
|
||||||
|
|
||||||
function formatDetail(detailJson?: string) {
|
function formatDetail(detailJson?: string) {
|
||||||
if (!detailJson) return '-'
|
if (!detailJson) return '-'
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(detailJson)
|
const parsed = JSON.parse(detailJson)
|
||||||
return typeof parsed === 'string' ? parsed : JSON.stringify(parsed, null, 0)
|
if (parsed === null || parsed === undefined) return '-'
|
||||||
|
return typeof parsed === 'string' ? parsed : JSON.stringify(parsed)
|
||||||
} catch {
|
} catch {
|
||||||
return detailJson
|
return detailJson
|
||||||
}
|
}
|
||||||
@ -151,7 +293,8 @@ function formatTime(value: string | number | null | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.responsive-toolbar :deep(.el-select),
|
.responsive-toolbar :deep(.el-select),
|
||||||
.responsive-toolbar :deep(.el-button) {
|
.responsive-toolbar :deep(.el-button),
|
||||||
|
.responsive-toolbar :deep(.el-input-number) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -530,6 +530,12 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="appPackageUploadProgress > 0" label="文件进度">
|
||||||
|
<div class="upload-progress-block">
|
||||||
|
<el-progress :percentage="appPackageUploadProgress" :status="appPackageUploadProgress === 100 ? 'success' : undefined" />
|
||||||
|
<span class="upload-progress-text">{{ appPackageUploadProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="appUploadForm.platform !== 'ANDROID'"
|
v-if="appUploadForm.platform !== 'ANDROID'"
|
||||||
type="info"
|
type="info"
|
||||||
@ -544,6 +550,12 @@
|
|||||||
show-icon
|
show-icon
|
||||||
title="选中 APK 后会先上传到文件服务,再读取包名、版本名和版本码;若识别到的包名与当前应用不一致,可选择强制继续使用。"
|
title="选中 APK 后会先上传到文件服务,再读取包名、版本名和版本码;若识别到的包名与当前应用不一致,可选择强制继续使用。"
|
||||||
/>
|
/>
|
||||||
|
<el-form-item v-if="appVersionUploadProgress > 0" label="提交进度">
|
||||||
|
<div class="upload-progress-block">
|
||||||
|
<el-progress :percentage="appVersionUploadProgress" :status="appVersionUploadProgress === 100 ? 'success' : undefined" />
|
||||||
|
<span class="upload-progress-text">{{ appVersionUploadProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
@ -559,6 +571,12 @@
|
|||||||
<el-button>选择文件</el-button>
|
<el-button>选择文件</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="rnInspectUploadProgress > 0" label="识别进度">
|
||||||
|
<div class="upload-progress-block">
|
||||||
|
<el-progress :percentage="rnInspectUploadProgress" :status="rnInspectUploadProgress === 100 ? 'success' : undefined" />
|
||||||
|
<span class="upload-progress-text">{{ rnInspectUploadProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-alert
|
<el-alert
|
||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
@ -579,6 +597,12 @@
|
|||||||
<el-input v-model="rnUploadForm.packageName" placeholder="可选,建议与应用包名一致" />
|
<el-input v-model="rnUploadForm.packageName" placeholder="可选,建议与应用包名一致" />
|
||||||
</el-form-item>
|
</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-item v-if="rnBundleUploadProgress > 0" label="提交进度">
|
||||||
|
<div class="upload-progress-block">
|
||||||
|
<el-progress :percentage="rnBundleUploadProgress" :status="rnBundleUploadProgress === 100 ? 'success' : undefined" />
|
||||||
|
<span class="upload-progress-text">{{ rnBundleUploadProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showUploadRn = false">取消</el-button>
|
<el-button @click="showUploadRn = false">取消</el-button>
|
||||||
@ -649,6 +673,10 @@ const grayMemberKeyword = ref('')
|
|||||||
const grayMemberGroupFilter = ref('')
|
const grayMemberGroupFilter = ref('')
|
||||||
const grayMemberIds = ref<string[]>([])
|
const grayMemberIds = ref<string[]>([])
|
||||||
const appPackageInspecting = ref(false)
|
const appPackageInspecting = ref(false)
|
||||||
|
const appPackageUploadProgress = ref(0)
|
||||||
|
const appVersionUploadProgress = ref(0)
|
||||||
|
const rnInspectUploadProgress = ref(0)
|
||||||
|
const rnBundleUploadProgress = ref(0)
|
||||||
const operationLogs = ref<{
|
const operationLogs = ref<{
|
||||||
id: string
|
id: string
|
||||||
appId: string
|
appId: string
|
||||||
@ -961,15 +989,19 @@ async function removeStoreConfig(type: StoreType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizePublishConfig(raw: Record<string, unknown> | null | undefined) {
|
function normalizePublishConfig(raw: Record<string, unknown> | null | undefined) {
|
||||||
|
const normalizeCallbackUrl = (value: unknown) => {
|
||||||
|
const text = String(value ?? '').trim()
|
||||||
|
return /^https?:\/\//i.test(text) ? text : ''
|
||||||
|
}
|
||||||
const grayMode = String(raw?.grayMode ?? 'PERCENT') as GrayMode
|
const grayMode = String(raw?.grayMode ?? 'PERCENT') as GrayMode
|
||||||
const graySelectionSource = String(raw?.graySelectionSource ?? 'LOCAL') as GraySelectionSource
|
const graySelectionSource = String(raw?.graySelectionSource ?? 'LOCAL') as GraySelectionSource
|
||||||
return {
|
return {
|
||||||
defaultGrayPercent: Number((raw as Record<string, unknown>)?.defaultGrayPercent ?? 0),
|
defaultGrayPercent: Number((raw as Record<string, unknown>)?.defaultGrayPercent ?? 0),
|
||||||
grayMode,
|
grayMode,
|
||||||
graySelectionSource,
|
graySelectionSource,
|
||||||
graySelectCallbackUrl: String((raw as Record<string, unknown>)?.graySelectCallbackUrl ?? ''),
|
graySelectCallbackUrl: normalizeCallbackUrl((raw as Record<string, unknown>)?.graySelectCallbackUrl),
|
||||||
graySelectCallbackSecret: String((raw as Record<string, unknown>)?.graySelectCallbackSecret ?? ''),
|
graySelectCallbackSecret: String((raw as Record<string, unknown>)?.graySelectCallbackSecret ?? ''),
|
||||||
grayDirectorySyncCallbackUrl: String((raw as Record<string, unknown>)?.grayDirectorySyncCallbackUrl ?? ''),
|
grayDirectorySyncCallbackUrl: normalizeCallbackUrl((raw as Record<string, unknown>)?.grayDirectorySyncCallbackUrl),
|
||||||
grayDirectorySyncCallbackSecret: String((raw as Record<string, unknown>)?.grayDirectorySyncCallbackSecret ?? ''),
|
grayDirectorySyncCallbackSecret: String((raw as Record<string, unknown>)?.grayDirectorySyncCallbackSecret ?? ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1003,9 +1035,15 @@ async function loadPublishConfig() {
|
|||||||
async function savePublishConfig() {
|
async function savePublishConfig() {
|
||||||
savingPublishConfig.value = true
|
savingPublishConfig.value = true
|
||||||
try {
|
try {
|
||||||
|
const normalizeCallbackUrl = (value: string) => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
return /^https?:\/\//i.test(trimmed) ? trimmed : ''
|
||||||
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
...publishConfigForm.value,
|
...publishConfigForm.value,
|
||||||
defaultGrayPercent: Math.min(Math.max(Number(publishConfigForm.value.defaultGrayPercent || 0), 0), 100),
|
defaultGrayPercent: Math.min(Math.max(Number(publishConfigForm.value.defaultGrayPercent || 0), 0), 100),
|
||||||
|
graySelectCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.graySelectCallbackUrl),
|
||||||
|
grayDirectorySyncCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.grayDirectorySyncCallbackUrl),
|
||||||
}
|
}
|
||||||
if (payload.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) {
|
if (payload.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) {
|
||||||
ElMessage.warning('成员模式至少需要配置一个回调地址')
|
ElMessage.warning('成员模式至少需要配置一个回调地址')
|
||||||
@ -1224,8 +1262,12 @@ async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
|||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
appPackageInspecting.value = true
|
appPackageInspecting.value = true
|
||||||
|
appPackageUploadProgress.value = 0
|
||||||
try {
|
try {
|
||||||
const uploaded = await fileApi.uploadFile(file)
|
const uploaded = await fileApi.uploadFile(file, null, (percent) => {
|
||||||
|
appPackageUploadProgress.value = percent
|
||||||
|
})
|
||||||
|
appPackageUploadProgress.value = 100
|
||||||
const fileInfo = uploaded.data.data
|
const fileInfo = uploaded.data.data
|
||||||
appUploadForm.value.fileUrl = fileInfo.url
|
appUploadForm.value.fileUrl = fileInfo.url
|
||||||
const res = await updateAdminApi.inspectAppPackage(fileInfo.url)
|
const res = await updateAdminApi.inspectAppPackage(fileInfo.url)
|
||||||
@ -1256,6 +1298,7 @@ async function onAppPackageChange(uploadFile: { raw?: File } | null) {
|
|||||||
} catch {
|
} catch {
|
||||||
ElMessage.warning('已选择文件,但未能完整识别,请补全版本信息后上传')
|
ElMessage.warning('已选择文件,但未能完整识别,请补全版本信息后上传')
|
||||||
} finally {
|
} finally {
|
||||||
|
appPackageUploadProgress.value = 0
|
||||||
appPackageInspecting.value = false
|
appPackageInspecting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1266,6 +1309,7 @@ async function submitAppUpload() {
|
|||||||
if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')
|
if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')
|
||||||
|
|
||||||
uploadingApp.value = true
|
uploadingApp.value = true
|
||||||
|
appVersionUploadProgress.value = 0
|
||||||
try {
|
try {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append('appId', appId)
|
fd.append('appId', appId)
|
||||||
@ -1277,11 +1321,15 @@ 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)
|
||||||
if (f.platform === 'ANDROID' && f.fileUrl) fd.append('apkUrl', f.fileUrl)
|
if (f.platform === 'ANDROID' && f.fileUrl) fd.append('apkUrl', f.fileUrl)
|
||||||
await updateAdminApi.uploadAppVersion(fd)
|
await updateAdminApi.uploadAppVersion(fd, (percent) => {
|
||||||
|
appVersionUploadProgress.value = percent
|
||||||
|
})
|
||||||
|
appVersionUploadProgress.value = 100
|
||||||
ElMessage.success('上传成功')
|
ElMessage.success('上传成功')
|
||||||
showUploadApp.value = false
|
showUploadApp.value = false
|
||||||
await loadAppVersions()
|
await loadAppVersions()
|
||||||
} finally {
|
} finally {
|
||||||
|
appVersionUploadProgress.value = 0
|
||||||
uploadingApp.value = false
|
uploadingApp.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1332,8 +1380,12 @@ async function onRnBundleChange(uploadFile: { raw?: File } | null) {
|
|||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('bundle', file)
|
formData.append('bundle', file)
|
||||||
|
rnInspectUploadProgress.value = 0
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.inspectRnBundle(formData)
|
const res = await updateAdminApi.inspectRnBundle(formData, (percent) => {
|
||||||
|
rnInspectUploadProgress.value = percent
|
||||||
|
})
|
||||||
|
rnInspectUploadProgress.value = 100
|
||||||
const inspected = res.data.data as RnBundleInspectResult
|
const inspected = res.data.data as RnBundleInspectResult
|
||||||
if (inspected.moduleId) rnUploadForm.value.moduleId = inspected.moduleId
|
if (inspected.moduleId) rnUploadForm.value.moduleId = inspected.moduleId
|
||||||
if (inspected.platform) rnUploadForm.value.platform = inspected.platform
|
if (inspected.platform) rnUploadForm.value.platform = inspected.platform
|
||||||
@ -1342,6 +1394,8 @@ async function onRnBundleChange(uploadFile: { raw?: File } | null) {
|
|||||||
if (inspected.packageName) rnUploadForm.value.packageName = inspected.packageName
|
if (inspected.packageName) rnUploadForm.value.packageName = inspected.packageName
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.warning('已选择文件,但未能从文件名识别出 RN Bundle 元数据,请补全后上传')
|
ElMessage.warning('已选择文件,但未能从文件名识别出 RN Bundle 元数据,请补全后上传')
|
||||||
|
} finally {
|
||||||
|
rnInspectUploadProgress.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1349,6 +1403,7 @@ async function submitRnUpload() {
|
|||||||
const f = rnUploadForm.value
|
const f = rnUploadForm.value
|
||||||
if (!f.moduleId || !f.version || !f.file) return ElMessage.warning('请填写模块ID、版本和 Bundle 文件')
|
if (!f.moduleId || !f.version || !f.file) return ElMessage.warning('请填写模块ID、版本和 Bundle 文件')
|
||||||
uploadingRn.value = true
|
uploadingRn.value = true
|
||||||
|
rnBundleUploadProgress.value = 0
|
||||||
try {
|
try {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append('appId', appId)
|
fd.append('appId', appId)
|
||||||
@ -1359,11 +1414,15 @@ async function submitRnUpload() {
|
|||||||
if (f.packageName) fd.append('packageName', f.packageName)
|
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, (percent) => {
|
||||||
|
rnBundleUploadProgress.value = percent
|
||||||
|
})
|
||||||
|
rnBundleUploadProgress.value = 100
|
||||||
ElMessage.success('Bundle 上传成功')
|
ElMessage.success('Bundle 上传成功')
|
||||||
showUploadRn.value = false
|
showUploadRn.value = false
|
||||||
await loadRnBundles()
|
await loadRnBundles()
|
||||||
} finally {
|
} finally {
|
||||||
|
rnBundleUploadProgress.value = 0
|
||||||
uploadingRn.value = false
|
uploadingRn.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1760,6 +1819,18 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-progress-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.release-store-checkbox-row {
|
.release-store-checkbox-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://192.168.116.9:8081',
|
target: 'http://127.0.0.1:8081',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户