feat: add quick service entry portal with in-page app switcher
- Add sidebar sub-menu for 服务管理 (IM / 离线推送 / 版本管理) - Service pages load directly with optional appId route param - Each service page shows a portal bar (app selector) when accessed via /services/* path - Content is guarded with v-if so empty state shows when no app is selected - Router-view keyed by path so component re-creates on app switch - App-level package name split into Android/iOS/HarmonyOS fields - Push vendor channel config: Xiaomi channelId, Huawei category, vivo category+receiptId, OPPO channelId - Remove packageName from push vendor config (moved to app-level) - Format device last-login time in push management view Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
f24b467308
当前提交
f36d657bba
2
tenant-platform/components.d.ts
vendored
2
tenant-platform/components.d.ts
vendored
@ -43,6 +43,7 @@ declare module 'vue' {
|
||||
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
@ -53,6 +54,7 @@ declare module 'vue' {
|
||||
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
|
||||
@ -4,6 +4,8 @@ export interface App {
|
||||
id: string
|
||||
tenantId: string
|
||||
packageName: string
|
||||
iosBundleId?: string
|
||||
harmonyBundleName?: string
|
||||
name: string
|
||||
description?: string
|
||||
iconUrl?: string
|
||||
@ -14,6 +16,8 @@ export interface App {
|
||||
|
||||
export interface CreateAppRequest {
|
||||
packageName: string
|
||||
iosBundleId?: string
|
||||
harmonyBundleName?: string
|
||||
name: string
|
||||
description?: string
|
||||
iconUrl?: string
|
||||
@ -61,7 +65,6 @@ export interface PushVendorConfig {
|
||||
appId?: string
|
||||
appKey?: string
|
||||
appSecret?: string
|
||||
packageName?: string
|
||||
masterSecret?: string
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
@ -71,6 +74,9 @@ export interface PushVendorConfig {
|
||||
keyPath?: string
|
||||
sandbox?: boolean
|
||||
serviceAccountJson?: string
|
||||
channelId?: string
|
||||
category?: string
|
||||
receiptId?: string
|
||||
}
|
||||
|
||||
export interface PushServiceConfig {
|
||||
|
||||
@ -81,6 +81,18 @@ const router = createRouter({
|
||||
path: 'apps/:appId/update',
|
||||
component: () => import('@/views/update/VersionManagementView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'services/im/:appId?',
|
||||
component: () => import('@/views/im/ImManagementView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'services/push/:appId?',
|
||||
component: () => import('@/views/push/PushManagementView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'services/update/:appId?',
|
||||
component: () => import('@/views/update/VersionManagementView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'accounts',
|
||||
component: () => import('@/views/accounts/SubAccountView.vue'),
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
<el-card class="info-card" style="margin-bottom:16px">
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||
<el-descriptions-item label="应用名称">{{ app.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="包名">{{ app.packageName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="Android 包名">{{ app.packageName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="iOS Bundle ID">{{ app.iosBundleId ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="鸿蒙 Bundle Name">{{ app.harmonyBundleName ?? '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="AppKey">
|
||||
<el-text class="mono">{{ app.appKey }}</el-text>
|
||||
<el-button link @click="copy(app.appKey)"><el-icon><CopyDocument /></el-icon></el-button>
|
||||
|
||||
@ -26,9 +26,15 @@
|
||||
|
||||
<el-dialog v-model="showCreate" title="创建应用" :width="dialogWidth">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-position="top">
|
||||
<el-form-item label="包名" prop="packageName">
|
||||
<el-form-item label="Android 包名" prop="packageName">
|
||||
<el-input v-model="createForm.packageName" placeholder="com.example.app" />
|
||||
</el-form-item>
|
||||
<el-form-item label="iOS Bundle ID">
|
||||
<el-input v-model="createForm.iosBundleId" placeholder="com.example.app" />
|
||||
</el-form-item>
|
||||
<el-form-item label="鸿蒙 Bundle Name">
|
||||
<el-input v-model="createForm.harmonyBundleName" placeholder="com.example.app" />
|
||||
</el-form-item>
|
||||
<el-form-item label="应用名称" prop="name">
|
||||
<el-input v-model="createForm.name" />
|
||||
</el-form-item>
|
||||
@ -58,7 +64,7 @@ const createFormRef = ref<FormInstance>()
|
||||
const isMobile = ref(false)
|
||||
const dialogWidth = computed(() => (isMobile.value ? 'calc(100vw - 24px)' : '480px'))
|
||||
|
||||
const createForm = reactive({ packageName: '', name: '', description: '' })
|
||||
const createForm = reactive({ packageName: '', iosBundleId: '', harmonyBundleName: '', name: '', description: '' })
|
||||
const createRules: FormRules = {
|
||||
packageName: [{ required: true, message: '请输入包名' }],
|
||||
name: [{ required: true, message: '请输入应用名' }],
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-page-header @back="$router.back()" :content="`即时通讯管理 — ${appKey}`" style="margin-bottom:20px" />
|
||||
<div v-if="isServicesPortal" class="portal-bar">
|
||||
<span class="portal-bar-title">即时通讯管理</span>
|
||||
<el-select :model-value="appKey" placeholder="选择应用" style="width:220px" @change="switchApp">
|
||||
<el-option v-for="a in portalApps" :key="a.appKey" :label="a.name" :value="a.appKey" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-page-header v-else @back="$router.back()" :content="`即时通讯管理 — ${appKey}`" style="margin-bottom:20px" />
|
||||
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
||||
|
||||
<template v-if="!isServicesPortal || appKey">
|
||||
<el-row :gutter="16" class="stat-grid">
|
||||
<el-col :xs="24" :sm="12" :md="6" v-for="item in statCards" :key="item.label">
|
||||
<el-card shadow="never">
|
||||
@ -738,12 +746,14 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@ -760,6 +770,9 @@ import {
|
||||
} from '@/api/im'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
||||
const portalApps = ref<App[]>([])
|
||||
// IM 管理页按 appKey 作用域查询;tenant-service 的应用主键不要传到这里。
|
||||
const appKey = computed(() => {
|
||||
const queryAppKey = route.query.appKey
|
||||
@ -1137,6 +1150,10 @@ function resetMessageSearch() {
|
||||
historyPage.value = 0
|
||||
}
|
||||
|
||||
function switchApp(val: string) {
|
||||
router.push(`/services/im/${val}`)
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await imAdminApi.getStats(appKey.value)
|
||||
@ -1779,6 +1796,10 @@ function handleOperationLogPageChange(page: number) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isServicesPortal.value) {
|
||||
appApi.list().then(res => { portalApps.value = res.data.data })
|
||||
if (!appKey.value) return
|
||||
}
|
||||
loadStats()
|
||||
loadUsers()
|
||||
})
|
||||
@ -2030,4 +2051,15 @@ function userAvatarFallback(row: ImUser) {
|
||||
max-height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.portal-bar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -6,16 +6,25 @@
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
:default-openeds="openedMenus"
|
||||
router
|
||||
background-color="#1d2129"
|
||||
text-color="#c9d1d9"
|
||||
active-text-color="#409eff"
|
||||
class="nav-menu"
|
||||
>
|
||||
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/dashboard"><el-icon><Odometer /></el-icon><span>控制台</span></el-menu-item>
|
||||
<el-menu-item index="/apps"><el-icon><Grid /></el-icon><span>我的应用</span></el-menu-item>
|
||||
<el-sub-menu index="services">
|
||||
<template #title><el-icon><List /></el-icon><span>服务管理</span></template>
|
||||
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span>即时通讯</span></el-menu-item>
|
||||
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>离线推送</span></el-menu-item>
|
||||
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span>版本管理</span></el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span>安全中心</span></el-menu-item>
|
||||
<el-menu-item index="/docs"><el-icon><Document /></el-icon><span>接入文档</span></el-menu-item>
|
||||
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span>操作日志</span></el-menu-item>
|
||||
<el-menu-item v-if="auth.user?.type === 'MAIN'" index="/accounts"><el-icon><User /></el-icon><span>子账号管理</span></el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
@ -31,6 +40,7 @@
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
:default-openeds="openedMenus"
|
||||
router
|
||||
background-color="#1d2129"
|
||||
text-color="#c9d1d9"
|
||||
@ -38,10 +48,18 @@
|
||||
class="nav-menu"
|
||||
@select="handleNavSelect"
|
||||
>
|
||||
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/dashboard"><el-icon><Odometer /></el-icon><span>控制台</span></el-menu-item>
|
||||
<el-menu-item index="/apps"><el-icon><Grid /></el-icon><span>我的应用</span></el-menu-item>
|
||||
<el-sub-menu index="services">
|
||||
<template #title><el-icon><List /></el-icon><span>服务管理</span></template>
|
||||
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span>即时通讯</span></el-menu-item>
|
||||
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>离线推送</span></el-menu-item>
|
||||
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span>版本管理</span></el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span>安全中心</span></el-menu-item>
|
||||
<el-menu-item index="/docs"><el-icon><Document /></el-icon><span>接入文档</span></el-menu-item>
|
||||
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span>操作日志</span></el-menu-item>
|
||||
<el-menu-item v-if="auth.user?.type === 'MAIN'" index="/accounts"><el-icon><User /></el-icon><span>子账号管理</span></el-menu-item>
|
||||
</el-menu>
|
||||
</el-drawer>
|
||||
|
||||
@ -78,7 +96,7 @@
|
||||
</el-header>
|
||||
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
<router-view :key="$route.path" />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
@ -88,7 +106,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Document, Grid, Lock, Menu, Odometer, User } from '@element-plus/icons-vue'
|
||||
import { Bell, ChatDotRound, Document, Grid, List, Lock, Menu, Odometer, Upload, User } from '@element-plus/icons-vue'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
@ -96,19 +114,9 @@ const router = useRouter()
|
||||
const isMobile = ref(false)
|
||||
const drawerVisible = ref(false)
|
||||
|
||||
const navItems = computed(() => {
|
||||
const items = [
|
||||
{ path: '/dashboard', label: '控制台', icon: Odometer },
|
||||
{ path: '/apps', label: '我的应用', icon: Grid },
|
||||
{ path: '/security', label: '安全中心', icon: Lock },
|
||||
{ path: '/docs', label: '接入文档', icon: Document },
|
||||
{ path: '/operation-logs', label: '操作日志', icon: Document },
|
||||
]
|
||||
if (auth.user?.type === 'MAIN') {
|
||||
items.push({ path: '/accounts', label: '子账号管理', icon: User })
|
||||
}
|
||||
return items
|
||||
})
|
||||
const openedMenus = computed(() =>
|
||||
route.path.startsWith('/services/') ? ['services'] : [],
|
||||
)
|
||||
|
||||
function updateViewport() {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
|
||||
@ -205,10 +205,10 @@ function updateViewport() {
|
||||
}
|
||||
|
||||
const pushConfig = reactive<Required<PushServiceConfig>>({
|
||||
huawei: { appId: '', appSecret: '' },
|
||||
xiaomi: { appId: '', appKey: '', appSecret: '', packageName: '' },
|
||||
oppo: { appId: '', appKey: '', masterSecret: '' },
|
||||
vivo: { appId: '', appKey: '', appSecret: '' },
|
||||
huawei: { appId: '', appSecret: '', category: '' },
|
||||
xiaomi: { appId: '', appKey: '', appSecret: '', channelId: '' },
|
||||
oppo: { appId: '', appKey: '', masterSecret: '', channelId: '' },
|
||||
vivo: { appId: '', appKey: '', appSecret: '', category: '', receiptId: '' },
|
||||
honor: { appId: '', clientId: '', clientSecret: '' },
|
||||
harmony: { appId: '', appSecret: '' },
|
||||
apns: { teamId: '', keyId: '', bundleId: '', keyPath: '', sandbox: false },
|
||||
@ -241,41 +241,45 @@ const vendorDefs: VendorDef[] = [
|
||||
{
|
||||
key: 'huawei',
|
||||
label: '华为 HMS',
|
||||
hint: '填写 AppId / AppSecret,供服务端推送使用。',
|
||||
hint: '填写 AppId / AppSecret;Category 用于消息分类(如 IM)。',
|
||||
fields: [
|
||||
{ key: 'appId', label: 'AppId' },
|
||||
{ key: 'appSecret', label: 'AppSecret' },
|
||||
{ key: 'category', label: 'Category', placeholder: 'IM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'xiaomi',
|
||||
label: '小米 MiPush',
|
||||
hint: '填写 AppId / AppKey / AppSecret 及 Android 包名。',
|
||||
hint: '填写 AppId / AppKey / AppSecret;Android 包名在应用设置中统一配置。Channel ID 为小米通知通道 ID(如 118060)。',
|
||||
fields: [
|
||||
{ key: 'appId', label: 'AppId' },
|
||||
{ key: 'appKey', label: 'AppKey' },
|
||||
{ key: 'appSecret', label: 'AppSecret' },
|
||||
{ key: 'packageName', label: 'Android 包名', placeholder: 'com.example.app' },
|
||||
{ key: 'channelId', label: 'Channel ID', placeholder: '118060' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'oppo',
|
||||
label: 'OPPO 推送',
|
||||
hint: '填写 AppId / AppKey / MasterSecret。',
|
||||
hint: '填写 AppId / AppKey / MasterSecret;Channel ID 为 OPPO 推送通道 ID(如 IM)。',
|
||||
fields: [
|
||||
{ key: 'appId', label: 'AppId' },
|
||||
{ key: 'appKey', label: 'AppKey' },
|
||||
{ key: 'masterSecret', label: 'MasterSecret' },
|
||||
{ key: 'channelId', label: 'Channel ID', placeholder: 'IM' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'vivo',
|
||||
label: 'vivo 推送',
|
||||
hint: '填写 AppId / AppKey / AppSecret。',
|
||||
hint: '填写 AppId / AppKey / AppSecret;Category 用于消息分类(IM 代表 IM 消息);回执 ID 为 vivo 控制台预注册的消息回执标识。',
|
||||
fields: [
|
||||
{ key: 'appId', label: 'AppId' },
|
||||
{ key: 'appKey', label: 'AppKey' },
|
||||
{ key: 'appSecret', label: 'AppSecret' },
|
||||
{ key: 'category', label: 'Category', placeholder: 'IM' },
|
||||
{ key: 'receiptId', label: '回执 ID', placeholder: '4470' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -354,10 +358,10 @@ function applyConfig(raw?: string | null) {
|
||||
}
|
||||
|
||||
function resetPushConfig() {
|
||||
pushConfig.huawei = { appId: '', appSecret: '' }
|
||||
pushConfig.xiaomi = { appId: '', appKey: '', appSecret: '', packageName: '' }
|
||||
pushConfig.oppo = { appId: '', appKey: '', masterSecret: '' }
|
||||
pushConfig.vivo = { appId: '', appKey: '', appSecret: '' }
|
||||
pushConfig.huawei = { appId: '', appSecret: '', category: '' }
|
||||
pushConfig.xiaomi = { appId: '', appKey: '', appSecret: '', channelId: '' }
|
||||
pushConfig.oppo = { appId: '', appKey: '', masterSecret: '', channelId: '' }
|
||||
pushConfig.vivo = { appId: '', appKey: '', appSecret: '', category: '', receiptId: '' }
|
||||
pushConfig.honor = { appId: '', clientId: '', clientSecret: '' }
|
||||
pushConfig.harmony = { appId: '', appSecret: '' }
|
||||
pushConfig.apns = { teamId: '', keyId: '', bundleId: '', keyPath: '', sandbox: false }
|
||||
@ -394,15 +398,20 @@ function toPushConfigRequest(): Record<string, unknown> {
|
||||
return {
|
||||
huaweiAppId: pushConfig.huawei.appId ?? '',
|
||||
huaweiAppSecret: pushConfig.huawei.appSecret ?? '',
|
||||
huaweiCategory: pushConfig.huawei.category ?? '',
|
||||
xiaomiAppId: pushConfig.xiaomi.appId ?? '',
|
||||
xiaomiAppKey: pushConfig.xiaomi.appKey ?? '',
|
||||
xiaomiAppSecret: pushConfig.xiaomi.appSecret ?? '',
|
||||
xiaomiChannelId: pushConfig.xiaomi.channelId ?? '',
|
||||
oppoAppId: pushConfig.oppo.appId ?? '',
|
||||
oppoAppKey: pushConfig.oppo.appKey ?? '',
|
||||
oppoMasterSecret: pushConfig.oppo.masterSecret ?? '',
|
||||
oppoChannelId: pushConfig.oppo.channelId ?? '',
|
||||
vivoAppId: pushConfig.vivo.appId ?? '',
|
||||
vivoAppKey: pushConfig.vivo.appKey ?? '',
|
||||
vivoAppSecret: pushConfig.vivo.appSecret ?? '',
|
||||
vivoCategory: pushConfig.vivo.category ?? '',
|
||||
vivoReceiptId: pushConfig.vivo.receiptId ?? '',
|
||||
honorAppId: pushConfig.honor.appId ?? '',
|
||||
honorClientId: pushConfig.honor.clientId ?? '',
|
||||
honorClientSecret: pushConfig.honor.clientSecret ?? '',
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-page-header @back="$router.back()" content="推送管理" style="margin-bottom:24px" />
|
||||
<div v-if="isServicesPortal" class="portal-bar">
|
||||
<span class="portal-bar-title">离线推送管理</span>
|
||||
<el-select :model-value="appId" placeholder="选择应用" style="width:220px" @change="switchApp">
|
||||
<el-option v-for="a in portalApps" :key="a.appKey" :label="a.name" :value="a.appKey" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-page-header v-else @back="$router.back()" content="推送管理" style="margin-bottom:24px" />
|
||||
<el-empty v-if="isServicesPortal && !appId" description="请选择一个应用" style="margin-top:80px" />
|
||||
|
||||
<template v-if="!isServicesPortal || appId">
|
||||
<el-card style="margin-bottom:16px">
|
||||
<template #header>用户设备状态查询</template>
|
||||
<el-form inline @submit.prevent="queryUser">
|
||||
@ -56,7 +64,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后登录" min-width="160">
|
||||
<template #default="{ row }">{{ row.lastLoginAt ?? '-' }}</template>
|
||||
<template #default="{ row }">{{ row.lastLoginAt ? formatDateTime(row.lastLoginAt) : '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
@ -135,17 +143,22 @@
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
import { pushAdminApi, type DeviceLoginLog, type TestPushResult, type UserPushStatus } from '@/api/push'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appId = route.params.appId as string
|
||||
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
||||
const portalApps = ref<App[]>([])
|
||||
|
||||
const isMobile = ref(window.innerWidth < 768)
|
||||
function updateViewport() { isMobile.value = window.innerWidth < 768 }
|
||||
@ -166,6 +179,10 @@ const logsPageSize = 20
|
||||
const logsTotal = ref(0)
|
||||
const logsTotalPages = ref(0)
|
||||
|
||||
function switchApp(val: string) {
|
||||
router.push(`/services/push/${val}`)
|
||||
}
|
||||
|
||||
async function queryUser() {
|
||||
const uid = queryUserId.value.trim()
|
||||
if (!uid) {
|
||||
@ -229,6 +246,29 @@ function formatTime(ms: number): string {
|
||||
return new Date(ms).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('resize', updateViewport))
|
||||
function formatDateTime(iso: string): string {
|
||||
if (!iso) return '-'
|
||||
return new Date(iso).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isServicesPortal.value) {
|
||||
appApi.list().then(res => { portalApps.value = res.data.data })
|
||||
}
|
||||
window.addEventListener('resize', updateViewport)
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', updateViewport))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.portal-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.portal-bar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-page-header @back="$router.back()" :content="`版本管理 — ${pageTitle}`" style="margin-bottom:20px" />
|
||||
<div v-if="isServicesPortal" class="portal-bar">
|
||||
<span class="portal-bar-title">版本管理</span>
|
||||
<el-select :model-value="appId" placeholder="选择应用" style="width:220px" @change="switchApp">
|
||||
<el-option v-for="a in portalApps" :key="a.id" :label="a.name" :value="a.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-page-header v-else @back="$router.back()" :content="`版本管理 — ${pageTitle}`" style="margin-bottom:20px" />
|
||||
<el-empty v-if="isServicesPortal && !appId" description="请选择一个应用" style="margin-top:80px" />
|
||||
|
||||
<template v-if="!isServicesPortal || appId">
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- App Versions -->
|
||||
@ -610,12 +618,13 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
import { fileApi } from '@/api/file'
|
||||
@ -639,7 +648,10 @@ import vivoGuideImage from '@/assets/update-store/vivo/01.png'
|
||||
import honorGuideImage from '@/assets/update-store/honor/01.png'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appId = route.params.appId as string
|
||||
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
||||
const portalApps = ref<App[]>([])
|
||||
const app = ref<App | null>(null)
|
||||
const pageTitle = computed(() => app.value?.name ?? appId)
|
||||
const isMobile = ref(false)
|
||||
@ -922,6 +934,10 @@ async function loadStoreConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
function switchApp(val: string) {
|
||||
router.push(`/services/update/${val}`)
|
||||
}
|
||||
|
||||
async function loadApp() {
|
||||
const res = await appApi.get(appId)
|
||||
app.value = res.data.data
|
||||
@ -1700,6 +1716,10 @@ watch(grayForm, () => {
|
||||
onMounted(() => {
|
||||
updateViewport()
|
||||
window.addEventListener('resize', updateViewport)
|
||||
if (isServicesPortal.value) {
|
||||
appApi.list().then(res => { portalApps.value = res.data.data })
|
||||
if (!appId) return
|
||||
}
|
||||
loadApp()
|
||||
loadAppVersions()
|
||||
loadRnBundles()
|
||||
@ -1900,4 +1920,15 @@ onBeforeUnmount(() => {
|
||||
margin: 8px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.portal-bar-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户