forked from gzt/A8000
23 changed files with 1094 additions and 225 deletions
-
4components.d.ts
-
1package.json
-
44public/json/mockErrorData.json
-
19src/App.vue
-
8src/assets/empty-stack.svg
-
1src/assets/fatal.svg
-
3src/assets/notify.svg
-
6src/assets/stack-icon.svg
-
345src/components/dialogs/ErrorModal.vue
-
233src/components/dialogs/StackInfoModal.vue
-
2src/components/dialogs/index.ts
-
19src/eventBus.ts
-
6src/main.ts
-
53src/pages/Index/Index.vue
-
2src/pages/Index/Regular.vue
-
131src/pages/Index/Regular/Consumables.vue
-
6src/pages/Index/Regular/Emergency.vue
-
386src/pages/Index/Regular/Running.vue
-
5src/pages/Index/Regular/TestTube.vue
-
6src/pages/Index/components/Running/LittleBufferDisplay.vue
-
6src/pages/Index/components/Running/PlateDisplay.vue
-
3src/pages/Login/Login.vue
-
30src/utils/axios.ts
@ -0,0 +1,44 @@ |
|||
{ |
|||
"mockErrorData": { |
|||
"ecode": "CODEERROR", |
|||
"dataType": "ZAppPromopt", |
|||
"data": { |
|||
"type": "Error", |
|||
"info": "代码错误", |
|||
"detailInfos": [ |
|||
{ |
|||
"name": "错误类型", |
|||
"description": "AECodeError" |
|||
}, |
|||
{ |
|||
"name": "错误位置", |
|||
"description": "ApiRetTestControler.java:37" |
|||
} |
|||
], |
|||
"stackInfo": { |
|||
"stackTraceElements": [ |
|||
"java.base/java.lang.Thread.getStackTrace(Thread.java:2450)", |
|||
"a8k.type.ecode.AECodeError.<init>(AECodeError.java:15)", |
|||
"a8k.controler.api.v1.app.assistant.ApiRetTestControler.getAppCodeError(ApiRetTestControler.java:37)", |
|||
"java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)", |
|||
"java.base/java.lang.reflect.Method.invoke(Method.java:580)", |
|||
"org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255)", |
|||
"org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188)", |
|||
"org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)", |
|||
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926)", |
|||
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831)", |
|||
"org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)", |
|||
"org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)", |
|||
"org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)", |
|||
"org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)", |
|||
"org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)", |
|||
"jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)", |
|||
"org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)", |
|||
"jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)", |
|||
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)", |
|||
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)" |
|||
] |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<rect x="20" y="20" width="80" height="80" rx="8" stroke="#E5E7EB" stroke-width="2"/> |
|||
<path d="M35 45H85" stroke="#E5E7EB" stroke-width="2" stroke-linecap="round"/> |
|||
<path d="M35 60H85" stroke="#E5E7EB" stroke-width="2" stroke-linecap="round"/> |
|||
<path d="M35 75H85" stroke="#E5E7EB" stroke-width="2" stroke-linecap="round"/> |
|||
<circle cx="60" cy="60" r="35" stroke="#E5E7EB" stroke-width="2" stroke-dasharray="4 4"/> |
|||
<path d="M50 60L57 67L70 54" stroke="#E5E7EB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
|||
</svg> |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="191" height="191" viewBox="0 0 191 191"><defs><clipPath id="master_svg0_2_98"><rect x="0" y="0" width="191" height="191" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_2_98)"><g><ellipse cx="95.5" cy="95.5" rx="94.5" ry="94.5" fill="#C84141" fill-opacity="1"/></g><g><path d="M56.258359999999996,143C54.14496,143,52.03121,142.1935,50.41863,140.5814C47.19379,137.3561,47.19379,132.1274,50.41863,128.9023L128.9022,50.41895C132.12709999999998,47.193684,137.356,47.193684,140.5814,50.41895C143.8062,53.64378,143.8062,58.8729,140.5814,62.098L62.0978,140.5814C60.4852,142.19389999999999,58.371700000000004,143,56.25803,143L56.258359999999996,143Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M134.7421,142.9996C132.6286,142.9996,130.515,142.19310000000002,128.9023,140.58089999999999L50.41863,62.097899999999996C47.193789,58.873,47.193789,53.64371,50.41863,50.41878C53.64336,47.193959,58.8726,47.19352,62.0977,50.41878L140.5814,128.9017C143.8062,132.127,143.8062,137.3557,140.5814,140.5808C138.9691,142.1935,136.85559999999998,143,134.7419,143L134.7421,142.9996Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg> |
@ -0,0 +1,3 @@ |
|||
<svg width="75" height="75" viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M37.5 0C16.7893 0 0 16.7893 0 37.5C0 58.2107 16.7893 75 37.5 75C58.2107 75 75 58.2107 75 37.5C75 16.7893 58.2107 0 37.5 0ZM37.5 62.3078C34.8599 62.3078 32.7196 60.1675 32.7196 57.5274C32.7196 54.8872 34.8599 52.7469 37.5 52.7469C40.1401 52.7469 42.2804 54.8872 42.2804 57.5274C42.2804 60.1675 40.1401 62.3078 37.5 62.3078ZM42.2804 42.0247C42.2804 44.6649 40.1401 46.8051 37.5 46.8051C34.8599 46.8051 32.7196 44.6649 32.7196 42.0247V21.6222C32.7196 18.9821 34.8599 16.8419 37.5 16.8419C40.1401 16.8419 42.2804 18.9821 42.2804 21.6223V42.0247Z" fill="#1890ff"/> |
|||
</svg> |
@ -0,0 +1,6 @@ |
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M20 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V6C22 4.89543 21.1046 4 20 4Z" stroke="#1890ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
|||
<path d="M6 8H18" stroke="#1890ff" stroke-width="2" stroke-linecap="round"/> |
|||
<path d="M6 12H18" stroke="#1890ff" stroke-width="2" stroke-linecap="round"/> |
|||
<path d="M6 16H18" stroke="#1890ff" stroke-width="2" stroke-linecap="round"/> |
|||
</svg> |
@ -0,0 +1,345 @@ |
|||
<template> |
|||
<teleport to="body"> |
|||
<div v-if="showErrorStack" class="error-stack-overlay"> |
|||
<div class="error-stack-container" :class="typeClass"> |
|||
<div class="error-stack-header"> |
|||
<div class="icon-wrapper"> |
|||
<img :src="iconMap[type]" alt="icon" /> |
|||
</div> |
|||
<span class="error-stack-title">{{ typeText }}</span> |
|||
</div> |
|||
<div class="error-stack-content"> |
|||
<p class="error-stack-message">{{ info || "未知错误" }}</p> |
|||
<div class="error-stack-details-wrapper"> |
|||
<div class="details-header" @click="toggleDetails"> |
|||
<span class="details-title">详细信息</span> |
|||
<span class="details-arrow" :class="{ 'is-expanded': isShow }"> |
|||
<svg viewBox="0 0 24 24" width="24" height="24"> |
|||
<path d="M7 10l5 5 5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /> |
|||
</svg> |
|||
</span> |
|||
</div> |
|||
<div class="error-stack-details" v-show="isShow"> |
|||
<template v-if="detailInfos.length"> |
|||
<div v-for="(detail, index) in detailInfos" :key="index" class="detail-item"> |
|||
<span class="detail-label">{{ detail.name }}</span> |
|||
<span class="detail-value">{{ detail.description }}</span> |
|||
</div> |
|||
</template> |
|||
<div v-else class="empty-details"> |
|||
<svg class="empty-icon" viewBox="0 0 24 24" width="48" height="48"> |
|||
<path |
|||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" |
|||
fill="currentColor" /> |
|||
</svg> |
|||
<span>暂无详细信息</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div v-if="stackInfo" class="stack-info-trigger" @click="showStackInfo"> |
|||
<svg viewBox="0 0 24 24" width="20" height="20"> |
|||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" fill="none" stroke="currentColor" |
|||
stroke-width="2" stroke-linecap="round" /> |
|||
</svg> |
|||
<span>查看栈错误信息</span> |
|||
</div> |
|||
</div> |
|||
<button :class="['error-stack-close', buttonClass]" @click="closeErrorStack">关闭</button> |
|||
</div> |
|||
</div> |
|||
</teleport> |
|||
</template> |
|||
|
|||
<style scoped lang="less"> |
|||
.error-stack-overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
backdrop-filter: blur(4px); |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
z-index: 9999; |
|||
} |
|||
|
|||
.error-stack-container { |
|||
width: 90%; |
|||
max-width: 800px; |
|||
background: #fff; |
|||
border-radius: 16px; |
|||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); |
|||
overflow: hidden; |
|||
animation: modalSlide 0.3s ease; |
|||
|
|||
&.error-fatal { |
|||
--theme-color: #ff4d4f; |
|||
} |
|||
|
|||
&.error-error { |
|||
--theme-color: #ff7875; |
|||
} |
|||
|
|||
&.error-warn { |
|||
--theme-color: #faad14; |
|||
} |
|||
|
|||
&.error-notify { |
|||
--theme-color: #1890ff; |
|||
} |
|||
} |
|||
|
|||
.error-stack-header { |
|||
padding: 24px; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16px; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
|
|||
.icon-wrapper { |
|||
width: 48px; |
|||
height: 48px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
.error-stack-title { |
|||
font-size: 32px; |
|||
font-weight: 600; |
|||
color: var(--theme-color); |
|||
} |
|||
} |
|||
|
|||
.error-stack-content { |
|||
padding: 24px; |
|||
|
|||
.error-stack-message { |
|||
font-size: 28px; |
|||
line-height: 1.5; |
|||
margin: 0 0 24px; |
|||
color: #1f2937; |
|||
} |
|||
} |
|||
|
|||
.error-stack-details-wrapper { |
|||
background: #f8f9fa; |
|||
border-radius: 12px; |
|||
overflow: hidden; |
|||
margin-bottom: 24px; |
|||
|
|||
.details-header { |
|||
padding: 16px 20px; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
transition: background-color 0.3s; |
|||
|
|||
&:hover { |
|||
background-color: #f0f2f5; |
|||
} |
|||
|
|||
.details-title { |
|||
font-size: 26px; |
|||
font-weight: 500; |
|||
color: #374151; |
|||
} |
|||
|
|||
.details-arrow { |
|||
color: #9ca3af; |
|||
transition: transform 0.3s; |
|||
|
|||
&.is-expanded { |
|||
transform: rotate(180deg); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.error-stack-details { |
|||
padding: 0 20px 20px; |
|||
|
|||
.detail-item { |
|||
display: flex; |
|||
padding: 12px 0; |
|||
border-bottom: 1px solid #e5e7eb; |
|||
|
|||
&:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.detail-label { |
|||
flex: 0 0 120px; |
|||
font-size: 26px; |
|||
color: #6b7280; |
|||
} |
|||
|
|||
.detail-value { |
|||
flex: 1; |
|||
font-size: 26px; |
|||
color: #111827; |
|||
} |
|||
} |
|||
|
|||
.empty-details { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
padding: 32px 0; |
|||
color: #9ca3af; |
|||
gap: 12px; |
|||
|
|||
.empty-icon { |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
span { |
|||
font-size: 26px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.stack-info-trigger { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
padding: 12px 20px; |
|||
background: #f8f9fa; |
|||
border-radius: 8px; |
|||
cursor: pointer; |
|||
color: var(--theme-color); |
|||
transition: all 0.3s; |
|||
|
|||
&:hover { |
|||
background: rgba(var(--theme-color), 0.1); |
|||
transform: translateX(4px); |
|||
} |
|||
|
|||
span { |
|||
font-size: 26px; |
|||
} |
|||
} |
|||
|
|||
.error-stack-close { |
|||
width: calc(100% - 48px); |
|||
margin: 0 24px 24px; |
|||
height: 80px; |
|||
border: none; |
|||
border-radius: 12px; |
|||
font-size: 26px; |
|||
font-weight: 500; |
|||
cursor: pointer; |
|||
transition: all 0.3s; |
|||
background: var(--theme-color); |
|||
color: white; |
|||
|
|||
&:hover { |
|||
opacity: 0.9; |
|||
transform: translateY(-2px); |
|||
} |
|||
} |
|||
|
|||
@keyframes modalSlide { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(-30px); |
|||
} |
|||
|
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
</style> |
|||
|
|||
<script setup lang="ts"> |
|||
import { eventBus } from '../../eventBus' |
|||
import { onMounted, onUnmounted, ref, computed } from 'vue' |
|||
import type { ErrorModalData, ErrorDetail } from '../../eventBus' |
|||
|
|||
let ErrorIcon = new URL('@/assets/Warn.svg', import.meta.url).href |
|||
let WarnIcon = new URL('@/assets/update-pin-icon.svg', import.meta.url).href |
|||
let NotifyIcon = new URL('@/assets/notify.svg', import.meta.url).href |
|||
let FatalIcon = new URL('@/assets/fatal.svg', import.meta.url).href |
|||
const showErrorStack = ref(false) |
|||
const info = ref('') |
|||
const type = ref<'Fatal' | 'Error' | 'Warn' | 'Notify'>('Notify') |
|||
const detailInfos = ref<ErrorDetail[]>([]) |
|||
const isShow = ref(true) |
|||
const stackInfo = ref('') |
|||
|
|||
// 类型映射 |
|||
const TYPE_MAP = { |
|||
Fatal: '严重错误', |
|||
Error: '错误', |
|||
Warn: '警告', |
|||
Notify: '提示', |
|||
} as const |
|||
|
|||
// 计算属性:类型文本 |
|||
const typeText = computed(() => { |
|||
return TYPE_MAP[type.value as keyof typeof TYPE_MAP] || '未知类型' |
|||
}) |
|||
|
|||
// 计算属性:类型样式 |
|||
const typeClass = computed(() => { |
|||
return { |
|||
'error-fatal': type.value === 'Fatal', |
|||
'error-error': type.value === 'Error', |
|||
'error-warn': type.value === 'Warn', |
|||
'error-notify': type.value === 'Notify' |
|||
} |
|||
}) |
|||
//计算属性 按钮样式 |
|||
const buttonClass = computed(() => { |
|||
return { |
|||
'error-fatal-button': type.value === 'Fatal', |
|||
'error-error-button': type.value === 'Error', |
|||
'error-warn-button': type.value === 'Warn', |
|||
'error-notify-button': type.value === 'Notify' |
|||
} |
|||
}) |
|||
// 计算属性:图标映射 |
|||
const iconMap = computed(() => { |
|||
return { |
|||
'Fatal': FatalIcon, |
|||
'Error': ErrorIcon, |
|||
'Warn': WarnIcon, |
|||
'Notify': NotifyIcon |
|||
} |
|||
}) |
|||
|
|||
const toggleDetails = () => { |
|||
isShow.value = !isShow.value |
|||
} |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-error-modal', handleErrorModal) |
|||
console.log('onMounted') |
|||
}) |
|||
onUnmounted(() => { |
|||
eventBus.off('show-error-modal', handleErrorModal) |
|||
}) |
|||
const handleErrorModal = (data: ErrorModalData) => { |
|||
info.value = data.info |
|||
type.value = data.type |
|||
detailInfos.value = data.detailInfos || [] |
|||
stackInfo.value = data.stackInfo || '' |
|||
showErrorStack.value = true |
|||
} |
|||
const closeErrorStack = () => { |
|||
showErrorStack.value = false |
|||
} |
|||
const showStackInfo = () => { |
|||
eventBus.emit('show-stack-modal', stackInfo.value as unknown as null | undefined) |
|||
|
|||
} |
|||
</script> |
@ -0,0 +1,233 @@ |
|||
<template> |
|||
<teleport to="body"> |
|||
<div class="stack-info-modal" v-if="showStackInfo"> |
|||
<div class="stack-info-modal-content"> |
|||
<div class="stack-info-modal-header"> |
|||
<div class="header-left"> |
|||
<img src="@/assets/stack-icon.svg" alt="stack" class="stack-icon" /> |
|||
<h3>完整错误信息</h3> |
|||
</div> |
|||
<div class="header-right"> |
|||
<span class="close-btn" @click="closeStackInfoModal">关闭</span> |
|||
</div> |
|||
</div> |
|||
<div class="stack-info-modal-body"> |
|||
<div class="stack-trace" |
|||
v-if="Array.isArray(stackInfo?.stackTraceElements) && stackInfo.stackTraceElements.length"> |
|||
<div v-for="(trace, index) in stackInfo.stackTraceElements" :key="index" class="stack-trace-item"> |
|||
<span class="line-number">{{ index + 1 }}</span> |
|||
<span class="stack-line">{{ trace }}</span> |
|||
</div> |
|||
</div> |
|||
<div v-else class="empty-state"> |
|||
<img src="@/assets/empty-stack.svg" alt="empty" class="empty-icon" /> |
|||
<p>暂无栈错误信息</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</teleport> |
|||
</template> |
|||
|
|||
<style scoped lang="less"> |
|||
.stack-info-modal { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-color: #fff; |
|||
z-index: 10000; |
|||
} |
|||
|
|||
.stack-info-modal-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.stack-info-modal-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 0 40px; |
|||
background: #f8f9fa; |
|||
border-bottom: 1px solid #eee; |
|||
height: 50px; |
|||
|
|||
.header-left { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16px; |
|||
|
|||
.stack-icon { |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
|
|||
h3 { |
|||
margin: 0; |
|||
font-size: 36px; |
|||
color: #1f2937; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
|
|||
.header-right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 24px; |
|||
|
|||
.copy-btn, |
|||
.close-btn { |
|||
cursor: pointer; |
|||
font-size: 28px; |
|||
padding: 12px 24px; |
|||
color: #1890ff; |
|||
border-radius: 8px; |
|||
transition: all 0.3s ease; |
|||
|
|||
&:hover { |
|||
background: rgba(24, 144, 255, 0.1); |
|||
color: #096dd9; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.stack-info-modal-body { |
|||
flex: 1; |
|||
padding: 0; |
|||
overflow-y: auto; |
|||
background: #f8f9fa; |
|||
|
|||
&::-webkit-scrollbar { |
|||
width: 12px; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-track { |
|||
background: #f1f1f1; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-thumb { |
|||
background: #ccc; |
|||
border-radius: 6px; |
|||
border: 3px solid #f1f1f1; |
|||
|
|||
&:hover { |
|||
background: #999; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.stack-trace { |
|||
padding: 20px 40px; |
|||
|
|||
.stack-trace-header { |
|||
margin-bottom: 20px; |
|||
|
|||
h4 { |
|||
font-size: 28px; |
|||
color: #374151; |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
.stack-trace-item { |
|||
display: flex; |
|||
background: #fff; |
|||
border-radius: 8px; |
|||
transition: all 0.3s ease; |
|||
|
|||
&:hover { |
|||
background: #f0f2f5; |
|||
transform: translateX(4px); |
|||
} |
|||
|
|||
.line-number { |
|||
flex-shrink: 0; |
|||
width: 60px; |
|||
color: #6b7280; |
|||
font-family: 'Fira Code', monospace; |
|||
font-size: 28px; |
|||
text-align: right; |
|||
margin-right: 24px; |
|||
opacity: 0.7; |
|||
} |
|||
|
|||
.stack-line { |
|||
color: #374151; |
|||
font-family: 'Fira Code', monospace; |
|||
font-size: 24px; |
|||
word-break: break-all; |
|||
line-height: 1.5; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
height: 100%; |
|||
color: #6b7280; |
|||
|
|||
.empty-icon { |
|||
width: 160px; |
|||
height: 160px; |
|||
margin-bottom: 24px; |
|||
opacity: 0.5; |
|||
} |
|||
|
|||
p { |
|||
font-size: 32px; |
|||
margin: 0; |
|||
} |
|||
} |
|||
</style> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted, onUnmounted } from 'vue' |
|||
import { eventBus } from '../../eventBus' |
|||
|
|||
interface StackTrace { |
|||
stackTraceElements?: string[] |
|||
[key: string]: any |
|||
} |
|||
|
|||
const showStackInfo = ref(false) |
|||
const stackInfo = ref<StackTrace | null>(null) |
|||
|
|||
const closeStackInfoModal = () => { |
|||
showStackInfo.value = false |
|||
} |
|||
|
|||
const copyStackInfo = () => { |
|||
if (stackInfo.value?.stackTraceElements) { |
|||
const text = stackInfo.value.stackTraceElements.join('\n') |
|||
navigator.clipboard.writeText(text) |
|||
.then(() => { |
|||
// 可以添加一个复制成功的提示 |
|||
alert('错误信息已复制到剪贴板') |
|||
}) |
|||
.catch(err => { |
|||
console.error('复制失败:', err) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-stack-modal', handleStackModal) |
|||
}) |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-stack-modal', handleStackModal) |
|||
}) |
|||
|
|||
const handleStackModal = (data: any) => { |
|||
stackInfo.value = data |
|||
showStackInfo.value = true |
|||
} |
|||
</script> |
@ -0,0 +1,2 @@ |
|||
export { default as ErrorModal } from './ErrorModal.vue' |
|||
export { default as StackInfoModal } from './StackInfoModal.vue' |
@ -1,7 +1,24 @@ |
|||
// src/eventBus.js
|
|||
// src/eventBus.ts
|
|||
import mitt from 'mitt' |
|||
|
|||
export type ErrorDetail = { |
|||
name: string |
|||
description: string |
|||
} |
|||
|
|||
export type ErrorModalData = { |
|||
type: 'Notify' | 'Warn' | 'Error' | 'Fatal' |
|||
info: string |
|||
detailInfos?: ErrorDetail[] |
|||
ecode?: string |
|||
stackInfo?: null |
|||
} |
|||
|
|||
type Events = { |
|||
confirm: { value: number; index: number } |
|||
'show-error-modal': ErrorModalData |
|||
'show-stack-modal': ErrorModalData['stackInfo'] | null | undefined |
|||
// 其他事件类型
|
|||
} |
|||
|
|||
export const eventBus = mitt<Events>() |
Write
Preview
Loading…
Cancel
Save
Reference in new issue