Files
zgty-mas-m/pages/analysis/sample/sample-work-detail.vue
2025-11-18 10:17:46 +08:00

1407 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view>
<navbar-back :autoBack="false" titleWidth="800" :title="title" @leftClick="customBack">
<view class="navbar-right" @click="navRightClick">
<text>填写任务单</text>
<u-icon color="" name="edit-pen" size="19"></u-icon>
</view>
</navbar-back>
<u-row gutter="8">
<u-col span="3">
<up-tabs
:current="activeAssayTypeIndex"
:list="assayGroups"
lineColor="#5ac725"
:activeStyle="{
fontWeight: 'bold',
color: '#0055a2'
}"
:itemStyle="{
height: '35px'
}"
@change="handleAssayTypeChange"
></up-tabs>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="6">
<view class="content-title-name">
<view class="current-sample-code">{{ currentSampleData.sampleCode }}</view>
<view>数据采集或录入</view>
</view>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
<u-col span="3">
<u-dropdown style="height: 35px">
<u-dropdown-item
v-model="curParameterKey"
:title="curParameterTitle"
height="70vh"
:options="fieldGroup"
@change="parameterClassifyChange"
></u-dropdown-item>
</u-dropdown>
<u-gap height="5" bg-color="#0055A2"></u-gap>
</u-col>
</u-row>
<u-row gutter="8" align="top">
<u-col span="3">
<scroll-view class="content-left-scroll" scroll-with-animation scroll-y :scroll-top="scrollTop">
<view
v-for="(sample, index) in sampleDataList"
:key="index"
class="u-tab-item"
:class="[
currentSampleIndex === index ? 'u-tab-item-active' : '',
sample.rollbackStatus === 'in_progress' ? 'u-tab-item-disabled' : ''
]"
:data-current="index"
@tap.stop="switchSample(index, false)"
>
<u-badge type="warning" :value="index + 1"></u-badge>
<view class="ml20">
<view>
{{ sample.sampleCode }}
</view>
<view> {{ sample.sampleName }} </view>
</view>
</view>
</scroll-view>
<u-button
v-if="taskIngredientsStatus === 'allow_submit'"
class="btn-operation"
type="primary"
@click="submitTask()"
>数据上报</u-button
>
<u-button
v-else
class="btn-operation"
type="primary"
:disabled="taskIngredientsStatus === 'in_progress'"
@click="handleIngredients()"
>{{ taskIngredientsStatus === 'initial' ? '下发配料' : '等待配料' }}</u-button
>
</u-col>
<u-col span="6">
<view class="field-name" v-html="selectedField.title" />
<zzjc-num-keyboard
ref="myKeyboard"
v-show="selectedField.fillingWay == 'keyboard' && selectedField.type != 'select'"
:numKeyboardParam="numKeyboardParam"
></zzjc-num-keyboard>
<view v-if="selectedField.fillingWay == 'collect'" class="y-f">
<view class="auncel" @click="selectAuncel">
<view class="code">{{ currentAuncel.code }}</view>
<view class="auncel-title"> 杯号{{ currentCupNum }} </view>
<view class="auncel-weight">
<view class="weight">
<view
:style="{ textAlign: !currentAuncel.isConnected ? 'center' : 'right' }"
:class="
currentAuncel.weightStable === 0
? 'weight-data-yellow'
: currentAuncel.weightStable === 1
? 'weight-data'
: 'weight-data-warning'
"
>
{{ currentAuncel.weightData }}
</view>
</view>
<view class="weight-unit">{{ currentAuncel.weightUnit }}</view>
</view>
</view>
<u-button
class="btn-operation"
type="success"
:disabled="confirmWeightDisabled"
shape="circle"
@click="saveAuncelData"
>
确认采集
</u-button>
</view>
<view v-if="selectedField.fillingWay === '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>
<up-button
@click="selectedField.value = inputValue"
style="width: 30%"
type="success"
text="确认"
></up-button>
</view>
</view>
</u-col>
<u-col span="3">
<view>
<scroll-view class="content-right-scroll" scroll-y scroll-with-animation>
<view>
<!-- <template v-for="(fields, groupIndex) in fieldGroup" :key="'group_' + groupIndex"> -->
<!-- <view> -->
<!-- 组名 -->
<!-- <view class="my-collapse" @click="fields.open = !fields.open">
<text class="title">{{ fields.title }}</text>
<u-icon :name="fields.open ? 'arrow-up' : 'arrow-down'"></u-icon>
</view>
<view
class="content"
:id="'elId' + groupIndex"
:style="{ height: fields.open ? collaHeights[groupIndex] + 'px' : '0' }"
> -->
<up-collapse ref="collapseRef" :value="activeCollapses" :border="false">
<template v-for="(fields, groupIndex) in currentGroup">
<up-collapse-item v-if="fields.label !== '全部'">
<template #title>
<text class="font-bold">{{ fields.label }}</text>
</template>
<template v-for="(field, fieldIndex) in fields.fields" :key="groupIndex + '-' + fieldIndex">
<view
class="form-item-my"
@click="fieldClick(field, groupIndex + '-' + fieldIndex)"
v-show="field.hidden != 1"
:class="{
'selected-field': groupFieldIndex === groupIndex + '-' + fieldIndex,
'disabled-field': !field.isEdit
}"
>
<view
:class="['label-my', { 'label-high-light': field.highlight == 1 }]"
v-html="field.title"
></view>
<view class="content-my">
<!--
如果是select渲染2个组件1个input1个picker.
field.valueText用于显示picker选中的文本
1键盘输入2天平3自动计算4文本输入
-->
<!-- <u-input
border="bottom"
style="width: 120px"
v-if="field.fillingWay == 'input'"
v-model="field.value"
placeholder="请输入"
/> -->
<!-- <view v-if="field.type === 'select'" class="x-bc select-my" @click="field.showPicker = true">
<text v-if="field.valueText">{{ field.valueText }}</text>
<text v-else>请选择</text>
<u-icon name="arrow-down" size="20"></u-icon>
</view>
<u-picker
v-if="field.type === 'select'"
:show="field.showPicker"
:columns="[field.options]"
keyName="dictValue"
@cancel="field.showPicker = false"
@confirm="event => dicPickerConfirm(event, field)"
/> -->
<!--普通输入框 使用文本显示-->
<view class="content-my-text" v-if="field.dataType != 'select'">
<text v-if="!field.value" class="content-my-text-placeholder">{{
!field.fillingWay || field.fillingWay == 'calculate' ? '计算值' : '请输入'
}}</text>
<text
v-else
:class="['content-my-text-value', { 'field-high-light': field.highlight == 1 }]"
>{{ field.value }}</text
>
</view>
</view>
</view>
<view
v-if="field.validation"
class="fs12"
:class="field.validation?.promptType === 'warning' ? 'valid-warning' : 'valid-error'"
>{{ field.validation?.promptMsg }}</view
>
</template>
</up-collapse-item>
</template>
</up-collapse>
<!-- </view>
</view> -->
<!-- </template> -->
</view>
</scroll-view>
<u-button class="btn-operation" type="success" @click="saveDetail()">保存样品数据</u-button>
</view>
</u-col>
</u-row>
<!-- 天平选择-->
<auncel-select-popup
ref="auncelSelector"
v-model:showAuncelSelector="showAuncelSelector"
:previousAuncelId="currentAuncel.id"
@doSelect="auncelDoSelect"
></auncel-select-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed, nextTick, watch, getCurrentInstance } from 'vue'
import { onLoad, onBackPress, onShow, onHide, onUnload } from '@dcloudio/uni-app'
import request from '@/nx/request'
import {
calcAnalysisValue,
handleRoundFiveNumber,
calcRowAnalysisValue,
math,
groupByField,
validateElementRange
} from '@/nx/helper/calcAnalysisValue'
import { number } from 'mathjs'
import AuncelSelectPopup from '@/components/sample/auncel-select-popup.vue'
import { getTenantId } from '@/defaultBaseUrl'
import { useScreenOrientation } from '@/nx/hooks/useScreenOrientation'
import nx from '@/nx'
import { getDataSourceTypeShow } from '../common'
import tools from '@/nx/utils/tools'
const { proxy } = getCurrentInstance()
// 响应式数据定义
const taskId = ref('')
const taskIngredientsStatus = ref('') //配料状态
const configReportTemplateKey = ref('')
const elId = nx.$helper.uuid()
const scrollTop = ref(0) //tab标题的滚动条位置
const menuHeight = ref(0) // 左边菜单的高度
const menuItemHeight = ref(0) // 左边菜单item的高度
const weightDataIsToZero = ref(false) //重量数据是否归零
const currentSampleIndex = ref(0) // 预设当前项的值
let sampleDataList = ref([])
const title = ref('')
const numKeyboardParam = reactive({
decimal: '-1' //数字键盘小数位数,-1为不限制
})
const showAuncelSelector = ref(false) //显示天平选择框
const currentAuncel = ref({
id: '',
name: '',
code: '',
weightStable: 0,
weightData: '请选天平',
weightUnit: ''
})
let selectedField = ref({})
const groupFieldIndex = ref('') //分组的索引
const curParameterTitle = ref('选择字段分类')
const curParameterKey = ref('all')
const conAssayTaskId = ref('')
const busSubCSampleId = ref('')
let fieldGroup = ref([])
const collaHeights = ref([])
const cupNumKey = '杯号'
const inputValue = ref('')
function handleResetInputValue() {
inputValue.value = ''
}
// refs
const myKeyboard = ref(null)
const auncelSelector = ref(null)
// 计算属性
const confirmWeightDisabled = computed(() => {
if (currentAuncel.value.weightStable === 1 && Number(currentAuncel.value.weightData) > 0) {
return false
}
return true
})
const userInfo = computed(() => nx.$store('user').userInfo)
// 方法定义
const getDomHeight = async () => {
await nextTick() // 等待 DOM 更新完成
collaHeights.value = []
if (fieldGroup.value.length) {
fieldGroup.value.forEach((item, index) => {
proxy.$u.getRect('#elId' + index).then(res => {
collaHeights.value.push(res.height)
})
})
}
}
//返回上一页
const customBack = () => {
uni.redirectTo({
url: '/pages/analysis/sample/sample-work-list'
})
}
const navRightClick = () => {
let url = '/pages/analysis/sample/sample-work-edit-task'
url += '?currentTaskId=' + taskId.value
uni.navigateTo({
url: url
})
}
const parameterClassifyChange = v => {
curParameterKey.value = v
curParameterTitle.value = currentGroup.value[0].label
groupFieldIndex.value = ''
selectedField.value = {}
collapseRef.value.init()
autoNextField()
}
const fieldClick = (field, key) => {
if (!field.isEdit) return
if (field.fillingWay === 'input') {
inputValue.value = field.value
}
selectedField.value = field
groupFieldIndex.value = key
if (myKeyboard.value) {
myKeyboard.value.clearNum()
}
//判断小数位数
let decimalPosition = field.decimalPosition
if (decimalPosition == null || decimalPosition < -1) decimalPosition = -1
numKeyboardParam.decimal = decimalPosition
if (field.fillingWay == 'collect') {
listenDeviceData()
} else {
closeDeviceListener()
}
}
//自动切换到下一个字段
const autoNextField = () => {
let groupIndex = 0
let fieldIndex = 0
if (typeof groupFieldIndex.value === 'undefined' || groupFieldIndex.value === '') groupFieldIndex.value = '0-'
const indexV = groupFieldIndex.value.split('-')
groupIndex = number(indexV[0])
fieldIndex = indexV[1] === '' ? -1 : number(indexV[1])
const group = currentGroup.value[groupIndex]
const fields = group.fields
if (fields.length > fieldIndex + 1) {
//切换到下一个字段
const key = groupIndex + '-' + (fieldIndex + 1)
fieldClick(fields[fieldIndex + 1], key)
}
}
//自动切换到下一个样品
const autoNextSample = indexParam => {
let index = 0
if (indexParam !== undefined) {
index = indexParam
} else {
index = currentSampleIndex.value + 1
}
// 在数组范围内,且当前样品是 in_progress就继续往后找
while (index < sampleDataList.value.length) {
const sample = sampleDataList.value[index]
if (sample.rollbackStatus !== 'in_progress') {
// 找到了合法的样品,切换
switchSample(index, true)
return
}
index++
}
}
//手动切换样品
const switchSample = async (index, autoFlag) => {
// if (!autoFlag) {
// const shouldContinue = await tools.showPromiseModal(
// '提示',
// `请确认样品【${currentSampleData.value.sampleCode}】数据已经保存,是否继续?`
// )
// if (!shouldContinue) {
// return // 用户点了取消,直接退出
// }
// }
//重置天平归0
weightDataIsToZero.value = false
if (index === currentSampleIndex.value) return
currentSampleIndex.value = index
// 如果为0意味着尚未初始化
if (menuHeight.value === 0 || menuItemHeight.value === 0) {
const rect = await getElRect('u-tab-item')
menuItemHeight.value = rect.height
}
// 将菜单菜单活动item垂直居中
scrollTop.value = index * menuItemHeight.value + menuItemHeight.value / 2 - menuHeight.value / 2 - 50
uni.showLoading({ title: '加载中...' })
setValueToField()
autoGenerateCupNum()
setTimeout(() => {
uni.hideLoading()
}, 500)
}
// 获取一个目标元素的高度
const getElRect = (elClass, maxRetry = 50) => {
return new Promise((resolve, reject) => {
let retryCount = 0
const execQuery = () => {
uni
.createSelectorQuery()
.in(proxy)
.select('.' + elClass)
.fields({ size: true, rect: true })
.exec(res => {
const result = res[0] // exec 返回数组,取第一个
if (result) {
resolve(result)
} else if (retryCount < maxRetry) {
retryCount++
setTimeout(execQuery, 10)
} else {
reject(new Error(`无法获取元素 .${elClass} 的尺寸,已重试 ${maxRetry}`))
}
})
}
execQuery()
})
}
//读取样品明细字段
const getDetailFieldsAndStatus = autoSelectNextField => {
const taskDetailId = currentSampleData.value.id
//读取回收率配置
loadConRecoveryList()
conAssayTaskId.value = res.additionalProperties.conAssayTaskId
busSubCSampleId.value = res.additionalProperties.busSubCSampleId
const detail = res.additionalProperties.taskDetail
//处理硫值、硫量:未保存过的数据,读取接口返回的硫值、硫量
loadSValue(detail)
calAndCheck()
}
const checkLoadSValue = () => {
const vKey = 'sRange'
const vF = getFieldByKey(vKey)
if (vF === null) return false
const v = vF.value
if (v === null || v === '') return true
try {
if (v === 0 || number(v) === 0) return true
} catch (e) {}
return false
}
//读取硫值、硫量
const loadSValue = detail => {
let flag = checkLoadSValue()
if (!flag) return
const vKey = 'sValue'
const rKey = 'sRange'
const vF = getFieldByKey(vKey)
if (vF !== null) {
vF.value = detail.svalue
}
const rF = getFieldByKey(rKey)
if (rF !== null) {
rF.value = detail.srange
}
}
const getFieldByKey = key => {
const group = fieldGroup.value
let field = null
for (let g of group) {
for (let f of g.fields) {
const dicKey = f.dicKey
if (dicKey && dicKey === key) {
field = f
break
}
}
}
return field
}
//按公式计算值,并检查原数据与计算后的数据是否一致
const calAndCheck = () => {
//提取原本数值
const oldVal = fitUpValues()
calcAnalysisValue(fieldGroup.value)
if (checkDataNew(oldVal)) return
const newVal = fitUpValues(oldVal)
//检查是否一致
let modifyFlag = false
for (const o in oldVal) {
let old = oldVal[o]
if (old === null) old = 0
//判断是否number
try {
if (number(old) !== number(newVal[o])) {
modifyFlag = true
}
} catch (e) {
if (old !== newVal[o]) modifyFlag = true
}
}
if (modifyFlag)
uni.showToast({
title: '数据有修改,请检查所有样品,并重新保存!',
duration: 2000,
icon: 'error'
})
}
//检查是否是新数据
const checkDataNew = data => {
for (const o in data) {
if (data[o] !== null && data[o] !== '0') return false
}
return true
}
//组装表单值,用于数据比对
const fitUpValues = () => {
const val = {}
for (const g of fieldGroup.value) {
for (const f of g.fields) {
const detailId = f.detailId
if (!detailId) continue
val[detailId] = f.value
}
}
return val
}
const checkBh = () => {
//检查杯号
let hasHb = true
for (const g of fieldGroup.value) {
for (const f of g.fields) {
if (f.title && f.title.indexOf(cupNumKey) >= 0) {
if (typeof f.value === 'undefined' || f.value === null || f.value === '') hasHb = false
}
}
}
if (!hasHb) {
nx.$helper.showToast({
title: '杯号不能为空,请填写杯号!'
})
return false
}
return true
}
const checkRange = async () => {
for (const g of fieldGroup.value) {
for (const f of g.fields) {
if (f.validation) {
if (f.validation.promptType === 'error') {
nx.$helper.showToast({
title: '存在范围校验错误,请修正后再保存!',
icon: 'error',
duration: 2000
})
return false
}
if (f.validation.promptType === 'warning') {
const confirm = await tools.showPromiseModal('提示', '检测到范围警告,是否继续保存?')
return confirm
}
}
}
}
return true // 无问题,通过
}
const saveAuncelData = () => {
//保存数据
if (!weightDataIsToZero.value) {
nx.$helper.showToast({
title: '天平必须先回零!'
})
return
}
//获取当前重量
let weight = currentAuncel.value.weightData
selectedField.value.value = weight
//计算
try {
calcAnalysisValue(fieldGroup.value)
} catch (e) {
console.error(e)
}
//自动跳转下一个字段
setTimeout(() => {
autoNextField()
weightDataIsToZero.value = false
}, 100)
}
let dynamicFormData = reactive({})
const saveDetail = async () => {
//检查杯号
if (!checkBh()) return
// 检查范围校验
if (!(await checkRange())) return
setValueToSample()
let params = {
businessAssayTaskId: taskId.value
}
// 如果是空白样和标样就需要根据配置信息来计算质控样、分析样
if (activeAssayTypeKey.value === 'kby' || activeAssayTypeKey.value === 'by') {
// 处理其他页签的输入变化
const configInfomation = currentAssayType.value.configQCSampleMethodInfo
const row = currentSampleData.value
configInfomation.forEach(item => {
const sourceKey = item.source
const sourceValue = row[sourceKey]
if (sourceValue !== undefined) {
if (item.calcMethod === 'calculateAverageValue') {
item.value = calcAverageValue(sourceKey, currentAssayType.value.tableData)
} else {
item.value = handleRoundFiveNumber(row[sourceKey].value, row.decimalPosition)
}
// 如果处理后的值不为空,重新赋值该字段到其他样品类型下的样品上,并触发每一个样品的计算
if (item.value !== null) {
for (const col of currentAssayType.value.columns) {
if (col.fieldIndex === sourceKey) {
dynamicFormData[item.target] = row[sourceKey].value
}
}
updateTableDataByConfigFields()
}
}
})
params.formValue = JSON.stringify(dynamicFormData)
params.assayTaskAnalysisDataList = assayGroups.value.map(item => ({
datas: item.tableData,
analysisType: item.value
}))
} else {
const datas = currentSampleData.value
params.assayTaskAnalysisDataList = [{ datas: [datas], analysisType: activeAssayTypeKey.value }]
}
await nx.$api.assayTask.saveBatchSmpleAndQcAnalysis(params)
// getSampleAnalysisByTaskId()
autoNextSample()
}
// 计算表格列平均值
function calcAverageValue(fieldIndex, tableData) {
const rows = tableData.map(row => row[fieldIndex])
const decimalPosition = rows[0].decimalPosition
const values = rows.map(row => Number(row.value))
if (values.some(item => item == null)) return null
return handleRoundFiveNumber(math.mean(values), decimalPosition)
}
// 表格数据更新后重新计算
function updateTableDataByConfigFields() {
let needCalcTabs = []
needCalcTabs = assayGroups.value.filter(t => t.value !== 'by' && t.value !== 'kby')
if (needCalcTabs.length === 0) return
for (const key in dynamicFormData) {
for (const tab of needCalcTabs) {
const columnObj = tab.columns.find(c => {
if (c.formula) {
let FromKey = c.formula.split(':')
return FromKey[1] === key
}
return false
})
if (!columnObj) continue
tab.tableData.forEach(row => {
// 赋值配置列参与计算
row[columnObj.fieldIndex].value = dynamicFormData[key]
calcRowAnalysisValue(row, columnObj, tab.columns)
})
}
}
}
// 把字段值保存回样品数据中
function setValueToSample() {
let fields = fieldGroup.value.flatMap(item => item.fields)
fields.forEach(item => {
if (
currentSampleData.value.hasOwnProperty(item.fieldIndex) &&
nx.$test.object(currentSampleData.value[item.fieldIndex])
) {
currentSampleData.value[item.fieldIndex].value = item.value
}
})
}
//自动生成杯号(仅顺序称重):第一杯:手填,后续杯 = 上一杯 + 1
const autoGenerateCupNum = () => {
let cupNum = 0
let currentIndex = currentSampleIndex.value
//第一杯
if (currentIndex === 0) return
//取上一个样品的杯号
const sample = sampleDataList.value[currentIndex - 1]
cupNum = sample[cupNumFieldIndex.value].value
//杯号赋值到当前样品
putCupNum(Number(cupNum) + 1, currentIndex, Number(cupNum))
}
const putCupNum = (cupNum, sampleIndex, lastCupNum) => {
for (const fields of fieldGroup.value) {
if (typeof fields === 'undefined') continue
//杯号
for (const field of fields.fields) {
if (field.fieldIndex === cupNumFieldIndex.value) {
if (typeof field.value === 'undefined' || field.value === null || field.value === '') {
field.value = cupNum
return true
}
}
}
}
}
const loadConRecoveryList = () => {
const conBaseSampleId = currentSampleData.value.conBaseSampleId
const rParam = {
pageNo: 1,
pageSize: -1,
conBaseSampleId: conBaseSampleId
}
const storageKey = 'ConRecoveryRateList'
nx.$api.assayTask
.queryConRecoveryRateList(rParam)
.then(res => {
const result = res.result
if (typeof result === 'undefined' || result === null || !result) {
uni.setStorage(storageKey, [])
return
}
const records = result.records
if (!records || records.length === 0) {
uni.setStorage(storageKey, [])
return
}
uni.setStorageSync(storageKey, records)
})
.catch(err => {
console.error(err)
})
}
// 分析样品组
let assayGroups = ref([])
const activeAssayTypeKey = ref('')
const activeAssayTypeIndex = ref(0)
//元素结果范围
let conRangeElementAnalysisList = ref([])
// 杯号fieldIndex
const cupNumFieldIndex = ref('')
const currentAssayType = computed(() => {
return assayGroups.value.find(item => item.value === activeAssayTypeKey.value)
})
// 当前样品数据
const currentSampleData = computed(() => {
if (sampleDataList.value.length > 0) {
return sampleDataList.value[currentSampleIndex.value]
}
return {}
})
watch(
() => currentAssayType.value,
() => {
sampleDataList.value = currentAssayType.value.tableData
fieldGroup.value = groupByField(currentAssayType.value.columns, cupNumFieldIndex)
autoNextSample(0)
setValueToField()
// getDomHeight()
activeCollapses.value = fieldGroup.value.map((_, index) => index)
}
)
const currentCupNum = computed(() => {
for (const item of fieldGroup.value) {
const field = item.fields.find(field => field.fieldIndex === cupNumFieldIndex.value)
if (field) {
return field.value
}
}
return null
})
const currentGroup = computed(() => {
if (!currentAssayType.value || curParameterKey.value === 'all') {
return fieldGroup.value
} else {
return fieldGroup.value.filter(g => g.value === curParameterKey.value)
}
})
watch(
() => currentGroup.value,
() => {
fieldGroup.value.forEach(item => {
item.fields.forEach(field => {
const validation = validateElementRange(
field.fieldIndex,
currentSampleData.value,
conRangeElementAnalysisList.value
)
field.validation = validation
})
})
}
)
const collapseRef = ref()
const activeCollapses = ref([])
function handleAssayTypeChange({ index, value }) {
activeAssayTypeKey.value = value
activeAssayTypeIndex.value = index
currentSampleIndex.value = 0
groupFieldIndex.value = ''
selectedField.value = {}
curParameterKey.value = 'all'
curParameterTitle.value = '选择字段分类'
collapseRef.value.init()
}
// 获取任务数据
async function getSampleAnalysisByTaskId() {
const {
assayTaskAnalysisDataList,
configAssayMethodProjectRangeList,
businessAssayTasNo,
formValue,
ingredientsStatus
} = await nx.$api.assayTask.batchSampleAndQcAnalysisByTaskId(taskId.value)
title.value = '样品分析-任务单编号:' + businessAssayTasNo
taskIngredientsStatus.value = ingredientsStatus
// 处理分析数据
assayGroups.value = assayTaskAnalysisDataList.map(group => {
// 必须深拷贝 datas防止多个表格共享引用
const tableData = JSON.parse(JSON.stringify(group.datas || []))
const columns = group.columns.filter(item => item.paramNo) || []
return {
value: group.analysisType,
name: group.analysisName,
columns,
tableData,
configQCSampleMethodInfo: group.configQCSampleMethod?.configInfomation
? JSON.parse(group.configQCSampleMethod['configInfomation'])['set']
: []
}
})
dynamicFormData = formValue ? JSON.parse(formValue) : {}
// 默认激活第一个 类型
if (!activeAssayTypeKey.value) {
activeAssayTypeKey.value = assayGroups.value.length > 0 ? assayGroups.value[0].value : ''
}
conRangeElementAnalysisList.value = configAssayMethodProjectRangeList || []
}
// 设置字段值
function setValueToField() {
for (const group of fieldGroup.value) {
group.fields.forEach(field => {
let value = getFieldValue(field)
field.value = value
})
}
}
function getFieldValue(field) {
const fieldValue = currentSampleData.value[field.fieldIndex]?.value
if (fieldValue) {
return fieldValue
} else {
return ''
}
}
const apiRequest = async url => {
return request({
url: url,
method: 'GET'
})
}
//读取字段里的API选项
const loadFieldApiData = async fieldGroups => {
for (const fields of fieldGroups) {
for (const field of fields.fields) {
const api = field.api
const type = field.type
if (type === 'select' && api && api !== '') {
//读取API选项
try {
const res = await apiRequest(api)
const data = res
field.options = data
field.rangeKey = 'displayName'
} catch (e) {}
}
}
}
}
/*
* 选择器组件确认事件
* event 事件默认参数
* field 当前字段
* 处理逻辑:
* field.value = 选中的值
* field.valueText = 选中的文本
* */
const dicPickerConfirm = (event, field) => {
const checked = event.value[0]
const displayName = checked.displayName
const value = checked.dictValue
const extField1 = checked.extField1
field.value = value
field.valueText = value
const relatedFieldNo = '8'
//查找并处理关联字段
for (const fieldGroup of fieldGroup.value) {
for (const field of fieldGroup.fields) {
if (field.paramNo === relatedFieldNo) {
field.value = extField1
break
}
}
}
//触发计算公式
calcAnalysisValue(fieldGroup.value)
}
// 天平监听
const listenDeviceData = () => {
//监听websocket返回来的数据
//设备数据
uni.$on('deviceData', res => {
switch (res.deviceType) {
case 'balance':
if (currentAuncel.value.id === res.deviceId) {
currentAuncel.value.weightData = res.weightData
currentAuncel.value.weightUnit = res.weightUnit
currentAuncel.value.weightStable = res.weightStable
currentAuncel.value.isConnected = true
if (Number(res.weightData) === 0) {
weightDataIsToZero.value = true
}
}
break
default:
break
}
})
//设备状态
uni.$on('deviceStatus', res => {
if (currentAuncel.value.id === res.deviceId) {
if (res.connected === 0) {
currentAuncel.value.weightStable = 0
currentAuncel.value.weightData = '天平断开'
currentAuncel.value.weightUnit = ''
}
}
currentAuncel.value.isConnected = res.connected
})
//控制设备状态
uni.$on('controlDevice', res => {
if (currentAuncel.value.id === res.deviceId) {
currentAuncel.value.id = ''
currentAuncel.value.name = ''
currentAuncel.value.code = ''
currentAuncel.value.weightStable = 0
currentAuncel.value.weightData = '请选天平'
currentAuncel.value.weightUnit = ''
}
})
//连接断开
uni.$on('connClose', res => {
//重置
currentAuncel.value.weightData = ''
currentAuncel.value.weightUnit = ''
currentAuncel.value.weightStable = 0
currentAuncel.value.controlUserName = ''
currentAuncel.value.isConnected = false
})
}
const closeDeviceListener = () => {
uni.$off('deviceData')
uni.$off('deviceStatus')
uni.$off('controlDevice')
uni.$off('connClose')
}
const loadDevice = () => {
//注册websocket
let regData = {
msgId: nx.$helper.uuid(),
cmd: 'register',
clientType: 'caaClient',
data: {
userId: userInfo.value.id,
tenantId: getTenantId(),
userRealName: userInfo.value.realname
}
}
nx.$measure.setRegData(JSON.stringify(regData))
nx.$measure.open()
}
// 键盘监听
const listenNumKeyboard = () => {
//键盘监听
uni.$on('keyboardOK', res => {
if (res === null) {
clearFieldVal()
return
}
//自动补全小数位数
const decimalPosition = selectedField.value.decimalPosition || 0
let val = res.val
selectedField.value.value = handleRoundFiveNumber(val, decimalPosition)
calcAnalysisValue(fieldGroup.value)
//自动跳转下一个字段
setTimeout(() => {
autoNextField()
}, 60)
})
}
const clearFieldVal = () => {
uni.showModal({
title: '提示',
content: '清空当前字段的值,是否继续?',
cancelColor: '#0055A2',
confirmColor: '#0055A2',
success: res => {
if (res.cancel) {
return
}
selectedField.value.value = ''
//重新计算
calcAnalysisValue(fieldGroup.value)
}
})
}
const closeNumKeyBoardListener = () => {
uni.$off('keyboardOK')
}
const closeDeviceLink = () => {
const deviceId = currentAuncel.value.id
releaseDeviceControl(deviceId)
}
//释放设备控制
const releaseDeviceControl = deviceId => {
if (!deviceId || deviceId === '') return
let controlDevice = {
msgId: deviceId,
cmd: 'controlDevice',
clientType: 'caaClient',
data: {
deviceId: deviceId,
isControl: false,
controlRealName: userInfo.value.nickname
}
}
//发送控制数据
nx.$measure.send(JSON.stringify(controlDevice))
}
// 天平选择
const selectAuncel = () => {
showAuncelSelector.value = true
}
// 天平选择回调
const auncelDoSelect = res => {
const data = res.data
if (data) {
currentAuncel.value.id = data.deviceId
currentAuncel.value.name = data.deviceName
currentAuncel.value.code = data.deviceCode
}
}
//提交任务单
const submitTask = () => {
if (checkSampleReturning()) return
const url = `/pages/analysis/sample/pdf-preview?businessAssayTaskId=${taskId.value}&reportKey=${configReportTemplateKey.value}&showConfirmBtn=true`
uni.navigateTo({ url })
}
// 配料下发
function handleIngredients() {
if (checkSampleReturning()) return
const params = {
businessAssayTaskId: taskId.value
}
nx.$api.assayTask.taskIngredients(params).then(res => {
getSampleAnalysisByTaskId()
})
}
function checkSampleReturning() {
const analysisSampleData = assayGroups.value.find(t => t.value == 'analysis').tableData
const hasReturning = analysisSampleData.some(item => item.rollbackStatus === 'in_progress')
if (hasReturning) {
nx.$helper.showToast('该任务单存在退回中的样品')
return true
}
return false
}
// 生命周期
const { lockOrientation } = useScreenOrientation()
onLoad(param => {
lockOrientation('landscape')
if (param.currentTaskId) {
taskId.value = param.currentTaskId
configReportTemplateKey.value = param.configReportTemplateKey
getSampleAnalysisByTaskId()
}
loadFieldApiData(fieldGroup.value)
listenNumKeyboard()
})
onShow(() => {
loadDevice()
listenDeviceData()
})
onHide(() => {
closeDeviceListener()
})
onUnload(() => {
closeDeviceLink()
closeDeviceListener()
closeNumKeyBoardListener()
})
onBackPress(() => {
customBack()
return true
})
</script>
<style lang="scss" scoped>
/* 原始样式保留不变 */
.navbar-right {
font-size: 16px;
color: #fff;
display: flex;
}
.content-title-name {
font-size: 18px;
font-weight: 300;
padding: 4px 0;
display: flex;
justify-content: center;
}
.current-sample-code {
font-weight: bold;
margin-right: 20px;
}
.form-item-my {
display: flex;
align-items: center;
min-height: 35px;
font-size: 13px;
border-bottom: 1px solid #dcdcdc;
}
.selected-field {
background-color: #bada55;
}
.disabled-field {
background-color: #eee;
}
.label-my {
flex: 5;
}
.label-high-light {
color: #000;
font-weight: 600;
font-size: 1.2em;
}
.field-high-light {
color: #000;
font-weight: 600;
font-size: 1.2em;
}
.label-my sub {
font-size: 0.6em;
vertical-align: sub;
}
.content-my {
flex: 2;
text-align: right;
padding-right: 4px;
.select-my {
color: #303133;
font-size: 15px;
padding: 6px 8px;
border: 1px solid #dcdcdc;
border-radius: 5px;
}
}
.content-my-text-value {
color: rgb(48, 49, 51);
}
.content-my-text-placeholder {
color: #c0c4cc;
}
.content-left-scroll {
height: 68vh;
}
.content-right-scroll {
height: 68vh;
}
.valid-warning {
color: orange;
}
.valid-error {
color: red;
}
.u-tab-item {
display: flex;
align-items: center;
font-size: 16px;
color: #444;
padding: 4px 8px;
box-sizing: border-box;
border-bottom: 2px dotted #444;
}
.u-tab-item-active {
color: #0055a2;
font-weight: 600;
}
.u-tab-item-disabled {
background-color: #ddd;
pointer-events: none;
opacity: 0.6;
}
.btn-operation {
width: 80%;
font-size: 18px;
margin-top: 10px;
}
.auncel {
width: 90%;
height: 55vh;
background-image: url(/static/images/auncel.png);
background-repeat: no-repeat;
background-size: 100% 100%;
background-position: center;
display: flex;
flex-direction: column;
position: relative;
.code {
position: absolute;
top: 10px;
left: 50px;
}
}
.auncel-title {
flex: 3;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 32px;
}
.auncel-weight {
flex: 2.5;
display: flex;
align-items: center;
padding-bottom: 20px;
box-sizing: border-box;
position: relative;
}
.weight {
font-size: 42px;
width: 100%;
padding: 0 60px;
}
.weight-data {
color: #4cd964;
font-family: zzjc-lcd;
}
.weight-data-yellow {
color: #ffff00;
font-family: zzjc-lcd;
}
.weight-data-warning {
color: #ff3333;
font-family: zzjc-lcd;
}
.weight-unit {
position: absolute;
right: 30px;
color: #ffffff;
font-size: 32px;
}
@media (max-width: 700px) {
.auncel {
height: 45vh;
}
.auncel-title {
font-size: 20px;
}
.auncel-weight {
padding-bottom: 15px;
}
.weight {
font-size: 26px;
}
.weight-unit {
font-size: 20px;
}
}
.field-name {
font-size: 26px;
padding: 8px;
}
.my-collapse {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 10px 5px 0;
.title {
font-weight: bold;
}
}
.content {
overflow: hidden;
padding-right: 10px;
transition: height 100ms;
}
@media (max-width: 700px) {
.content-left-scroll {
height: 45vh;
}
.content-right-scroll {
height: 55vh;
}
.field-name {
font-size: 16px;
}
}
</style>