You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
545 lines
18 KiB
545 lines
18 KiB
<script setup lang="ts">
|
|
import { getContainerList } from 'apis/container'
|
|
import { createCraft, updateCraft } from 'apis/crafts'
|
|
import { getSolsList } from 'apis/solution'
|
|
import emptyIcon from 'assets/images/empty.svg'
|
|
import { FtMessage } from 'libs/message'
|
|
import { allPropertiesDefined } from 'libs/utils'
|
|
import { cloneDeep } from 'lodash'
|
|
import { onMounted, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
|
|
const props = defineProps({
|
|
sourceData: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
})
|
|
|
|
const emits = defineEmits(['ok', 'cancel'])
|
|
|
|
const route = useRoute()
|
|
const oresId: number = route.query.oreId as unknown as number
|
|
|
|
const containerList = ref<Container.ContainerItem[]>([])
|
|
const solutionList = ref<Solution.SolutionItem[]>([])
|
|
|
|
const loading = ref(true)
|
|
onMounted(async () => {
|
|
containerList.value = (await getContainerList())?.filter(item => item.type === 0)
|
|
solutionList.value = (await getSolsList()).list
|
|
if (props.sourceData) {
|
|
form.value = { ...props.sourceData, stepList: JSON.parse(props.sourceData.steps || '[]') }
|
|
form.value.stepList?.forEach((step: CraftTypes.StepItem) => {
|
|
if (step.params.second) {
|
|
step.params.minutes = Math.floor(step.params.second / 60)
|
|
step.params.seconds = step.params.second % 60
|
|
}
|
|
if (step.params.coolingSecond) {
|
|
step.params.coolingMinutes = Math.floor(step.params.coolingSecond / 60)
|
|
step.params.coolingSeconds = step.params.coolingSecond % 60
|
|
}
|
|
})
|
|
}
|
|
loading.value = false
|
|
})
|
|
|
|
const form = ref<{
|
|
name?: string
|
|
stepList?: CraftTypes.StepItem[]
|
|
steps?: string
|
|
oresId?: number
|
|
id?: number
|
|
}>({
|
|
stepList: [],
|
|
})
|
|
const formRef = ref()
|
|
|
|
const rules = {
|
|
name: [
|
|
{ required: true, trigger: 'blur', message: '请输入工艺名称' },
|
|
],
|
|
}
|
|
|
|
const okHandle = async () => {
|
|
try {
|
|
const valid = await formRef.value.validate()
|
|
if (!valid) {
|
|
return
|
|
}
|
|
// 找到第一个参数不完整的步骤
|
|
const invalidStepIndex = form.value.stepList?.findIndex(
|
|
(step: any, index) => {
|
|
if (['startHeating', 'shaking'].includes(step.method)) {
|
|
if (step.params.minutes || step.params.seconds) {
|
|
step.params.second = (step.params.minutes || 0) * 60 + (step.params.seconds || 0) || undefined
|
|
}
|
|
if (step.params.coolingMinutes || step.params.coolingSeconds) {
|
|
step.params.coolingSecond = (step.params.coolingMinutes || 0) * 60 + (step.params.coolingSeconds || 0) || undefined
|
|
}
|
|
}
|
|
step.params.description = `${index + 1}.`
|
|
switch (step.method) {
|
|
case 'addLiquid':
|
|
step.params.description = step.params.list.map(item => `试管[${item.tubeNums.length === 16 ? '全部' : item.tubeNums.join(',')}]: ${
|
|
item.solutionList.map(s => `添加${solutionList.value.find(ss => ss.id === s.solutionId)?.name}- ${s.volume}ml-偏移量${s.offset}ml`).join(';')
|
|
}`).join(';')
|
|
break
|
|
case 'startHeating':
|
|
step.params.description = `加热: ${step.params.temperature}度, 保持${step.params.minutes || 0}分${step.params.seconds || 0}秒`
|
|
break
|
|
case 'shaking':
|
|
step.params.description = `摇匀: ${step.params.second}秒`
|
|
break
|
|
case 'takePhoto':
|
|
step.params.description = `拍照`
|
|
break
|
|
}
|
|
return !allPropertiesDefined(step.params, ['minutes', 'seconds', 'description'])
|
|
},
|
|
|
|
)
|
|
console.log(form.value)
|
|
|
|
if (invalidStepIndex !== -1) {
|
|
FtMessage.error(`步骤${(invalidStepIndex || 0) + 1}: 请填写完整参数`)
|
|
return
|
|
}
|
|
|
|
form.value.steps = JSON.stringify(form.value.stepList)
|
|
form.value.oresId = oresId
|
|
if (form.value.id) {
|
|
await updateCraft(form.value)
|
|
}
|
|
else {
|
|
await createCraft(form.value)
|
|
}
|
|
FtMessage.success('保存成功')
|
|
emits('ok')
|
|
}
|
|
catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
const cancel = () => {
|
|
emits('cancel')
|
|
}
|
|
|
|
const stepMap: Record<string, CraftTypes.StepItem> = {
|
|
// addLiquid: { name: '加液', method: 'addLiquid', params: { addLiquidList: [{ containerId: undefined, volume: undefined }], description: undefined } },
|
|
startHeating: { name: '加热', method: 'startHeating', params: { temperature: undefined, second: undefined, description: undefined, minutes: undefined, seconds: undefined } },
|
|
shaking: { name: '摇匀', method: 'shaking', params: { second: undefined } },
|
|
takePhoto: { name: '拍照', method: 'takePhoto', params: { } },
|
|
}
|
|
|
|
const addStep = (data: CraftTypes.StepItem) => {
|
|
form.value.stepList?.push(data)
|
|
}
|
|
|
|
const addLiquidForm = ref<any>({
|
|
tubeNums: [],
|
|
solutionList: [
|
|
{
|
|
solutionId: undefined,
|
|
volume: undefined,
|
|
offset: undefined,
|
|
},
|
|
],
|
|
})
|
|
const addLiquidRules = {
|
|
tubeNums: [
|
|
{ required: true, message: '请选择试管', trigger: 'change' },
|
|
],
|
|
}
|
|
|
|
const addLiquidFormRef = ref()
|
|
|
|
const activeTube = ref(Array.from({ length: 16 }).fill(false))
|
|
|
|
const selectVisible = ref(false)
|
|
const checkChange = () => {
|
|
activeTube.value = Array.from({ length: 16 }).fill(selectVisible.value)
|
|
addLiquidForm.value.tubeNums = activeTube.value.map((item, index) => index + 1).filter(item => activeTube.value[item - 1])
|
|
addLiquidFormRef.value.validateField('tubeNums')
|
|
}
|
|
|
|
const mousedownHandle = async (e: Event) => {
|
|
let event
|
|
if ('touches' in e) {
|
|
event = (e.touches as TouchList)[0]
|
|
}
|
|
else {
|
|
event = e
|
|
}
|
|
if (event.target!.classList!.contains('tube-inner')) {
|
|
const num = event.target!.getAttribute('index')
|
|
activeTube.value[Number(num) - 1] = !activeTube.value[Number(num) - 1]
|
|
addLiquidForm.value.tubeNums = activeTube.value.map((item, index) => index + 1).filter(item => activeTube.value[item - 1])
|
|
}
|
|
}
|
|
|
|
const addHandle = async () => {
|
|
try {
|
|
const valid = await addLiquidFormRef.value.validate()
|
|
if (!valid) {
|
|
return
|
|
}
|
|
console.log(addLiquidForm.value)
|
|
// addList.value!.push(addLiquidForm)
|
|
const index = form.value.stepList?.findIndex(item => item.method === 'addLiquid')
|
|
if (index !== -1) {
|
|
form.value.stepList?.[index || 0].params?.list?.push(cloneDeep(addLiquidForm.value))
|
|
}
|
|
else {
|
|
form.value.stepList?.push({
|
|
name: '加液',
|
|
method: 'addLiquid',
|
|
params: { list: [cloneDeep(addLiquidForm.value)] },
|
|
} as CraftTypes.StepItem)
|
|
}
|
|
addLiquidForm.value = {
|
|
tubeNums: [],
|
|
solutionList: [
|
|
{
|
|
solutionId: undefined,
|
|
volume: undefined,
|
|
offset: undefined,
|
|
},
|
|
],
|
|
}
|
|
activeTube.value = Array.from({ length: 16 }).fill(false)
|
|
selectVisible.value = false
|
|
addLiquidFormRef.value.resetFields()
|
|
}
|
|
catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<FtDialog visible :loading :title="form.id ? '编辑工艺' : '新增工艺'" width="90%" :ok-handle="okHandle" @cancel="cancel">
|
|
<el-form ref="formRef" label-width="auto" :model="form" :rules="rules" class="form-box">
|
|
<el-row :gutter="20">
|
|
<el-col :span="12">
|
|
<el-form-item label="工艺名称" prop="name">
|
|
<el-input v-model="form.name" placeholder="请输入工艺名称" />
|
|
</el-form-item>
|
|
<el-form-item label="步骤列表">
|
|
<div class="button-content">
|
|
<el-tag v-for="item in stepMap" :key="item" size="large" @click="() => addStep(item)">
|
|
<div style="display: flex;align-items: center;justify-content: space-around;width: 100%;">
|
|
<el-icon><Plus /></el-icon>
|
|
<span> {{ item.name }}</span>
|
|
</div>
|
|
</el-tag>
|
|
</div>
|
|
<el-form ref="addLiquidFormRef" :model="addLiquidForm" :rules="addLiquidRules" label-width="auto" class="liquid-box">
|
|
<p>加液</p>
|
|
<el-form-item label="" prop="tubeNums">
|
|
<el-checkbox v-model="selectVisible" style="margin-right: 10px" @change="checkChange">
|
|
全选
|
|
</el-checkbox>
|
|
<div class="tube-item" @click.prevent="mousedownHandle" @touch.prevent="mousedownHandle">
|
|
<span v-for="item in 16" :key="item" class="tube-inner" :class="{ 'tube-inner-active': activeTube[item - 1] }" :index="item" />
|
|
</div>
|
|
</el-form-item>
|
|
<el-row>
|
|
<el-col :span="7" style="text-align: center">
|
|
溶液
|
|
</el-col>
|
|
<el-col :span="7" style="text-align: center">
|
|
容量
|
|
</el-col>
|
|
<el-col :span="7" style="text-align: center">
|
|
偏移量
|
|
</el-col>
|
|
</el-row>
|
|
<div class="solution-list">
|
|
<el-row v-for="(s, index) in addLiquidForm.solutionList" :key="index" :gutter="5" class="solution-item">
|
|
<el-col :span="7">
|
|
<el-form-item
|
|
label=""
|
|
:prop="`solutionList.${index}.solutionId`"
|
|
:rules="{
|
|
required: true,
|
|
message: '请选择溶液',
|
|
trigger: 'change',
|
|
}"
|
|
>
|
|
<el-select v-model="s.solutionId" size="small" style="width: 100%" placeholder="溶液">
|
|
<el-option v-for="item in solutionList" :key="item.id" :label="item.name" :value="item.id" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="7">
|
|
<el-form-item
|
|
label="" :prop="`solutionList.${index}.volume`" :rules="{
|
|
required: true,
|
|
message: '请输入容量',
|
|
trigger: 'blur',
|
|
}"
|
|
>
|
|
<el-input v-model.number="s.volume" size="small" type="number" style="width: 100%" placeholder="容量">
|
|
<template #append>
|
|
ml
|
|
</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="7">
|
|
<el-form-item
|
|
label="" :prop="`solutionList.${index}.offset`" :rules="{
|
|
required: true,
|
|
message: '请输入偏移量',
|
|
trigger: 'blur',
|
|
}"
|
|
>
|
|
<el-input v-model.number="s.offset" size="small" type="number" style="width: 100%" placeholder="偏移量">
|
|
<template #append>
|
|
ml
|
|
</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="3" class="icon-box">
|
|
<el-icon v-if="addLiquidForm.solutionList.length > 1" color="red" @click="addLiquidForm.solutionList.splice(index, 1)">
|
|
<RemoveFilled />
|
|
</el-icon>
|
|
<el-icon
|
|
v-if="index === addLiquidForm.solutionList.length - 1" color="green" @click="addLiquidForm.solutionList.push({ solutionId: undefined,
|
|
volume: undefined,
|
|
offset: undefined })"
|
|
>
|
|
<CirclePlusFilled />
|
|
</el-icon>
|
|
</el-col>
|
|
</el-row>
|
|
</div>
|
|
|
|
<el-form-item>
|
|
<div style="width: 100%;display: flex;justify-content: center">
|
|
<ft-button type="primary" @click="addHandle">
|
|
添加
|
|
</ft-button>
|
|
</div>
|
|
</el-form-item>
|
|
</el-form>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<div v-if="form.stepList?.length" class="step-box">
|
|
<div v-for="(item, index) in form.stepList" :key="index" class="step-item">
|
|
<el-form-item :label="`${index + 1}: ${item.name}`">
|
|
<div v-if="item.method === 'addLiquid'" class="list-box">
|
|
<div v-for="(liquid, liquidIndex) in item.params?.list || []" :key="liquidIndex">
|
|
<span>试管: {{ liquid.tubeNums.length === 16 ? '全部' : liquid.tubeNums.join(',') }} </span>
|
|
<div v-for="(s, sIndex) in liquid.solutionList" :key="sIndex">
|
|
<span>添加</span>
|
|
<span>{{ solutionList.find(solution => solution.id === s.solutionId)?.name }}</span>
|
|
<span>{{ s.volume }}</span>
|
|
<span>ml-</span>
|
|
<span>偏移量</span>
|
|
<span>{{ s.offset }}</span>
|
|
<span>ml</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="['startHeating'].includes(item.method)">
|
|
<el-input v-model.number="item.params.temperature" type="number" size="small" placeholder="加热温度">
|
|
<template #append>
|
|
℃
|
|
</template>
|
|
</el-input>
|
|
<br>
|
|
<div>
|
|
<span>保持时间</span>
|
|
<el-select v-model="item.params.minutes" style="width: 70px" clearable size="small" placeholder="分钟">
|
|
<el-option v-for="i in 60" :key="i" :label="i" :value="i" />
|
|
</el-select>
|
|
<span class="unit-text">分</span>
|
|
<el-select v-model="item.params.seconds" style="width: 70px" clearable size="small" placeholder="秒">
|
|
<el-option v-for="i in 60" :key="i" :label="i" :value="i" />
|
|
</el-select>
|
|
<span class="unit-text">秒</span>
|
|
</div>
|
|
<div>
|
|
<span>冷却时间</span>
|
|
<el-select v-model="item.params.coolingMinutes" style="width: 70px" clearable size="small" placeholder="分钟">
|
|
<el-option v-for="i in 60" :key="i" :label="i" :value="i" />
|
|
</el-select>
|
|
<span class="unit-text">分</span>
|
|
<el-select v-model="item.params.coolingSeconds" style="width: 70px" clearable size="small" placeholder="秒">
|
|
<el-option v-for="i in 60" :key="i" :label="i" :value="i" />
|
|
</el-select>
|
|
<span class="unit-text">秒</span>
|
|
</div>
|
|
</div>
|
|
<template v-else-if="['shaking'].includes(item.method)">
|
|
<el-select v-model="item.params.minutes" style="width: 70px" clearable size="small" placeholder="请选择">
|
|
<el-option v-for="i in 60" :key="i" :label="i" :value="i" />
|
|
</el-select>
|
|
<span class="unit-text">分</span>
|
|
<el-select v-model="item.params.seconds" style="width: 70px" clearable size="small" placeholder="请选择">
|
|
<el-option v-for="i in 60" :key="i" :label="i" :value="i" />
|
|
</el-select>
|
|
<span class="unit-text">秒</span>
|
|
</template>
|
|
<el-icon style="margin-left: auto;" @click="() => form.stepList?.splice(index, 1)">
|
|
<Close />
|
|
</el-icon>
|
|
</el-form-item>
|
|
</div>
|
|
</div>
|
|
<div v-else class="empty-box">
|
|
<img :src="emptyIcon" alt="">
|
|
<span>暂无步骤</span>
|
|
</div>
|
|
</el-col>
|
|
</el-row>
|
|
</el-form>
|
|
</FtDialog>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.form-box {
|
|
height: 70vh;
|
|
.el-row {
|
|
height: 100%;
|
|
.el-col:first-child {
|
|
border-right: 1px solid #eee;
|
|
}
|
|
.el-col {
|
|
height: 100%;
|
|
.step-box {
|
|
height: 100%;
|
|
overflow: auto;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.button-content {
|
|
width: 100%;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr); /* 创建3列等宽轨道 */
|
|
grid-template-rows: repeat(2, auto); /* 创建2行自动高度 */
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
:deep(.el-tag__content) {
|
|
width: 100%;
|
|
}
|
|
.empty-box {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #ccc;
|
|
img {
|
|
margin-bottom: 10px;
|
|
}
|
|
}
|
|
.step-item {
|
|
.el-form-item {
|
|
background: rgba(82, 148, 215, 0.06);
|
|
padding: 5px;
|
|
margin-bottom: 10px;
|
|
:deep(.el-form-item__label) {
|
|
height: 25px;
|
|
line-height: 25px;
|
|
}
|
|
:deep(.el-form-item__content) {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.el-input, .el-select {
|
|
width: 120px;
|
|
margin: 0 5px;
|
|
}
|
|
.list-box {
|
|
width: 90%;
|
|
height: 100%;
|
|
.list-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding-bottom: 10px;
|
|
.el-icon {
|
|
cursor: pointer;
|
|
font-size: 20px;
|
|
margin-left: 10px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.unit-text {
|
|
font-size: 12px;
|
|
line-height: 25px;
|
|
}
|
|
|
|
.tube-item {
|
|
padding: 5px;
|
|
background: #384D5D;
|
|
border-radius: 10px;
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
grid-template-rows: repeat(4, 1fr);
|
|
grid-gap: 5px;
|
|
position: relative;
|
|
.tube-disable {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 0;
|
|
background: rgba(255,255,255,0.9);
|
|
border-radius: 9px;
|
|
}
|
|
.tube-inner {
|
|
display: inline-block;
|
|
width: 25px;
|
|
height: 25px;
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
margin: 2px;
|
|
transition: background 0.5s;
|
|
}
|
|
.tube-inner-active {
|
|
background: #26D574;
|
|
}
|
|
}
|
|
|
|
.liquid-box {
|
|
background: #ecf5ff;
|
|
padding: 0 10px;
|
|
p {
|
|
text-align: center;
|
|
color: #1989fa;
|
|
}
|
|
}
|
|
.solution-list {
|
|
max-height: 100px;
|
|
overflow: hidden;
|
|
overflow-y: auto;
|
|
}
|
|
.solution-item {
|
|
margin-bottom: 10px;
|
|
|
|
}
|
|
.el-form-item .el-form-item {
|
|
margin-bottom: 10px;
|
|
}
|
|
.icon-box {
|
|
display: flex;
|
|
margin-top: 4px;
|
|
justify-content: space-between;
|
|
.el-icon {
|
|
font-size: 18px;
|
|
line-height: 100%;
|
|
}
|
|
}
|
|
</style>
|