8 changed files with 363 additions and 31 deletions
-
21src/src/main/java/com/my/graphiteDigesterBg/api/ApiCamera.java
-
45src/src/main/java/com/my/graphiteDigesterBg/api/ApiDigestionPreset.java
-
5src/src/main/java/com/my/graphiteDigesterBg/diframe/mapper/DiActiveRecordMapper.java
-
BINsrc/web/src/assets/img/camera-demo.png
-
35src/web/src/pages/main/contents/Operation.vue
-
100src/web/src/pages/main/contents/OperationCamera.vue
-
171src/web/src/pages/main/contents/TaskStepManagement.vue
-
17src/web/src/utils/ApiClient.js
@ -0,0 +1,21 @@ |
|||||
|
package com.my.graphiteDigesterBg.api; |
||||
|
import com.my.graphiteDigesterBg.diframe.DiApiControllerBase; |
||||
|
import com.my.graphiteDigesterBg.diframe.DiApiResponse; |
||||
|
import org.springframework.stereotype.Controller; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.ResponseBody; |
||||
|
import java.nio.ByteBuffer; |
||||
|
import java.util.Base64; |
||||
|
import java.util.Map; |
||||
|
@Controller |
||||
|
public class ApiCamera extends DiApiControllerBase { |
||||
|
@ResponseBody |
||||
|
@PostMapping("/api/camera/image") |
||||
|
public DiApiResponse getImage() { |
||||
|
// ByteBuffer buffer = ByteBuffer.allocate(1024); |
||||
|
// byte[] bytes = buffer.array(); |
||||
|
// String base64Text = Base64.getEncoder().encodeToString(bytes); |
||||
|
String base64Data = "data:image/image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAUCAYAAACJfM0wAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAH8SURBVDiNtZM/aBNxFMc/791PLJihQ4QMFhJQ7ODgmIJDbxAEXQoJuAqSzOLSQ4c4lFAoOGSQdHEtpOBSQXDo4pChg2NBQQfBDBkyKLRw93sOl1KT3LVC4ne5433f+7w/x0GO2t39Tnu3d5jr7/YO2939Tp4vAK3X75avXosf+sTfOjNUFTx4fGahoqDg/bmvgX45/e3et55tjGTrzd4dleAjQmmi0hNC/B113zLJPq6AK6NMbmUMvCX3nQZBB5uCziOhpBp0HMZ6ZkLAc/yVIVg2QN0LhFKmbay73M7GIyQHmuppXk+AM/AnYnuCJvFFpEvlA4eTt8C9M3BRHLe9BL/m4apSMCjC+cSrhhxcvPnl+rtc50PlK/PjiUjfsBOMVaAEnCD0x2NVgSVggHAsyJKZVacZsxOb9DcbtbWoUQ9Vkw0A7/121KiHUaMeeu+3AVSTjahRDzcbtTVM+jPDtbu9ycsaMUJ//LwB3DQYifE57ctdgWXgK8YPBIdRRSa3nz1FmlBGJAYrpiEKiJTTdyuMM0uIODA3Dc08hcBR1KyvRM1aJTEJ05jsRM1aJWrWKoLsACQmYRqrrwgc/esp9jCLgVVEqhgDsA/jkgfpr2x94BgRh/F4eupZ8IKkAqP/wB2qGd3Fc62rp4PiS7AtYLgA4tDDq9Of11t/AC8CsohkwY6rAAAAAElFTkSuQmCC"; |
||||
|
return this.success(Map.of("data",base64Data)); |
||||
|
} |
||||
|
} |
@ -0,0 +1,45 @@ |
|||||
|
package com.my.graphiteDigesterBg.api; |
||||
|
import com.my.graphiteDigesterBg.diframe.DiActiveRecord; |
||||
|
import com.my.graphiteDigesterBg.diframe.DiActiveRecordCriteria; |
||||
|
import com.my.graphiteDigesterBg.diframe.DiApiControllerBase; |
||||
|
import com.my.graphiteDigesterBg.diframe.DiApiResponse; |
||||
|
import com.my.graphiteDigesterBg.model.MdbDigestionTask; |
||||
|
import org.springframework.stereotype.Controller; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.ResponseBody; |
||||
|
import java.util.Map; |
||||
|
@Controller |
||||
|
public class ApiDigestionPreset extends DiApiControllerBase { |
||||
|
@ResponseBody |
||||
|
@PostMapping("/api/digestion-preset/save") |
||||
|
public DiApiResponse save( @RequestBody Map<String,Object> params ) { |
||||
|
Integer id = (Integer)params.get("id"); |
||||
|
Map<String,Object> data = (Map<String,Object>)params.get("data"); |
||||
|
|
||||
|
MdbDigestionTask preset = new MdbDigestionTask(); |
||||
|
if ( id != null ) { |
||||
|
preset = DiActiveRecord.findOne(MdbDigestionTask.class, id); |
||||
|
} |
||||
|
preset.setAttributes(data); |
||||
|
preset.save(); |
||||
|
return this.success(); |
||||
|
} |
||||
|
|
||||
|
@ResponseBody |
||||
|
@PostMapping("/api/digestion-preset/list") |
||||
|
public DiApiResponse list() { |
||||
|
var criteria = new DiActiveRecordCriteria(); |
||||
|
var list = DiActiveRecord.find(MdbDigestionTask.class, criteria); |
||||
|
return this.success(list); |
||||
|
} |
||||
|
|
||||
|
@ResponseBody |
||||
|
@PostMapping("/api/digestion-preset/delete") |
||||
|
public DiApiResponse delete( @RequestBody Map<String,Object> params ) { |
||||
|
Integer id = (Integer)params.get("id"); |
||||
|
MdbDigestionTask preset = DiActiveRecord.findOne(MdbDigestionTask.class, id); |
||||
|
preset.delete(); |
||||
|
return this.success(); |
||||
|
} |
||||
|
} |
After Width: 1029 | Height: 904 | Size: 851 KiB |
@ -0,0 +1,100 @@ |
|||||
|
<template> |
||||
|
<div class="h-full flex flex-col bg-white rounded-2xl p-5"> |
||||
|
<div class="camera h-0 grow rounded-2xl flex flex-col justify-center items-center"> |
||||
|
<div v-if="null === imageData"> |
||||
|
<p class="m-0 text-center"><img src="../../../assets/icon/camera-off.svg" /></p> |
||||
|
<p class="m-0 mt-2 text-2xl text-white">未检测到照相设备</p> |
||||
|
</div> |
||||
|
<img v-else :src="imageData" class="w-full rounded-xl" /> |
||||
|
</div> |
||||
|
<div class="mt-3 flex flex-row justify-between"> |
||||
|
<div> |
||||
|
<a-button @click="actionTakeShot"><CameraOutlined /></a-button> |
||||
|
</div> |
||||
|
<div> |
||||
|
<a-button><SplitCellsOutlined /></a-button> |
||||
|
<a-button class="ml-1"><MergeCellsOutlined /></a-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 拍照 --> |
||||
|
<a-modal v-if="imageModalOpen" v-model:open="imageModalOpen" title="拍照结果" width="80%" :footer="null"> |
||||
|
<a-row> |
||||
|
<a-col :span="12"> |
||||
|
<img :src="imageShotData" class="w-full rounded-xl" /> |
||||
|
</a-col> |
||||
|
<a-col :span="12" class="p-5 pt-0"> |
||||
|
<a-row class="mb-5" style="border: solid 1px #d9d9d9;padding: 10px;border-radius: 10px;background: #dddddd;"> |
||||
|
<a-col :span="6" v-for="i in 16" :key="i" class="mb-5 text-center"> |
||||
|
<a-button shape="circle" size="large" |
||||
|
:type="errorTubes.includes(i) ? 'primary' : 'default'" |
||||
|
@click="actionTubeErrorToggle(i)" |
||||
|
><ExperimentOutlined /></a-button> |
||||
|
</a-col> |
||||
|
</a-row> |
||||
|
<div class="text-right"> |
||||
|
<a-button>移动至异常区</a-button> |
||||
|
</div> |
||||
|
</a-col> |
||||
|
</a-row> |
||||
|
</a-modal> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import ApiClient from '@/utils/ApiClient'; |
||||
|
import { onMounted, onUnmounted, ref } from 'vue'; |
||||
|
/** @var {Boolean} */ |
||||
|
const imageModalOpen = ref(false); |
||||
|
/** @var {Object} */ |
||||
|
const imageShotData = ref(null); |
||||
|
/** @var {Array} */ |
||||
|
const errorTubes = ref([]); |
||||
|
/** @var {String} */ |
||||
|
const imageData = ref(null); |
||||
|
/** @var {Number} */ |
||||
|
let refreshTimer = null; |
||||
|
/** @var {ApiClient} */ |
||||
|
let client = null; |
||||
|
// on mounted |
||||
|
onMounted(mounted); |
||||
|
// on unmounted |
||||
|
onUnmounted(unmounted); |
||||
|
|
||||
|
// on mounted |
||||
|
function mounted() { |
||||
|
client = ApiClient.getClient(); |
||||
|
refresh(); |
||||
|
} |
||||
|
|
||||
|
// on unmounted |
||||
|
function unmounted() { |
||||
|
if ( refreshTimer ) { |
||||
|
clearTimeout(refreshTimer); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// refresh |
||||
|
async function refresh() { |
||||
|
let response = await client.call('camera/image'); |
||||
|
imageData.value = response.data; |
||||
|
refreshTimer = setTimeout(() => refresh(), 200); |
||||
|
} |
||||
|
|
||||
|
// 拍照 |
||||
|
function actionTakeShot() { |
||||
|
imageModalOpen.value = true; |
||||
|
imageShotData.value = imageData.value; |
||||
|
} |
||||
|
|
||||
|
// 异常区 |
||||
|
function actionTubeErrorToggle( index ) { |
||||
|
if ( errorTubes.value.includes(index) ) { |
||||
|
errorTubes.value.splice(errorTubes.value.indexOf(index), 1); |
||||
|
} else { |
||||
|
errorTubes.value.push(index); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
<style> |
||||
|
.camera {background: linear-gradient(180deg, rgba(5, 10, 39, 0.5) 0%, rgba(4, 10, 52, 0.5) 97%);backdrop-filter: blur(194px);} |
||||
|
</style> |
@ -1,21 +1,168 @@ |
|||||
<template> |
<template> |
||||
<div class="p-1"> |
<div class="p-1"> |
||||
<a-table :dataSource="dataSource" :columns="columns"></a-table> |
|
||||
|
<a-table :dataSource="dataSource" :columns="columns"> |
||||
|
<template #headerCell="{ column }"> |
||||
|
<template v-if="column.key === 'action'"> |
||||
|
<PlusCircleOutlined @click="actionCreate"/> |
||||
|
</template> |
||||
|
</template> |
||||
|
<template #bodyCell="{ column, record }"> |
||||
|
<template v-if="column.key === 'action'"> |
||||
|
<a-popconfirm title="是否删除当前预设 ?" @confirm="actionDelete(record)"> |
||||
|
<DeleteOutlined /> |
||||
|
</a-popconfirm> |
||||
|
<FormOutlined @click="actionEdit(record)" class="ml-5"/> |
||||
|
</template> |
||||
|
</template> |
||||
|
</a-table> |
||||
</div> |
</div> |
||||
</template> |
|
||||
|
|
||||
|
<!-- edit modal --> |
||||
|
<a-modal v-if="edit.enable" v-model:open="edit.enable" title="预设编辑" @ok="actionEditOk" ok-text="确定" cancel-text="取消"> |
||||
|
<p><a-input v-model:value="edit.data.name" placeholder="名称" /></p> |
||||
|
<a-row class="py-3"> |
||||
|
<a-col :span="12">执行步骤</a-col> |
||||
|
<a-col :span="12" class="text-right"> |
||||
|
<a-button size="small" @click="actionStepAdd"><PlusCircleOutlined /></a-button> |
||||
|
</a-col> |
||||
|
</a-row> |
||||
|
<a-collapse accordion v-model:activeKey="editActiveStepKey"> |
||||
|
<a-collapse-panel v-for="(step,index) in edit.data.steps" :key="index"> |
||||
|
<template #header> |
||||
|
步骤 : {{ index + 1 }} - {{ stepNameGet(step) }} |
||||
|
</template> |
||||
|
<template #extra><delete-outlined @click="actionStepDelete($event, index)" /></template> |
||||
|
<a-form :label-col="{span:4}" :wrapper-col="{span:20}"> |
||||
|
<a-form-item label="操作"> |
||||
|
<a-radio-group v-model:value="step.action" button-style="solid"> |
||||
|
<a-radio-button value="Heating">加热</a-radio-button> |
||||
|
<a-radio-button value="Pump">加酸</a-radio-button> |
||||
|
</a-radio-group> |
||||
|
</a-form-item> |
||||
|
<a-form-item label="目标温度" v-if="'Heating' === step.action"> |
||||
|
<a-input-number v-model:value="step.temperature" addon-after="℃" /> |
||||
|
</a-form-item> |
||||
|
<a-form-item label="持续时间" v-if="'Heating' === step.action"> |
||||
|
<a-input-number v-model:value="step.duration" addon-after="秒" /> |
||||
|
</a-form-item> |
||||
|
<a-form-item label="酸液类型" v-if="'Pump' === step.action"> |
||||
|
<a-select v-model:value="step.type"> |
||||
|
<a-select-option value="hydrochloric">盐酸</a-select-option> |
||||
|
<a-select-option value="nitric">硝酸</a-select-option> |
||||
|
<a-select-option value="sulfuric">硫酸</a-select-option> |
||||
|
<a-select-option value="hydrofluoric">氢氟酸</a-select-option> |
||||
|
<a-select-option value="perchloric">高氯酸</a-select-option> |
||||
|
<a-select-option value="hydrobromic">液溴</a-select-option> |
||||
|
<a-select-option value="phosphoric">磷酸</a-select-option> |
||||
|
<a-select-option value="tartaric">酒石酸</a-select-option> |
||||
|
</a-select> |
||||
|
</a-form-item> |
||||
|
<a-form-item label="加酸量" v-if="'Pump' === step.action"> |
||||
|
<a-input-number v-model:value="step.amount" /> |
||||
|
</a-form-item> |
||||
|
<a-form-item label="摇匀次数" v-if="'Pump' === step.action"> |
||||
|
<a-input-number v-model:value="step.shake" /> |
||||
|
</a-form-item> |
||||
|
</a-form> |
||||
|
</a-collapse-panel> |
||||
|
</a-collapse> |
||||
|
</a-modal> |
||||
|
</template> |
||||
<script setup> |
<script setup> |
||||
import { ref } from 'vue'; |
|
||||
|
import ApiClient from '@/utils/ApiClient'; |
||||
|
import { onMounted, ref, nextTick } from 'vue'; |
||||
/** @var {Array<Object>} */ |
/** @var {Array<Object>} */ |
||||
const columns = [ |
const columns = [ |
||||
{key:'name',dataIndex:'name',title:'名称',align:'center'}, |
|
||||
{key:'steps',dataIndex:'steps',title:'步骤',align:'left'}, |
|
||||
{key:'action',dataIndex:'action',title:'操作',align:'center'}, |
|
||||
|
{key:'name',dataIndex:'name',title:'名称',align:'left',width:200}, |
||||
|
{key:'steps',dataIndex:'stepSummary',title:'步骤',align:'left'}, |
||||
|
{key:'action',dataIndex:'action',align:'right',width:80}, |
||||
]; |
]; |
||||
/** @var {Array<Object>} */ |
/** @var {Array<Object>} */ |
||||
const dataSource = ref([ |
|
||||
{name:'预设001',steps:'加酸:硫酸 -> 加热: 270℃ 15分钟 -> 加酸:硫酸 -> 加热: 270℃ 15分钟 -> 加酸:硫酸 -> 加热: 270℃ 15分钟',action:'删除 编辑'}, |
|
||||
{name:'预设002',steps:'加酸:盐酸 -> 加热: 270℃ 15分钟 -> 加酸:硫酸 -> 加热: 270℃ 15分钟 加热: 270℃ 15分钟',action:'删除 编辑'}, |
|
||||
{name:'预设003',steps:'加酸:氢氟酸 -> 加热: 270℃ 15分钟 -> 加酸:盐酸 -> 加热: 270℃ 15分钟 -> 加酸:盐酸 -> 加热: 270℃ 15分钟',action:'删除 编辑'}, |
|
||||
{name:'预设004',steps:'加酸:盐酸 -> 加热: 270℃ 15分钟 -> 加酸:氢氟酸 -> 加热: 270℃ 15分钟 -> 加酸:盐酸 -> 加热: 270℃ 15分钟',action:'删除 编辑'}, |
|
||||
]); |
|
||||
|
const dataSource = ref([]); |
||||
|
/** @var {Ref<Object>} */ |
||||
|
const edit = ref({enable:false,id:null,data:{name:'',steps:[]}}); |
||||
|
/** @var {Ref<String>} */ |
||||
|
const editActiveStepKey = ref(null); |
||||
|
// mounted |
||||
|
onMounted(mounted); |
||||
|
|
||||
|
// mounted |
||||
|
async function mounted() { |
||||
|
await refresh(); |
||||
|
} |
||||
|
|
||||
|
// step name get |
||||
|
function stepNameGet(step) { |
||||
|
let acidMap = {hydrochloric:'盐酸',nitric:'硝酸',sulfuric:'硫酸',hydrofluoric:'氢氟酸',perchloric:'高氯酸',hydrobromic:'液溴',phosphoric:'磷酸',tartaric:'酒石酸'}; |
||||
|
if ( 'Heating' === step.action ) { |
||||
|
return `加热 ${step.temperature}℃ ${step.duration}秒`; |
||||
|
} else if ( 'Pump' === step.action ) { |
||||
|
return `加酸 ${acidMap[step.type]} ${step.amount}ml 摇匀${step.shake}次`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 刷新 |
||||
|
async function refresh() { |
||||
|
let client = ApiClient.getClient(); |
||||
|
dataSource.value = []; |
||||
|
let list = await client.digestionPresetList(); |
||||
|
for ( let item of list ) { |
||||
|
item.steps = JSON.parse(item.steps); |
||||
|
item.stepSummary = []; |
||||
|
for ( let sitem in item.steps ) { |
||||
|
item.stepSummary.push(stepNameGet(item.steps[sitem])); |
||||
|
} |
||||
|
item.stepSummary = item.stepSummary.join(' -> '); |
||||
|
|
||||
|
} |
||||
|
dataSource.value = list; |
||||
|
} |
||||
|
|
||||
|
// create |
||||
|
function actionCreate() { |
||||
|
edit.value.enable = true; |
||||
|
edit.value.id = null; |
||||
|
edit.value.data = {}; |
||||
|
edit.value.data.name = '未命名预设'; |
||||
|
edit.value.data.steps = [{action:'Heating',temperature:270,duration:15,type:'sulfuric',amount:1000,shake:5}]; |
||||
|
} |
||||
|
|
||||
|
// 添加步骤 |
||||
|
function actionStepAdd() { |
||||
|
edit.value.data.steps.push({action:'Heating',temperature:270,duration:15,type:'sulfuric',amount:1000,shake:5}); |
||||
|
} |
||||
|
|
||||
|
// 删除步骤 |
||||
|
function actionStepDelete(event, index) { |
||||
|
event.stopPropagation(); |
||||
|
event.preventDefault(); |
||||
|
edit.value.data.steps.splice(index,1); |
||||
|
} |
||||
|
|
||||
|
// edit ok |
||||
|
async function actionEditOk() { |
||||
|
edit.value.enable = false; |
||||
|
let steps = JSON.stringify(edit.value.data.steps); |
||||
|
let params = {}; |
||||
|
params.id = edit.value.id; |
||||
|
params.data = edit.value.data; |
||||
|
params.data.steps = steps; |
||||
|
let client = ApiClient.getClient(); |
||||
|
await client.digestionPresetSave(params); |
||||
|
await refresh(); |
||||
|
} |
||||
|
|
||||
|
// 删除 |
||||
|
async function actionDelete(record) { |
||||
|
let client = ApiClient.getClient(); |
||||
|
await client.digestionPresetDelete(record.id); |
||||
|
await refresh(); |
||||
|
} |
||||
|
|
||||
|
// 编辑 |
||||
|
async function actionEdit(record) { |
||||
|
edit.value.enable = true; |
||||
|
edit.value.id = record.id; |
||||
|
edit.value.data = record; |
||||
|
} |
||||
</script> |
</script> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue