Skip to content

Commit 2c7b732

Browse files
committed
feat: 全局错误捕获管道
未处理的 Promise rejection 与 Vue 运行时错误统一弹窗, 解析 axios 响应与异常详情,支持复制与一键反馈到 GitHub
1 parent 9e6a308 commit 2c7b732

7 files changed

Lines changed: 132 additions & 1 deletion

File tree

ChuChartManager/Front/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Sidebar from '@/components/Sidebar.vue'
55
import StatusBar from '@/components/StatusBar.vue'
66
import StartupErrorDialog from '@/components/StartupErrorDialog'
77
import ChangelogModal from '@/components/ChangelogModal'
8+
import ErrorDialog from '@/components/ErrorDialog'
89
import MusicList from '@/views/MusicList.vue'
910
import Course from '@/views/Course/index'
1011
import ResourceManager from '@/views/ResourceManager/index'
@@ -50,6 +51,7 @@ const handleRefresh = () => {
5051
<GlobalElementsContainer />
5152
<StartupErrorDialog />
5253
<ChangelogModal :ready="ready" />
54+
<ErrorDialog />
5355
<div class="main-layout">
5456
<Sidebar v-model:active="sidebarActive" @refresh="handleRefresh" />
5557
<div class="main-content">
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { defineComponent, ref } from 'vue'
2+
import { Modal, Button, addToast } from '@munet/ui'
3+
import { useI18n } from 'vue-i18n'
4+
import { errorDialogShow, errorMessage, errorDetail } from '@/utils/globalCapture'
5+
6+
const REPO_URL = 'https://github.com/MuNET-OSS/ChuChartManager'
7+
8+
export default defineComponent({
9+
setup() {
10+
const { t } = useI18n()
11+
const showDetail = ref(false)
12+
13+
const copy = async () => {
14+
try {
15+
await navigator.clipboard.writeText(`${errorMessage.value}\n\n${errorDetail.value}`)
16+
addToast({ message: t('error.copied'), type: 'success' })
17+
} catch {
18+
addToast({ message: t('error.copyFailed'), type: 'error' })
19+
}
20+
}
21+
22+
const report = () => {
23+
const title = `[Bug] ${errorMessage.value}`.slice(0, 120)
24+
const body = `## 问题描述\n\n\n## 错误信息\n\`\`\`\n${errorDetail.value}\n\`\`\``
25+
const url = `${REPO_URL}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`
26+
window.open(url, '_blank')
27+
}
28+
29+
return () => (
30+
<Modal
31+
width="min(85vw,45em)"
32+
title={t('error.title')}
33+
v-model:show={errorDialogShow.value}
34+
>
35+
{{
36+
default: () => (
37+
<div class="flex flex-col gap-3">
38+
<div class="c-#d33 break-all">{errorMessage.value}</div>
39+
40+
<div
41+
class="text-sm op-60 cursor-pointer flex items-center gap-1"
42+
onClick={() => showDetail.value = !showDetail.value}
43+
>
44+
<div class={showDetail.value ? 'i-mdi-chevron-down' : 'i-mdi-chevron-right'} />
45+
{t('error.detail')}
46+
</div>
47+
{showDetail.value && (
48+
<pre class="text-xs bg-black/5 rd p-2 of-auto max-h-50vh whitespace-pre-wrap break-all m-0">
49+
{errorDetail.value}
50+
</pre>
51+
)}
52+
</div>
53+
),
54+
actions: () => (
55+
<>
56+
<Button onClick={copy}>{t('error.copy')}</Button>
57+
<Button onClick={report}>{t('error.report')}</Button>
58+
</>
59+
),
60+
}}
61+
</Modal>
62+
)
63+
},
64+
})

ChuChartManager/Front/src/locales/en.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ common:
123123
import: Import
124124
loading: Loading...
125125
error: Error
126+
error:
127+
title: An error occurred
128+
detail: Error details
129+
copy: Copy
130+
copied: Copied to clipboard
131+
copyFailed: Copy failed
132+
report: Report on GitHub
126133
music:
127134
sortById: ID
128135
sortByName: Name

ChuChartManager/Front/src/locales/ja.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ common:
123123
import: インポート
124124
loading: 読み込み中…
125125
error: エラー
126+
error:
127+
title: エラーが発生しました
128+
detail: エラー詳細
129+
copy: コピー
130+
copied: クリップボードにコピーしました
131+
copyFailed: コピーに失敗しました
132+
report: GitHub に報告
126133
music:
127134
sortById: ID
128135
sortByName: 名前

ChuChartManager/Front/src/locales/zh.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ common:
123123
import: 导入
124124
loading: 加载中…
125125
error: 错误
126+
error:
127+
title: 发生错误
128+
detail: 错误详情
129+
copy: 复制
130+
copied: 已复制到剪贴板
131+
copyFailed: 复制失败
132+
report: 反馈到 GitHub
126133
music:
127134
sortById: ID
128135
sortByName: 名称

ChuChartManager/Front/src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import App from './App.vue'
77
import './global.sass'
88
import { initThemeDefaults, selectedThemeName, UIThemes } from '@munet/ui'
99
import i18n from '@/locales'
10+
import { globalCapture } from '@/utils/globalCapture'
1011

1112
initThemeDefaults({ hue: 353 })
1213
selectedThemeName.value = UIThemes.DynamicLight
1314

15+
window.addEventListener('unhandledrejection', e => globalCapture(e.reason, 'Unhandled rejection'))
16+
1417
if ((window as any).chrome?.webview) {
1518
(window as any).chrome.webview.addEventListener('message', (e: any) => {
1619
;(globalThis as any).backendUrl = e.data
@@ -20,6 +23,8 @@ if ((window as any).chrome?.webview) {
2023
})
2124
}
2225

23-
createApp(App)
26+
const app = createApp(App)
27+
app.config.errorHandler = err => globalCapture(err, 'Vue error')
28+
app
2429
.use(i18n)
2530
.mount('#app')
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ref } from 'vue'
2+
3+
export const errorDialogShow = ref(false)
4+
export const errorMessage = ref('')
5+
export const errorDetail = ref('')
6+
7+
function extract(error: any): { message: string; detail: string } {
8+
const resp = error?.response
9+
if (resp) {
10+
const data = resp.data
11+
let msg = ''
12+
if (typeof data === 'string') msg = data
13+
else if (data?.title) msg = data.title
14+
else if (data?.error) msg = data.error
15+
else if (data?.detail) msg = data.detail
16+
17+
const method = error.config?.method ? String(error.config.method).toUpperCase() : ''
18+
const detail = [
19+
`${method} ${error.config?.url ?? ''}`.trim(),
20+
`HTTP ${resp.status}`,
21+
typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data ?? ''),
22+
].filter(Boolean).join('\n')
23+
return { message: msg || `HTTP ${resp.status}`, detail }
24+
}
25+
26+
if (error instanceof Error) {
27+
return { message: error.message, detail: error.stack ?? error.message }
28+
}
29+
30+
return { message: String(error), detail: String(error) }
31+
}
32+
33+
export function globalCapture(error: any, context?: string) {
34+
console.error('[globalCapture]', context ?? '', error)
35+
const { message, detail } = extract(error)
36+
errorMessage.value = context ? `${context}: ${message}` : message
37+
errorDetail.value = detail
38+
errorDialogShow.value = true
39+
}

0 commit comments

Comments
 (0)