feat(bugcollect): 将崩溃收集移为服务管理子菜单,并增加服务开通校验

- MainLayout: 移除独立的 bug-collect 顶级子菜单,改为嵌套在服务管理下的 services-bugcollect 子菜单(桌面 + 移动抽屉均已同步)
- openedMenus: /bugcollect/* 路径下同时展开 services 和 services-bugcollect
- FeatureService.serviceType: 补充 BUG_COLLECT 枚举值
- appApi.requestActivation: 支持 BUG_COLLECT 服务类型
- useBugCollectApp: 增加 gateStatus / serviceEnabled / checkingService;appKey 变化时自动调用 getServices 检查 BUG_COLLECT 是否已开通;提供 applyDialogVisible / submitActivation 申请流程
- 全部 7 个 BugCollect 视图: 替换原 !appKey 判断为 gateStatus 四态门控(no-app / loading / not-enabled / enabled),并附加申请开通对话框

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-17 05:25:54 +08:00
父节点 2d650d2b7a
当前提交 fb5e9753a3
共有 10 个文件被更改,包括 193 次插入41 次删除

查看文件

@ -27,7 +27,7 @@ export interface FeatureService {
id: string
appKey: string
platform: 'ANDROID' | 'IOS' | 'HARMONY'
serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'LICENSE'
serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'LICENSE' | 'BUG_COLLECT'
enabled: boolean
config?: string | null
createdAt: string
@ -202,7 +202,7 @@ export const appApi = {
regenerateConfigFile: (appKey: string) =>
client.post<{ data: null }>(`/apps/${appKey}/config-file/regenerate`),
requestActivation: (appKey: string, serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'LICENSE', reason: string) =>
requestActivation: (appKey: string, serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'LICENSE' | 'BUG_COLLECT', reason: string) =>
client.post<{ data: null }>(`/apps/${appKey}/services/request-activation`, null, {
params: { platform: 'ANDROID', serviceType, applyReason: reason },
}),

查看文件

@ -1,6 +1,7 @@
import { ref, computed, onMounted } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appApi, type App } from '@/api/app'
import { ElMessage } from 'element-plus'
const STORAGE_KEY = 'bugcollect_selected_app_key'
@ -10,6 +11,11 @@ export function useBugCollectApp() {
const apps = ref<App[]>([])
const loadingApps = ref(false)
const selectedAppKey = ref(localStorage.getItem(STORAGE_KEY) ?? '')
const serviceEnabled = ref<boolean | null>(null)
const checkingService = ref(false)
const applyDialogVisible = ref(false)
const applyReason = ref('')
const applyLoading = ref(false)
const appKey = computed(() => {
const q = route.query.appKey
@ -17,6 +23,13 @@ export function useBugCollectApp() {
return selectedAppKey.value
})
// 'no-app' | 'loading' | 'not-enabled' | 'enabled'
const gateStatus = computed(() => {
if (!appKey.value) return 'no-app'
if (checkingService.value || serviceEnabled.value === null) return 'loading'
return serviceEnabled.value ? 'enabled' : 'not-enabled'
})
async function loadApps() {
loadingApps.value = true
try {
@ -29,12 +42,49 @@ export function useBugCollectApp() {
}
}
async function checkService(key: string) {
if (!key) {
serviceEnabled.value = null
return
}
checkingService.value = true
serviceEnabled.value = null
try {
const res = await appApi.getServices(key)
const found = (res.data.data ?? []).find(s => s.serviceType === 'BUG_COLLECT')
serviceEnabled.value = found?.enabled ?? false
} catch {
serviceEnabled.value = false
} finally {
checkingService.value = false
}
}
function setApp(key: string) {
selectedAppKey.value = key
localStorage.setItem(STORAGE_KEY, key)
router.replace({ query: { ...route.query, appKey: key } })
}
async function submitActivation() {
if (!appKey.value || !applyReason.value.trim()) return
applyLoading.value = true
try {
await appApi.requestActivation(appKey.value, 'BUG_COLLECT', applyReason.value.trim())
ElMessage.success('申请已提交,请等待审核')
applyDialogVisible.value = false
applyReason.value = ''
} catch {
ElMessage.error('申请提交失败,请稍后重试')
} finally {
applyLoading.value = false
}
}
watch(appKey, (key) => {
checkService(key)
})
onMounted(() => {
const q = route.query.appKey
if (typeof q === 'string' && q.trim()) {
@ -42,7 +92,20 @@ export function useBugCollectApp() {
localStorage.setItem(STORAGE_KEY, q.trim())
}
loadApps()
if (appKey.value) checkService(appKey.value)
})
return { apps, loadingApps, appKey, setApp }
return {
apps,
loadingApps,
appKey,
setApp,
gateStatus,
serviceEnabled,
checkingService,
applyDialogVisible,
applyReason,
applyLoading,
submitActivation,
}
}

查看文件

@ -15,7 +15,13 @@
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<el-card shadow="never">
@ -84,6 +90,13 @@
/>
</el-card>
</template>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
@ -92,7 +105,7 @@ import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectEventItem } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const events = ref<BugCollectEventItem[]>([])
const loading = ref(false)

查看文件

@ -15,7 +15,13 @@
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<el-card style="margin-bottom: 16px">
@ -54,6 +60,13 @@
</div>
</el-card>
</template>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
@ -62,7 +75,7 @@ import { ref } from 'vue'
import { bugCollectApi, type BugCollectFunnelStep } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const steps = ref(['', ''])
const funnelData = ref<BugCollectFunnelStep[]>([])

查看文件

@ -15,7 +15,13 @@
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<el-card shadow="never">
@ -83,6 +89,13 @@
/>
</el-card>
</template>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
@ -91,7 +104,7 @@ import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectIssue } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const issues = ref<BugCollectIssue[]>([])
const loading = ref(false)

查看文件

@ -15,7 +15,13 @@
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<!-- Stats Cards -->
@ -94,6 +100,13 @@
</el-table>
</el-card>
</template>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
@ -103,7 +116,7 @@ import { bugCollectApi, type BugCollectOverview } from '@/api/bugcollect'
import { Warning, Plus, User } from '@element-plus/icons-vue'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const overview = ref<BugCollectOverview>({
totalIssues: 0,

查看文件

@ -15,7 +15,13 @@
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<el-card shadow="never">
@ -44,6 +50,13 @@
</el-table>
</el-card>
</template>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
@ -52,7 +65,7 @@ import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectIssueRanking } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const rankings = ref<BugCollectIssueRanking[]>([])
const loading = ref(false)

查看文件

@ -15,7 +15,13 @@
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<el-card shadow="never">
@ -49,6 +55,13 @@
</el-table>
</el-card>
</template>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
@ -57,7 +70,7 @@ import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectIssueRanking } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const rankings = ref<BugCollectIssueRanking[]>([])
const loading = ref(false)

查看文件

@ -18,7 +18,13 @@
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<el-card shadow="never">
@ -83,6 +89,13 @@
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
@ -92,7 +105,7 @@ import { ElMessage } from 'element-plus'
import { bugCollectApi, type BugCollectWebhook } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const webhooks = ref<BugCollectWebhook[]>([])
const loading = ref(false)

查看文件

@ -21,7 +21,16 @@
<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-menu-item index="/services/license"><el-icon><Key /></el-icon><span></span></el-menu-item>
<el-menu-item index="/services/bugcollect"><el-icon><DataLine /></el-icon><span></span></el-menu-item>
<el-sub-menu index="services-bugcollect">
<template #title><el-icon><DataLine /></el-icon><span></span></template>
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/issues"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/events"><el-icon><List /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/funnels"><el-icon><Filter /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/freq"><el-icon><Sort /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/risk"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/webhooks"><el-icon><Link /></el-icon><span>Webhook</span></el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span></span></el-menu-item>
<el-sub-menu index="ops">
@ -30,16 +39,6 @@
<el-menu-item v-if="isPrivateDeploy" index="/database"><el-icon><Coin /></el-icon><span></span></el-menu-item>
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-sub-menu index="bug-collect">
<template #title><el-icon><DataLine /></el-icon><span>Bug</span></template>
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/issues"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/events"><el-icon><List /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/funnels"><el-icon><Filter /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/freq"><el-icon><Sort /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/risk"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/webhooks"><el-icon><Link /></el-icon><span>Webhook</span></el-menu-item>
</el-sub-menu>
<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>
@ -72,7 +71,16 @@
<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-menu-item index="/services/license"><el-icon><Key /></el-icon><span></span></el-menu-item>
<el-menu-item index="/services/bugcollect"><el-icon><DataLine /></el-icon><span></span></el-menu-item>
<el-sub-menu index="services-bugcollect">
<template #title><el-icon><DataLine /></el-icon><span></span></template>
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/issues"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/events"><el-icon><List /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/funnels"><el-icon><Filter /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/freq"><el-icon><Sort /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/risk"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/webhooks"><el-icon><Link /></el-icon><span>Webhook</span></el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span></span></el-menu-item>
<el-sub-menu index="ops">
@ -81,16 +89,6 @@
<el-menu-item v-if="isPrivateDeploy" index="/database"><el-icon><Coin /></el-icon><span></span></el-menu-item>
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-sub-menu index="bug-collect">
<template #title><el-icon><DataLine /></el-icon><span>Bug</span></template>
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/issues"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/events"><el-icon><List /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/funnels"><el-icon><Filter /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/freq"><el-icon><Sort /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/rank/risk"><el-icon><Warning /></el-icon><span></span></el-menu-item>
<el-menu-item index="/bugcollect/webhooks"><el-icon><Link /></el-icon><span>Webhook</span></el-menu-item>
</el-sub-menu>
<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>
@ -152,7 +150,7 @@ const openedMenus = computed(() => {
const menus: string[] = []
if (route.path.startsWith('/services/')) menus.push('services')
if (['/system-logs', '/database', '/operation-logs'].includes(route.path)) menus.push('ops')
if (route.path.startsWith('/bugcollect/')) menus.push('bug-collect')
if (route.path.startsWith('/bugcollect/')) menus.push('services', 'services-bugcollect')
return menus
})