feat(bug-collect): 所有视图新增应用选择器,API 调用补充 appKey 参数

这个提交包含在:
XuqmGroup 2026-06-17 05:00:51 +08:00
父节点 4c6be2c489
当前提交 5e87a17765
共有 10 个文件被更改,包括 227 次插入24 次删除

查看文件

@ -88,10 +88,11 @@ export interface BugCollectPageResult<T> {
export const bugCollectApi = { export const bugCollectApi = {
// Overview // Overview
overview: () => client.get<{ data: BugCollectOverview }>('/bugcollect/v1/overview'), overview: (appKey: string) =>
client.get<{ data: BugCollectOverview }>('/bugcollect/v1/overview', { params: { appKey } }),
// Issues // Issues
issues(params: { issues(appKey: string, params: {
type?: string type?: string
platform?: string platform?: string
startDate?: string startDate?: string
@ -99,7 +100,7 @@ export const bugCollectApi = {
page?: number page?: number
size?: number size?: number
}) { }) {
return client.get<{ data: BugCollectPageResult<BugCollectIssue> }>('/bugcollect/v1/issues', { params }) return client.get<{ data: BugCollectPageResult<BugCollectIssue> }>('/bugcollect/v1/issues', { params: { appKey, ...params } })
}, },
issueDetail(id: string) { issueDetail(id: string) {
@ -107,16 +108,16 @@ export const bugCollectApi = {
}, },
// Rankings // Rankings
frequencyRanking() { frequencyRanking(appKey: string) {
return client.get<{ data: BugCollectIssueRanking[] }>('/bugcollect/v1/issues/rankings/frequency') return client.get<{ data: BugCollectIssueRanking[] }>('/bugcollect/v1/issues/rankings/frequency', { params: { appKey } })
}, },
riskRanking() { riskRanking(appKey: string) {
return client.get<{ data: BugCollectIssueRanking[] }>('/bugcollect/v1/issues/rankings/risk') return client.get<{ data: BugCollectIssueRanking[] }>('/bugcollect/v1/issues/rankings/risk', { params: { appKey } })
}, },
// Events // Events
events(params: { events(appKey: string, params: {
eventName?: string eventName?: string
userId?: string userId?: string
startDate?: string startDate?: string
@ -124,21 +125,21 @@ export const bugCollectApi = {
page?: number page?: number
size?: number size?: number
}) { }) {
return client.get<{ data: BugCollectPageResult<BugCollectEventItem> }>('/bugcollect/v1/events', { params }) return client.get<{ data: BugCollectPageResult<BugCollectEventItem> }>('/bugcollect/v1/events', { params: { appKey, ...params } })
}, },
// Funnel // Funnel
funnel(steps: string[]) { funnel(appKey: string, steps: string[]) {
return client.get<{ data: BugCollectFunnelStep[] }>('/bugcollect/v1/events/funnel', { return client.get<{ data: BugCollectFunnelStep[] }>('/bugcollect/v1/events/funnel', {
params: { steps: steps.join(',') }, params: { appKey, steps: steps.join(',') },
}) })
}, },
// Webhooks // Webhooks
webhooks: { webhooks: {
list: () => client.get<{ data: BugCollectWebhook[] }>('/bugcollect/v1/webhooks'), list: (appKey: string) => client.get<{ data: BugCollectWebhook[] }>('/bugcollect/v1/webhooks', { params: { appKey } }),
create: (data: Omit<BugCollectWebhook, 'id' | 'createdAt' | 'updatedAt'>) => create: (appKey: string, data: Omit<BugCollectWebhook, 'id' | 'createdAt' | 'updatedAt'>) =>
client.post<{ data: BugCollectWebhook }>('/bugcollect/v1/webhooks', data), client.post<{ data: BugCollectWebhook }>('/bugcollect/v1/webhooks', data, { params: { appKey } }),
update: (id: string, data: Partial<Omit<BugCollectWebhook, 'id' | 'createdAt' | 'updatedAt'>>) => update: (id: string, data: Partial<Omit<BugCollectWebhook, 'id' | 'createdAt' | 'updatedAt'>>) =>
client.put<{ data: BugCollectWebhook }>(`/bugcollect/v1/webhooks/${id}`, data), client.put<{ data: BugCollectWebhook }>(`/bugcollect/v1/webhooks/${id}`, data),
delete: (id: string) => client.delete(`/bugcollect/v1/webhooks/${id}`), delete: (id: string) => client.delete(`/bugcollect/v1/webhooks/${id}`),

查看文件

@ -0,0 +1,48 @@
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appApi, type App } from '@/api/app'
const STORAGE_KEY = 'bugcollect_selected_app_key'
export function useBugCollectApp() {
const route = useRoute()
const router = useRouter()
const apps = ref<App[]>([])
const loadingApps = ref(false)
const selectedAppKey = ref(localStorage.getItem(STORAGE_KEY) ?? '')
const appKey = computed(() => {
const q = route.query.appKey
if (typeof q === 'string' && q.trim()) return q.trim()
return selectedAppKey.value
})
async function loadApps() {
loadingApps.value = true
try {
const res = await appApi.list()
apps.value = res.data.data ?? []
} catch {
apps.value = []
} finally {
loadingApps.value = false
}
}
function setApp(key: string) {
selectedAppKey.value = key
localStorage.setItem(STORAGE_KEY, key)
router.replace({ query: { ...route.query, appKey: key } })
}
onMounted(() => {
const q = route.query.appKey
if (typeof q === 'string' && q.trim()) {
selectedAppKey.value = q.trim()
localStorage.setItem(STORAGE_KEY, q.trim())
}
loadApps()
})
return { apps, loadingApps, appKey, setApp }
}

查看文件

@ -196,10 +196,10 @@
</div> </div>
<div class="service-actions"> <div class="service-actions">
<template v-if="isServiceEnabled('BUG_COLLECT')"> <template v-if="isServiceEnabled('BUG_COLLECT')">
<el-button size="small" type="primary" plain @click="$router.push('/bugcollect/overview')"> <el-button size="small" type="primary" plain @click="$router.push({ path: '/bugcollect/overview', query: { appKey: app.appKey } })">
崩溃概览 崩溃概览
</el-button> </el-button>
<el-button size="small" @click="$router.push('/bugcollect/issues')"> <el-button size="small" @click="$router.push({ path: '/bugcollect/issues', query: { appKey: app.appKey } })">
错误列表 错误列表
</el-button> </el-button>
</template> </template>

查看文件

@ -2,6 +2,22 @@
<div> <div>
<h2 style="margin-bottom: 24px">事件流水</h2> <h2 style="margin-bottom: 24px">事件流水</h2>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<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" />
<template v-else>
<el-card shadow="never"> <el-card shadow="never">
<div class="toolbar responsive-toolbar"> <div class="toolbar responsive-toolbar">
<el-input <el-input
@ -67,12 +83,16 @@
@size-change="handleSizeChange" @size-change="handleSizeChange"
/> />
</el-card> </el-card>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectEventItem } from '@/api/bugcollect' import { bugCollectApi, type BugCollectEventItem } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const events = ref<BugCollectEventItem[]>([]) const events = ref<BugCollectEventItem[]>([])
const loading = ref(false) const loading = ref(false)
@ -94,7 +114,7 @@ function formatTime(ts: string) {
async function loadData() { async function loadData() {
loading.value = true loading.value = true
try { try {
const res = await bugCollectApi.events({ const res = await bugCollectApi.events(appKey.value, {
eventName: filters.value.eventName || undefined, eventName: filters.value.eventName || undefined,
userId: filters.value.userId || undefined, userId: filters.value.userId || undefined,
startDate: filters.value.dateRange?.[0] || undefined, startDate: filters.value.dateRange?.[0] || undefined,
@ -126,6 +146,8 @@ onMounted(loadData)
</script> </script>
<style scoped> <style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.responsive-toolbar { .responsive-toolbar {
display: flex; display: flex;
gap: 12px; gap: 12px;

查看文件

@ -2,6 +2,22 @@
<div> <div>
<h2 style="margin-bottom: 24px">漏斗分析</h2> <h2 style="margin-bottom: 24px">漏斗分析</h2>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<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" />
<template v-else>
<el-card style="margin-bottom: 16px"> <el-card style="margin-bottom: 16px">
<template #header>配置漏斗步骤</template> <template #header>配置漏斗步骤</template>
<div class="funnel-builder"> <div class="funnel-builder">
@ -37,12 +53,16 @@
</div> </div>
</div> </div>
</el-card> </el-card>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { bugCollectApi, type BugCollectFunnelStep } from '@/api/bugcollect' import { bugCollectApi, type BugCollectFunnelStep } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const steps = ref(['', '']) const steps = ref(['', ''])
const funnelData = ref<BugCollectFunnelStep[]>([]) const funnelData = ref<BugCollectFunnelStep[]>([])
@ -66,7 +86,7 @@ async function analyze() {
if (validSteps.length < 2) return if (validSteps.length < 2) return
loading.value = true loading.value = true
try { try {
const res = await bugCollectApi.funnel(validSteps) const res = await bugCollectApi.funnel(appKey.value, validSteps)
funnelData.value = res.data.data funnelData.value = res.data.data
} catch { } catch {
} finally { } finally {
@ -76,6 +96,8 @@ async function analyze() {
</script> </script>
<style scoped> <style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.funnel-builder { .funnel-builder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

查看文件

@ -2,6 +2,22 @@
<div> <div>
<h2 style="margin-bottom: 24px">错误列表</h2> <h2 style="margin-bottom: 24px">错误列表</h2>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<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" />
<template v-else>
<el-card shadow="never"> <el-card shadow="never">
<div class="toolbar responsive-toolbar"> <div class="toolbar responsive-toolbar">
<el-select v-model="filters.type" placeholder="错误类型" style="width: 150px" clearable @change="loadData"> <el-select v-model="filters.type" placeholder="错误类型" style="width: 150px" clearable @change="loadData">
@ -66,12 +82,16 @@
@size-change="handleSizeChange" @size-change="handleSizeChange"
/> />
</el-card> </el-card>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectIssue } from '@/api/bugcollect' import { bugCollectApi, type BugCollectIssue } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const issues = ref<BugCollectIssue[]>([]) const issues = ref<BugCollectIssue[]>([])
const loading = ref(false) const loading = ref(false)
@ -103,7 +123,7 @@ function formatTime(ts: string) {
async function loadData() { async function loadData() {
loading.value = true loading.value = true
try { try {
const res = await bugCollectApi.issues({ const res = await bugCollectApi.issues(appKey.value, {
type: filters.value.type || undefined, type: filters.value.type || undefined,
platform: filters.value.platform || undefined, platform: filters.value.platform || undefined,
startDate: filters.value.dateRange?.[0] || undefined, startDate: filters.value.dateRange?.[0] || undefined,
@ -135,6 +155,8 @@ onMounted(loadData)
</script> </script>
<style scoped> <style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.responsive-toolbar { .responsive-toolbar {
display: flex; display: flex;
gap: 12px; gap: 12px;

查看文件

@ -2,6 +2,22 @@
<div> <div>
<h2 style="margin-bottom: 24px">Bug 概览</h2> <h2 style="margin-bottom: 24px">Bug 概览</h2>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<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" />
<template v-else>
<!-- Stats Cards --> <!-- Stats Cards -->
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="6"> <el-col :span="6">
@ -77,6 +93,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</template>
</div> </div>
</template> </template>
@ -84,6 +101,9 @@
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { bugCollectApi, type BugCollectOverview } from '@/api/bugcollect' import { bugCollectApi, type BugCollectOverview } from '@/api/bugcollect'
import { Warning, Plus, User } from '@element-plus/icons-vue' import { Warning, Plus, User } from '@element-plus/icons-vue'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const overview = ref<BugCollectOverview>({ const overview = ref<BugCollectOverview>({
totalIssues: 0, totalIssues: 0,
@ -115,13 +135,15 @@ function issueTypeTag(type: string) {
onMounted(async () => { onMounted(async () => {
try { try {
const res = await bugCollectApi.overview() const res = await bugCollectApi.overview(appKey.value)
overview.value = res.data.data overview.value = res.data.data
} catch {} } catch {}
}) })
</script> </script>
<style scoped> <style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.trend-chart { .trend-chart {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;

查看文件

@ -2,6 +2,22 @@
<div> <div>
<h2 style="margin-bottom: 24px">高频错误排行</h2> <h2 style="margin-bottom: 24px">高频错误排行</h2>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<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" />
<template v-else>
<el-card shadow="never"> <el-card shadow="never">
<el-table :data="rankings" v-loading="loading" border stripe> <el-table :data="rankings" v-loading="loading" border stripe>
<el-table-column type="index" label="#" width="60" /> <el-table-column type="index" label="#" width="60" />
@ -27,12 +43,16 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectIssueRanking } from '@/api/bugcollect' import { bugCollectApi, type BugCollectIssueRanking } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const rankings = ref<BugCollectIssueRanking[]>([]) const rankings = ref<BugCollectIssueRanking[]>([])
const loading = ref(false) const loading = ref(false)
@ -55,7 +75,7 @@ function formatTime(ts: string) {
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true
try { try {
const res = await bugCollectApi.frequencyRanking() const res = await bugCollectApi.frequencyRanking(appKey.value)
rankings.value = res.data.data rankings.value = res.data.data
} catch { } catch {
} finally { } finally {
@ -65,6 +85,8 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.time-text { .time-text {
font-size: 13px; font-size: 13px;
color: #666; color: #666;

查看文件

@ -2,6 +2,22 @@
<div> <div>
<h2 style="margin-bottom: 24px">高危排行</h2> <h2 style="margin-bottom: 24px">高危排行</h2>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<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" />
<template v-else>
<el-card shadow="never"> <el-card shadow="never">
<el-table :data="rankings" v-loading="loading" border stripe> <el-table :data="rankings" v-loading="loading" border stripe>
<el-table-column type="index" label="#" width="60" /> <el-table-column type="index" label="#" width="60" />
@ -32,12 +48,16 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { bugCollectApi, type BugCollectIssueRanking } from '@/api/bugcollect' import { bugCollectApi, type BugCollectIssueRanking } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const rankings = ref<BugCollectIssueRanking[]>([]) const rankings = ref<BugCollectIssueRanking[]>([])
const loading = ref(false) const loading = ref(false)
@ -67,7 +87,7 @@ function formatTime(ts: string) {
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true
try { try {
const res = await bugCollectApi.riskRanking() const res = await bugCollectApi.riskRanking(appKey.value)
rankings.value = res.data.data rankings.value = res.data.data
} catch { } catch {
} finally { } finally {
@ -77,6 +97,8 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.time-text { .time-text {
font-size: 13px; font-size: 13px;
color: #666; color: #666;

查看文件

@ -5,6 +5,22 @@
<el-button type="primary" @click="openDialog()">新增 Webhook</el-button> <el-button type="primary" @click="openDialog()">新增 Webhook</el-button>
</div> </div>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<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" />
<template v-else>
<el-card shadow="never"> <el-card shadow="never">
<el-table :data="webhooks" v-loading="loading" border stripe> <el-table :data="webhooks" v-loading="loading" border stripe>
<el-table-column prop="url" label="回调地址" min-width="300" show-overflow-tooltip /> <el-table-column prop="url" label="回调地址" min-width="300" show-overflow-tooltip />
@ -33,6 +49,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</template>
<!-- Dialog --> <!-- Dialog -->
<el-dialog <el-dialog
@ -73,6 +90,9 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { bugCollectApi, type BugCollectWebhook } from '@/api/bugcollect' import { bugCollectApi, type BugCollectWebhook } from '@/api/bugcollect'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
const webhooks = ref<BugCollectWebhook[]>([]) const webhooks = ref<BugCollectWebhook[]>([])
const loading = ref(false) const loading = ref(false)
@ -90,7 +110,7 @@ const form = ref({
async function loadWebhooks() { async function loadWebhooks() {
loading.value = true loading.value = true
try { try {
const res = await bugCollectApi.webhooks.list() const res = await bugCollectApi.webhooks.list(appKey.value)
webhooks.value = res.data.data webhooks.value = res.data.data
} catch { } catch {
} finally { } finally {
@ -122,7 +142,7 @@ async function handleSave() {
await bugCollectApi.webhooks.update(editingId.value, form.value) await bugCollectApi.webhooks.update(editingId.value, form.value)
ElMessage.success('更新成功') ElMessage.success('更新成功')
} else { } else {
await bugCollectApi.webhooks.create(form.value) await bugCollectApi.webhooks.create(appKey.value, form.value)
ElMessage.success('创建成功') ElMessage.success('创建成功')
} }
dialogVisible.value = false dialogVisible.value = false
@ -145,6 +165,8 @@ onMounted(loadWebhooks)
</script> </script>
<style scoped> <style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.toolbar-space-between { .toolbar-space-between {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;