forked from gzt/A8000
34 changed files with 1016 additions and 342 deletions
-
7components.d.ts
-
4src/eventBus.ts
-
4src/pages/Index/History.vue
-
132src/pages/Index/Index.vue
-
2src/pages/Index/Regular.vue
-
196src/pages/Index/Regular/Consumables.vue
-
4src/pages/Index/Regular/Running.vue
-
4src/pages/Index/Regular/TestTube.vue
-
2src/pages/Index/Settings/Users.vue
-
2src/pages/Index/TestTube/ChangeUser.vue
-
5src/pages/Index/components/Consumables/BallGrid.vue
-
181src/pages/Index/components/Consumables/BallGrid2.vue
-
5src/pages/Index/components/Consumables/ChangeNum.vue
-
140src/pages/Index/components/Consumables/DragAreaEx.vue
-
7src/pages/Index/components/Consumables/InfoBar.vue
-
12src/pages/Index/components/Consumables/MainComponent.vue
-
277src/pages/Index/components/Consumables/MoveLiquidArea.vue
-
36src/pages/Index/components/Consumables/Plate.vue
-
130src/pages/Index/components/Consumables/SliderAreaEx.vue
-
158src/pages/Index/components/Consumables/SpttingPlates.vue
-
3src/pages/Index/components/Consumables/Warn/InitWarn.vue
-
2src/pages/Index/components/History/HistoryTable.vue
-
1src/pages/Index/components/History/HistoryWarn.vue
-
2src/pages/Index/components/Running/LittleBufferDisplay.vue
-
2src/pages/Index/components/Running/PlateDisplay.vue
-
4src/pages/Index/components/Running/SampleDisplay.vue
-
2src/pages/Index/components/Setting/AddUserModal.vue
-
2src/pages/Index/components/Setting/DelMessage.vue
-
2src/pages/Index/components/Setting/DelWarn.vue
-
2src/pages/Index/components/Setting/EnterPinModal.vue
-
2src/pages/Index/components/TestTube/ProjectSetting.vue
-
4src/pages/Index/components/TestTube/TestTubeRack.vue
-
8src/utils/errorHandler.ts
-
14vite.config.ts
@ -0,0 +1,181 @@ |
|||
<template> |
|||
<div class="ball-area" :style="gridStyle"> |
|||
<div class="ball-grid"> |
|||
<div |
|||
v-for="(isActive, index) in ballStates" |
|||
:key="index" |
|||
:class="['ball', { active: isActive }]" |
|||
:style="ballStyle(index)" |
|||
> |
|||
<div class="inner-circle" :style="innerCircleStyle(index)"></div> |
|||
</div> |
|||
</div> |
|||
<!-- 拖动区 --> |
|||
<DragAreaEx |
|||
class="drag-area" |
|||
v-if="canUpdate" |
|||
:hTotal="columns" |
|||
:vTotal="total / columns" |
|||
:hVal="(total - activated) % columns" |
|||
:vVal="(total - activated) / columns" |
|||
@update:sliderValue="updateSliderVal" |
|||
@update:sliderEndValue="updateSliderEndVal" |
|||
/> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue' |
|||
import DragAreaEx from './DragAreaEx.vue' |
|||
import { useDeviceStore } from '../../../../store/index' |
|||
|
|||
// 移液盘复用组件 |
|||
|
|||
// Props 定义 |
|||
const props = defineProps({ |
|||
order: { |
|||
type: Number, |
|||
default: 0, |
|||
}, |
|||
total: { |
|||
type: Number, |
|||
required: true, |
|||
}, |
|||
activated: { |
|||
type: Number, |
|||
default: 0, |
|||
}, |
|||
width: { |
|||
type: String, |
|||
default: '300px', |
|||
}, |
|||
height: { |
|||
type: String, |
|||
default: '300px', |
|||
}, |
|||
columns: { |
|||
type: Number, |
|||
default: 10, // 每行的小球数量 |
|||
}, |
|||
activeColor: { |
|||
type: String, |
|||
default: '#4caf50', // 激活的小球颜色 |
|||
}, |
|||
innerColor: { |
|||
type: String, |
|||
default: 'lightgray', // 未激活时内部小球的颜色 |
|||
}, |
|||
canUpdate: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}) |
|||
const { total, columns } = props |
|||
const deviceStore = useDeviceStore() |
|||
const isDragging = ref(false) |
|||
const ballStates = ref(Array.from({ length: props.total }, () => false)) // 小球激活状态数组 |
|||
|
|||
// 发出自定义事件通知父组件 |
|||
const emit = defineEmits(['update:TipNum']) |
|||
|
|||
const updateSliderVal = async (hVal, vVal) => { |
|||
if (deviceStore.status === 'IDLE') { |
|||
isDragging.value = true |
|||
const empty = parseInt(vVal) * columns + parseInt(hVal) |
|||
const activated = total - empty |
|||
for (let i = 0; i < total; i++) { |
|||
ballStates.value[i] = i < activated |
|||
} |
|||
emit('update:TipNum', activated, props.order, false) |
|||
} else { |
|||
ElMessage.error('设备正在工作,无法修改数值') |
|||
} |
|||
} |
|||
|
|||
const updateSliderEndVal = async (hVal, vVal) => { |
|||
isDragging.value = false |
|||
if (deviceStore.status === 'IDLE') { |
|||
const empty = parseInt(vVal) * columns + parseInt(hVal) |
|||
const activated = total - empty |
|||
emit('update:TipNum', activated, props.order, true) |
|||
} else { |
|||
ElMessage.error('设备正在工作,无法修改数值') |
|||
} |
|||
} |
|||
// 计算网格样式 |
|||
const gridStyle = computed(() => ({ |
|||
width: props.width, |
|||
height: props.height, |
|||
})) |
|||
|
|||
// 计算小球样式 |
|||
const ballStyle = (index) => { |
|||
const ballSize = `${parseFloat(props.width) / props.columns - 2}px` |
|||
return { |
|||
width: ballSize, |
|||
height: ballSize, |
|||
} |
|||
} |
|||
|
|||
// 动态计算内部小球的颜色变化 |
|||
const innerCircleStyle = (index) => { |
|||
const isActive = ballStates.value[index] |
|||
return { |
|||
width: '80%', |
|||
height: '80%', |
|||
backgroundColor: isActive ? props.activeColor : props.innerColor, |
|||
borderRadius: '50%', |
|||
transition: 'background-color 0.3s ease', |
|||
} |
|||
} |
|||
|
|||
// 监听 `props.activated` 的变化,更新小球激活状态 |
|||
watch( |
|||
() => props.activated, |
|||
(newVal) => { |
|||
if (!isDragging.value) { |
|||
for (let i = 0; i < props.total; i++) { |
|||
ballStates.value[i] = i < newVal |
|||
} |
|||
} |
|||
}, |
|||
{ immediate: true }, // 在初次渲染时立即执行 |
|||
) |
|||
</script> |
|||
|
|||
<style scoped lang="less"> |
|||
.ball-area { |
|||
position: relative; |
|||
} |
|||
.ball-grid { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-wrap: wrap-reverse; |
|||
flex-direction: row-reverse; |
|||
background-color: #666; |
|||
border-radius: 10px; |
|||
padding-right: 4px; |
|||
} |
|||
.drag-area { |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
} |
|||
|
|||
.ball { |
|||
margin: 0 2px 2px 0; |
|||
background-color: #fff; |
|||
border-radius: 999px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
transition: background-color 0.3s ease; |
|||
} |
|||
|
|||
.inner-circle { |
|||
transition: background-color 0.3s ease; |
|||
} |
|||
</style> |
@ -0,0 +1,140 @@ |
|||
<template> |
|||
<div |
|||
class="slider-container" |
|||
@mousedown="handleStart" |
|||
@touchstart.prevent="handleStart" |
|||
> |
|||
<div class="slider-track" ref="sliderTrack"> |
|||
<!-- <div |
|||
class="slider-fill" |
|||
:style="{ width: `${(hValue / hTotalVal) * 100}%`, height: `${(vValue / vTotalVal) * 100}%` }" |
|||
></div> |
|||
<span |
|||
:style="{ visibility: `${isDragging ? 'visible' : 'hidden'}` }" |
|||
class="slider-thumb" |
|||
> |
|||
{{ `${hValue.toFixed()}; ${vValue.toFixed()}` }} |
|||
</span> --> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, onMounted, onUnmounted, watch, useTemplateRef } from 'vue' |
|||
|
|||
const props = defineProps({ |
|||
hVal: { |
|||
type: Number, |
|||
default: 25, |
|||
}, |
|||
hTotal: { |
|||
type: Number, |
|||
default: 50, |
|||
}, |
|||
vVal: { |
|||
type: Number, |
|||
default: 25, |
|||
}, |
|||
vTotal: { |
|||
type: Number, |
|||
default: 50, |
|||
}, |
|||
}) |
|||
|
|||
const emit = defineEmits(['update:sliderValue','update:sliderEndValue']) |
|||
|
|||
const hValue = ref(props.hVal) |
|||
const hTotalVal = ref(props.hTotal) |
|||
const vValue = ref(props.vVal) |
|||
const vTotalVal = ref(props.vTotal) |
|||
|
|||
const isDragging = ref(false) |
|||
const startX = ref(0) |
|||
const startY = ref(0) |
|||
const startXValue = ref(0) |
|||
const startYValue = ref(0) |
|||
const sliderTrack = useTemplateRef('sliderTrack') |
|||
|
|||
watch( |
|||
[() => props.hVal, () => props.vVal], |
|||
(newVal) => { |
|||
if (!isDragging.value) { |
|||
hValue.value = newVal[0] |
|||
vValue.value = newVal[1] |
|||
} |
|||
}, |
|||
) |
|||
|
|||
function handleStart(event) { |
|||
const touchEvent = event.touches ? event.touches[0] : event |
|||
startX.value = touchEvent.clientX |
|||
startY.value = touchEvent.clientY |
|||
const rect = sliderTrack.value.getBoundingClientRect() |
|||
startXValue.value = (hValue.value / hTotalVal.value) * rect.width |
|||
startYValue.value = (vValue.value / vTotalVal.value) * rect.height |
|||
|
|||
isDragging.value = true |
|||
document.addEventListener('mousemove', handleDrag) |
|||
document.addEventListener('touchmove', handleDrag, { passive: false }) |
|||
document.addEventListener('mouseup', handleEnd) |
|||
document.addEventListener('touchend', handleEnd) |
|||
} |
|||
|
|||
function handleDrag(event) { |
|||
if (!isDragging.value) return |
|||
|
|||
const touchEvent = event.touches ? event.touches[0] : event |
|||
const deltaX = touchEvent.clientX - startX.value |
|||
const deltaY = touchEvent.clientY - startY.value |
|||
const rect = sliderTrack.value.getBoundingClientRect() |
|||
let percentX = (deltaX + startXValue.value) / rect.width |
|||
let percentY = (deltaY + startYValue.value) / rect.height |
|||
|
|||
hValue.value = Math.min(1, Math.max(0, percentX)) * hTotalVal.value |
|||
vValue.value = Math.min(1, Math.max(0, percentY)) * vTotalVal.value |
|||
|
|||
emit('update:sliderValue', hValue.value.toFixed(), vValue.value.toFixed()) |
|||
} |
|||
|
|||
function handleEnd() { |
|||
emit('update:sliderEndValue', hValue.value.toFixed(), vValue.value.toFixed()) |
|||
isDragging.value = false |
|||
document.removeEventListener('mousemove', handleDrag) |
|||
document.removeEventListener('touchmove', handleDrag) |
|||
document.removeEventListener('mouseup', handleEnd) |
|||
document.removeEventListener('touchend', handleEnd) |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.slider-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
touch-action: none; /* Prevents the browser's default panning behavior */ |
|||
} |
|||
|
|||
.slider-track { |
|||
position: relative; |
|||
height: 100%; |
|||
} |
|||
|
|||
.slider-fill { |
|||
position: absolute; |
|||
/* height: 100%; */ |
|||
background-color: #4caf50; |
|||
} |
|||
|
|||
.slider-thumb { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translateX(-50%) translateY(-50%); |
|||
font-size: 36px; |
|||
color: #FFF; |
|||
-webkit-text-stroke: 2px #000; |
|||
} |
|||
|
|||
.slider-thumb:active { |
|||
cursor: grabbing; |
|||
} |
|||
</style> |
@ -0,0 +1,130 @@ |
|||
<template> |
|||
<div |
|||
class="slider-container" |
|||
@mousedown="handleStart" |
|||
@touchstart.prevent="handleStart" |
|||
> |
|||
<div class="slider-track" ref="sliderTrack"> |
|||
<!-- <div |
|||
class="slider-fill" |
|||
:style="{ |
|||
width: `${(sliderValue / totalVal) * 100}%`, |
|||
visibility: `${isDragging ? 'visible' : 'hidden'}`, |
|||
}" |
|||
></div> |
|||
<div |
|||
:style="{ visibility: `${isDragging ? 'visible' : 'hidden'}` }" |
|||
class="slider-thumb" |
|||
> |
|||
{{ sliderValue.toFixed() }} |
|||
</div> --> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, onMounted, onUnmounted, watch, useTemplateRef } from 'vue' |
|||
|
|||
const props = defineProps({ |
|||
currValue: { |
|||
type: Number, |
|||
default: 25, |
|||
}, |
|||
totalValue: { |
|||
type: Number, |
|||
default: 25, |
|||
}, |
|||
order: { |
|||
type: Number, |
|||
default: 0, |
|||
}, |
|||
}) |
|||
|
|||
const sliderValue = ref(props.currValue) |
|||
const totalVal = ref(props.totalValue) |
|||
const isDragging = ref(false) |
|||
const startX = ref(0) |
|||
const startValue = ref(0) |
|||
const sliderTrack = useTemplateRef('sliderTrack') |
|||
|
|||
const emit = defineEmits(['update:sliderValue', 'update:sliderEndValue']) |
|||
|
|||
watch( |
|||
() => props.currValue, |
|||
(newValue) => { |
|||
if (!isDragging.value) { |
|||
sliderValue.value = newValue |
|||
} |
|||
}, |
|||
) |
|||
|
|||
function handleStart(event) { |
|||
const touchEvent = event.touches ? event.touches[0] : event |
|||
startX.value = touchEvent.clientX |
|||
const rect = sliderTrack.value.getBoundingClientRect() |
|||
startValue.value = |
|||
rect.width - (sliderValue.value / totalVal.value) * rect.width |
|||
// console.log(sliderValue.value, totalVal.value) |
|||
// console.log(startValue.value, rect.width) |
|||
isDragging.value = true |
|||
document.addEventListener('mousemove', handleDrag) |
|||
document.addEventListener('touchmove', handleDrag, { passive: false }) |
|||
document.addEventListener('mouseup', handleEnd) |
|||
document.addEventListener('touchend', handleEnd) |
|||
} |
|||
|
|||
function handleDrag(event) { |
|||
if (!isDragging.value) return |
|||
|
|||
const touchEvent = event.touches ? event.touches[0] : event |
|||
const deltaX = touchEvent.clientX - startX.value |
|||
const rect = sliderTrack.value.getBoundingClientRect() |
|||
let percent = (deltaX + startValue.value) / rect.width |
|||
|
|||
sliderValue.value = |
|||
totalVal.value - Math.min(1, Math.max(0, percent)) * totalVal.value |
|||
// console.log(percent, sliderValue.value) |
|||
emit('update:sliderValue', sliderValue.value.toFixed(), props.order) |
|||
} |
|||
|
|||
function handleEnd() { |
|||
emit('update:sliderEndValue', sliderValue.value.toFixed(), props.order) |
|||
isDragging.value = false |
|||
document.removeEventListener('mousemove', handleDrag) |
|||
document.removeEventListener('touchmove', handleDrag) |
|||
document.removeEventListener('mouseup', handleEnd) |
|||
document.removeEventListener('touchend', handleEnd) |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.slider-container { |
|||
touch-action: none; /* Prevents the browser's default panning behavior */ |
|||
} |
|||
|
|||
.slider-track { |
|||
position: relative; |
|||
height: 100%; |
|||
/* background-color: #ddd; */ |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.slider-fill { |
|||
position: absolute; |
|||
right: 0; |
|||
height: 100%; |
|||
background-color: rgba(255, 255, 255, 0.2); |
|||
} |
|||
|
|||
.slider-thumb { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translateX(-50%) translateY(-50%); |
|||
font-size: 36px; |
|||
font-weight: 600; |
|||
color: #fff; |
|||
-webkit-text-stroke: 2px #000; |
|||
} |
|||
|
|||
</style> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue