sige 2 years ago
parent
commit
f84a581946
  1. 21
      src/src/main/java/com/my/graphiteDigesterBg/api/ApiCamera.java
  2. 45
      src/src/main/java/com/my/graphiteDigesterBg/api/ApiDigestionPreset.java
  3. 5
      src/src/main/java/com/my/graphiteDigesterBg/diframe/mapper/DiActiveRecordMapper.java
  4. BIN
      src/web/src/assets/img/camera-demo.png
  5. 35
      src/web/src/pages/main/contents/Operation.vue
  6. 100
      src/web/src/pages/main/contents/OperationCamera.vue
  7. 171
      src/web/src/pages/main/contents/TaskStepManagement.vue
  8. 17
      src/web/src/utils/ApiClient.js

21
src/src/main/java/com/my/graphiteDigesterBg/api/ApiCamera.java

@ -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));
}
}

45
src/src/main/java/com/my/graphiteDigesterBg/api/ApiDigestionPreset.java

@ -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();
}
}

5
src/src/main/java/com/my/graphiteDigesterBg/diframe/mapper/DiActiveRecordMapper.java

@ -12,8 +12,9 @@ public interface DiActiveRecordMapper {
"<if test='conditions != null'>" + "<if test='conditions != null'>" +
"WHERE " + "WHERE " +
"<foreach collection='conditions' item='value' index='key' separator=' AND '>${key} = #{value}</foreach>" + "<foreach collection='conditions' item='value' index='key' separator=' AND '>${key} = #{value}</foreach>" +
"</if>" +
"LIMIT #{limit}" +
"</if> " +
"ORDER BY id DESC " +
"<if test='limit != null'>LIMIT #{limit}</if> " +
"</script>" "</script>"
) )
List<Map<String,Object>> find(DiActiveRecordCriteria criteria); List<Map<String,Object>> find(DiActiveRecordCriteria criteria);

BIN
src/web/src/assets/img/camera-demo.png

After

Width: 1029  |  Height: 904  |  Size: 851 KiB

35
src/web/src/pages/main/contents/Operation.vue

@ -3,17 +3,7 @@
<a-row class="h-0 grow"> <a-row class="h-0 grow">
<!-- 拍照区 --> <!-- 拍照区 -->
<a-col :span="9" class="p-1"> <a-col :span="9" class="p-1">
<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>
<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>
</div>
<div class="mt-3">
<a-button><CameraOutlined /></a-button>
</div>
</div>
<operation-camera />
</a-col> </a-col>
<!-- 加热区 --> <!-- 加热区 -->
@ -53,7 +43,7 @@
</a-col> </a-col>
<a-col :span="11" class="text-right"> <a-col :span="11" class="text-right">
<a-button class="ml-1" @click="actionSampleAdd"><PlusCircleOutlined /></a-button> <a-button class="ml-1" @click="actionSampleAdd"><PlusCircleOutlined /></a-button>
<a-button class="ml-1" @click="actionSampleTakeOut"><CheckCircleOutlined /></a-button>
<a-button class="ml-1" @click="actionSampleTakeOut"><MinusCircleOutlined /></a-button>
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
@ -70,7 +60,11 @@
<div class="h-0 grow"> <div class="h-0 grow">
<div v-for="tubeRackSlot in tubeRackSlots" :key="tubeRackSlot.index" class="bg-gray-100 mb-2 flex flex-row p-2 rounded-2xl"> <div v-for="tubeRackSlot in tubeRackSlots" :key="tubeRackSlot.index" class="bg-gray-100 mb-2 flex flex-row p-2 rounded-2xl">
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<a-progress type="circle" :percent="getTubeRackSlotHeatingProgress(tubeRackSlot)" :size="50" :strokeWidth="18"/>
<a-progress type="circle" :size="50" :strokeWidth="18">
<template #format>
<span class="text-red-400 text-xs">00:00</span>
</template>
</a-progress>
</div> </div>
<div class="ml-2 bg-amber-500 rounded-2xl py-2 px-4 text-white w-0 grow text-center"> <div class="ml-2 bg-amber-500 rounded-2xl py-2 px-4 text-white w-0 grow text-center">
<p class="mb-0 text-2xl">{{ tubeRackSlot.temperature || '---' }}</p> <p class="mb-0 text-2xl">{{ tubeRackSlot.temperature || '---' }}</p>
@ -191,8 +185,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from 'vue';
import { onMounted, ref, onUnmounted } from 'vue';
import ApiClient from '@/utils/ApiClient.js'; import ApiClient from '@/utils/ApiClient.js';
import OperationCamera from './OperationCamera.vue';
/** @var {Object} */ /** @var {Object} */
const sampleAdd = ref({enable:false}); const sampleAdd = ref({enable:false});
/** @var {Object} */ /** @var {Object} */
@ -207,14 +202,23 @@ const heating = ref({enable:false,slotIndex:0,temperature:100,duration:10});
const tubeRackSlots = ref([]); const tubeRackSlots = ref([]);
/** @var {Array} */ /** @var {Array} */
const acidBuckets = ref([]); const acidBuckets = ref([]);
/** @var {Number} */
let refreshTimer = null;
// mounted // mounted
onMounted(mounted); onMounted(mounted);
// unmounted
onUnmounted(unmounted);
// mounted // mounted
function mounted() { function mounted() {
refreshResource(); refreshResource();
} }
// unmounted
function unmounted() {
clearTimeout(refreshTimer);
}
// refresh resource // refresh resource
async function refreshResource() { async function refreshResource() {
let client = ApiClient.getClient(); let client = ApiClient.getClient();
@ -224,7 +228,7 @@ async function refreshResource() {
response = await client.resourceDataGet('Acid'); response = await client.resourceDataGet('Acid');
acidBuckets.value = structuredClone(response); acidBuckets.value = structuredClone(response);
setTimeout(refreshResource,1000);
refreshTimer = setTimeout(refreshResource,1000);
} }
// //
@ -343,5 +347,4 @@ function actionHeatingCancel() {
.tube-rack-slot.active .tube-rack {background:#1B1B1B;} .tube-rack-slot.active .tube-rack {background:#1B1B1B;}
.tube-rack-slot .tube {background:#FFFFFF;} .tube-rack-slot .tube {background:#FFFFFF;}
.tube-rack-slot.active .tube {background:#26D574;} .tube-rack-slot.active .tube {background:#26D574;}
.camera {background: linear-gradient(180deg, rgba(5, 10, 39, 0.5) 0%, rgba(4, 10, 52, 0.5) 97%);backdrop-filter: blur(194px);}
</style> </style>

100
src/web/src/pages/main/contents/OperationCamera.vue

@ -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>

171
src/web/src/pages/main/contents/TaskStepManagement.vue

@ -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>

17
src/web/src/utils/ApiClient.js

@ -21,7 +21,7 @@ export default class ApiClient {
} }
// call api // call api
async call( name, params ) {
async call( name, params={} ) {
const appStore = useAppStore(); const appStore = useAppStore();
let headers = {}; let headers = {};
@ -91,4 +91,19 @@ export default class ApiClient {
async resourceDataGet( name ) { async resourceDataGet( name ) {
return await this.call('resource/data-get', {name}); return await this.call('resource/data-get', {name});
} }
// digestion preset save
async digestionPresetSave(params) {
return await this.call('digestion-preset/save', params);
}
// digestion preset save
async digestionPresetList() {
return await this.call('digestion-preset/list');
}
// digestion preset delete
async digestionPresetDelete(id) {
return await this.call('digestion-preset/delete', {id});
}
} }
Loading…
Cancel
Save