2026-06-16 18:30:01 +08:00
|
|
|
<template>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 style="margin-bottom: 24px">错误列表</h2>
|
|
|
|
|
|
2026-06-17 05:00:51 +08:00
|
|
|
<!-- 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>
|
2026-06-17 05:25:54 +08:00
|
|
|
<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>
|
2026-06-17 05:00:51 +08:00
|
|
|
<template v-else>
|
|
|
|
|
|
2026-06-16 18:30:01 +08:00
|
|
|
<el-card shadow="never">
|
|
|
|
|
<div class="toolbar responsive-toolbar">
|
|
|
|
|
<el-select v-model="filters.type" placeholder="错误类型" style="width: 150px" clearable @change="loadData">
|
|
|
|
|
<el-option label="全部类型" value="" />
|
|
|
|
|
<el-option label="CRASH" value="CRASH" />
|
|
|
|
|
<el-option label="ERROR" value="ERROR" />
|
|
|
|
|
<el-option label="ANR" value="ANR" />
|
|
|
|
|
<el-option label="WARNING" value="WARNING" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-select v-model="filters.platform" placeholder="平台" style="width: 150px" clearable @change="loadData">
|
|
|
|
|
<el-option label="全部平台" value="" />
|
|
|
|
|
<el-option label="Android" value="Android" />
|
|
|
|
|
<el-option label="iOS" value="iOS" />
|
|
|
|
|
<el-option label="HarmonyOS" value="HarmonyOS" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="filters.dateRange"
|
|
|
|
|
type="daterange"
|
|
|
|
|
range-separator="至"
|
|
|
|
|
start-placeholder="开始日期"
|
|
|
|
|
end-placeholder="结束日期"
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
style="width: 280px"
|
|
|
|
|
@change="loadData"
|
|
|
|
|
/>
|
|
|
|
|
<el-button :loading="loading" @click="loadData">刷新</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-wrap">
|
|
|
|
|
<el-table :data="issues" v-loading="loading" border stripe>
|
|
|
|
|
<el-table-column prop="title" label="标题" min-width="300" show-overflow-tooltip>
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button link type="primary" @click="$router.push(`/bugcollect/issues/${row.id}`)">
|
|
|
|
|
{{ row.title }}
|
|
|
|
|
</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="type" label="类型" width="110">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag size="small" :type="issueTypeTag(row.type)">{{ row.type }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="count" label="次数" width="100" sortable />
|
2026-06-17 10:21:00 +08:00
|
|
|
<el-table-column prop="isResolved" label="状态" width="90"><template #default="{ row }"><el-tag size="small" :type="row.isResolved ? 'success' : 'danger'">{{ row.isResolved ? '已解决' : '未解决' }}</el-tag></template></el-table-column>
|
2026-06-16 18:30:01 +08:00
|
|
|
<el-table-column prop="platform" label="平台" width="110" />
|
|
|
|
|
<el-table-column prop="lastSeenAt" label="最后出现" width="170" sortable>
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span class="time-text">{{ formatTime(row.lastSeenAt) }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
style="margin-top: 16px"
|
|
|
|
|
layout="total, sizes, prev, pager, next"
|
|
|
|
|
:total="total"
|
|
|
|
|
:page-size="pageSize"
|
|
|
|
|
:current-page="currentPage"
|
|
|
|
|
:page-sizes="[20, 50, 100]"
|
|
|
|
|
@current-change="handlePageChange"
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
/>
|
|
|
|
|
</el-card>
|
2026-06-17 05:00:51 +08:00
|
|
|
</template>
|
2026-06-17 05:25:54 +08:00
|
|
|
<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>
|
2026-06-16 18:30:01 +08:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-06-17 10:07:32 +08:00
|
|
|
import { ref, onMounted, watch } from 'vue'
|
2026-06-16 18:30:01 +08:00
|
|
|
import { bugCollectApi, type BugCollectIssue } from '@/api/bugcollect'
|
2026-06-17 05:00:51 +08:00
|
|
|
import { useBugCollectApp } from '@/composables/useBugCollectApp'
|
|
|
|
|
|
2026-06-17 05:25:54 +08:00
|
|
|
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
|
2026-06-16 18:30:01 +08:00
|
|
|
|
|
|
|
|
const issues = ref<BugCollectIssue[]>([])
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
const currentPage = ref(1)
|
|
|
|
|
const pageSize = ref(20)
|
|
|
|
|
|
|
|
|
|
const filters = ref({
|
|
|
|
|
type: '',
|
|
|
|
|
platform: '',
|
|
|
|
|
dateRange: null as [string, string] | null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function issueTypeTag(type: string) {
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
CRASH: 'danger',
|
|
|
|
|
ERROR: 'warning',
|
|
|
|
|
ANR: 'danger',
|
|
|
|
|
WARNING: '',
|
|
|
|
|
}
|
|
|
|
|
return (map[type] ?? '') as '' | 'success' | 'warning' | 'info' | 'danger'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatTime(ts: string) {
|
|
|
|
|
if (!ts) return '-'
|
|
|
|
|
return new Date(ts).toLocaleString('zh-CN')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadData() {
|
2026-06-17 10:07:32 +08:00
|
|
|
if (!appKey.value || gateStatus.value !== 'enabled') return
|
2026-06-16 18:30:01 +08:00
|
|
|
loading.value = true
|
|
|
|
|
try {
|
2026-06-17 05:00:51 +08:00
|
|
|
const res = await bugCollectApi.issues(appKey.value, {
|
2026-06-16 18:30:01 +08:00
|
|
|
type: filters.value.type || undefined,
|
|
|
|
|
platform: filters.value.platform || undefined,
|
|
|
|
|
startDate: filters.value.dateRange?.[0] || undefined,
|
|
|
|
|
endDate: filters.value.dateRange?.[1] || undefined,
|
|
|
|
|
page: currentPage.value - 1,
|
|
|
|
|
size: pageSize.value,
|
|
|
|
|
})
|
|
|
|
|
const data = res.data.data
|
2026-06-17 10:21:00 +08:00
|
|
|
issues.value = data.items ?? []
|
|
|
|
|
total.value = data.total ?? 0
|
2026-06-16 18:30:01 +08:00
|
|
|
} catch {
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePageChange(page: number) {
|
|
|
|
|
currentPage.value = page
|
|
|
|
|
loadData()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSizeChange(size: number) {
|
|
|
|
|
pageSize.value = size
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
loadData()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 10:07:32 +08:00
|
|
|
watch(gateStatus, (s) => { if (s === 'enabled') loadData() })
|
2026-06-16 18:30:01 +08:00
|
|
|
onMounted(loadData)
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-06-17 05:00:51 +08:00
|
|
|
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
|
|
|
|
|
.selector-label { font-size:14px; color:#606266; }
|
2026-06-16 18:30:01 +08:00
|
|
|
.responsive-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
.table-wrap {
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
|
|
|
|
.time-text {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #666;
|
|
|
|
|
}
|
|
|
|
|
</style>
|