Browse Source

update

feature/history-20250108
gzt 8 months ago
parent
commit
957f79c6f7
  1. 4
      components.d.ts
  2. 1
      package.json
  3. 44
      public/json/mockErrorData.json
  4. 19
      src/App.vue
  5. 8
      src/assets/empty-stack.svg
  6. 1
      src/assets/fatal.svg
  7. 3
      src/assets/notify.svg
  8. 6
      src/assets/stack-icon.svg
  9. 345
      src/components/dialogs/ErrorModal.vue
  10. 233
      src/components/dialogs/StackInfoModal.vue
  11. 2
      src/components/dialogs/index.ts
  12. 19
      src/eventBus.ts
  13. 6
      src/main.ts
  14. 53
      src/pages/Index/Index.vue
  15. 2
      src/pages/Index/Regular.vue
  16. 131
      src/pages/Index/Regular/Consumables.vue
  17. 6
      src/pages/Index/Regular/Emergency.vue
  18. 386
      src/pages/Index/Regular/Running.vue
  19. 5
      src/pages/Index/Regular/TestTube.vue
  20. 6
      src/pages/Index/components/Running/LittleBufferDisplay.vue
  21. 6
      src/pages/Index/components/Running/PlateDisplay.vue
  22. 3
      src/pages/Login/Login.vue
  23. 30
      src/utils/axios.ts

4
components.d.ts

@ -13,14 +13,16 @@ declare module 'vue' {
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ErrorModal: typeof import('./src/components/dialogs/ErrorModal.vue')['default']
Keyboard: typeof import('./src/components/Keyboard.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ShowErrorModal: typeof import('./src/components/dialogs/ShowErrorModal.vue')['default']
SimpleKeyboard: typeof import('./src/components/SimpleKeyboard.vue')['default']
StackInfoModal: typeof import('./src/components/dialogs/StackInfoModal.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']

1
package.json

@ -10,6 +10,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vicons/ionicons5": "^0.12.0",
"axios": "^1.7.7",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",

44
public/json/mockErrorData.json

@ -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)"
]
}
}
}
}

19
src/App.vue

@ -1,20 +1,5 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
// import { getDeviceStatus } from './services';
//500ms
// const pollingInterval = 500;
let pollingTimer: any;
// const getEvent = async () => {
// const res = await getDeviceStatus();
// console.log(res);
// };
onMounted(() => {
// pollingTimer = setInterval(getEvent, pollingInterval);
})
onUnmounted(() => {
clearInterval(pollingTimer);
});
import { ErrorModal, StackInfoModal } from './components/dialogs';
</script>
@ -22,6 +7,8 @@ onUnmounted(() => {
<div>
<router-view></router-view>
</div>
<ErrorModal />
<StackInfoModal />
</template>
<style>

8
src/assets/empty-stack.svg

@ -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>

1
src/assets/fatal.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>

3
src/assets/notify.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>

6
src/assets/stack-icon.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>

345
src/components/dialogs/ErrorModal.vue

@ -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>

233
src/components/dialogs/StackInfoModal.vue

@ -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>

2
src/components/dialogs/index.ts

@ -0,0 +1,2 @@
export { default as ErrorModal } from './ErrorModal.vue'
export { default as StackInfoModal } from './StackInfoModal.vue'

19
src/eventBus.ts

@ -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>()

6
src/main.ts

@ -9,9 +9,9 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 动态加载 Mock
// if (import.meta.env.VITE_USE_MOCK === 'true') {
// import('./mock/index')
// }
if (import.meta.env.VITE_USE_MOCK === 'true') {
import('./mock/index')
}
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App)

53
src/pages/Index/Index.vue

@ -68,7 +68,7 @@
@confirm="handleAlreadyConfirm" />
<!-- 自动自检失败 -->
<InitWarn v-if="showFailModal" :visible="showFailModal" title="检测失败" :message="failMessage"
icon="/src/assets/Warn.svg" cancelText="返回" confirmText="确认" @close="showFailModal = false"
icon="/src/assets/Warn.svg" cancelText="返回" confirmText="重试" @close="showFailModal = false"
@confirm="checkIfResetCompleted" />
<InitWarn v-if="idCardInserted" :visible="idCardInserted" title="检测到id卡插入" message="是否保存id卡信息" cancelText="返回"
icon="/src/assets/update-pin-icon.svg" confirmText="确认保存" @close="idCardInserted = false" @confirm="saveIdInfo" />
@ -121,6 +121,7 @@ interface EventType {
actionStep?: string,
actionStepName?: string
}
// A8kEcodeContextListPromptEvent
// const handleA8kEcodeContextListPromptEvent = (error: any): string => {
// switch (error.code) {
@ -226,7 +227,7 @@ const saveIdInfo = async () => {
idCardInserted.value = false;
}
}
// 10
// 500ms
const pollingInterval = 500;
let pollingTimer: ReturnType<typeof setInterval>;
@ -250,14 +251,21 @@ const generateErrorMessages = (data: CheckItem[]): string[] => {
//
const startTest = async () => {
const res = await startWork();
try {
const res = await startWork();
if (res.success) {
isTesting.value = true;
}
} catch (error) {
console.error('开始测试失败:', error);
isTesting.value = false;
}
const isCheck = sessionStorage.getItem('testStarted');
if (!isCheck) {
isTesting.value = false;
await checkIfResetCompleted()
}
if (res.success) {
isTesting.value = true;
}
};
//
@ -293,17 +301,23 @@ const handleConfirm = async () => {
//
const checkIfResetCompleted = async () => {
if (showFailModal.value) {
showFailModal.value = false;
}
showLoadingModal.value = true; // LoadingModal
// 2
await new Promise(resolve => setTimeout(resolve, 2000));
//
const initState = await getInitState();
if (initState.ecode === "SUC") {
//
if (initState.data.passed) {
console.log("初始化成功")
sessionStorage.setItem('testStarted', "true");
showLoadingModal.value = false;
showAlreadyModal.value = true;
} else {
console.log("初始化失败")
await getCheckData(); //
const failedItems = checkData.value.filter(item => !item.pass);
if (failedItems.length > 0) {
@ -314,8 +328,10 @@ const checkIfResetCompleted = async () => {
showLoadingModal.value = false; // LoadingModal
showFailModal.value = true; //
} else {
console.log("初始化失败,但是没有失败项")
showLoadingModal.value = false; // LoadingModal
showAlreadyModal.value = true; //
sessionStorage.setItem('testStarted', "true");
}
}
}
@ -346,18 +362,27 @@ const openTest = () => {
// openTest
if (!hasExecuted) {
showModal.value = true;
sessionStorage.setItem('testStarted', 'true'); //
}
};
//
const getCheckData = async () => {
const res = await getCheckList();
checkData.value = res.data;
console.log('设备检查的结果:', checkData.value);
//
checkTestTubeSlotStatus(checkData.value);
try {
const res = await getCheckList();
if (res.data && res.data.ecode == "SUC") {
checkData.value = res.data;
console.log('设备检查的结果:', checkData.value);
//
checkTestTubeSlotStatus(checkData.value);
} else {
console.log("获取检测数据失败")
showFailModal.value = true;
failMessage.value = "获取检测数据失败,请检查设备状态。"
}
} catch (error) {
console.error('获取检测数据失败:', error);
showFailModal.value = true;
failMessage.value = "获取检测数据失败,请检查设备状态。"
}
};

2
src/pages/Index/Regular.vue

@ -19,6 +19,8 @@ import TabBar from './components/Consumables/TabBar.vue'
#regular-container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
.main-top {
display: flex;

131
src/pages/Index/Regular/Consumables.vue

@ -1,5 +1,15 @@
<template>
<div id="regular-container">
<!-- 添加加载蒙层 -->
<div class="loading-overlay" v-if="isLoading">
<div class="loading-content">
<div class="loading-spinner"></div>
<span class="loading-text">正在加载耗材...</span>
<div class="loading-progress">
<div class="progress-bar" :style="{ width: `${loadingProgress}%` }"></div>
</div>
</div>
</div>
<!--耗材页面 -->
<div class="main-top">
<div class="plate-area">
@ -40,6 +50,8 @@ import {
} from '../../../types/Index'
const consumableStore = useConsumablesStore()
const emergencyStore = useEmergencyStore()
//
const loadingProgress = ref(0)
//
const isLoad = ref(false)
const isLoading = ref(false)
@ -116,15 +128,21 @@ const emergencyInfo = ref<Tube>({} as Tube)
//
const GetLoadConsumables = async () => {
isLoading.value = true
const res = await scanConsumables()
moveLiquids.value = res.data.consumableState.tips
plates.value = res.data.consumableState.reactionPlateGroup
bufferLittles.value = res.data.consumableState.littBottleGroup
bufferBig.value = res.data.consumableState.larBottleGroup
tempTipNum.value = [...moveLiquids.value.map((liquid) => liquid.tipNum)]
isLoad.value = true
isLoading.value = false
consumableStore.setConsumablesData(res.data.consumableState)
try {
const res = await scanConsumables()
moveLiquids.value = res.data.consumableState.tips
plates.value = res.data.consumableState.reactionPlateGroup
bufferLittles.value = res.data.consumableState.littBottleGroup
bufferBig.value = res.data.consumableState.larBottleGroup
tempTipNum.value = [...moveLiquids.value.map((liquid) => liquid.tipNum)]
isLoad.value = true
isLoading.value = false
consumableStore.setConsumablesData(res.data.consumableState)
} catch (error) {
//
isLoading.value = false
}
}
const getEmergencyInfo = async () => {
const res = await isTubeExist()
@ -177,8 +195,39 @@ onActivated(() => {
}
})
//
const handleIsLoad = () => {
GetLoadConsumables()
//
const handleIsLoad = async () => {
isLoading.value = true
loadingProgress.value = 0
//
const startTime = Date.now()
const duration = 3000 // 3
const updateProgress = () => {
const elapsed = Date.now() - startTime
const progress = Math.min((elapsed / duration) * 100, 100)
loadingProgress.value = progress
if (progress < 100) {
requestAnimationFrame(updateProgress)
}
}
requestAnimationFrame(updateProgress)
// 3
await new Promise(resolve => setTimeout(resolve, duration))
//
try {
await GetLoadConsumables()
} catch (error) {
console.error('加载耗材失败:', error)
} finally {
isLoading.value = false
loadingProgress.value = 0
}
}
const handleIsUnload = () => {
isLoad.value = !isLoad.value
@ -246,6 +295,66 @@ const updateTipNum = ({ index, tipNum }: { index: number; tipNum: number }) => {
display: flex;
flex-direction: column;
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 4px solid #f3f3f3;
border-top: 4px solid #4caf50;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
color: white;
font-size: 24px;
font-weight: 500;
}
.loading-progress {
width: 300px;
height: 6px;
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #4caf50;
border-radius: 3px;
transition: width 0.1s linear;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.main-top {
display: flex;

6
src/pages/Index/Regular/Emergency.vue

@ -82,11 +82,11 @@
<!-- 键盘 -->
<transition name="slide-up">
<!-- <div class="keyboard" v-if="keyboardVisible">
<div class="keyboard" v-if="keyboardVisible">
<Keyboard v-model="currentInputValue" layoutName="default" @onChange="handleKeyboardInput"
@close="hideKeyboard" />
</div> -->
<SimpleKeyboard />
</div>
<!-- <SimpleKeyboard /> -->
</transition>
</div>
</template>

386
src/pages/Index/Regular/Running.vue

@ -72,7 +72,7 @@
{{ consumablesStore.moveLiquids[0].tipNum }}/120
</div>
</div>
<BallGrid :total="6" :customColors="true" width="210px" height="160px" :data="consumablesStore.bufferBig"
<BallGrid :total="6" :customColors="true" width="160px" height="110px" :data="consumablesStore.bufferBig"
:columns="3" class="buffer-grid" />
</div>
<!-- 废料区域 -->
@ -238,14 +238,24 @@ const selectedItem = ref<Subtank | null>(null) // 当前选中的样本
const selectedItemId = ref<string | null>(null) // IDsampleId
const TOTAL_SLOTS = 20 //
const emergencyData = ref<Subtank | null>(null)
const getAngleStep = () => 360 / TOTAL_SLOTS
//
//
const getRotationStyle = (item: Subtank, index: number) => {
const angleStep = getAngleStep() //
const angle = angleStep * index
const totalItems = TOTAL_SLOTS
const angleStep = 360 / totalItems
const angle = index * angleStep
return {
transform: `rotate(${angle}deg) translate(-50%,-50%)`,
backgroundColor: item.projInfo.color || '#d9d9d9',
transform: `
translate(-50%, -50%) /* 将矩形中心点移到圆心 */
rotate(${angle}deg) /* 旋转到对应角度 */
translateY(-400px) /* 向上偏移到圆环位置 */
`,
backgroundColor: item.projInfo?.color || '#ffffff',
borderColor: item.isPlaceholder ? '#f0f0f0' :
selectedItemId.value === item.sampleId ? '#1890ff' :
getRemainingTime(item) === '已完成' ? '#ff4d4f' : 'transparent',
borderWidth: '3px',
borderStyle: 'solid',
}
}
//
@ -520,38 +530,44 @@ onUnmounted(() => {
#running-container {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 20px 0;
background-color: #f9fafb;
height: 93.5vh;
/* 更柔和的背景色 */
//
.circular-loader {
position: relative;
width: 1036px;
height: 1036px;
width: 800px;
height: 800px;
border-radius: 50%;
background-color: #ebebeb;
background-color: #f0f2f5;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
border: 5px solid #ffffff;
border: 8px solid #ffffff;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
/* 轻微阴影增加立体感 */
.center-square {
width: 400px; //
height: 400px; //
width: 380px; //
height: 380px; //
background-color: #ffffff;
border-radius: 50%;
position: absolute;
border: 5px solid #ffffff;
border: 8px solid #ffffff;
box-shadow: inset 0px 4px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 20px;
z-index: 2;
span {
text-align: center;
}
span:nth-child(3),
span:nth-child(6) {
@ -569,69 +585,55 @@ onUnmounted(() => {
}
.rectangular-item {
position: absolute;
left: 50%; //
top: 74.5%; //
width: 75px;
height: 170px;
transform-origin: center -110px; //
border-radius: 12px;
background-color: #ffffff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
position: absolute;
width: 70px;
height: 200px;
background-color: #d9d9d9;
top: 15%;
left: 50%;
transform-origin: 0 360px;
/* 设置旋转基点在容器外 */
border-radius: 20px;
border: 5px solid #ffffff;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
.emergency-icon {
.icon {
position: relative;
width: 70px;
height: 70px;
}
.text {
font-size: 32px;
color: white;
position: absolute;
top: 90px;
left: 20px;
}
}
padding: 15px 8px;
box-sizing: border-box;
cursor: pointer;
&.placeholder {
background-color: #f9f9f9; //
border: 1px dashed #d0d0d0; // 线
color: #aaa; //
}
span {
font-size: 16px;
color: #333;
&.placeholder {
color: #aaa;
font-style: italic;
}
&.placeholder {
background-color: #f8f8f8;
box-shadow: none;
opacity: 0.6;
}
span:first-child {
.barcode {
font-size: 22px;
font-weight: bold;
margin-top: 10px;
color: #333;
margin-bottom: 10px;
word-break: break-all;
text-align: center;
}
span:nth-child(2) {
.blood-type {
font-size: 18px;
font-weight: 700;
color: #666;
margin-bottom: auto;
}
span:last-child {
.time {
font-size: 20px;
font-weight: bold;
color: #333;
position: absolute;
bottom: 10px;
bottom: 15px;
&.completed {
color: #ff4d4f;
}
}
}
}
@ -640,34 +642,41 @@ onUnmounted(() => {
.consumables-container {
width: 100%;
box-sizing: border-box;
padding: 10px 10px;
margin-top: 50px;
padding: 20px 30px;
display: flex;
flex-direction: column;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.05);
background-color: #ffffff;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.05);
border-radius: 20px 20px 0 0;
//
.row-first {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 20px;
margin-bottom: 30px;
gap: 30px;
//
.emergency-button {
width: 120px;
height: 120px;
background-color: #ff6b6b;
border-radius: 15px;
width: 140px;
height: 140px;
background: linear-gradient(135deg, #ff6b6b, #ff4757);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.3s;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
}
span {
font-size: 20px;
font-size: 32px;
color: #ffffff;
font-weight: bold;
}
@ -677,101 +686,144 @@ onUnmounted(() => {
.test-tube-rack-area {
display: flex;
align-items: center;
margin-left: 20px;
gap: 20px;
.tube-project-tab {
width: 100px;
height: 40px;
background-color: #d1e9ff;
border-radius: 10px;
width: 120px;
height: 50px;
background: linear-gradient(to right, #e3f2fd, #bbdefb);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-size: 24px;
font-weight: bold;
color: #0073e6;
margin-right: 10px;
color: #1976d2;
position: relative;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.1);
&::after {
content: '';
width: 2px;
height: 100px;
background-color: black;
width: 3px;
height: 120px;
background: linear-gradient(to bottom, #1976d2, #64b5f6);
position: absolute;
top: -30px;
right: -10px;
transform: translateX(-50%);
}
span {
font-size: 26px;
font-weight: bold;
top: -35px;
right: -15px;
border-radius: 3px;
}
}
}
}
//
.row-second {
display: grid;
grid-template-columns: 3fr 4fr 2fr 1fr;
grid-gap: 10px;
gap: 20px;
padding: 10px 0;
.tips-and-big-buffer {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 20px;
.tips-item {
width: 210px;
height: 160px;
border-radius: 10px;
width: 160px;
height: 110px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background: #f5f5f5;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
background: #f8f9fa;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
.tip-fill {
position: absolute;
width: 100%;
height: 100%;
border-radius: 10px;
transition: background 0.3s;
border-radius: 16px;
transition: all 0.3s ease;
background: linear-gradient(to top, rgba(92, 184, 92, 0.1), transparent);
}
.tip-text {
font-size: 30px;
color: #333;
font-size: 36px;
color: #2c3e50;
font-weight: bold;
z-index: 1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
}
.waste-area {
width: 80px;
height: 330px;
border-radius: 20px;
align-items: center;
width: 100px;
height: 380px;
border-radius: 25px;
display: flex;
flex-direction: column;
justify-content: center;
transition: background-color 0.3s;
align-items: center;
transition: all 0.3s ease;
background: #f8f9fa;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
.waste-text {
font-size: 32px;
font-size: 36px;
font-weight: 700;
color: #fff;
color: #ffffff;
writing-mode: vertical-rl;
/* 文字垂直排列 */
text-orientation: upright;
/* 使文字正常显示而非旋转 */
line-height: 2.5;
/* 增大间隔 */
letter-spacing: 2px;
/* 增加文字间距 */
letter-spacing: 4px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
&:hover {
transform: translateY(-2px);
}
}
}
//
@media screen and (max-width: 1366px) {
padding: 15px 20px;
.row-first {
margin-bottom: 20px;
gap: 20px;
.emergency-button {
width: 120px;
height: 120px;
span {
font-size: 28px;
}
}
}
.row-second {
gap: 15px;
.tips-item {
width: 220px;
height: 160px;
.tip-text {
font-size: 32px;
}
}
.waste-area {
width: 90px;
height: 340px;
.waste-text {
font-size: 32px;
}
}
}
}
@ -785,75 +837,77 @@ onUnmounted(() => {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(5px);
.alert-icon {
position: relative;
img {
width: 90px;
height: 75px;
}
span {
color: #fff;
font-size: 32px;
position: absolute;
top: 15px;
right: 235px;
}
}
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 40px;
width: 500px;
.el-button {
width: 200px;
height: 80px;
border-radius: 40px;
font-size: 32px;
font-weight: 800;
}
}
/* 添加模糊效果,突显弹窗 */
.alert-container {
background-color: #fff;
padding: 50px;
border-radius: 10px;
width: 600px;
height: 300px;
background-color: #ffffff;
padding: 40px;
border-radius: 20px;
text-align: center;
display: grid;
place-content: center;
max-width: 600px;
animation: slideIn 0.3s ease;
.alert-title {
font-size: 40px;
font-size: 36px;
font-weight: bold;
color: #d9534f;
margin-bottom: 20px;
}
.alert-message {
font-size: 32px;
color: #555;
margin-bottom: 20px;
font-size: 28px;
color: #495057;
margin-bottom: 30px;
}
.alert-button {
width: 360px;
height: 120px;
border-radius: 50px;
font-size: 32px;
font-weight: 700;
.action-buttons {
display: flex;
justify-content: center;
gap: 20px;
.el-button {
min-width: 160px;
height: 50px;
font-size: 24px;
border-radius: 25px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
}
}
}
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
//
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
</style>

5
src/pages/Index/Regular/TestTube.vue

@ -288,8 +288,9 @@ const handleSelectedSamples = ({
justify-content: center;
.icon {
width: 86px;
height: 86px;
width: 60px;
height: 100px;
margin-right: 40px;
}
.text {

6
src/pages/Index/components/Running/LittleBufferDisplay.vue

@ -31,8 +31,8 @@ const getFillStyle = (item: BottleGroup) => {
.buffer-item {
position: relative;
width: 160px;
height: 160px;
width: 110px;
height: 155px;
background-color: #d3d3d3;
border-radius: 10px;
display: flex;
@ -50,7 +50,7 @@ const getFillStyle = (item: BottleGroup) => {
.buffer-text {
position: relative;
color: #fff;
font-size: 32px;
font-size: 20px;
font-weight: 700;
z-index: 1;
}

6
src/pages/Index/components/Running/PlateDisplay.vue

@ -40,13 +40,13 @@ const getPercentage = (num: number) => {
justify-content: space-between;
.project-info {
width: 200px;
width: 160px;
height: 46px;
border-radius: 40px;
flex: 1;
.project-name {
font-size: 24px;
font-size: 20px;
font-weight: 700;
color: white;
}
@ -60,7 +60,7 @@ const getPercentage = (num: number) => {
align-items: center;
justify-content: center;
display: flex;
font-size: 24px;
font-size: 20px;
font-weight: 700;
color: white;
}

3
src/pages/Login/Login.vue

@ -107,7 +107,8 @@ const submitPin = async () => {
sessionStorage.setItem('token', JSON.stringify(res.data))
router.push('/index')
} else {
loginStatus.value = '请输入正确的PIN'
loginStatus.value = `${res.info}`
pin.value = ''
}
}

30
src/utils/axios.ts

@ -4,6 +4,8 @@ import axios, {
InternalAxiosRequestConfig,
AxiosHeaders,
} from 'axios'
import { eventBus } from '../eventBus'
// 创建 Axios 实例
const apiClient: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
@ -40,10 +42,36 @@ apiClient.interceptors.request.use(
// 响应拦截器
apiClient.interceptors.response.use(
(response: AxiosResponse<any>) => {
if (response.data && response.data.dataType === 'ZAppPromopt') {
if (response.data.ecode === 'USR_NOT_EXIT') {
return Promise.resolve(response.data)
} else if (response.data.ecode === 'USR_PASSWORD_ERROR') {
return Promise.resolve(response.data)
} else {
console.log('接口出错', response.data)
eventBus.emit('show-error-modal', {
type: response.data.data.type,
info: response.data.data.info,
detailInfos: response.data.data.detailInfos,
ecode: response.data.data.ecode,
stackInfo: response.data.data.stackInfo,
})
return Promise.reject(response.data)
}
}
return response
},
(error) => {
console.error('API Error:', error)
eventBus.emit('show-error-modal', {
type: 'Error',
info: '网络请求失败',
detailInfos: [
{
name: '错误详情',
description: error.message,
},
],
})
return Promise.reject(error)
},
)

Loading…
Cancel
Save