Browse Source

update

feature/history-20250108
gzt 8 months ago
parent
commit
c4093bd845
  1. 1
      auto-imports.d.ts
  2. 1
      components.d.ts
  3. 3
      src/assets/Index/History/success.svg
  4. 26
      src/components/dialogs/StackInfoModal.vue
  5. 610
      src/pages/Index/History.vue
  6. 64
      src/pages/Index/Index.vue
  7. 22
      src/pages/Index/Regular.vue
  8. 20
      src/pages/Index/Regular/Consumables.vue
  9. 110
      src/pages/Index/Regular/Emergency.vue
  10. 1
      src/pages/Index/Regular/Running.vue
  11. 49
      src/pages/Index/Regular/TestTube.vue
  12. 182
      src/pages/Index/Settings/Device.vue
  13. 315
      src/pages/Index/Settings/Users.vue
  14. 452
      src/pages/Index/TestTube/ChangeUser.vue
  15. 43
      src/pages/Index/components/Consumables/ChangeNum.vue
  16. 46
      src/pages/Index/components/Consumables/MoveLiquidArea.vue
  17. 23
      src/pages/Index/components/Consumables/ProjectSelector.vue
  18. 112
      src/pages/Index/components/History/HistoryMessage.vue
  19. 277
      src/pages/Index/components/History/HistoryTable.vue
  20. 2
      src/pages/Index/components/Setting/DelMessage.vue
  21. 39
      src/services/Index/Test-tube/test-tube.ts
  22. 10
      src/services/Index/history.ts
  23. 19
      src/services/Index/regular.ts
  24. 2
      src/store/index.ts
  25. 24
      src/store/modules/device.ts
  26. 60
      src/store/modules/settingTestTube.ts
  27. 14
      src/types/Index/TestTube.ts
  28. 2
      src/websocket/socket.ts
  29. 2
      tsconfig.app.tsbuildinfo

1
auto-imports.d.ts

@ -6,5 +6,6 @@
// biome-ignore lint: disable
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

1
components.d.ts

@ -20,7 +20,6 @@ declare module 'vue' {
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']
}

3
src/assets/Index/History/success.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.7892 0 0 16.7892 0 37.5C0 58.2108 16.7892 75 37.5 75C58.2108 75 75 58.2108 75 37.5C75 16.7892 58.2108 0 37.5 0ZM60.0861 27.0143L34.1377 53.5248C34.0492 53.6575 33.9508 53.7843 33.8348 53.9017C32.8177 54.9347 31.169 54.9347 30.1513 53.9017L17.2622 40.8143C16.246 39.7813 16.246 38.1073 17.2622 37.0758C18.2792 36.0427 19.9279 36.0427 20.945 37.0758L31.9541 48.2529L56.4026 23.2743C57.4204 22.2414 59.0683 22.2414 60.0861 23.2743C61.1023 24.3073 61.1023 25.9812 60.0861 27.0143Z" fill="#528DFE"/>
</svg>

26
src/components/dialogs/StackInfoModal.vue

@ -204,19 +204,19 @@ 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)
})
}
}
// 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)

610
src/pages/Index/History.vue

@ -13,15 +13,16 @@
</el-input>
</div>
<div class="filter-button">
<el-button type="primary" class="search-button" @click="handleSearch">搜索</el-button>
<el-button type="primary" class="search-button" @click="handleSearch" disabled>搜索</el-button>
<el-button class="reload-button" @click="handleReset">重置</el-button>
</div>
</div>
<!-- 表格 -->
<div class="history-table" @scroll="onScroll" ref="tableContainer">
<HistoryTable @selectItems="handleSelection" @selectIds="handleSelectIds" @select-row="handleSelectRow"
:tableData="tableData" :loading=loading :key="tableKey" :loadingText="loadingText" />
<HistoryTable ref="historyTableRef" @selectItems="handleSelection" @selectIds="handleSelectIds"
@select-row="handleSelectRow" :tableData="tableData" :loading="loading" :key="tableKey"
:loadingText="loadingText" />
</div>
<!-- 功能 -->
@ -43,36 +44,87 @@
@confirm="handleWarnClose" />
</div>
<HistoryMessage :isVisible="isVisible" @update:isVisible="isVisible = $event">
<div class="page-container">
<div class="list-container">
<ul style="list-style: none">
<li>日期:</li>
<div class="divider"></div>
<li>SampleID</li>
<div class="divider"></div>
<li>ProjectShortName</li>
<li>subResult1</li>
<li>subResult2</li>
<li>subResult3</li>
<ul style="list-style: disc">
<li>样本种类:</li>
<li>操次:</li>
<li>Rec:</li>
<li>有效期:</li>
<div class="divider"></div>
<!--分割-->
<li>操作人:</li>
<li>序列号:</li>
<li>App Wer:</li>
<li>F/W Ver:</li>
<div class="divider"></div>
<li>参考值:</li>
</ul>
</ul>
<div class="detail-container">
<div class="detail-section">
<div class="detail-item">
<span class="label">日期:</span>
<span class="value">2024-01-01</span>
</div>
<div class="divider"></div>
<div class="detail-item">
<span class="label">SampleID:</span>
<span class="value">12345</span>
</div>
<div class="divider"></div>
<div class="detail-item">
<span class="label">ProjectShortName:</span>
<span class="value">Test Project</span>
</div>
<div class="detail-item">
<span class="label">subResult1:</span>
<span class="value">Result 1</span>
</div>
<div class="detail-item">
<span class="label">subResult2:</span>
<span class="value">Result 2</span>
</div>
<div class="detail-item">
<span class="label">subResult3:</span>
<span class="value">Result 3</span>
</div>
</div>
<div class="detail-section">
<div class="detail-item">
<span class="label">样本种类:</span>
<span class="value">Type A</span>
</div>
<div class="detail-item">
<span class="label">操次:</span>
<span class="value">1</span>
</div>
<div class="detail-item">
<span class="label">Rec:</span>
<span class="value">Record 1</span>
</div>
<div class="detail-item">
<span class="label">有效期:</span>
<span class="value">2024-12-31</span>
</div>
<div class="divider"></div>
<div class="detail-item">
<span class="label">操作人:</span>
<span class="value">John Doe</span>
</div>
<div class="detail-item">
<span class="label">序列号:</span>
<span class="value">SN12345</span>
</div>
<div class="detail-item">
<span class="label">App Ver:</span>
<span class="value">1.0.0</span>
</div>
<div class="detail-item">
<span class="label">F/W Ver:</span>
<span class="value">2.0.0</span>
</div>
</div>
<div class="detail-footer">
<button class="confirm-btn" @click="handleClose">确认</button>
</div>
<footer>
<button @click="handleClose" class="close-btn">确认</button>
</footer>
</div>
</HistoryMessage>
</template>
@ -89,6 +141,9 @@ import {
import HistoryMessage from './components/History/HistoryMessage.vue'
import type { TableItem } from '../../types/Index'
//
const historyTableRef = ref()
//
const isVisible = ref<boolean>(false)
@ -199,17 +254,23 @@ const tableKey = ref(0)//控制重新渲染
const tableContainer = ref(null as HTMLElement | null)
const hasMore = ref(true)
const loadingText = ref("加载中...")
const getTableData = async () => {
hasMore.value = true
const getTableData = async (isReset: boolean = false) => {
if (isReset) {
//
tableData.value = []
currentPage.value = 1
hasMore.value = true
}
if (loading.value || !hasMore.value) return
loading.value = true
try {
const res = await getHistoryInfoApi()
if (currentPage.value > totalPage.value) {
hasMore.value = false
loadingText.value = "没有更多数据了"
} else {
tableData.value = [...tableData.value, ...res.data.list]
currentPage.value++;
loadingText.value = "加载中..."
@ -248,13 +309,15 @@ const onScroll = (event: any) => {
//
const handleSearch = async () => {
// inputValue tableData
console.log('搜索内容:', inputValue.value)
//
try {
const res = await searchHistoryInfo(inputValue.value)
console.log(res.data.list)
//
tableData.value = res.data.list as TableItem[]
//
currentPage.value = 1
hasMore.value = false
} catch (error) {
console.error('搜索失败', error)
}
@ -263,7 +326,7 @@ const handleSearch = async () => {
//
const handleReset = () => {
inputValue.value = ''
getTableData()
getTableData(true) // true
}
//
@ -272,7 +335,6 @@ const handleConfirm = async () => {
showWarn.value = false
const actionType = currentAction.value.type
if (actionType === 'delete') {
console.log('调用删除接口')
await handleDelete()
} else if (actionType === 'print') {
//
@ -283,6 +345,7 @@ const handleConfirm = async () => {
}
}
const handleWarnClose = () => {
getTableData()
showWarn.value = false
}
@ -295,22 +358,27 @@ const handleCancel = () => {
//
const handleDelete = async () => {
try {
// ID
if (selectedIds.value.length > 0) {
selectedIds.value.forEach(async item => {
for (const item of selectedIds.value) {
const res = await deleteHistoryInfo(item)
if (res.success) {
tableKey.value++
getTableData()
//
//
selectedItems.value = []
selectedIds.value = []
//
if (historyTableRef.value?.clearSelection) {
historyTableRef.value.clearSelection()
}
// true
await getTableData(true)
//
warnMessage.value = '删除成功'
showWarn.value = true
} else {
throw new Error(res.message || '删除失败')
}
})
}
}
} catch (error) {
console.error('删除失败', error)
@ -324,33 +392,29 @@ const handlePrint = async () => {
try {
if (selectedItems.value.length > 10) {
warnMessage.value = '一次最多只能打印 10 条记录'
warnIcon = new URL('@/assets/Index/History/warn.svg', import.meta.url)
.href
warnIcon = new URL('@/assets/Index/History/warn.svg', import.meta.url).href
showWarn.value = true
return
}
const idsToPrint = selectedItems.value.map((item) => item.id)
//
const res = await printHistoryInfo(idsToPrint)
if (res.success) {
//
const printData = res.data.list
//
await executePrint(printData)
//
warnMessage.value = '打印成功'
warnIcon = new URL('@/assets/Index/History/success.svg', import.meta.url)
.href
showWarn.value = true
//
selectedItems.value = []
} else {
throw new Error(res.message || '打印失败')
const idsToPrint = selectedItems.value.map((item) => item.id)
for (const item of idsToPrint) {
const res = await printHistoryInfo(item)
if (res.success && res.ecode === "SUC") {
warnMessage.value = '打印成功'
warnIcon = new URL('@/assets/Index/History/success.svg', import.meta.url).href
showWarn.value = true
//
selectedItems.value = []
selectedIds.value = []
//
if (historyTableRef.value?.clearSelection) {
historyTableRef.value.clearSelection()
}
} else {
throw new Error(res.message || '打印失败')
}
}
} catch (error) {
console.error('打印失败', error)
@ -360,70 +424,70 @@ const handlePrint = async () => {
}
}
//
const executePrint = async (data: any) => {
//
const printContent = createPrintTemplate(data)
//
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(printContent)
printWindow.document.close()
printWindow.focus()
printWindow.print()
printWindow.close()
} else {
throw new Error('无法打开打印窗口')
}
}
//
const createPrintTemplate = (data: any) => {
// HTML
let content = `
<html>
<head>
<title>打印</title>
<style>
/* 在这里添加打印样式 */
body { font-family: Arial, sans-serif; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #000; padding: 8px; text-align: left; }
</style>
</head>
<body>
<h1>打印内容</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>用户ID</th>
<th>项目名称</th>
<!-- 其他表头 -->
</tr>
</thead>
<tbody>
`
data.forEach((item: any) => {
content += `
<tr>
<td>${item.id}</td>
<td>${item.sampleUserid}</td>
<td>${item.projName}</td>
<!-- 其他数据 -->
</tr>
`
})
content += `
</tbody>
</table>
</body>
</html>
`
return content
}
// const executePrint = async (data: any) => {
// //
// const printContent = createPrintTemplate(data)
// //
// const printWindow = window.open('', '_blank')
// if (printWindow) {
// printWindow.document.write(printContent)
// printWindow.document.close()
// printWindow.focus()
// printWindow.print()
// printWindow.close()
// } else {
// throw new Error('')
// }
// }
//
// const createPrintTemplate = (data: any) => {
// // HTML
// let content = `
// <html>
// <head>
// <title></title>
// <style>
// /* */
// body { font-family: Arial, sans-serif; }
// table { width: 100%; border-collapse: collapse; }
// th, td { border: 1px solid #000; padding: 8px; text-align: left; }
// </style>
// </head>
// <body>
// <h1></h1>
// <table>
// <thead>
// <tr>
// <th>ID</th>
// <th>ID</th>
// <th></th>
// <!-- -->
// </tr>
// </thead>
// <tbody>
// `
// data.forEach((item: any) => {
// content += `
// <tr>
// <td>${item.id}</td>
// <td>${item.sampleUserid}</td>
// <td>${item.projName}</td>
// <!-- -->
// </tr>
// `
// })
// content += `
// </tbody>
// </table>
// </body>
// </html>
// `
// return content
// }
//
const handleExport = () => {
//
@ -441,145 +505,237 @@ onMounted(() => {
<style scoped lang="less">
#history-container {
width: 100%;
padding-bottom: 96px;
height: 90vh;
display: flex;
flex-direction: column;
background-color: #f5f7fa;
box-sizing: border-box;
.history-filter {
width: 70.5625rem;
height: 4.3125rem;
margin: 0 auto;
width: 100%;
height: 70px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
.filter-input {
flex: 1;
max-width: 800px;
.el-input {
height: 50px;
font-size: 26px;
.el-input__wrapper {
border-radius: 25px;
padding: 0 20px;
box-shadow: 0 0 0 1px #dcdfe6;
&:hover {
box-shadow: 0 0 0 1px #409eff;
}
&.is-focus {
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
}
.filter-input,
.el-input {
width: 44.8125rem;
height: 4.3125rem;
line-height: 4.3125rem;
border-radius: 0.625rem;
font-size: 1.625rem;
font-weight: 700;
.el-input__wrapper {
width: 100%;
height: 100%;
border: 0.0625rem solid #dcdfe6;
}
.el-input__icon {
font-size: 2.8125rem;
}
.el-input__inner {
height: 3.3125rem;
.el-input__icon {
font-size: 26px;
color: #909399;
}
}
}
.filter-button {
display: flex;
align-items: center;
margin-left: 1.25rem;
.search-button,
.reload-button {
height: 50px;
border-radius: 25px;
font-size: 26px;
font-weight: 500;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
}
.search-button {
width: 16.5625rem;
height: 4.3125rem;
margin-right: 1.25rem;
font-size: 2.1875rem;
font-weight: 400;
width: 200px;
&:disabled {
background-color: #a0cfff;
border-color: #a0cfff;
&:hover {
transform: none;
box-shadow: none;
}
}
}
.reload-button {
width: 5.625rem;
height: 4.3125rem;
font-size: 2.1875rem;
font-weight: 400;
width: 90px;
color: #606266;
border: 1px solid #dcdfe6;
&:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
}
}
.history-table {
height: 88rem;
margin: 0 auto;
margin-top: 2.5rem;
overflow-y: auto;
flex: 1;
overflow-y: auto; //
overflow-x: hidden; //
background-color: #fff;
border-radius: 10px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin: 0 20px;
padding: 20px;
position: relative;
//
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 3px;
&:hover {
background: #909399;
}
}
&::-webkit-scrollbar-track {
background: #f5f7fa;
}
//
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 30px;
background: linear-gradient(to top, rgba(255, 255, 255, 0.9), transparent);
pointer-events: none;
}
}
.history-function {
width: 66rem;
height: 5.25rem;
margin: 0 auto;
width: 100%;
height: 84px;
margin-top: 20px;
background-color: #fff;
box-shadow: 0 -2px 12px 0 rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
margin-top: 2.5rem;
background-color: white;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
position: sticky;
bottom: 0;
z-index: 10;
gap: 20px;
.el-button {
font-size: 2.375rem;
font-weight: 400;
background-color: #fff;
color: black;
}
height: 50px;
border-radius: 25px;
font-size: 26px;
font-weight: 500;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-button:nth-child(1) {
width: 13.4375rem;
height: 5.25rem;
}
&:nth-child(1) {
width: 215px;
}
&:nth-child(2),
&:nth-child(3) {
width: 392px;
}
&.is-plain {
background-color: #fff;
.el-button:nth-child(2),
.el-button:nth-child(3) {
width: 24.5rem;
height: 5.25rem;
&:hover {
background-color: #ecf5ff;
}
}
}
}
}
.page-container {
width: 100%;
max-width: 600px;
height: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
.detail-container {
.detail-section {
margin-bottom: 24px;
}
header {
text-align: center;
margin-bottom: 20px;
.detail-item {
display: flex;
align-items: center;
padding: 16px 0;
font-size: 28px;
h1 {
font-size: 24px;
color: #333;
.label {
color: #606266;
min-width: 200px;
font-weight: 500;
}
}
.list-container {
.divider {
height: 40px;
.value {
color: #303133;
flex: 1;
}
}
ul {
li {
font-size: 32px;
}
}
.divider {
height: 1px;
background-color: #ebeef5;
margin: 16px 0;
}
footer {
.detail-footer {
margin-top: 40px;
text-align: center;
margin-top: 20px;
.close-btn {
.confirm-btn {
width: 90%;
background-color: #528dfe;
height: 100px;
border-radius: 50px;
color: #fff;
font-size: 40px;
font-weight: 700;
height: 88px;
background-color: #409eff;
border: none;
border-radius: 44px;
color: white;
font-size: 32px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #66b1ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
}
&:active {
transform: translateY(0);
}
}
}
}

64
src/pages/Index/Index.vue

@ -115,8 +115,8 @@ const WarnMessage = ref<string>('')
// WebSocket
const { wsUrl } = getServerInfo('/api/v1/app/ws/event')
const ws = createWebSocket(wsUrl)
console.log("🚀 ~ ws:", ws)
//
@ -151,68 +151,6 @@ const handleAppEvent = (data: AppEventMessage['data']) => {
}
};
// const getEventText = (data: EventType | EventType[]): string => {
// let eventName = '';
// // data
// const processEvent = async (item: EventType) => {
// //id
// if (item.typeName === "AppIDCardMountEvent") {
// consumableStore.isIdCardInserted = true;
// idCardInserted.value = true;
// eventName = "id"
// } else if (item.typeName === "AppIDCardUnmountEvent") {
// consumableStore.isIdCardInserted = false;
// eventName = "id"
// } else if (item.typeName === "DoA8kStepActionEvent") {
// eventName = item.actionStepName!
// } else if (item.typeName === "AppPromptEvents") {
// //propmt
// if (Array.isArray(item.prompt)) {
// console.log("propmt")
// item.prompt.forEach(async (item) => {
// if (item.type === "Error") {
// showErrorModal.value = true
// ErrorMessage.value = item.info
// await openBuzzer()
// } else if (item.type === "Warn") {
// showWarnModal.value = true
// WarnMessage.value = item.info
// }
// })
// } else {
// console.log("propmt", item)
// }
// } else if (item.typeName === "AppTubeholderSettingUpdateEvent ") {
// eventName = ""
// //
// }
// else {
// eventName = "..."
// }
// };
// if (Array.isArray(data)) {
// // item
// data.forEach(processEvent);
// } else {
// //
// processEvent(data);
// }
// return eventName;
// };
//
// const getEvent = async () => {
// const res = await pollAllAppEvents();
// if (res.success && res.data.length > 0) {
// EventText.value = getEventText(res.data);
// } else {
// //return
// return
// }
// }
//
const confirmError = async () => {
showErrorModal.value = false

22
src/pages/Index/Regular.vue

@ -13,6 +13,28 @@
<script setup lang="ts">
import TabBar from './components/Consumables/TabBar.vue'
import { createWebSocket } from '../../websocket/socket'
import { getServerInfo } from '../../utils/getServerInfo'
import { onMounted, onDeactivated } from 'vue';
import type { DeviceWorkStateMessage } from '../../websocket/socket';
import { useDeviceStore } from '../../store/index';
const deviceStore = useDeviceStore()
const { wsUrl } = getServerInfo('/api/v1/app/ws/state')
const ws = createWebSocket(wsUrl)
//
const handleDeviceState = (data: DeviceWorkStateMessage['data']) => {
deviceStore.setDeviceState(data)
}
onMounted(() => {
ws.connect();
ws.subscribe<DeviceWorkStateMessage>('DeviceWorkState', handleDeviceState);
});
onDeactivated(() => {
ws.unsubscribe<DeviceWorkStateMessage>('DeviceWorkState', handleDeviceState);
ws.disconnect();
console.log('🚀 ~ onBeforeUnmount ~ regular页面销毁:')
});
</script>
<style scoped lang="less">

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

@ -39,8 +39,9 @@
<script setup lang="ts">
import { MoveLiquidArea, SpttingPlates, MainComponent } from '../components'
import { ref, onMounted, onActivated, onBeforeUnmount } from 'vue'
import { scanConsumables, isTubeExist } from '../../../services/Index/index'
import { scanConsumables, isTubeExist, updateTipsNum } from '../../../services/Index/index'
import { useConsumablesStore, useEmergencyStore } from '../../../store'
import { useDeviceStore } from '../../../store/index'
import { eventBus } from '../../../eventBus'
import {
ReactionPlate,
@ -51,12 +52,14 @@ import {
import { createWebSocket } from '../../../websocket/socket'
import type { ConsumablesStateMessage, SensorStateMessage } from '../../../websocket/socket';
import { getServerInfo } from '../../../utils/getServerInfo'
import { ElMessage } from 'element-plus'
const { wsUrl } = getServerInfo('/api/v1/app/ws/state')
const socket = createWebSocket(wsUrl)
console.log("🚀 ~ socket:", socket)
const consumableStore = useConsumablesStore()
const emergencyStore = useEmergencyStore()
const deviceStore = useDeviceStore()
//
const currentTemperature = ref(40);
//
@ -183,10 +186,10 @@ const handleSensorState = (data: SensorStateMessage['data']) => {
//
}
};
//
const handleConsumablesState = (data: ConsumablesStateMessage['data']) => {
if (isAlreadyLoad.value) {
console.log('🚀 ~ handleConsumablesState ~ data:', data)
moveLiquids.value = data.tips
plates.value = data.reactionPlateGroup as ReactionPlate[]
bufferLittles.value = data.littBottleGroup as BufferLittle[]
@ -321,9 +324,20 @@ const handleIsUnload = () => {
]
bufferBig.value = []
}
const updateTipNum = ({ index, tipNum }: { index: number; tipNum: number }) => {
const updateTipNum = async ({ index, tipNum }: { index: number; tipNum: number }) => {
//
tempTipNum.value[index] = tipNum
console.log('🚀 ~ updateTipNum ~ tempTipNum:', tempTipNum.value)
//
try {
if (deviceStore.status === 'IDLE') {
await updateTipsNum({ group: `GROUP${index}`, num: tipNum })
} else {
ElMessage.error('设备正在工作,无法修改数值')
}
} catch (error) {
console.error('修改耗材数量失败:', error)
}
}
</script>

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

@ -83,7 +83,7 @@
<!-- 键盘 -->
<transition name="slide-up">
<div class="keyboard" v-if="keyboardVisible">
<SimpleKeyboard @onChange="handleKeyboardInput" @close="hideKeyboard" v-model="currentInputValue" />
<SimpleKeyboard :input="currentInputValue" @onChange="handleKeyboardInput" @onKeyPress="handleKeyPress" />
</div>
</transition>
</div>
@ -91,7 +91,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { nanoid } from 'nanoid';
import { insertEmergency } from '../../../services/Index/index';
@ -246,7 +246,6 @@ const toggleEmergency = () => {
//
onMounted(() => {
const { emergencyInfo } = emergencyStore.$state
console.log(emergencyInfo);
if (Object.keys(emergencyInfo).length > 0) {
isEmergencyEnabled.value = true
emergencyPosition.value.bloodType = emergencyInfo.bloodType;
@ -257,46 +256,62 @@ onMounted(() => {
emergencyPosition.value.userid = emergencyInfo.userid;
bloodType.value = emergencyInfo.bloodType;
projectName.value = '';
}
});
//
const keyboardVisible = ref(false); //
const currentInputField = ref(''); //
const currentInputValue = ref(''); //
//
const keyboardVisible = ref(false)
const currentInputValue = ref('')
const currentInputField = ref<'sampleBarcode' | 'userid' | ''>('')
//
const showKeyboard = (field: string) => {
const showKeyboard = (field: 'sampleBarcode' | 'userid') => {
//
currentInputValue.value = ''
currentInputField.value = field
keyboardVisible.value = true
}
if (field === 'sampleBarcode') {
console.log("sampleBarcode");
currentInputField.value = 'sampleBarcode';
currentInputValue.value = emergencyPosition.value.sampleBarcode || ''; //
} else {
console.log("userid");
currentInputField.value = 'userid';
currentInputValue.value = emergencyPosition.value.userid || ''; //
}
keyboardVisible.value = true;
};
//
const handleKeyboardInput = (value: string) => {
if (!currentInputField.value) return
//
currentInputValue.value = value
//
if (currentInputField.value === 'sampleBarcode') {
console.log("sampleBarcode");
emergencyPosition.value.sampleBarcode = value;
currentInputValue.value = value; //
} else if (currentInputField.value === 'userid') {
console.log("userid");
emergencyPosition.value.userid = value;
currentInputValue.value = value; //
emergencyPosition.value.sampleBarcode = value
} else {
emergencyPosition.value.userid = value
}
};
}
//
const handleKeyPress = (button: string) => {
if (button === '{enter}') {
hideKeyboard()
} else if (button === '{bksp}') {
// 退
const value = currentInputValue.value
if (value.length > 0) {
const newValue = value.slice(0, -1)
handleKeyboardInput(newValue)
}
}
}
//
const hideKeyboard = () => {
keyboardVisible.value = false;
currentInputField.value = '';
};
keyboardVisible.value = false
currentInputField.value = ''
currentInputValue.value = ''
}
//
onUnmounted(() => {
hideKeyboard()
})
</script>
@ -306,8 +321,8 @@ const hideKeyboard = () => {
margin: 0;
padding: 0;
position: relative;
height: 1200px;
width: 800px;
height: 100%;
width: 100%;
background-color: #f4f6f9;
box-sizing: border-box;
@ -329,7 +344,7 @@ const hideKeyboard = () => {
.page-header {
width: 100%;
height: 80px;
height: 100px;
display: flex;
align-items: center;
background-color: #ffffff;
@ -744,7 +759,7 @@ const hideKeyboard = () => {
.emergency-controller {
width: 100%;
height: 100px;
height: 120px;
display: flex;
margin-top: 20px;
@ -778,29 +793,22 @@ const hideKeyboard = () => {
position: fixed;
bottom: 0;
left: 0;
/* 固定左边 */
right: 0;
/* 固定右边 */
width: 100vw;
/* 宽度为视口宽度,确保不超出 */
max-width: 100%;
/* 最多占满父容器的宽度 */
height: 400px;
background-color: #f0f0f0;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.15);
overflow: hidden;
/* 防止溢出 */
width: 100%;
height: 300px;
background-color: #f5f7fa;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
/* 键盘滑动动画 */
//
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s ease;
}
.slide-up-enter,
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
}

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

@ -208,6 +208,7 @@ const confirmEmergency = () => {
}
//
const confirmEmergencyWarn = (val: any) => {
console.log('急诊结果:', val)
isDialogVisible.value = false
}
//

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

@ -18,9 +18,9 @@
<div class="text">添加试管架</div>
</div>
<!-- 试管信息编辑 -->
<ProjectSelector ref="projectSelectorInstance" @updateSample="handleSampleUpdate"
v-if="selectedSampleIdsInParent.length > 0" @confirm="handleConfirmProjectSelector" @cancel="closeProjectSelector"
@clearSelection="clearProjectSelection" />
<ProjectSelector ref="projectSelectorInstance" :uuid="UUID" :selectedSampleIds="selectedSampleIdsInParent"
@updateSample="handleSampleUpdate" v-if="selectedSampleIdsInParent.length > 0"
@confirm="handleConfirmProjectSelector" @cancel="closeProjectSelector" @clearSelection="clearProjectSelection" />
</div>
</template>
@ -33,6 +33,7 @@ import {
getTestTube,
getProjectInfo,
deleteTube,
updateTubeConfig,
} from '../../../services/Index/Test-tube/test-tube'
import type {
DataItem,
@ -42,9 +43,10 @@ import type {
import ProjectSelector from '../components/Consumables/ProjectSelector.vue'
import { useConsumablesStore } from '../../../store'
import { ElMessage } from 'element-plus'
import { useTestTubeStore } from '../../../store'
import { useTestTubeStore, useSettingTestTubeStore } from '../../../store'
const router = useRouter()
const testTubeStore = useTestTubeStore()
const settingTestTubeStore = useSettingTestTubeStore()
const consumables = useConsumablesStore()
const tubeRacks = ref<DataItem[]>([])
const loading = ref(false) //
@ -81,7 +83,8 @@ const clearProjectSelection = () => {
selectedProject.value = null //
}
//
const handleChangeUser = (uuid: string) => {
const handleChangeUser = async (uuid: string) => {
await updateTubeSettingsHandler()
const tubeData = tubeRacks.value.find((item) => item.uuid === uuid)
testTubeStore.$state.tubeInfo = tubeData!
router.push({
@ -199,7 +202,8 @@ const selectedSampleIdsInParent = ref<number[]>([])
const componentRefreshKey = ref(0)
// handleConfirmProjectSelector
const handleConfirmProjectSelector = () => {
const handleConfirmProjectSelector = async () => {
await updateTubeSettingsHandler()
//
selectedSampleIdsInParent.value = []
@ -219,16 +223,7 @@ const handleConfirmProjectSelector = () => {
})
}
const tubeRackComponentInstance = ref()
//
// const emitClearSelectedSamples = (uuid: string) => {
// console.log('uuid', uuid)
// const tubeRackComponent = tubeRacks.value.find((rack) => rack.uuid === uuid)
// if (tubeRackComponent) {
// //
// console.log('')
// tubeRackComponentInstance.value?.clearSelection()
// }
// }
//
const closeProjectSelector = () => {
@ -245,6 +240,28 @@ const handleUpdateSelectedSamples = ({ sampleIds, uuid }: { sampleIds: number[];
UUID.value = uuid
}
}
//
const updateTubeSettingsHandler = async () => {
const { uuid, setting } = settingTestTubeStore.currentConfig
if (uuid && setting.tubeIndex >= 0) {
try {
const response = await updateTubeConfig({ uuid, setting })
if (response.success) {
ElMessage.success('设置更新成功')
await getTubeData()
settingTestTubeStore.clearConfig()
} else {
ElMessage.error('设置更新失败')
}
} catch (error) {
console.error('更新试管设置失败:', error)
ElMessage.error('设置更新失败')
}
}
}
</script>
<style scoped lang="less">

182
src/pages/Index/Settings/Device.vue

@ -1,16 +1,16 @@
<template>
<div class="device-management">
<div class="setting-item">
<span>日期</span>
<span>{{ formattedDate }}</span>
<span class="label">日期</span>
<span class="value">{{ formattedDate }}</span>
</div>
<div class="setting-item">
<span>日期格式</span>
<span class="label">日期格式</span>
<div class="options">
<button
v-for="(format, index) in dateFormats"
:key="index"
<button
v-for="(format, index) in dateFormats"
:key="index"
:class="{ active: selectedDateFormat === format }"
@click="setDateFormat(format)"
>
@ -20,16 +20,16 @@
</div>
<div class="setting-item">
<span>时间</span>
<span>{{ time }}</span>
<span class="label">时间</span>
<span class="value">{{ time }}</span>
</div>
<div class="setting-item">
<span>语言</span>
<span class="label">语言</span>
<div class="options">
<button
v-for="(item, index) in languages"
:key="index"
<button
v-for="(item, index) in languages"
:key="index"
:class="{ active: selectedLanguage === item.name }"
@click="setLanguage(item.name)"
>
@ -39,11 +39,11 @@
</div>
<div class="setting-item">
<span>打印</span>
<span class="label">打印</span>
<div class="options">
<button
v-for="(mode, index) in printModes"
:key="index"
<button
v-for="(mode, index) in printModes"
:key="index"
:class="{ active: selectedPrintMode === mode }"
@click="setPrintMode(mode)"
>
@ -53,11 +53,11 @@
</div>
<div class="setting-item">
<span>注销时间</span>
<span class="label">注销时间</span>
<div class="options">
<button
v-for="(time, index) in logoutTimes"
:key="index"
<button
v-for="(time, index) in logoutTimes"
:key="index"
:class="{ active: selectedLogoutTime === time }"
@click="setLogoutTime(time)"
>
@ -130,51 +130,125 @@ onMounted(() => {
<style scoped lang="less">
.device-management {
width: 100%;
font-family: Arial, sans-serif;
color: #333;
background-color: #f9f9f9;
height: 50vh;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 90vh;
padding: 20px;
box-sizing: border-box;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #f5f7fa;
display: flex;
flex-direction: column;
gap: 20px;
.setting-item {
background-color: #fff;
border-radius: 12px;
padding: 24px 32px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
}
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
.setting-item span {
font-size: 32px;
font-weight: bold;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08);
}
.options {
display: flex;
gap: 10px;
button {
border: 1px solid #007bff;
background: none;
font-size: 32px;
color: #007bff;
cursor: pointer;
padding: 20px 10px;
transition:
color 0.3s,
background-color 0.3s;
border-radius: 4px;
.label {
font-size: 28px;
font-weight: 500;
color: #303133;
}
.value {
font-size: 28px;
color: #606266;
}
.options {
display: flex;
gap: 12px;
flex-wrap: wrap;
button {
min-width: 120px;
height: 56px;
padding: 0 24px;
border: 1px solid #dcdfe6;
background-color: #fff;
border-radius: 8px;
font-size: 24px;
color: #606266;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
&.active {
color: #fff;
background-color: #409eff;
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
&:hover {
background-color: #66b1ff;
border-color: #66b1ff;
}
}
}
}
//
&:nth-child(2) {
.options button {
min-width: 160px;
font-family: monospace;
letter-spacing: 1px;
}
}
.active {
color: #fff;
background-color: #007bff;
//
&:nth-child(4) {
.options button {
min-width: 140px;
}
}
//
&:nth-child(5) {
.options button {
min-width: 100px;
}
}
//
&:nth-child(6) {
.options button {
min-width: 100px;
font-family: monospace;
}
}
}
}
//
@media screen and (max-width: 768px) {
.device-management {
.setting-item {
flex-direction: column;
align-items: flex-start;
gap: 16px;
.options {
width: 100%;
justify-content: flex-start;
}
}
}
}

315
src/pages/Index/Settings/Users.vue

@ -1,47 +1,106 @@
<template>
<div class="user-table" :key="refreshKey">
<el-table :data="tableData" header-cell-class-name="table-header" row-class-name="table-row" style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" />
<el-table-column label="用户" width="200" property="account" />
<el-table-column label="权限" property="usrRole" />
</el-table>
<div class="table-footer">
<el-button type="primary" @click="addUser">新增</el-button>
<el-button type="primary" @click="modifyPin">PIN</el-button>
<el-button type="danger" @click="deleteSelectedUsers">删除</el-button>
<div class="user-management">
<div class="user-table" :key="refreshKey">
<el-table
:data="tableData"
header-cell-class-name="table-header"
row-class-name="table-row"
>
<el-table-column type="selection" width="80" />
<el-table-column label="用户" property="account" min-width="200" />
<el-table-column label="权限" property="usrRole" min-width="200" />
</el-table>
<div class="table-footer">
<el-button type="primary" @click="addUser">新增</el-button>
<el-button type="primary" @click="modifyPin">PIN</el-button>
<el-button type="danger" @click="deleteSelectedUsers">删除</el-button>
</div>
</div>
<!-- 删除用户 -->
<DelWarn v-if="delShowModal" :visible="delShowModal" icon="/src/assets/Warn.svg" title="删除用户"
:message="deleteUserMessage" description="您正在删除用户,请谨慎操作" confirm-text="确认删除" cancel-text="取消删除"
@confirm="handleConfirmDelete" @cancel="handleCancelDelete" />
<!-- 通知用户删除成功 -->
<DelMessage v-if="delMessageShowModal" v-model:visible="delMessageShowModal" icon="/src/assets/OK.svg"
message="已成功删除用户" :username="selectedUsers[0]?.account" @confirm="handleConfirmMsgDelete" />
<!-- 确认是否更改该用户的PIN码 -->
<DelWarn v-if="updatePinModal" :visible="updatePinModal" icon="/src/assets/update-pin-icon.svg" title="PIN码更新"
:message="updatePinMessage" description="您正在更改PIN码,请谨慎操作" confirm-text="确认更新" cancel-text="取消更新"
@confirm="handleConfirmUpdatePin" @cancel="handleCancelUpdatePin" />
<!-- 让用户输入新的pin -->
<EnterPinModal v-if="enterPinModal" :visible="enterPinModal" :loading="updatePinLoading" @confirm="updatePinConfirm"
@cancel="closeEnterPinModal" />
<!-- 通知用户PIN码更新成功 -->
<DelMessage v-if="updatePinMsgModal" :visible="updatePinMsgModal" icon="/src/assets/OK.svg" message="PIN码更新成功"
:username="selectedUsers[0].account" @confirm="handleConfirmMsg" />
<div v-if="currentStep === 'username'">
<AddUserModal :visible="insertUserShowModal" :already-exist="isExist" placeholder="请输入用户名" :tips="tips"
@confirm="handleConfirmInsert" @cancel="handleCancelInsert" @resetAlreadyExist="resetAlreadyExist" />
</div>
<div v-else-if="currentStep === 'pin'">
<EnterPinModal :visible="isPinModalVisible" :loading="registerLoading" @confirm="handlePinConfirm"
@cancel="closeModal" />
</div>
<!-- 通知成功添加用户 -->
<DelMessage v-if="confirmInsert" :visible="confirmInsert" icon="/src/assets/OK.svg" message="已成功添加新用户"
:username="newUser.account" @confirm="handleConfirmMsg" />
<!-- 通知用户选中用户 -->
<DelMessage v-if="isChecked" :visible="isChecked" icon="/src/assets/Warn.svg" message="请先选中用户" username=""
@confirm="handleConfirmMsg" />
<!-- 弹窗组件 -->
<DelWarn
v-if="delShowModal"
:visible="delShowModal"
icon="/src/assets/Warn.svg"
title="删除用户"
:message="deleteUserMessage"
description="您正在删除用户,请谨慎操作"
confirm-text="确认删除"
cancel-text="取消删除"
@confirm="handleConfirmDelete"
@cancel="handleCancelDelete"
/>
<DelMessage
v-if="delMessageShowModal"
v-model:visible="delMessageShowModal"
icon="/src/assets/OK.svg"
message="已成功删除用户"
:username="selectedUsers[0]?.account"
@confirm="handleConfirmMsgDelete"
/>
<DelWarn
v-if="updatePinModal"
:visible="updatePinModal"
icon="/src/assets/update-pin-icon.svg"
title="PIN码更新"
:message="updatePinMessage"
description="您正在更改PIN码,请谨慎操作"
confirm-text="确认更新"
cancel-text="取消更新"
@confirm="handleConfirmUpdatePin"
@cancel="handleCancelUpdatePin"
/>
<EnterPinModal
v-if="enterPinModal"
:visible="enterPinModal"
:loading="updatePinLoading"
@confirm="updatePinConfirm"
@cancel="closeEnterPinModal"
/>
<DelMessage
v-if="updatePinMsgModal"
:visible="updatePinMsgModal"
icon="/src/assets/OK.svg"
message="PIN码更新成功"
:username="selectedUsers[0].account"
@confirm="handleConfirmMsg"
/>
<!-- 添加用户相关弹窗 -->
<template v-if="currentStep === 'username'">
<AddUserModal
:visible="insertUserShowModal"
:already-exist="isExist"
placeholder="请输入用户名"
:tips="tips"
@confirm="handleConfirmInsert"
@cancel="handleCancelInsert"
@resetAlreadyExist="resetAlreadyExist"
/>
</template>
<template v-else-if="currentStep === 'pin'">
<EnterPinModal
:visible="isPinModalVisible"
:loading="registerLoading"
@confirm="handlePinConfirm"
@cancel="closeModal"
/>
</template>
<DelMessage
v-if="confirmInsert"
:visible="confirmInsert"
icon="/src/assets/OK.svg"
message="已成功添加新用户"
:username="newUser.account"
@confirm="handleConfirmMsg"
/>
</div>
</template>
@ -289,82 +348,150 @@ const deleteUserMessage = computed(
)
</script>
<style scoped lang="less">
.user-table {
.user-management {
width: 100%;
background-color: #f9f9f9;
height: 90vh;
padding: 20px;
box-sizing: border-box;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
.user-table {
flex: 1;
background-color: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
transition: all 0.3s ease;
::v-deep {
.el-table {
&:hover {
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08);
}
:deep(.el-table) {
flex: 1;
border: none;
border-radius: 8px;
overflow: hidden;
.table-header {
font-weight: bold;
text-align: center;
font-size: 32px;
line-height: normal; // 使
padding: 20px 0; //
&::before {
display: none;
}
.el-table__header-wrapper {
height: auto; //
}
.el-table__header {
th {
background-color: #f5f7fa;
border: none;
height: 60px;
font-size: 24px;
font-weight: 500;
color: #303133;
.table-row:nth-child(odd) {
background-color: #f2f2f2;
&.is-leaf {
border: none;
}
}
}
.table-row {
font-size: 32px;
color: #333;
text-align: center;
}
.el-table__body {
td {
border: none;
height: 60px;
font-size: 24px;
color: #606266;
transition: background-color 0.3s;
}
.el-table__cell {
height: 100px;
line-height: normal; //
padding: 0 20px; //
}
tr {
border: none;
transition: all 0.3s;
.cell {
height: 100px;
line-height: 100px;
padding: 0 10px; //
text-align: center;
}
&:hover > td {
background-color: #f5f7fa;
}
th,
td {
border-right: none;
&.selected-row > td {
background-color: #ecf5ff;
}
}
}
/* 放大多选框 */
//
.el-checkbox {
transform: scale(1.8);
transform: scale(1.5);
.el-checkbox__inner {
border-color: #dcdfe6;
transition: all 0.3s;
&:hover {
border-color: #409eff;
}
}
}
}
.table-footer {
margin-top: 24px;
display: flex;
justify-content: center;
gap: 20px;
.el-button {
min-width: 160px;
height: 56px;
border-radius: 8px;
font-size: 24px;
font-weight: 500;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
}
/* 确保表格对齐 */
.el-checkbox__input {
margin-right: 0 !important; //
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
&.el-button--primary {
background-color: #409eff;
border-color: #409eff;
&:hover {
background-color: #66b1ff;
border-color: #66b1ff;
}
}
&.el-button--danger {
&:hover {
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.2);
}
}
}
}
}
}
.table-footer {
position: absolute;
bottom: 100px;
width: 100%;
display: flex;
//
@media screen and (max-width: 768px) {
.user-management {
padding: 10px;
justify-content: center;
.user-table {
padding: 16px;
.table-footer {
flex-direction: column;
align-items: stretch;
gap: 12px;
button {
font-size: 32px;
width: 320px;
height: 120px;
margin: 0 20px;
border-radius: 50px;
.el-button {
width: 100%;
}
}
}
}

452
src/pages/Index/TestTube/ChangeUser.vue

@ -1,52 +1,83 @@
<template>
<div id="changeUser-container">
<div class="navigator-box">
<img class="nav-icon" src="@/assets/Index/left.svg" alt="" @click.stop="goBack" />
<div class="nav-text">患者信息</div>
<div class="tube-type-text">试管类型({{ tubeType }})</div>
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<img src="@/assets/Index/left.svg" alt="返回" @click.stop="goBack" />
<span class="title">患者信息</span>
<div class="divider"></div>
<span class="tube-type">试管类型({{ tubeType }})</span>
</div>
</div>
<div class="sample-container">
<!-- 上半部分显示前五个样本 -->
<div class="sample-box" v-for="box in boxes" :key="box.id">
<div class="sample-info">
<div class="tube-info-title">
<span>序号</span>
<span>试管信息</span>
</div>
<div class="tube-info">
<span>样本条形码</span>
<span>用户ID</span>
<!-- 主要内容区域 -->
<div class="content">
<!-- 上半部分样本 -->
<div class="sample-section" v-for="box in boxes" :key="box.id">
<!-- 左侧标题栏 -->
<div class="section-labels">
<div class="label-column">
<div class="label-item">
<span class="label">序号</span>
</div>
<div class="label-item">
<span class="label">试管信息</span>
</div>
<div class="label-item">
<span class="label">样本条形码</span>
</div>
<div class="label-item">
<span class="label">用户ID</span>
</div>
</div>
</div>
<div class="sample-display">
<div class="sample-item" v-for="item in box.samples" :key="item.tubeIndex"
:class="{ selected: selectedSampleId === item.tubeIndex }" @click="selectSample(item.tubeIndex)">
<div class="sample-circle" :data-index="item.tubeIndex + 1" :style="generateSampleBackground(item.projId!)">
<span style="color: black; font-size: 32px">{{
item.bloodType
}}</span>
</div>
<div class="sample-info-box">
<input class="barcode-input" v-model="item.sampleBarcode" />
<input class="userid-input" v-model="item.userid" />
<!-- 右侧样本展示 -->
<div class="samples-grid">
<div v-for="item in box.samples" :key="item.tubeIndex" class="sample-item"
:class="{ 'selected': selectedSampleId === item.tubeIndex }" @click="selectSample(item.tubeIndex)">
<div class="sample-content">
<!-- 序号 -->
<div class="item-index">{{ item.tubeIndex + 1 }}</div>
<!-- 试管圆圈 -->
<div class="sample-circle" :style="generateSampleBackground(item.projId!)">
<span class="blood-type">{{ item.bloodType }}</span>
</div>
<!-- 输入框组 -->
<div class="inputs">
<input class="input-field" v-model="item.sampleBarcode" placeholder="条形码"
@focus="showKeyboard('barcode', item)" readonly />
<input class="input-field" v-model="item.userid" placeholder="用户ID"
@focus="showKeyboard('userid', item)" readonly />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="button-box">
<el-button class="cancel-button" @click="goBack">取消</el-button>
<el-button class="confirm-button" type="primary" @click="confirmChange">确定</el-button>
<!-- 底部按钮 -->
<div class="footer">
<button class="btn cancel" @click="goBack">取消</button>
<button class="btn confirm" @click="confirmChange">确定</button>
</div>
<Keyboard />
<!-- 键盘组件 -->
<transition name="slide-up">
<div class="keyboard-container">
<SimpleKeyboard v-if="keyboardVisible" :input="currentInputValue" @onChange="handleKeyboardInput"
@onKeyPress="handleKeyPress" />
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watchEffect, onUnmounted } from 'vue'
import { onMounted, ref, watchEffect } from 'vue'
import { useRouter } from 'vue-router'
import { useTestTubeStore, useConsumablesStore } from '../../../store'
import Keyboard from '../../../components/Keyboard.vue'
import { useTestTubeStore, useConsumablesStore, useSettingTestTubeStore } from '../../../store'
import SimpleKeyboard from '../../../components/SimpleKeyboard.vue'
import type {
DataItem,
ReactionPlate,
@ -58,7 +89,7 @@ import {
processTubeSettings,
generateSampleBackground,
} from '../utils'
import { updateTubeInfo } from '../../../services/Index/'
import { updateTubeConfig } from '../../../services/Index/Test-tube/test-tube'
const testTubeStore = useTestTubeStore()
const consumableStore = useConsumablesStore()
@ -68,7 +99,18 @@ const processedTubeInfo = ref<handleTube>({} as handleTube) //经过清洗的试
const tubeSettings = ref<TubeRack[]>([]) //
const tubeType = ref<string>(testTubeStore.type || '自动') //
const plates = ref<ReactionPlate[]>(consumableStore.plates) //
const selectedSampleId = ref<number | null>(null) //
const selectedSampleId = ref<number | null>(null) //
const keyboardVisible = ref(false)
const currentInputValue = ref('')
const currentInput = ref<{
type: 'barcode' | 'userid',
item: any
}>({
type: 'barcode',
item: null
})
const settingTestTubeStore = useSettingTestTubeStore()
onMounted(() => {
// startPolling()
tubeInfo.value = testTubeStore.tubeInfo
@ -82,40 +124,6 @@ onMounted(() => {
tubeSettings: tubeSettings.value,
}
})
onUnmounted(() => {
// stopPolling()
})
//
// const intervalId = ref<NodeJS.Timeout | null>(null)
//
//
// const startPolling = () => {
// intervalId.value = setInterval(async () => {
// const response = await pollTubeInfo()
// if (response && response.success) {
// //
// const latestTubeSettings = response.data.find(
// (tube: handleTube) => tube.uuid === tubeInfo.value.uuid,
// )?.tubeSettings
// if (latestTubeSettings) {
// //
// tubeSettings.value = processTubeSettings(
// latestTubeSettings,
// plates.value,
// getBloodTypeLabel,
// )
// }
// }
// }, 3000) // 3
// }
//
// const stopPolling = () => {
// if (intervalId.value) {
// clearInterval(intervalId.value)
// }
// }
//
const goBack = () => {
// stopPolling()
@ -150,11 +158,42 @@ watchEffect(() => {
})
//
const confirmChange = async () => {
const res = await updateTubeInfo(processedTubeInfo.value)
if (res && res.success) {
const { uuid, setting } = settingTestTubeStore.currentConfig
const res = await updateTubeConfig({ uuid, setting })
if (res.success) {
settingTestTubeStore.clearConfig()
goBack()
}
}
//
const showKeyboard = (type: 'barcode' | 'userid', item: any) => {
keyboardVisible.value = true
currentInput.value = { type, item }
currentInputValue.value = type === 'barcode' ? item.sampleBarcode : item.userid
}
//
const handleKeyboardInput = (input: string) => {
if (!currentInput.value.item) return
settingTestTubeStore.updateTubeSetting(tubeInfo.value.uuid, {
tubeIndex: currentInput.value.item.tubeIndex,
[currentInput.value.type === 'userid' ? 'userid' : 'sampleBarcode']: input
})
// UI
if (currentInput.value.type === 'barcode') {
currentInput.value.item.sampleBarcode = input
} else {
console.log('试管信息更新失败')
currentInput.value.item.userid = input
}
}
//
const handleKeyPress = (button: string) => {
if (button === '{enter}') {
keyboardVisible.value = false
}
}
</script>
@ -162,158 +201,131 @@ const confirmChange = async () => {
<style lang="less" scoped>
#changeUser-container {
width: 100%;
height: 100%;
/* 留出 200px 空间用于虚拟键盘 */
height: 95vh;
display: flex;
flex-direction: column;
position: relative;
background-color: #f5f7fa;
overflow: hidden;
.navigator-box {
box-sizing: border-box;
width: 100%;
.header {
height: 80px;
/* 调整导航条高度 */
padding: 10px;
background-color: #fff;
padding: 0 20px;
display: flex;
align-items: center;
font-size: 32px;
/* 缩小字体 */
font-weight: 900;
.nav-icon {
width: 32px;
/* 调整图标大小 */
height: 32px;
}
.nav-text {
margin-left: 40px;
.header-left {
display: flex;
align-items: center;
gap: 12px;
img {
width: 24px;
height: 24px;
cursor: pointer;
}
&::after {
content: '|';
margin: 0 40px;
.title {
font-size: 28px;
color: #303133;
}
.tube-type {
font-size: 24px;
color: #606266;
}
}
}
.sample-container {
.content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
padding: 10px;
height: 60%;
/* 减去导航条高度 */
overflow: hidden;
.sample-box {
.sample-section {
background-color: #fff;
border-radius: 8px;
padding: 20px;
display: flex;
padding: 15px;
background-color: #f9f9f9;
border-radius: 10px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
height: 45%;
/* 上下两部分各占 45% */
box-sizing: border-box;
.sample-info {
height: 380px;
.section-labels {
width: 120px;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 32px;
/* 调整文字大小 */
font-weight: bold;
color: #333;
padding-right: 20px;
white-space: nowrap;
.tube-info-title {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 130px;
}
padding-top: 20px;
.tube-info {
display: flex;
flex-direction: column;
justify-content: space-evenly;
height: 200px;
.label-column {
.label-item {
margin-bottom: 16px;
.label {
font-size: 24px;
color: #606266;
}
}
}
}
.sample-display {
.samples-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
/* 调整样本间距 */
width: 100%;
align-items: start;
gap: 12px;
.sample-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
gap: 10px;
padding: 10px;
box-sizing: border-box;
height: 400px;
position: relative;
&.selected {
background-color: #b3e5fc;
/* 比默认颜色深的选中背景 */
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.sample-index {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
font-weight: bold;
}
background-color: #f5f7fa;
border-radius: 4px;
padding: 12px;
.sample-circle {
width: 100px;
/* 缩小圆形区域 */
height: 100px;
.sample-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 50%;
background-color: #4caf50;
color: #fff;
font-size: 14px;
margin-top: 20px;
&::before {
content: attr(data-index);
position: absolute;
top: 10px;
/* 试管上方显示 */
font-size: 18px;
/* 调整序号字体大小 */
font-weight: bold;
color: #333;
.item-index {
font-size: 22px;
color: #606266;
margin-bottom: 8px;
}
}
.sample-info-box {
display: flex;
flex-direction: column;
gap: 20px;
.barcode-input,
.userid-input {
width: 80px;
/* 缩小输入框 */
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 20px;
font-weight: bold;
text-align: center;
background-color: #fff;
.sample-circle {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 8px 0;
.blood-type {
font-size: 20px;
color: #fff;
}
}
.inputs {
width: 100%;
margin-top: 12px;
.input-field {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
margin-bottom: 8px;
font-size: 20px;
text-align: center;
background-color: #fff;
&::placeholder {
color: #c0c4cc;
font-size: 18px;
}
}
}
}
}
@ -321,22 +333,52 @@ const confirmChange = async () => {
}
}
.button-box {
.footer {
height: 80px;
padding: 10px 20px;
background-color: #fff;
margin: 40px 0;
.cancel-button,
.confirm-button {
width: 500px;
height: 100px;
border-radius: 50px;
font-size: 32px;
margin: 0 40px;
}
display: flex;
justify-content: center;
gap: 16px;
.cancel-button {
background-color: #f2f2f2;
.btn {
width: 320px;
height: 60px;
border-radius: 30px;
font-size: 24px;
font-weight: normal;
&.cancel {
background-color: #f5f7fa;
color: #606266;
border: 1px solid #dcdfe6;
}
&.confirm {
background-color: #409eff;
color: #fff;
}
}
}
.keyboard-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20vh;
background-color: #fff;
}
//
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
}
}
</style>

43
src/pages/Index/components/Consumables/ChangeNum.vue

@ -12,15 +12,7 @@
<!-- 主体内容 -->
<div class="modal-body">
<div class="input-box">
<input
id="slider"
type="range"
v-model="value"
min="0"
max="25"
step="1"
class="slider"
/>
<input id="slider" type="range" v-model="value" min="0" max="25" step="1" class="slider" />
<div class="change-num">
<button class="minus" @click="handleMinus">-</button>
<input type="text" v-model="value" class="input" />
@ -46,7 +38,8 @@
import { ref } from 'vue'
import { updateConsumables } from '../../../../services'
import { eventBus } from '../../../../eventBus'
import { useDeviceStore } from '../../../../store/index'
const deviceStore = useDeviceStore()
//
const isOpen = ref(false)
const value = ref(0)
@ -62,10 +55,12 @@ const query = ref({
const openDialog = (plate, index) => {
isOpen.value = true
value.value = Number(plate.num)
plateIndex.value = index
title.value = plate.projShortName || ''
dialogTitle.value = `请选择${title.value}数值`
query.value.group = `GROUP${plateIndex.value}`
query.value.num = value.value
console.log('🚀 ~ openDialog ~ query:', query.value)
}
defineExpose({
openDialog,
@ -85,14 +80,16 @@ const handleMinus = () => {
//
const handleConfirm = async () => {
isOpen.value = false
console.log('提交的值:', value.value)
// API
const res = await updateConsumables(query.value)
eventBus.emit('confirm', {
value: Number(value.value),
index: plateIndex.value,
})
if (deviceStore.status === 'IDLE') {
// API
const res = await updateConsumables(query.value)
eventBus.emit('confirm', {
value: Number(value.value),
index: plateIndex.value,
})
} else {
ElMessage.error('设备正在工作,无法修改数值')
}
}
</script>
@ -146,32 +143,39 @@ const handleConfirm = async () => {
.modal-body {
width: 100%;
margin-bottom: 20px;
.input-box {
display: flex;
align-items: center;
/* Slider 样式 */
.slider {
width: 70%;
margin: 10px 0;
}
.change-num {
width: 30%;
border: 1px solid #ccc;
display: flex;
justify-content: space-around;
margin-left: 20px;
button {
width: 52px;
height: 46px;
background-color: #f5f7fa;
font-size: 26px;
}
.minus {
border-right: 1px solid #ccc;
}
.plus {
border-left: 1px solid #ccc;
}
.input {
width: 100px;
outline-color: #66b1ff;
@ -188,6 +192,7 @@ const handleConfirm = async () => {
display: flex;
justify-content: flex-end;
width: 100%;
.footer-btn {
width: 100px;
height: 60px;

46
src/pages/Index/components/Consumables/MoveLiquidArea.vue

@ -102,9 +102,6 @@ import { ref, watch, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useEmergencyStore, useConsumablesStore } from '../../../../store'
import { Tube, LiquidState, BottleGroup } from '../../../../types/Index/index'
// import {
// wasteArea,
// } from '../../../../services/index'
import wasteFullIcon from '@/assets/Index/waste-full.svg'
import wasteIcon from '@/assets/Index/waste.svg'
@ -148,44 +145,6 @@ const handleIsUnload = () => {
const activeTab = ref(0)
//
const wasteStatus = ref(props.wasteStatus)
//
const pollInterVal = 3000
//id
// const fetchIdCardStatus = async () => {
// try {
// const res = await getMountedCardInfo()
// if (res.data) {
// IDCardStatus.value = true
// //
// await saveMountedCardInfo()
// } else {
// IDCardStatus.value = false
// }
// } catch (error) {
// console.log('Id', error)
// }
// }
// //
// const fetchWasteStatus = async () => {
// try {
// const res = await wasteArea()
// wasteStatus.value = res.data.wasteBinFullFlag
// } catch (error) {
// console.log('', error)
// }
// }
//
// const startPolling = () => {
// const idCardPoll = setInterval(fetchIdCardStatus, pollInterVal)
// const wastePoll = setInterval(fetchWasteStatus, pollInterVal)
//
// onBeforeUnmount(() => {
// console.log("")
// clearInterval(idCardPoll)
// clearInterval(wastePoll)
// })
// }
const isActive = ref(false)
watch(
@ -200,10 +159,7 @@ watch(
},
{ immediate: true }, //
)
//
// onMounted(() => {
// startPolling()
// })
const emergencyInfo = reactive(emergencyStore.$state.emergencyInfo)
const router = useRouter()
//

23
src/pages/Index/components/Consumables/ProjectSelector.vue

@ -51,9 +51,11 @@
import { ref } from 'vue'
import type { ReactionPlate } from '../../../../types/Index'
import { nanoid } from 'nanoid'
import { useConsumablesStore } from '../../../../store'
import { useConsumablesStore, useSettingTestTubeStore } from '../../../../store'
import { updateTubeConfig } from '../../../../services/Index/Test-tube/test-tube'
const consumables = useConsumablesStore()
const settingTestTubeStore = useSettingTestTubeStore()
const emit = defineEmits<{
(e: 'confirm'): void
(e: 'cancel'): void
@ -72,7 +74,7 @@ defineExpose({
const projects = ref<ReactionPlate[]>(consumables.$state.plates || [])
const bloodType = ref('')
const selectedProjects = ref<number[]>([]) //
const selectedProjects = ref<number[]>([]) //
const bloodTypes = ref([
{
id: nanoid(),
@ -123,9 +125,15 @@ const toggleAutoProject = () => {
}
//
const handleConfirm = () => {
//
emitUpdate()
//
//
props.selectedSampleIds.forEach(async (tubeIndex) => {
settingTestTubeStore.updateTubeSetting(props.uuid, {
tubeIndex,
projId: selectedProjects.value,
bloodType: bloodType.value
})
})
emit('confirm')
}
@ -160,6 +168,11 @@ const emitUpdate = () => {
bloodType: bloodType.value,
})
}
const props = defineProps<{
uuid: string
selectedSampleIds: number[]
}>()
</script>
<style lang="less" scoped>

112
src/pages/Index/components/History/HistoryMessage.vue

@ -1,16 +1,22 @@
<template>
<teleport to="body">
<div v-if="isVisible" class="modal-overlay" @click.self="close">
<div class="modal-content">
<slot></slot>
<button class="close-btn" @click="close">×</button>
<transition name="fade">
<div v-if="isVisible" class="modal-overlay" @click.self="close">
<div class="modal-content">
<div class="modal-header">
<h2>检测详情</h2>
<button class="close-btn" @click="close">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<script lang="ts" setup>
// isVisible
defineProps({
isVisible: {
type: Boolean,
@ -18,12 +24,10 @@ defineProps({
},
})
//
const emit = defineEmits(['update:isVisible'])
//
const close = () => {
emit('update:isVisible', false) // isVisible
emit('update:isVisible', false)
}
</script>
@ -42,29 +46,79 @@ const close = () => {
.modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
max-width: 700px;
max-height: 1100px;
height: 100%;
width: 100%;
box-sizing: border-box;
position: relative;
border-radius: 16px;
width: 90%;
max-width: 800px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
.close-btn {
position: absolute;
top: 10px;
right: 10px;
background-color: transparent;
border: none;
font-size: 32px;
cursor: pointer;
color: #333;
margin-bottom: 10px;
&:hover {
color: #d9534f;
.modal-header {
padding: 24px 32px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
font-size: 32px;
font-weight: 500;
color: #303133;
}
.close-btn {
background: transparent;
border: none;
font-size: 36px;
color: #909399;
cursor: pointer;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s;
&:hover {
background-color: #f5f7fa;
color: #f56c6c;
}
}
}
.modal-body {
padding: 32px;
overflow-y: auto;
flex: 1;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: #f5f7fa;
}
}
}
}
//
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scale(0.9);
}
</style>

277
src/pages/Index/components/History/HistoryTable.vue

@ -1,35 +1,21 @@
<template>
<div id="table-container" class="custom-table-container">
<!-- 骨架屏占位符 -->
<!-- <div v-if="loading" class="skeleton-table">
<div class="skeleton-row" v-for="i in 20" :key="i">
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
<div class="skeleton-cell"></div>
</div>
</div> -->
<table class="custom-table">
<thead>
<tr>
<th>
<th style="width: 5%">
<input type="checkbox" @change="toggleSelectAll" :checked="isAllSelected" class="custom-checkbox" />
</th>
<th>序号</th>
<th>患者ID</th>
<th>项目</th>
<th>样本种类</th>
<th>结果</th>
<th>日期</th>
<th>批次</th>
<th style="width: 8%">序号</th>
<th style="width: 15%">患者ID</th>
<th style="width: 15%">项目</th>
<th style="width: 12%">样本种类</th>
<th style="width: 15%">结果</th>
<th style="width: 15%">日期</th>
<th style="width: 15%">批次</th>
</tr>
</thead>
<tbody>
<!-- 空数据时显示提示行 -->
<tr v-if="isTableEmpty">
<td colspan="8" class="no-data-row">暂无数据</td>
</tr>
@ -37,19 +23,18 @@
'row-selected': selectedRows.includes(item),
'row-active': activeRow === item,
}">
<td @click.stop>
<td>
<input type="checkbox" :value="item" :checked="selectedRows.includes(item)" @change="toggleSelectRow(item)"
class="custom-checkbox" />
</td>
<td>{{ item.id }}</td>
<td class="no-wrap">{{ item.sampleUserid }}</td>
<td>{{ item.projName }}</td>
<td>{{ item.sampleBloodType }}</td>
<td>{{ "无结果" }}</td>
<td class="ellipsis" :title="item.sampleUserid">{{ item.sampleUserid }}</td>
<td class="ellipsis" :title="item.projName">{{ item.projName }}</td>
<td class="ellipsis" :title="item.sampleBloodType">{{ item.sampleBloodType }}</td>
<td class="ellipsis">{{ "无结果" }}</td>
<td>{{ formatDate(item.creatDate) }}</td>
<td>{{ item.lotId }}</td>
<td class="ellipsis" :title="item.lotId">{{ item.lotId }}</td>
</tr>
<!-- 加载状态 -->
<tr v-if="loading">
<td colspan="8" class="loading">{{ loadingText }}</td>
</tr>
@ -132,177 +117,155 @@ const selectRow = (item: TableItem) => {
selectedIds.value = [item.id]
emit('selectRow', item)
}
</script>
<style scoped lang="less">
#table-container {
max-width: 100%;
position: relative;
.loading {
font-size: 32px;
font-weight: 700;
}
.skeleton-table {
width: 100%;
border-collapse: collapse;
text-align: center;
height: 88rem;
/* 设置最大高度,和表格一致 */
overflow-y: auto;
/* 保持滚动条的功能 */
display: block;
.skeleton-row {
display: flex;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
/* 骨架屏的样式与表格列宽一致 */
.skeleton-cell:nth-child(1) {
width: 36px;
/* 设置复选框列的宽度 */
}
.skeleton-cell:nth-child(2),
.skeleton-cell:nth-child(3),
.skeleton-cell:nth-child(4),
.skeleton-cell:nth-child(5),
.skeleton-cell:nth-child(6),
.skeleton-cell:nth-child(7),
.skeleton-cell:nth-child(8) {
width: 150px;
/* 设置表格数据列的宽度 */
}
.skeleton-cell:nth-child(3) {
width: 180px;
/* 对应 '患者ID' 列更宽一些 */
}
}
.skeleton-cell {
/* 设置每列的宽度与实际表格一致 */
padding: 21px;
margin: 0 10px;
background-color: #e0e0e0;
border-radius: 4px;
animation: pulse 1.5s infinite ease-in-out;
}
@keyframes pulse {
0% {
background-color: #e0e0e0;
}
const tableRef = ref()
50% {
background-color: #d0d0d0;
}
100% {
background-color: #e0e0e0;
}
}
//
const clearSelection = () => {
if (tableRef.value) {
tableRef.value.clearSelection()
}
}
/* 空数据提示样式 */
.no-data-row {
text-align: center;
font-size: 1.25rem;
color: #888;
height: 50px;
line-height: 50px;
font-weight: bold;
}
//
defineExpose({
clearSelection
})
</script>
<style scoped lang="less">
#table-container {
width: 100%;
height: 100%;
position: relative;
.custom-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
border-collapse: separate;
border-spacing: 0;
text-align: center;
th,
td {
padding: 12px;
word-wrap: break-word;
white-space: normal;
font-size: 1.875rem;
}
/* 针对“患者ID”列,禁用自动换行 */
.no-wrap {
padding: 16px 8px;
font-size: 24px;
transition: background-color 0.3s;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: normal;
}
/* 设置除“患者ID”列之外的列的最大宽度 */
td:not(.no-wrap),
th:not(.no-wrap) {
max-width: 150px;
/* 根据需要调整最大宽度 */
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 0;
&:hover {
cursor: pointer;
}
}
th {
color: #689bfe;
border-bottom: none;
font-weight: 600;
color: #606266;
font-weight: 500;
background-color: #f5f7fa;
position: sticky;
top: 0;
background-color: #fff;
z-index: 1;
z-index: 2;
border-bottom: 1px solid #ebeef5;
&:first-child {
border-top-left-radius: 8px;
}
&:last-child {
border-top-right-radius: 8px;
}
}
td {
border-bottom: 1px solid #f0f0f0;
background-color: white;
color: #606266;
border-bottom: 1px solid #ebeef5;
}
.row-selected {
background-color: #d9d9d9 !important;
}
tr {
&:hover td {
background-color: #f5f7fa;
}
&.row-selected td {
background-color: #ecf5ff;
}
.row-active {
background-color: #98b9f9 !important;
&.row-active td {
background-color: #409eff15;
}
}
.custom-checkbox {
width: 36px;
height: 36px;
width: 24px;
height: 24px;
cursor: pointer;
position: relative;
appearance: none;
border: 1px solid #c0c4cc;
border: 1px solid #dcdfe6;
border-radius: 4px;
transition: all 0.3s;
background-color: white;
&:checked::before {
content: '✔';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #689bfe;
font-size: 20px;
font-weight: bold;
&:checked {
background-color: #409eff;
border-color: #409eff;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(45deg);
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
margin-top: -1px;
}
}
&:hover {
border-color: #689bfe;
border-color: #409eff;
}
}
}
/* 隐藏滚动条但保留滚动功能 */
.custom-table-container {
max-height: 88rem;
overflow-y: scroll;
/* 保留滚动功能 */
scrollbar-width: none;
/* Firefox 隐藏滚动条 */
.loading {
font-size: 26px;
font-weight: 500;
color: #909399;
text-align: center;
padding: 20px 0;
}
.no-data-row {
text-align: center;
padding: 32px 0;
color: #909399;
font-size: 24px;
background-color: #fff;
}
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 3px;
}
.custom-table-container::-webkit-scrollbar {
display: none;
/* Chrome 和 Safari 隐藏滚动条 */
&::-webkit-scrollbar-track {
background: #f5f7fa;
}
}
</style>

2
src/pages/Index/components/Setting/DelMessage.vue

@ -20,7 +20,7 @@
</template>
<script setup lang="ts">
import { defineProps, ref, onMounted, nextTick } from 'vue'
import { defineProps, ref, onMounted } from 'vue'
//
const props = withDefaults(defineProps<{

39
src/services/Index/Test-tube/test-tube.ts

@ -1,5 +1,11 @@
import apiClient from '../../../utils/axios'
import type { handleTube, TubeActivationStatus } from '../../../types/Index'
import type {
handleTube,
TubeActivationStatus,
DataItem,
Setting,
TubeSetting,
} from '../../../types/Index'
//获取已经配置的试管信息
export const getTestTube = async () => {
try {
@ -47,19 +53,6 @@ export const getProjectInfo = async () => {
}
}
//更新试管信息
export const updateTubeInfo = async (data: handleTube) => {
try {
const res = await apiClient.post(
'/api/v1/app/appTubeSettingMgr/updateTubeHolderSetting',
data,
)
return res.data
} catch (error) {
console.log('更新试管信息时出错:', error)
}
}
// 轮询获取实时的条形码和用户 ID 信息
export const pollTubeInfo = async () => {
try {
@ -86,3 +79,21 @@ export const updateTubeActivationStatus = async (
console.log('更新试管激活状态时出错:', error)
}
}
// 更新试管配置
export const updateTubeConfig = async (config: {
uuid: string
setting: TubeSetting
}) => {
console.log('🚀 ~ updateTubeConfig ~ config:', config)
try {
const res = await apiClient.post(
'/api/v1/app/appTubeSettingMgr/updateTubeSetting',
config.setting,
{ params: { uuid: config.uuid } },
)
return res.data
} catch (error) {
console.error('更新试管配置失败:', error)
return { success: false }
}
}

10
src/services/Index/history.ts

@ -35,7 +35,6 @@ export const searchHistoryInfo = async (search: string) => {
'/api/v1/app/reactionResult/searchRecord',
{ search },
)
console.log('搜索返回的结果', res.data)
return res.data
} catch (error) {
console.log(error)
@ -43,12 +42,11 @@ export const searchHistoryInfo = async (search: string) => {
}
//打印
export const printHistoryInfo = async (ids: number[]) => {
export const printHistoryInfo = async (id: number) => {
try {
const res = await apiClient.post('/api/v1/app/reactionResult/printRecord', {
ids,
})
console.log(res.data)
const res = await apiClient.post(
`/api/v1/app/reactionResult/printfRecord?id=${id}`,
)
return res.data
} catch (error) {
console.log('打印出错', error)

19
src/services/Index/regular.ts

@ -43,12 +43,11 @@ export const oneScanConsumable = async (data: oneConsumableParams) => {
}
//修改耗材数量
export const updateConsumables = async (data: any) => {
console.log('🚀 ~ updateConsumables ~ data:', data)
const { group, num } = data
try {
const res = await apiClient.post(
'/api/v1/app/consumablesMgr/setCounsumableNum',
{
...data,
},
`/api/v1/app/consumablesMgr/setCounsumableNum?group=${group}&num=${num}`,
)
console.log('修改耗材参数', data)
return res.data
@ -56,6 +55,18 @@ export const updateConsumables = async (data: any) => {
console.log(error)
}
}
//修改tips数量
export const updateTipsNum = async (data: any) => {
const { group, num } = data
try {
const res = await apiClient.post(
`/api/v1/app/consumablesMgr/setTipNum?group=${group}&num=${num}`,
)
return res.data
} catch (error) {
console.log(error)
}
}
//废料区接口轮询 查询
export const wasteArea = async () => {
try {

2
src/store/index.ts

@ -1,3 +1,5 @@
export * from './modules/consumables'
export * from './modules/emergency'
export * from './modules/test-tube'
export * from './modules/device'
export * from './modules/settingTestTube'

24
src/store/modules/device.ts

@ -0,0 +1,24 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { DeviceWorkStateMessage } from '../../websocket/socket'
export const useDeviceStore = defineStore('device', () => {
//设备状态
const status = ref<string>('')
//是否出现严重错误
const isFatalError = ref<boolean>(false)
const deviceState = ref<DeviceWorkStateMessage['data']>(
{} as DeviceWorkStateMessage['data'],
)
function setDeviceState(data: DeviceWorkStateMessage['data']) {
// console.log('🚀 ~ setDeviceState ~ data:', data)
deviceState.value = data
status.value = data.workState
isFatalError.value = data.fatalErrorFlag
}
return {
deviceState,
status,
isFatalError,
setDeviceState,
}
})

60
src/store/modules/settingTestTube.ts

@ -0,0 +1,60 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface TubeSetting {
tubeIndex: number
userid: string
sampleBarcode: string
projId: number[]
bloodType: string
}
interface TubeConfig {
uuid: string
setting: TubeSetting
}
export const useSettingTestTubeStore = defineStore('settingTestTube', () => {
const currentConfig = ref<TubeConfig>({
uuid: '',
setting: {
tubeIndex: 0,
userid: '',
sampleBarcode: '',
projId: [],
bloodType: ''
}
})
// 更新试管设置
const updateTubeSetting = (uuid: string, setting: Partial<TubeSetting> & { tubeIndex: number }) => {
currentConfig.value = {
uuid,
setting: {
...currentConfig.value.setting,
...setting
}
}
}
// 清空配置
const clearConfig = () => {
currentConfig.value = {
uuid: '',
setting: {
tubeIndex: 0,
userid: '',
sampleBarcode: '',
projId: [],
bloodType: ''
}
}
}
return {
currentConfig,
updateTubeSetting,
clearConfig
}
})

14
src/types/Index/TestTube.ts

@ -48,3 +48,17 @@ export interface TubeActivationStatus {
uuid: string
active: boolean
}
export interface Setting {
tubeIndex: number
userid: string
sampleBarcode: string
projId: number[]
bloodType: string
}
export interface UpdateTubeSettingsResponse {
success: boolean
message?: string
data?: any
}

2
src/websocket/socket.ts

@ -337,7 +337,7 @@ class WebSocketClient {
}
// 创建单例
let wsInstance: WebSocketClient | null = null
// let wsInstance: WebSocketClient | null = null
// 导出消息类型
export type {

2
tsconfig.app.tsbuildinfo

@ -1 +1 @@
{"root":["./src/eventbus.ts","./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/mock/os-control.ts","./src/mock/user-manage.ts","./src/mock/index.ts","./src/mock/index/consumables.ts","./src/mock/index/emergency.ts","./src/mock/index/history.ts","./src/mock/index/initable.ts","./src/mock/index/running.ts","./src/mock/index/testtube.ts","./src/pages/index/components/index.ts","./src/pages/index/components/consumables/index.ts","./src/pages/index/components/consumables/warn/index.ts","./src/pages/index/components/history/index.ts","./src/pages/index/components/running/index.ts","./src/pages/index/components/setting/index.ts","./src/pages/index/components/testtube/index.ts","./src/pages/index/utils/generatesamplebackground.ts","./src/pages/index/utils/getbloodtypelabel.ts","./src/pages/index/utils/index.ts","./src/pages/index/utils/processtubesettings.ts","./src/router/router.ts","./src/services/index.ts","./src/services/index/emergency.ts","./src/services/index/history.ts","./src/services/index/idcard.ts","./src/services/index/index.ts","./src/services/index/init.ts","./src/services/index/regular.ts","./src/services/index/user-manage.ts","./src/services/index/test-tube/test-tube.ts","./src/services/index/running/index.ts","./src/services/index/running/running.ts","./src/services/index/settings/index.ts","./src/services/index/settings/settings.ts","./src/services/login/index.ts","./src/services/login/login.ts","./src/services/oscontrol/index.ts","./src/services/oscontrol/os.ts","./src/store/index.ts","./src/store/modules/consumables.ts","./src/store/modules/emergency.ts","./src/store/modules/test-tube.ts","./src/types/env.d.ts","./src/types/index/consumables.ts","./src/types/index/emergency.ts","./src/types/index/history.ts","./src/types/index/idcard.ts","./src/types/index/init.ts","./src/types/index/running.ts","./src/types/index/settings.ts","./src/types/index/testtube.ts","./src/types/index/user.ts","./src/types/index/index.ts","./src/types/index/osctrl.ts","./src/utils/axios.ts","./src/utils/formdate.ts","./src/utils/fuzzymatchbysequence.ts","./src/app.vue","./src/components/keyboard.vue","./src/components/simplekeyboard.vue","./src/pages/index/history.vue","./src/pages/index/index.vue","./src/pages/index/regular.vue","./src/pages/index/setting.vue","./src/pages/index/regular/consumables.vue","./src/pages/index/regular/emergency.vue","./src/pages/index/regular/running.vue","./src/pages/index/regular/testtube.vue","./src/pages/index/settings/device.vue","./src/pages/index/settings/lis.vue","./src/pages/index/settings/navbar.vue","./src/pages/index/settings/users.vue","./src/pages/index/settings/version.vue","./src/pages/index/testtube/changeuser.vue","./src/pages/index/components/consumables/ballgrid.vue","./src/pages/index/components/consumables/changenum.vue","./src/pages/index/components/consumables/idcardinfo.vue","./src/pages/index/components/consumables/infobar.vue","./src/pages/index/components/consumables/maincomponent.vue","./src/pages/index/components/consumables/moveliquidarea.vue","./src/pages/index/components/consumables/plate.vue","./src/pages/index/components/consumables/projectselector.vue","./src/pages/index/components/consumables/spttingplates.vue","./src/pages/index/components/consumables/tabbar.vue","./src/pages/index/components/consumables/time.vue","./src/pages/index/components/consumables/warn/initwarn.vue","./src/pages/index/components/consumables/warn/loadingmodal.vue","./src/pages/index/components/history/historymessage.vue","./src/pages/index/components/history/historytable.vue","./src/pages/index/components/history/historywarn.vue","./src/pages/index/components/running/emergencyresultdialog.vue","./src/pages/index/components/running/littlebufferdisplay.vue","./src/pages/index/components/running/platedisplay.vue","./src/pages/index/components/running/sampledisplay.vue","./src/pages/index/components/setting/addusermodal.vue","./src/pages/index/components/setting/delmessage.vue","./src/pages/index/components/setting/delwarn.vue","./src/pages/index/components/setting/enterpinmodal.vue","./src/pages/index/components/testtube/projectsetting.vue","./src/pages/index/components/testtube/testtuberack.vue","./src/pages/login/login.vue","./src/pages/notfound/notfound.vue"],"version":"5.6.3"}
{"root":["./src/eventbus.ts","./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/components/dialogs/index.ts","./src/mock/os-control.ts","./src/mock/user-manage.ts","./src/mock/index.ts","./src/mock/index/consumables.ts","./src/mock/index/emergency.ts","./src/mock/index/history.ts","./src/mock/index/initable.ts","./src/mock/index/running.ts","./src/mock/index/testtube.ts","./src/pages/index/components/index.ts","./src/pages/index/components/consumables/index.ts","./src/pages/index/components/consumables/warn/index.ts","./src/pages/index/components/history/index.ts","./src/pages/index/components/running/index.ts","./src/pages/index/components/setting/index.ts","./src/pages/index/components/testtube/index.ts","./src/pages/index/utils/generatesamplebackground.ts","./src/pages/index/utils/getbloodtypelabel.ts","./src/pages/index/utils/index.ts","./src/pages/index/utils/processtubesettings.ts","./src/router/router.ts","./src/services/index.ts","./src/services/index/emergency.ts","./src/services/index/history.ts","./src/services/index/idcard.ts","./src/services/index/index.ts","./src/services/index/init.ts","./src/services/index/regular.ts","./src/services/index/user-manage.ts","./src/services/index/test-tube/test-tube.ts","./src/services/index/running/index.ts","./src/services/index/running/running.ts","./src/services/index/settings/index.ts","./src/services/index/settings/settings.ts","./src/services/login/index.ts","./src/services/login/login.ts","./src/services/oscontrol/index.ts","./src/services/oscontrol/os.ts","./src/store/index.ts","./src/store/modules/consumables.ts","./src/store/modules/emergency.ts","./src/store/modules/test-tube.ts","./src/types/env.d.ts","./src/types/index/consumables.ts","./src/types/index/emergency.ts","./src/types/index/history.ts","./src/types/index/idcard.ts","./src/types/index/init.ts","./src/types/index/running.ts","./src/types/index/settings.ts","./src/types/index/testtube.ts","./src/types/index/user.ts","./src/types/index/index.ts","./src/types/index/osctrl.ts","./src/utils/axios.ts","./src/utils/formdate.ts","./src/utils/fuzzymatchbysequence.ts","./src/utils/getserverinfo.ts","./src/websocket/socket.ts","./src/app.vue","./src/components/keyboard.vue","./src/components/simplekeyboard.vue","./src/components/dialogs/errormodal.vue","./src/components/dialogs/stackinfomodal.vue","./src/pages/index/history.vue","./src/pages/index/index.vue","./src/pages/index/regular.vue","./src/pages/index/setting.vue","./src/pages/index/regular/consumables.vue","./src/pages/index/regular/emergency.vue","./src/pages/index/regular/running.vue","./src/pages/index/regular/testtube.vue","./src/pages/index/settings/device.vue","./src/pages/index/settings/lis.vue","./src/pages/index/settings/navbar.vue","./src/pages/index/settings/users.vue","./src/pages/index/settings/version.vue","./src/pages/index/testtube/changeuser.vue","./src/pages/index/components/consumables/ballgrid.vue","./src/pages/index/components/consumables/changenum.vue","./src/pages/index/components/consumables/idcardinfo.vue","./src/pages/index/components/consumables/infobar.vue","./src/pages/index/components/consumables/maincomponent.vue","./src/pages/index/components/consumables/moveliquidarea.vue","./src/pages/index/components/consumables/plate.vue","./src/pages/index/components/consumables/projectselector.vue","./src/pages/index/components/consumables/spttingplates.vue","./src/pages/index/components/consumables/tabbar.vue","./src/pages/index/components/consumables/time.vue","./src/pages/index/components/consumables/warn/initwarn.vue","./src/pages/index/components/consumables/warn/loadingmodal.vue","./src/pages/index/components/history/historymessage.vue","./src/pages/index/components/history/historytable.vue","./src/pages/index/components/history/historywarn.vue","./src/pages/index/components/running/emergencyresultdialog.vue","./src/pages/index/components/running/littlebufferdisplay.vue","./src/pages/index/components/running/platedisplay.vue","./src/pages/index/components/running/sampledisplay.vue","./src/pages/index/components/setting/addusermodal.vue","./src/pages/index/components/setting/delmessage.vue","./src/pages/index/components/setting/delwarn.vue","./src/pages/index/components/setting/enterpinmodal.vue","./src/pages/index/components/testtube/projectsetting.vue","./src/pages/index/components/testtube/testtuberack.vue","./src/pages/login/login.vue","./src/pages/notfound/notfound.vue"],"version":"5.6.3"}
Loading…
Cancel
Save