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' |
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 = { |
type Events = { |
||||
confirm: { value: number; index: number } |
confirm: { value: number; index: number } |
||||
|
'show-error-modal': ErrorModalData |
||||
|
'show-stack-modal': ErrorModalData['stackInfo'] | null | undefined |
||||
// 其他事件类型
|
// 其他事件类型
|
||||
} |
} |
||||
|
|
||||
export const eventBus = mitt<Events>() |
export const eventBus = mitt<Events>() |
Write
Preview
Loading…
Cancel
Save
Reference in new issue