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

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
  1. <script setup lang="ts">
  2. import { getContainerList } from 'apis/container'
  3. import { createCraft, updateCraft } from 'apis/crafts'
  4. import { getSolsList } from 'apis/solution'
  5. import emptyIcon from 'assets/images/empty.svg'
  6. import { FtMessage } from 'libs/message'
  7. import { allPropertiesDefined } from 'libs/utils'
  8. import { cloneDeep } from 'lodash'
  9. import { onMounted, ref } from 'vue'
  10. import { useRoute } from 'vue-router'
  11. const props = defineProps({
  12. sourceData: {
  13. type: Object,
  14. default: () => ({}),
  15. },
  16. })
  17. const emits = defineEmits(['ok', 'cancel'])
  18. const route = useRoute()
  19. const oresId: number = route.query.oreId as unknown as number
  20. const containerList = ref<Container.ContainerItem[]>([])
  21. const solutionList = ref<Solution.SolutionItem[]>([])
  22. const loading = ref(true)
  23. onMounted(async () => {
  24. containerList.value = (await getContainerList())?.filter(item => item.type === 0)
  25. solutionList.value = (await getSolsList()).list
  26. if (props.sourceData) {
  27. form.value = { ...props.sourceData, stepList: JSON.parse(props.sourceData.steps || '[]') }
  28. form.value.stepList?.forEach((step: CraftTypes.StepItem) => {
  29. if (step.params.second) {
  30. step.params.minutes = Math.floor(step.params.second / 60)
  31. step.params.seconds = step.params.second % 60
  32. }
  33. if (step.params.coolingSecond) {
  34. step.params.coolingMinutes = Math.floor(step.params.coolingSecond / 60)
  35. step.params.coolingSeconds = step.params.coolingSecond % 60
  36. }
  37. })
  38. }
  39. loading.value = false
  40. })
  41. const form = ref<{
  42. name?: string
  43. stepList?: CraftTypes.StepItem[]
  44. steps?: string
  45. oresId?: number
  46. id?: number
  47. }>({
  48. stepList: [],
  49. })
  50. const formRef = ref()
  51. const rules = {
  52. name: [
  53. { required: true, trigger: 'blur', message: '请输入工艺名称' },
  54. ],
  55. }
  56. const okHandle = async () => {
  57. try {
  58. const valid = await formRef.value.validate()
  59. if (!valid) {
  60. return
  61. }
  62. // 找到第一个参数不完整的步骤
  63. const invalidStepIndex = form.value.stepList?.findIndex(
  64. (step: any, index) => {
  65. if (['startHeating', 'shaking'].includes(step.method)) {
  66. if (step.params.minutes || step.params.seconds) {
  67. step.params.second = (step.params.minutes || 0) * 60 + (step.params.seconds || 0) || undefined
  68. }
  69. if (step.params.coolingMinutes || step.params.coolingSeconds) {
  70. step.params.coolingSecond = (step.params.coolingMinutes || 0) * 60 + (step.params.coolingSeconds || 0) || undefined
  71. }
  72. }
  73. step.params.description = `${index + 1}.`
  74. switch (step.method) {
  75. case 'addLiquid':
  76. step.params.description = step.params.list.map(item => `试管[${item.tubeNums.length === 16 ? '全部' : item.tubeNums.join(',')}]: ${
  77. item.solutionList.map(s => `添加${solutionList.value.find(ss => ss.id === s.solutionId)?.name}- ${s.volume}ml-偏移量${s.offset}ml`).join(';')
  78. }`).join(';')
  79. break
  80. case 'startHeating':
  81. step.params.description = `加热: ${step.params.temperature}度, 保持${step.params.minutes || 0}${step.params.seconds || 0}`
  82. break
  83. case 'shaking':
  84. step.params.description = `摇匀: ${step.params.second}`
  85. break
  86. case 'takePhoto':
  87. step.params.description = `拍照`
  88. break
  89. }
  90. return !allPropertiesDefined(step.params, ['minutes', 'seconds', 'description'])
  91. },
  92. )
  93. console.log(form.value)
  94. if (invalidStepIndex !== -1) {
  95. FtMessage.error(`步骤${(invalidStepIndex || 0) + 1}: 请填写完整参数`)
  96. return
  97. }
  98. form.value.steps = JSON.stringify(form.value.stepList)
  99. form.value.oresId = oresId
  100. if (form.value.id) {
  101. await updateCraft(form.value)
  102. }
  103. else {
  104. await createCraft(form.value)
  105. }
  106. FtMessage.success('保存成功')
  107. emits('ok')
  108. }
  109. catch (error) {
  110. console.log(error)
  111. }
  112. }
  113. const cancel = () => {
  114. emits('cancel')
  115. }
  116. const stepMap: Record<string, CraftTypes.StepItem> = {
  117. // addLiquid: { name: '加液', method: 'addLiquid', params: { addLiquidList: [{ containerId: undefined, volume: undefined }], description: undefined } },
  118. startHeating: { name: '加热', method: 'startHeating', params: { temperature: undefined, second: undefined, description: undefined, minutes: undefined, seconds: undefined } },
  119. shaking: { name: '摇匀', method: 'shaking', params: { second: undefined } },
  120. takePhoto: { name: '拍照', method: 'takePhoto', params: { } },
  121. }
  122. const addStep = (data: CraftTypes.StepItem) => {
  123. form.value.stepList?.push(data)
  124. }
  125. const addLiquidForm = ref<any>({
  126. tubeNums: [],
  127. solutionList: [
  128. {
  129. solutionId: undefined,
  130. volume: undefined,
  131. offset: undefined,
  132. },
  133. ],
  134. })
  135. const addLiquidRules = {
  136. tubeNums: [
  137. { required: true, message: '请选择试管', trigger: 'change' },
  138. ],
  139. }
  140. const addLiquidFormRef = ref()
  141. const activeTube = ref(Array.from({ length: 16 }).fill(false))
  142. const selectVisible = ref(false)
  143. const checkChange = () => {
  144. activeTube.value = Array.from({ length: 16 }).fill(selectVisible.value)
  145. addLiquidForm.value.tubeNums = activeTube.value.map((item, index) => index + 1).filter(item => activeTube.value[item - 1])
  146. addLiquidFormRef.value.validateField('tubeNums')
  147. }
  148. const mousedownHandle = async (e: Event) => {
  149. let event
  150. if ('touches' in e) {
  151. event = (e.touches as TouchList)[0]
  152. }
  153. else {
  154. event = e
  155. }
  156. if (event.target!.classList!.contains('tube-inner')) {
  157. const num = event.target!.getAttribute('index')
  158. activeTube.value[Number(num) - 1] = !activeTube.value[Number(num) - 1]
  159. addLiquidForm.value.tubeNums = activeTube.value.map((item, index) => index + 1).filter(item => activeTube.value[item - 1])
  160. }
  161. }
  162. const addHandle = async () => {
  163. try {
  164. const valid = await addLiquidFormRef.value.validate()
  165. if (!valid) {
  166. return
  167. }
  168. console.log(addLiquidForm.value)
  169. // addList.value!.push(addLiquidForm)
  170. const index = form.value.stepList?.findIndex(item => item.method === 'addLiquid')
  171. if (index !== -1) {
  172. form.value.stepList?.[index || 0].params?.list?.push(cloneDeep(addLiquidForm.value))
  173. }
  174. else {
  175. form.value.stepList?.push({
  176. name: '加液',
  177. method: 'addLiquid',
  178. params: { list: [cloneDeep(addLiquidForm.value)] },
  179. } as CraftTypes.StepItem)
  180. }
  181. addLiquidForm.value = {
  182. tubeNums: [],
  183. solutionList: [
  184. {
  185. solutionId: undefined,
  186. volume: undefined,
  187. offset: undefined,
  188. },
  189. ],
  190. }
  191. activeTube.value = Array.from({ length: 16 }).fill(false)
  192. selectVisible.value = false
  193. addLiquidFormRef.value.resetFields()
  194. }
  195. catch (error) {
  196. console.log(error)
  197. }
  198. }
  199. </script>
  200. <template>
  201. <FtDialog visible :loading :title="form.id ? '编辑工艺' : '新增工艺'" width="90%" :ok-handle="okHandle" @cancel="cancel">
  202. <el-form ref="formRef" label-width="auto" :model="form" :rules="rules" class="form-box">
  203. <el-row :gutter="20">
  204. <el-col :span="12">
  205. <el-form-item label="工艺名称" prop="name">
  206. <el-input v-model="form.name" placeholder="请输入工艺名称" />
  207. </el-form-item>
  208. <el-form-item label="步骤列表">
  209. <div class="button-content">
  210. <el-tag v-for="item in stepMap" :key="item" size="large" @click="() => addStep(item)">
  211. <div style="display: flex;align-items: center;justify-content: space-around;width: 100%;">
  212. <el-icon><Plus /></el-icon>
  213. <span> {{ item.name }}</span>
  214. </div>
  215. </el-tag>
  216. </div>
  217. <el-form ref="addLiquidFormRef" :model="addLiquidForm" :rules="addLiquidRules" label-width="auto" class="liquid-box">
  218. <p>加液</p>
  219. <el-form-item label="" prop="tubeNums">
  220. <el-checkbox v-model="selectVisible" style="margin-right: 10px" @change="checkChange">
  221. 全选
  222. </el-checkbox>
  223. <div class="tube-item" @click.prevent="mousedownHandle" @touch.prevent="mousedownHandle">
  224. <span v-for="item in 16" :key="item" class="tube-inner" :class="{ 'tube-inner-active': activeTube[item - 1] }" :index="item" />
  225. </div>
  226. </el-form-item>
  227. <el-row>
  228. <el-col :span="7" style="text-align: center">
  229. 溶液
  230. </el-col>
  231. <el-col :span="7" style="text-align: center">
  232. 容量
  233. </el-col>
  234. <el-col :span="7" style="text-align: center">
  235. 偏移量
  236. </el-col>
  237. </el-row>
  238. <div class="solution-list">
  239. <el-row v-for="(s, index) in addLiquidForm.solutionList" :key="index" :gutter="5" class="solution-item">
  240. <el-col :span="7">
  241. <el-form-item
  242. label=""
  243. :prop="`solutionList.${index}.solutionId`"
  244. :rules="{
  245. required: true,
  246. message: '请选择溶液',
  247. trigger: 'change',
  248. }"
  249. >
  250. <el-select v-model="s.solutionId" size="small" style="width: 100%" placeholder="溶液">
  251. <el-option v-for="item in solutionList" :key="item.id" :label="item.name" :value="item.id" />
  252. </el-select>
  253. </el-form-item>
  254. </el-col>
  255. <el-col :span="7">
  256. <el-form-item
  257. label="" :prop="`solutionList.${index}.volume`" :rules="{
  258. required: true,
  259. message: '请输入容量',
  260. trigger: 'blur',
  261. }"
  262. >
  263. <el-input v-model.number="s.volume" size="small" type="number" style="width: 100%" placeholder="容量">
  264. <template #append>
  265. ml
  266. </template>
  267. </el-input>
  268. </el-form-item>
  269. </el-col>
  270. <el-col :span="7">
  271. <el-form-item
  272. label="" :prop="`solutionList.${index}.offset`" :rules="{
  273. required: true,
  274. message: '请输入偏移量',
  275. trigger: 'blur',
  276. }"
  277. >
  278. <el-input v-model.number="s.offset" size="small" type="number" style="width: 100%" placeholder="偏移量">
  279. <template #append>
  280. ml
  281. </template>
  282. </el-input>
  283. </el-form-item>
  284. </el-col>
  285. <el-col :span="3" class="icon-box">
  286. <el-icon v-if="addLiquidForm.solutionList.length > 1" color="red" @click="addLiquidForm.solutionList.splice(index, 1)">
  287. <RemoveFilled />
  288. </el-icon>
  289. <el-icon
  290. v-if="index === addLiquidForm.solutionList.length - 1" color="green" @click="addLiquidForm.solutionList.push({ solutionId: undefined,
  291. volume: undefined,
  292. offset: undefined })"
  293. >
  294. <CirclePlusFilled />
  295. </el-icon>
  296. </el-col>
  297. </el-row>
  298. </div>
  299. <el-form-item>
  300. <div style="width: 100%;display: flex;justify-content: center">
  301. <ft-button type="primary" @click="addHandle">
  302. 添加
  303. </ft-button>
  304. </div>
  305. </el-form-item>
  306. </el-form>
  307. </el-form-item>
  308. </el-col>
  309. <el-col :span="12">
  310. <div v-if="form.stepList?.length" class="step-box">
  311. <div v-for="(item, index) in form.stepList" :key="index" class="step-item">
  312. <el-form-item :label="`${index + 1}: ${item.name}`">
  313. <div v-if="item.method === 'addLiquid'" class="list-box">
  314. <div v-for="(liquid, liquidIndex) in item.params?.list || []" :key="liquidIndex">
  315. <span>试管: {{ liquid.tubeNums.length === 16 ? '全部' : liquid.tubeNums.join(',') }} </span>
  316. <div v-for="(s, sIndex) in liquid.solutionList" :key="sIndex">
  317. <span>添加</span>
  318. <span>{{ solutionList.find(solution => solution.id === s.solutionId)?.name }}</span>
  319. <span>{{ s.volume }}</span>
  320. <span>ml-</span>
  321. <span>偏移量</span>
  322. <span>{{ s.offset }}</span>
  323. <span>ml</span>
  324. </div>
  325. </div>
  326. </div>
  327. <div v-else-if="['startHeating'].includes(item.method)">
  328. <el-input v-model.number="item.params.temperature" type="number" size="small" placeholder="加热温度">
  329. <template #append>
  330. </template>
  331. </el-input>
  332. <br>
  333. <div>
  334. <span>保持时间</span>
  335. <el-select v-model="item.params.minutes" style="width: 70px" clearable size="small" placeholder="分钟">
  336. <el-option v-for="i in 60" :key="i" :label="i" :value="i" />
  337. </el-select>
  338. <span class="unit-text"></span>
  339. <el-select v-model="item.params.seconds" style="width: 70px" clearable size="small" placeholder="秒">
  340. <el-option v-for="i in 60" :key="i" :label="i" :value="i" />
  341. </el-select>
  342. <span class="unit-text"></span>
  343. </div>
  344. <div>
  345. <span>冷却时间</span>
  346. <el-select v-model="item.params.coolingMinutes" style="width: 70px" clearable size="small" placeholder="分钟">
  347. <el-option v-for="i in 60" :key="i" :label="i" :value="i" />
  348. </el-select>
  349. <span class="unit-text"></span>
  350. <el-select v-model="item.params.coolingSeconds" style="width: 70px" clearable size="small" placeholder="秒">
  351. <el-option v-for="i in 60" :key="i" :label="i" :value="i" />
  352. </el-select>
  353. <span class="unit-text"></span>
  354. </div>
  355. </div>
  356. <template v-else-if="['shaking'].includes(item.method)">
  357. <el-select v-model="item.params.minutes" style="width: 70px" clearable size="small" placeholder="请选择">
  358. <el-option v-for="i in 60" :key="i" :label="i" :value="i" />
  359. </el-select>
  360. <span class="unit-text"></span>
  361. <el-select v-model="item.params.seconds" style="width: 70px" clearable size="small" placeholder="请选择">
  362. <el-option v-for="i in 60" :key="i" :label="i" :value="i" />
  363. </el-select>
  364. <span class="unit-text"></span>
  365. </template>
  366. <el-icon style="margin-left: auto;" @click="() => form.stepList?.splice(index, 1)">
  367. <Close />
  368. </el-icon>
  369. </el-form-item>
  370. </div>
  371. </div>
  372. <div v-else class="empty-box">
  373. <img :src="emptyIcon" alt="">
  374. <span>暂无步骤</span>
  375. </div>
  376. </el-col>
  377. </el-row>
  378. </el-form>
  379. </FtDialog>
  380. </template>
  381. <style scoped lang="scss">
  382. .form-box {
  383. height: 70vh;
  384. .el-row {
  385. height: 100%;
  386. .el-col:first-child {
  387. border-right: 1px solid #eee;
  388. }
  389. .el-col {
  390. height: 100%;
  391. .step-box {
  392. height: 100%;
  393. overflow: auto;
  394. }
  395. }
  396. }
  397. }
  398. .button-content {
  399. width: 100%;
  400. display: grid;
  401. grid-template-columns: repeat(2, 1fr); /* 创建3列等宽轨道 */
  402. grid-template-rows: repeat(2, auto); /* 创建2行自动高度 */
  403. gap: 10px;
  404. margin-bottom: 10px;
  405. }
  406. :deep(.el-tag__content) {
  407. width: 100%;
  408. }
  409. .empty-box {
  410. width: 100%;
  411. height: 100%;
  412. display: flex;
  413. flex-direction: column;
  414. align-items: center;
  415. justify-content: center;
  416. color: #ccc;
  417. img {
  418. margin-bottom: 10px;
  419. }
  420. }
  421. .step-item {
  422. .el-form-item {
  423. background: rgba(82, 148, 215, 0.06);
  424. padding: 5px;
  425. margin-bottom: 10px;
  426. :deep(.el-form-item__label) {
  427. height: 25px;
  428. line-height: 25px;
  429. }
  430. :deep(.el-form-item__content) {
  431. width: 100%;
  432. display: flex;
  433. align-items: center;
  434. .el-input, .el-select {
  435. width: 120px;
  436. margin: 0 5px;
  437. }
  438. .list-box {
  439. width: 90%;
  440. height: 100%;
  441. .list-item {
  442. display: flex;
  443. align-items: center;
  444. padding-bottom: 10px;
  445. .el-icon {
  446. cursor: pointer;
  447. font-size: 20px;
  448. margin-left: 10px;
  449. }
  450. }
  451. }
  452. }
  453. }
  454. }
  455. .unit-text {
  456. font-size: 12px;
  457. line-height: 25px;
  458. }
  459. .tube-item {
  460. padding: 5px;
  461. background: #384D5D;
  462. border-radius: 10px;
  463. display: grid;
  464. grid-template-columns: repeat(4, 1fr);
  465. grid-template-rows: repeat(4, 1fr);
  466. grid-gap: 5px;
  467. position: relative;
  468. .tube-disable {
  469. position: absolute;
  470. width: 100%;
  471. height: 100%;
  472. top: 0;
  473. left: 0;
  474. background: rgba(255,255,255,0.9);
  475. border-radius: 9px;
  476. }
  477. .tube-inner {
  478. display: inline-block;
  479. width: 25px;
  480. height: 25px;
  481. border-radius: 50%;
  482. background: #fff;
  483. margin: 2px;
  484. transition: background 0.5s;
  485. }
  486. .tube-inner-active {
  487. background: #26D574;
  488. }
  489. }
  490. .liquid-box {
  491. background: #ecf5ff;
  492. padding: 0 10px;
  493. p {
  494. text-align: center;
  495. color: #1989fa;
  496. }
  497. }
  498. .solution-list {
  499. max-height: 100px;
  500. overflow: hidden;
  501. overflow-y: auto;
  502. }
  503. .solution-item {
  504. margin-bottom: 10px;
  505. }
  506. .el-form-item .el-form-item {
  507. margin-bottom: 10px;
  508. }
  509. .icon-box {
  510. display: flex;
  511. margin-top: 4px;
  512. justify-content: space-between;
  513. .el-icon {
  514. font-size: 18px;
  515. line-height: 100%;
  516. }
  517. }
  518. </style>