feat:样品库管理

This commit is contained in:
houjunxiang
2025-11-19 11:02:11 +08:00
parent 06210e79fd
commit 0494d224be
33 changed files with 1282 additions and 673 deletions

View File

@@ -7,7 +7,7 @@
leftIcon=""
:leftText="`您好!${userInfo.nickname}`"
>
<u-icon @click="popupShow = true" size="28" color="#FFF" name="account-fill" />
<u-icon @click="popupShow = true" size="28" color="#FFF" name="setting-fill" />
</navbar-back>
<up-grid :col="gridCol" :border="false">
@@ -29,7 +29,6 @@ import mePopup from '@/pages/index/me-popup.vue'
// 响应式数据
const popupShow = ref(false)
const isAllowAgainPrint = ref(false)
const menuItemList = ref([
{
@@ -57,63 +56,13 @@ const goTo = url => {
nx.$router.go(url)
}
const checkAllowAgainPrint = () => {
const param = {
userId: userInfo.value.id,
clientId: 'auncel'
}
nx.$api.user.getAppPermission(param).then(res => {
if (!res.success) return
const ret = res.result || []
if (ret.length === 0) return
for (let i = 0; i < ret.length; i++) {
if (ret[i].roleValue && ret[i].roleValue === 2) {
isAllowAgainPrint.value = true
}
}
})
}
const openLaboratoryWs = () => {
const regData = {
msgId: nx.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: userInfo.value.id,
tenantId: userInfo.value.loginTenantId,
userRealName: userInfo.value.realname
}
}
nx.$measure.setRegData(JSON.stringify(regData))
nx.$measure.open()
}
// 生命周期
onMounted(() => {
// checkAllowAgainPrint()
// openLaboratoryWs()
})
onMounted(() => {})
// 动态设置 grid 列数
const { gridCol } = useGridCol([400], [2, 3])
</script>
<style scoped lang="scss">
.title_content {
text-align: center;
padding-top: 75px;
letter-spacing: 10px;
width: 100%;
font-size: 36px;
color: #fff;
}
.banner {
overflow: hidden;
background-repeat: no-repeat;
background-size: 100%;
}
.grid-text {
font-size: 24px;
}

View File

@@ -1,583 +0,0 @@
<template>
<view class="page">
<u-navbar
title="化学分析天平"
:is-back="false"
:border-bottom="false"
title-color="#fff"
:background="{ 'background-image': 'linear-gradient(45deg, rgb(54, 138, 217), rgb(64, 160, 255))' }"
></u-navbar>
<view style="text-align: center"><u-loading mode="flower" :show="showLoading" size="48"></u-loading></view>
<view class="container">
<view class="auncel" v-for="(item, index) in deviceLaboratoryList" :key="item.id">
<view class="title">
<u-row style="width: 100%; padding-bottom: 18rpx" gutter="16">
<u-col span="8">
<text class="measurePointName"
>{{ item.name }} {{ item.controlUserName ? '[' + item.controlUserName + ']远程控制' : '' }}</text
>
</u-col>
<u-col span="4">
<!--
<view style="width: 100%; display: flex; flex-direction: row; justify-content: flex-end;">
<text class="measurePointName" >{{item.isAutoWork ? '自动计量' : '集控接管'}}</text>
<u-icon style="padding: 0 20rpx;" name="file-text" label-pos="bottom" color="#2979ff" size="40" @click="goto(item.measurePointId)"></u-icon>
<u-icon style="padding: 0 20rpx;" name="setting" label-pos="bottom" label="设置" color="#2979ff" size="40" @click="goto(item.measurePointId)"></u-icon>
</view>
-->
</u-col>
</u-row>
</view>
<view :class="item.isConnected == 1 ? 'title-line-green' : 'title-line-gray'"></view>
<view class="weight-container">
<!-- <view class="infrared-container">
<view :class="item.infraredLeft == 0 ? 'infrared-line-gray' : item.infraredLeft == 1 ? 'infrared-line-green':'infrared-line-red'"></view>
</view>
-->
<block v-if="item.deviceType == 'auncel'">
<view class="weight">
<view
:class="
item.weightStable == 0
? 'weight-data-yellow'
: item.weightStable == 1
? 'weight-data'
: 'weight-data-warning'
"
>
{{ item.weightData }}
</view>
<view class="weight-unit">{{ item.weightUnit }}</view>
</view>
</block>
<block v-if="item.deviceType == 'temp'">
<view class="temp">
<view class="charts-box"
><qiun-data-charts
type="gauge"
:opts="temperatureOpts"
:chartData="temperatureChartData"
background="none"
/></view>
<view class="charts-box"
><qiun-data-charts type="gauge" :opts="humidityOpts" :chartData="humidityChartData" background="none"
/></view>
</view>
</block>
<!--
<view class="infrared-container">
<view :class="item.infraredRight == 0 ? 'infrared-line-gray' : item.infraredRight == 1 ? 'infrared-line-green':'infrared-line-red'"></view>
</view>
-->
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
showLoading: false,
runLogMaxLength: 8,
deviceLaboratoryList: [],
temperatureOpts: {},
temperatureChartData: {
categories: [
{
value: 0.2,
color: '#1890ff'
},
{
value: 1,
color: '#2fc25b'
}
],
series: [
{
name: '温度',
data: 0.66
}
]
},
humidityOpts: {},
humidityChartData: {
categories: [
{
value: 1,
color: '#1890ff'
}
],
series: [
{
name: '湿度',
data: 0.66
}
]
}
}
},
onLoad() {
//获取数据
this.getPageData()
//监听websocket返回来的数据
//设备数据
uni.$on('deviceData', res => {
switch (res.deviceType) {
case 'weighbridge':
this.mearsuPointList.forEach((item, index) => {
if (item.measurePointId === res.measurePointId) {
//item.weightData = (index + Number(res.deviceValue)).toFixed(2) + " "+ res.deviceUnit;
item.weightData = res.deviceValue
item.weightUnit = res.deviceUnit
item.weightStable = res.deviceStable
item.measureIsConnected = 1
item.isAutoWork = res.isAutoWork
}
})
break
case 'auncel':
this.deviceLaboratoryList.forEach((item, index) => {
if (item.id === res.deviceId) {
//item.weightData = (index + Number(res.deviceValue)).toFixed(2) + " "+ res.deviceUnit;
item.weightData = res.weightData
item.weightUnit = res.weightUnit
item.weightStable = 1
item.isConnected = 1
}
})
break
default:
break
}
// this.weight = res.weighbridge;
// this.infraredLeft = res.infrared;
// this.infraredRight = res.infrared;
})
//设备状态
uni.$on('deviceStatus', res => {
this.mearsuPointList.forEach((item, index) => {
if (item.measurePointId === res.measurePointId) {
item.devices.forEach((d, i) => {
if (d.id === res.deviceId) {
d.isConnected = res.connected
}
})
}
})
})
//日志数据
uni.$on('logData', res => {
this.mearsuPointList.forEach((item, index) => {
if (item.measurePointId === res.measurePointId) {
if (item.runLogs.length >= this.runLogMaxLength) {
//删除并返回数组的第一个元素
//item.runLogs.shift();
//删除并返回数组的最后一个元素
item.runLogs.pop()
}
//向数组的末尾添加
//item.runLogs.push(res);
//向数组的开头添
item.runLogs.unshift(res)
//滚动
// this.$nextTick(function() {
// item.scrollTop = 20*item.runLogs.length;
// });
}
})
})
//监听控制
uni.$on('controlMeasurePoint', res => {
console.log(res)
let data = res.data
this.mearsuPointList.forEach(i => {
if (i.measurePointId === data.measurePointId) {
if (data.success && data.isControl) {
i.controlUserName = data.controlUserName
} else {
i.controlUserName = ''
}
console.log(i)
}
})
})
//计量点状态
uni.$on('measurePointStatus', res => {
//断开
this.mearsuPointList.forEach(i => {
if (i.measurePointId === res.measurePointId) {
if (res.measureIsConnected != 1) {
i.weightData = ''
i.weightUnit = ''
i.weightStable = 0
i.infraredLeft = 0
i.infraredRight = 0
i.measureIsConnected = 0
if (i.runLogs.length >= this.runLogMaxLength) {
i.runLogs.pop()
}
i.runLogs.unshift({
id: this.$helper.uuid(),
content: '计量点连接断开!',
measurePointId: i.measurePointId,
time: this.$helper.dateFormat(new Date(), 'yy-MM-dd hh:mm:ss')
})
i.devices.forEach(d => {
d.isConnected = 0
})
} else {
i.measureIsConnected = 1
/* if(i.runLogs.length >= 8) {
i.runLogs.shift();
}
i.runLogs.push({
id: this.$helper.uuid(),
content: "计量点连接成功!",
measurePointId: i.measurePointId,
time: this.$helper.dateFormat(new Date(), "yy-MM-dd hh:mm:ss")
}); */
}
}
})
})
//连接断开
uni.$on('connClose', res => {
//重置
this.mearsuPointList.forEach(i => {
i.weightData = ''
i.weightUnit = ''
i.weightStable = 0
i.controlUserName = ''
i.measureIsConnected = 0
if (i.runLogs.length >= this.runLogMaxLength) {
i.runLogs.pop()
}
let lastLog = i.runLogs[i.runLogs.length - 1]
if (lastLog && lastLog.content !== '控制中心连接断开!') {
i.runLogs.unshift({
id: this.$helper.uuid(),
content: '控制中心连接断开!',
measurePointId: i.measurePointId,
time: this.$helper.dateFormat(new Date(), 'yy-MM-dd hh:mm:ss')
})
}
i.devices.forEach(d => {
d.isConnected = 0
})
})
})
},
onUnload() {
//移除监听websocket返回来的数据
uni.$off('deviceData')
uni.$off('deviceStatus')
uni.$off('logData')
uni.$off('controlMeasurePoint')
uni.$off('measurePointStatus')
uni.$off('connClose')
},
filters: {
numFilter(value) {
// 截取当前数据到小数点后两位
let realVal = value.toFixed(2)
return realVal
}
},
computed: {},
methods: {
getPageData() {
//显示loading
this.showLoading = true
//获取计量点数量
this.$u.api
.getDeviceLaboratoryListBy()
.then(res => {
let dataList = res.data
dataList.forEach(i => {
i.weightData = ''
i.weightUnit = ''
i.isConnected = 0
i.weightStable = 0
i.temperature = 0
i.humidity = 0
i.controlUserName = ''
})
this.deviceLaboratoryList = dataList
this.showLoading = false
//打开websocket
this.openLaboratoryWs()
//发送检查计量点控制情况
let checkControl = {
msgId: this.$helper.uuid(),
cmd: 'checkControl',
clientType: 'consoleClient'
}
if (this.$measure.isOpen) {
this.$measure.send(JSON.stringify(checkControl))
}
})
.catch(err => {
this.showLoading = false
console.log(err)
})
},
openLaboratoryWs() {
//注册websocket
let regData = {
msgId: this.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: this.userInfo.user_id,
tenantId: this.userInfo.tenant_id,
userRealName: this.userInfo.real_name
}
}
this.$measure.setRegData(JSON.stringify(regData))
this.$measure.open()
},
scroll(e) {
// console.log(e);
// this.mearsuPointList.forEach((i) => {
// if(i.measurepointid === e.currentTarget.dataset.measurepointid) {
// i.oldScrollTop = e.detail.scrollTop;
// }
// });
},
goto(measurePointId) {
uni.reLaunch({
url: '/pages/measure/measure?measurePointId=' + measurePointId
})
}
}
}
</script>
<style>
.container {
width: 100%;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
/* #ifdef H5 */
.auncel {
width: 31.33%;
display: flex;
flex-direction: column;
padding: 20rpx;
margin: 1%;
border-radius: 24rpx;
box-shadow: 1px 1px 10px #909399;
}
@media only screen and (max-width: 1200px) {
.auncel {
width: 48%;
}
}
@media only screen and (max-width: 600px) {
.auncel {
width: 100%;
}
}
/* #endif */
/* #ifndef H5 */
.auncel {
width: 98%;
display: flex;
flex-direction: column;
padding: 20rpx;
margin: 1%;
border-radius: 24rpx;
box-shadow: 1px 1px 10px #909399;
}
/* #endif */
.title-line-gray {
width: 100%;
height: 6rpx;
top: 0;
left: 0;
background-color: rgb(128, 134, 149);
border-radius: 4rpx 4rpx 0 0;
}
.title-line-green {
width: 100%;
height: 6rpx;
top: 0;
left: 0;
background-color: rgb(27, 201, 142);
border-radius: 4rpx 4rpx 0 0;
}
.title {
display: flex;
flex-direction: row;
justify-content: center;
}
.temp {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
}
.charts-box {
width: 50%;
height: 200px;
}
.measurePointName {
font-size: 30rpx;
}
.weight-container {
padding: 10rpx;
display: flex;
flex-direction: row;
}
.infrared-container {
width: 30%;
display: flex;
justify-content: center;
}
.infrared-line-green {
width: 8rpx;
height: 90rpx;
background-color: #18b566;
}
.infrared-line-red {
width: 8rpx;
height: 90rpx;
background-color: #ff3333;
}
.infrared-line-gray {
width: 8rpx;
height: 90rpx;
background-color: #909399;
}
.infrared-left {
width: 8rpx;
height: 90rpx;
background-color: #18b566;
}
.infrared-left-warning {
width: 8rpx;
height: 90rpx;
background-color: #ff3333;
}
.weight {
width: 100%;
min-width: 200px;
height: 90rpx;
padding: 0 8rpx;
display: flex;
flex-direction: row;
background-color: #2c405a;
}
.weight-data {
flex-grow: 1;
height: 100%;
color: #4cd964;
text-align: right;
line-height: 90rpx;
letter-spacing: 4rpx;
font-size: 90rpx;
font-family: zzjc-lcd;
}
.weight-data-yellow {
flex-grow: 1;
height: 100%;
color: #ffff00;
text-align: right;
line-height: 90rpx;
letter-spacing: 4rpx;
font-size: 90rpx;
font-family: zzjc-lcd;
}
.weight-data-warning {
flex-grow: 1;
height: 100%;
color: #ff3333;
text-align: right;
line-height: 90rpx;
font-size: 90rpx;
font-family: zzjc-lcd;
}
.weight-unit {
color: #ffffff;
font-size: 60rpx;
line-height: 90rpx;
padding-left: 20rpx;
}
.infrared-right {
width: 8rpx;
height: 90rpx;
background-color: #18b566;
}
.infrared-right-warning {
width: 8rpx;
height: 90rpx;
background-color: #ff3333;
}
.device-container {
padding: 20rpx;
width: 100%;
height: 200rpx;
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
justify-content: flex-start;
}
.device {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
}
.device-icon {
width: 40rpx;
height: 40rpx;
background-color: #ff3333;
border-radius: 50%;
}
.device-icon-green {
width: 40rpx;
height: 40rpx;
background-color: #18b566;
border-radius: 50%;
}
.device-icon-gray {
width: 40rpx;
height: 40rpx;
background-color: rgb(128, 134, 149);
border-radius: 50%;
}
.device-icon-red {
width: 40rpx;
height: 40rpx;
background-color: #ff3333;
border-radius: 50%;
}
.run-log {
width: 100%;
padding: 10rpx 20rpx;
height: 400rpx;
background-color: #ecf5ff;
border-radius: 24rpx;
--box-shadow: 1px 1px 10px #909399;
}
.log-content {
font-size: 28rpx;
font-weight: 200;
margin-bottom: 10rpx;
}
</style>

View File

@@ -84,13 +84,25 @@
>
</u-col>
<u-col span="6">
<view class="field-name" v-html="selectedField.title" />
<view class="x-bc">
<view class="field-name" v-html="selectedField.title" />
<u-button
v-if="fillingWay.length > 1"
type="primary"
shape="circle"
plain
size="small"
style="width: 15%"
:text="`切换${currentFillingWay == 'collect' ? '录入' : '采集'}`"
@click="switchFillingWay"
></u-button>
</view>
<zzjc-num-keyboard
ref="myKeyboard"
v-show="selectedField.fillingWay == 'keyboard' && selectedField.type != 'select'"
v-show="currentFillingWay == 'keyboard'"
:numKeyboardParam="numKeyboardParam"
></zzjc-num-keyboard>
<view v-if="selectedField.fillingWay == 'collect'" class="y-f">
<view v-if="currentFillingWay == 'collect'" class="y-f">
<view class="auncel" @click="selectAuncel">
<view class="code">{{ currentAuncel.code }}</view>
<view class="auncel-title"> 杯号{{ currentCupNum }} </view>
@@ -123,7 +135,7 @@
确认采集
</u-button>
</view>
<view v-if="selectedField.fillingWay === 'input'" class="p8">
<view v-if="currentFillingWay === 'input'" class="p8">
<up-textarea v-model="inputValue" placeholder="请输入内容"></up-textarea>
<view class="x-c mt20 pl100 pr100">
<up-button @click="handleResetInputValue" style="width: 30%" :plain="true" text="清空"></up-button>
@@ -354,7 +366,7 @@ const parameterClassifyChange = v => {
const fieldClick = (field, key) => {
if (!field.isEdit) return
if (field.fillingWay === 'input') {
if (currentFillingWay.value === 'input') {
inputValue.value = field.value
}
selectedField.value = field
@@ -366,13 +378,26 @@ const fieldClick = (field, key) => {
let decimalPosition = field.decimalPosition
if (decimalPosition == null || decimalPosition < -1) decimalPosition = -1
numKeyboardParam.decimal = decimalPosition
if (field.fillingWay == 'collect') {
if (currentFillingWay.value == 'collect') {
listenDeviceData()
} else {
closeDeviceListener()
}
}
const currentFillingIndex = ref(0)
const fillingWay = computed(() => {
if (selectedField.value.fillingWay) {
return selectedField.value.fillingWay.split(',')
} else {
return []
}
})
const currentFillingWay = computed(() => {
return fillingWay.value[currentFillingIndex.value] || ''
})
const switchFillingWay = () => {
currentFillingIndex.value = (currentFillingIndex.value + 1) % fillingWay.value.length
}
//自动切换到下一个字段
const autoNextField = () => {
let groupIndex = 0
@@ -1053,9 +1078,13 @@ const listenNumKeyboard = () => {
return
}
//自动补全小数位数
const decimalPosition = selectedField.value.decimalPosition || 0
const decimalPosition = selectedField.value.decimalPosition
let val = res.val
selectedField.value.value = handleRoundFiveNumber(val, decimalPosition)
if (decimalPosition == null) {
selectedField.value.value = val
} else {
selectedField.value.value = handleRoundFiveNumber(val, decimalPosition)
}
calcAnalysisValue(fieldGroup.value)
//自动跳转下一个字段
setTimeout(() => {
@@ -1098,7 +1127,8 @@ const releaseDeviceControl = deviceId => {
data: {
deviceId: deviceId,
isControl: false,
controlRealName: userInfo.value.nickname
controlRealName: userInfo.value.nickname,
controlUserId: userInfo.value.id
}
}
//发送控制数据
@@ -1304,8 +1334,8 @@ onBackPress(() => {
position: relative;
.code {
position: absolute;
top: 10px;
left: 50px;
top: 15px;
left: 55px;
}
}
@@ -1375,6 +1405,7 @@ onBackPress(() => {
.field-name {
font-size: 26px;
padding: 8px;
width: 100%;
}
.my-collapse {
@@ -1401,6 +1432,7 @@ onBackPress(() => {
}
.field-name {
font-size: 16px;
width: 100%;
}
}
</style>

View File

@@ -14,11 +14,13 @@ import nx from '@/nx'
import { useGridCol } from '@/nx/hooks/useGridCol'
let list = reactive([
{ url: '/pages/lims/index/index', name: '设备管理', icon: 'device' },
{ url: '/pages/analysis/index/index', name: '分析管理', icon: 'analyse' }
{ url: '/pages/analysis/index/index', name: '分析管理', icon: 'analyse' },
{ url: '/pages/sampleWarehouse/index/index', name: '样品库管理', icon: 'sampleWarehouse' }
])
// const sysMenus = computed(() => nx.$store('user').sysMenus)
function goSystem(url) {
uni.setStorageSync('defaultModule', url)
uni.reLaunch({ url })
}

View File

@@ -9,6 +9,7 @@
<u-cell-group>
<u-cell icon="grid-fill" title="模块选择" :is-link="true" @click="handleTo('/pages/index/index')" />
<u-cell icon="order" title="打印设置" :isLink="true" @click="handleTo('/pages/setting/print')"></u-cell>
<u-cell icon="info-circle-fill" title="关于我们" :is-link="true" @click="handleTo('/pages/me/aboutMe')" />
</u-cell-group>

View File

@@ -1,7 +1,12 @@
<template>
<view>
<navbar-back title="设备管理系统" :autoBack="false" leftIcon="" :leftText="`您好!${userInfo.realname}`">
<u-icon @click="popupShow = true" size="28" color="#FFF" name="account-fill" />
<navbar-back
title="实验室管理系统【设备管理】"
:autoBack="false"
leftIcon=""
:leftText="`您好!${userInfo.realname}`"
>
<u-icon @click="popupShow = true" size="28" color="#FFF" name="setting-fill" />
</navbar-back>
<up-grid :border="false" :col="gridCol">
<up-grid-item

View File

@@ -5,6 +5,7 @@
</view>
<u-cell-group>
<u-cell icon="setting-fill" title="切换系统" :isLink="true" @click="handleTo('/pages/index/index')"></u-cell>
<u-cell icon="order" title="打印设置" :isLink="true" @click="handleTo('/pages/setting/print')"></u-cell>
<u-cell icon="info-circle-fill" title="关于我们" :isLink="true" @click="handleTo('/pages/me/aboutMe')"></u-cell>
</u-cell-group>
<u-button class="mt40" type="warning" :plain="true" text="退出当前账号" @click="handleLoginOut"></u-button>

View File

@@ -0,0 +1,148 @@
<template>
<view class="p8">
<navbar-back title="库位变更"></navbar-back>
<uni-section type="line" title="库位信息修改"> </uni-section>
<up-input
v-model="locationCode"
placeholder="请扫描库位编码"
prefixIcon="scan"
fontSize="16"
prefixIconStyle="font-size: 30px;"
>
</up-input>
<up-radio-group v-model="changeType">
<up-radio
:customStyle="{ marginBottom: '8px' }"
v-for="(item, index) in changeTypeOptions"
:key="index"
:label="item.label"
:name="item.name"
>
</up-radio>
</up-radio-group>
<up-input
v-model="sampleCode"
:placeholder="`请扫描${changeType == 'sample' ? '样品编号' : '库位编码'}`"
prefixIcon="scan"
fontSize="16"
prefixIconStyle="font-size: 30px;"
>
</up-input>
<uni-section type="line" title="样品及当前归库信息">
<uni-card>
<view
>样品名称<text>{{ sampleData.sampleName }}</text></view
>
<view class="mt4"
>样品库名称<text>{{ sampleData.sampleCode }}</text></view
>
<view class="mt4"
>归库编码<text>{{ sampleData.sampleCode }}</text></view
>
<view class="mt4"
>()库位编码<text>{{ sampleData.sampleCode }}</text></view
>
</uni-card>
</uni-section>
<up-button class="mt20" type="primary" style="width: 50%" text="提交" @click="handleReset"></up-button>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted, toRefs, watch } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import nx from '@/nx'
const changeType = ref('sample')
const changeTypeOptions = reactive([
{
name: 'sample',
label: '按样品变更'
},
{
name: 'location',
label: '按库位变更'
}
])
function isJsonString(str) {
if (typeof str !== 'string') return false
try {
const parsed = JSON.parse(str)
return typeof parsed === 'object' && parsed !== null
} catch (e) {
return false
}
}
const { scanQRInfo } = toRefs(nx.$store('biz'))
watch(scanQRInfo, newVal => {
if (newVal && nx.$router.getCurrentPage().route == 'pages/sampleWarehouse/returnToStock/index') {
try {
if (!isJsonString(newVal)) {
if (!locationCode.value) {
uni.showToast({
title: '请先扫描库位码',
icon: 'none'
})
scanQRInfo.value = ''
return
} else {
if (changeType.value == 'sample') {
sampleCode.value = newVal
} else {
}
// 执行
// handleReturnToStock()
}
} else {
const codeObj = JSON.parse(newVal)
locationCode.value = codeObj.code
}
scanQRInfo.value = ''
} catch (error) {
scanQRInfo.value = ''
uni.showToast({
title: '请扫描样品编码',
icon: 'none'
})
}
}
})
onShow(() => {
scanQRInfo.value = ''
})
let needPrint = ref(false)
let locationCode = ref('')
let sampleCode = ref('')
function handleReturnToStock() {
nx.$api.sampleWarehouse
.execReturnToStock({
warehouseLocationCode: locationCode.value,
sampleCode: sampleCode.value
})
.then(res => {
successCount.value++
if (res.print) {
uni.showToast({
title: `归库成功,归库码为【${res.code}`,
duration: 3000,
icon: 'none'
})
// 执行打印
}
})
}
const successCount = ref(2)
function handleReset() {
locationCode.value = ''
sampleCode.value = ''
successCount.value = 0
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,89 @@
<template>
<view>
<navbar-back
title="样品库管理"
titleWidth="800"
:autoBack="false"
leftIcon=""
:leftText="`您好!${userInfo.nickname}`"
>
<u-icon @click="popupShow = true" size="28" color="#FFF" name="setting-fill" />
</navbar-back>
<up-grid :col="gridCol" :border="false">
<up-grid-item class="mb20 mt20" v-for="item in menuItemList" :key="item.url" @click="goTo(item.url)">
<u-icon :name="item.otherConf.icon" color="#0055A2" size="80" />
<view class="grid-text">{{ item.name }}</view>
</up-grid-item>
</up-grid>
<mePopup :show="popupShow" @update:show="val => (popupShow = val)" />
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import nx from '@/nx'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useGridCol } from '@/nx/hooks/useGridCol'
import mePopup from '@/pages/index/me-popup.vue'
// 响应式数据
const popupShow = ref(false)
const menuItemList = ref([
{
url: '/pages/sampleWarehouse/sampleSearch/index',
otherConf: { icon: '/static/images/menus/records.png' },
name: '样品查询'
},
{
url: '/pages/sampleWarehouse/returnToStock/index',
otherConf: { icon: '/static/images/menus/returnToStock.png' },
name: '样品归库'
},
{
url: '/pages/sampleWarehouse/execChangeLocation/index',
otherConf: { icon: '/static/images/menus/execChangeLocation.png' },
name: '库位变更'
}
])
// 计算属性
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法
const goTo = url => {
nx.$router.go(url)
}
onShow(() => {
//连接打印服务
let printList = uni.getStorageSync('KEY_PRINT_LIST')
if (printList && printList.length > 0) {
for (let print of printList) {
nx.$print.open(print.printIp, print.printPort)
}
} else {
uni.showModal({
title: '提示',
showCancel: false,
content: '打印服务未配置,请在系统设置中配置打印服务',
success: function (res) {
uni.navigateTo({
url: '/pages/setting/print'
})
}
})
}
})
// 生命周期
onMounted(() => {})
// 动态设置 grid 列数
const { gridCol } = useGridCol([400], [2, 3])
</script>
<style scoped lang="scss">
.grid-text {
font-size: 24px;
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<view class="p8">
<navbar-back title="样品归库"></navbar-back>
<uni-section type="line" title="库位编码"> </uni-section>
<up-input
v-model="locationCode"
placeholder="请扫描库位编码"
prefixIcon="scan"
fontSize="16"
prefixIconStyle="font-size: 30px;"
>
</up-input>
<uni-section type="line" title="样品编号"> </uni-section>
<up-input
v-model="sampleCode"
placeholder="请扫描样品编号"
prefixIcon="scan"
fontSize="16"
prefixIconStyle="font-size: 30px;"
>
<template #suffix>
<view class="fs18 font-bold" style="color: red" v-if="successCount > 0">{{ successCount }}</view>
</template>
</up-input>
<up-button class="mt20" type="primary" style="width: 50%" text="清空" @click="handleReset"></up-button>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted, toRefs, watch } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import nx from '@/nx'
function isJsonString(str) {
if (typeof str !== 'string') return false
try {
const parsed = JSON.parse(str)
return typeof parsed === 'object' && parsed !== null
} catch (e) {
return false
}
}
const { scanQRInfo } = toRefs(nx.$store('biz'))
watch(scanQRInfo, newVal => {
if (newVal && nx.$router.getCurrentPage().route == 'pages/sampleWarehouse/returnToStock/index') {
try {
if (!isJsonString(newVal)) {
if (!locationCode.value) {
uni.showToast({
title: '请先扫描库位码',
icon: 'none'
})
scanQRInfo.value = ''
return
} else {
sampleCode.value = newVal
// 执行归库
handleReturnToStock()
}
} else {
const codeObj = JSON.parse(newVal)
locationCode.value = codeObj.code
}
scanQRInfo.value = ''
} catch (error) {
scanQRInfo.value = ''
uni.showToast({
title: '请扫描样品编码',
icon: 'none'
})
}
}
})
onShow(() => {
scanQRInfo.value = ''
})
let needPrint = ref(false)
let locationCode = ref('')
let sampleCode = ref('')
function handleReturnToStock() {
nx.$api.sampleWarehouse
.execReturnToStock({
warehouseLocationCode: locationCode.value,
sampleCode: sampleCode.value
})
.then(res => {
successCount.value++
if (res.print) {
uni.showToast({
title: `归库成功,归库码为【${res.code}`,
duration: 3000,
icon: 'none'
})
// 执行打印
}
})
}
const successCount = ref(2)
function handleReset() {
locationCode.value = ''
sampleCode.value = ''
successCount.value = 0
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,85 @@
<template>
<view class="p4 pt8">
<navbar-back title="样品查询"></navbar-back>
<up-input
v-model="sampleCode"
placeholder="请扫描或者输入样品编号"
prefixIcon="scan"
fontSize="16"
prefixIconStyle="font-size: 30px;"
>
<template #suffix>
<!-- <up-button type="primary" text="查询" icon="search"></up-button> -->
<!-- <up-icon size="20" name="search" @click="handleSearch"></up-icon> -->
</template>
</up-input>
<uni-section type="line" title="样品详情">
<uni-card>
<view>
<view class="x-bc">
<view
>样品名称<text>{{ sampleData.sampleName }}</text></view
>
<up-tag :text="sampleData.returnStatus_dictText"></up-tag>
</view>
<view
>样品编号<text>{{ sampleData.sampleCode }}</text></view
>
<view class="mt4"
>归库编码<text>{{ sampleData.sampleCode }}</text></view
>
<view class="mt4"
>样品库名称<text>{{ sampleData.sampleCode }}</text></view
>
<view class="mt4"
>库位信息<text>{{ sampleData.sampleCode }}</text></view
>
</view>
</uni-card>
</uni-section>
<up-button type="primary" style="width: 90%" text="打印归库标签" @click="handlePrint"></up-button>
</view>
</template>
<script setup>
import { ref, computed, onMounted, toRefs, watch } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import nx from '@/nx'
const { scanQRInfo } = toRefs(nx.$store('biz'))
watch(scanQRInfo, newVal => {
if (newVal && nx.$router.getCurrentPage().route == 'pages/sampleWarehouse/sampleSearch/index') {
try {
sampleCode.value = newVal
handleSearch()
scanQRInfo.value = ''
} catch (error) {
scanQRInfo.value = ''
uni.showToast({
title: '请扫描样品编码',
icon: 'none'
})
}
}
})
onShow(() => {
scanQRInfo.value = ''
})
let sampleCode = ref('')
function handleSearch() {
getSampleDetail()
}
let sampleData = ref({})
async function getSampleDetail() {
sampleData.value = await nx.$api.sample.getSampleDetail({ sampleReturnCode: sampleCode.value })
}
function handlePrint() {}
</script>
<style lang="scss" scoped>
text {
color: #000;
}
</style>

233
pages/setting/print.vue Normal file
View File

@@ -0,0 +1,233 @@
<template>
<view>
<navbar-back title="打印服务">
<view class="navbar-right" slot="right">
<view class="message-box right-item" @click="refreshPrint">
<up-icon name="reload" color="#fff" size="20" />
</view>
<view class="dot-box right-item" @click="addPrintShow">
<up-icon name="plus" color="#fff" size="20" />
</view>
</view>
</navbar-back>
<view>
<up-swipe-action>
<up-swipe-action-item
v-for="(print, index) in printList"
:key="index"
:index="index"
:options="options"
@click="printSwipeClick"
>
<view class="x-f p10 border-b">
<image :src="`/static/images/print${print.isOpen ? '' : '-close'}.png`" mode="aspectFill" />
<view>
<view>服务名称{{ print.printName }}</view>
<view>服务IP地址{{ print.printIp }}</view>
<view>服务端口{{ print.printPort }}</view>
<view>连接状态{{ print.isOpen ? '连接正常!!!' : '连接关闭!!!' }}</view>
</view>
</view>
</up-swipe-action-item>
</up-swipe-action>
</view>
<u-modal
:show="printShow"
title="添加打印服务"
show-cancel-button
confirm-text="添加"
@confirm="addPrint"
@cancel="printShow = false"
>
<view class="slot-content">
<u-form :model="form">
<u-form-item label="名称">
<u-input v-model="form.printName" />
</u-form-item>
<u-form-item label="IP">
<u-input v-model="form.printIp" />
</u-form-item>
<u-form-item label="端口">
<u-input v-model="form.printPort" type="number" />
</u-form-item>
</u-form>
</view>
</u-modal>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import nx from '@/nx'
// 响应式数据
const printShow = ref(false)
const printList = ref([])
const form = ref({
printName: '',
printIp: '',
printPort: 22333
})
const options = [
{
text: '删除',
style: {
fontSize: '14px',
backgroundColor: 'rgb(255,58,49)'
}
}
]
// 方法
const addPrintShow = () => {
if (printList.value.length > 0) {
uni.showToast({
title: '已存在打印服务!',
icon: 'none'
})
return
}
form.value = {
printName: '',
printIp: '',
printPort: 22333
}
printShow.value = true
}
const refreshPrint = () => {
const printListData = uni.getStorageSync('KEY_PRINT_LIST') || []
if (printListData.length > 0) {
printListData.forEach(print => {
nx.$print.open(print.printIp)
})
uni.showToast({
title: '刷新成功!',
icon: 'none'
})
} else {
uni.showModal({
title: '提示',
content: '未配置打印服务,请点击右边的“+”配置打印服务!',
showCancel: false
})
}
}
const addPrint = () => {
const { printName, printIp, printPort } = form.value
if (!printName.trim()) {
uni.showToast({ title: '打印服务名称不允许为空!', icon: 'none' })
return
}
if (!printIp.trim()) {
uni.showToast({ title: '打印服务IP不允许为空', icon: 'none' })
return
}
const ipReg =
/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
if (!ipReg.test(printIp)) {
uni.showToast({ title: '打印服务IP不正确', icon: 'none' })
return
}
let storedList = uni.getStorageSync('KEY_PRINT_LIST') || []
const exists = storedList.some(p => p.printIp === printIp)
if (exists) {
uni.showToast({ title: '打印服务已存在!', icon: 'none' })
return
}
storedList.push({
printName,
printIp,
printPort,
isDefault: storedList.length === 0
})
nx.$print.open(printIp)
uni.setStorageSync('KEY_PRINT_LIST', storedList)
refreshList()
printShow.value = false
}
const printSwipeClick = ({ index }) => {
if (index === 0) {
const delPrint = printList.value[index]
nx.$print.close(delPrint.printIp)
printList.value.splice(index, 1)
if (delPrint.isDefault && printList.value.length > 0) {
printList.value[0].isDefault = true
}
uni.setStorageSync('KEY_PRINT_LIST', printList.value)
uni.showToast({ title: '删除成功!', icon: 'none' })
}
}
const refreshList = () => {
const stored = uni.getStorageSync('KEY_PRINT_LIST') || []
printList.value = stored.map(print => {
const printer = nx.$print.printMap?.get(print.printIp)
return {
...print,
isOpen: printer ? printer.isOpen : false
}
})
}
// 生命周期
onMounted(() => {
refreshList()
uni.$on('printStatus', refreshList)
})
onUnmounted(() => {
uni.$off('printStatus', refreshList)
})
</script>
<style lang="scss" scoped>
.navbar-right {
margin-right: 12px;
display: flex;
}
.right-item {
margin: 0 6px;
position: relative;
color: #ffffff;
display: flex;
}
.slot-content {
font-size: 14px;
color: $u-content-color;
padding-left: 15px;
padding-right: 15px;
}
.item {
display: flex;
padding: 20rpx;
}
image {
width: 60px;
flex: 0 0 60px;
height: 60px;
margin-right: 10px;
border-radius: 6px;
}
</style>