[Project Combat: Nucleic Acid Detection Platform] Chapter 4 Charge

Project Combat: Nucleic Acid Detection Platform Chapter 4 Charge

Abstract: In war, the most dangerous people are always at the forefront. In the battle against the new crown, the frontline is medical staff and epidemic prevention workers.

As an important weapon of the vanguard, the APP of the collectors of the nucleic acid detection platform must be easy to use and practical to solve the problem.

Objectives of this chapter

Complete the collector APP

Technology used:

  • stored procedure
  • cursor
  • Barcode, QR code scanning component

difficulty

  • Select collection point page search filter effect
  • Administrative division cascade selection and default data display

overview

In war, the most dangerous people are always at the forefront. In the war against the new crown, it is medical staff and epidemic prevention workers who are at the forefront.

As an important weapon of the vanguard, the nucleic acid detection platform's collector APP must be easy to use and practical in order to become a sharp weapon for the nucleic acid collection force.

Although we are doing simulation and not optimizing performance, the design of business logic must be as practical and easy to use as possible. Also ensure the accuracy of the data.

Let’s review the nucleic acid collection process again. This time we added a list of business logic instructions at the end of the flow chart.

insert image description here

Although the personnel collection module is an independent module, it still needs to work with other modules, and the coordination points are as follows:

  1. The box code and test tube code barcode should be generated in advance, and the box code and test tube code should also be generated in advance.
  2. The nucleic acid code is a two-dimensional code generated by ordinary users after filling in the data.
  3. After the box is sealed, the transshipment personnel will continue the subsequent operations

Generally speaking, the following points should be paid attention to in the functions of the personnel collection module, and should be paid attention to when coding the function page.

  1. Data is not created during unpacking and tube opening operations, but the status corresponding to the box code and test tube code is modified.

  2. When adding sample (tested person) information, there are three ways to add: scan ID card, scan nucleic acid code, and manually enter.

    1. Scanning the ID card involves the ID card identification module, which can call the AI ​​interface provided by the manufacturer, and finally return the text data, which will not be implemented for the time being.
    2. When scanning the nucleic acid code, the data in the nucleic acid code should be the information identification of ordinary users, which is just a number, and this number should correspond to the identification in people. Therefore, after the scan is completed, it is necessary to extract personnel information in the background.
    3. When manually entering, if there is already data in the people database, the personnel information can be automatically extracted after the ID card input is completed; when submitting the saved information, it is necessary to check whether the entered person’s information is in the people, if not, it should be in the people Add personnel information, so that you can manually enter the information next time.
  3. When adding samples, the number of test tubes should be checked, and the front-end interface should remind the number of remaining samples. When the number of test tubes is exceeded, a reminder should be given. For the convenience of collectors, a small amount of excessive collection should also be allowed. Therefore, no mandatory constraints are imposed when adding samples at the back end.

  4. When adding samples, it is necessary to check the double entry of ID card information.

  5. Samples allow modification and deletion

  6. The test tube should allow deletion, but the deletion does not delete the data, but restores the state of the test tube.

  7. When sealing the box, check whether there is an unsealed test tube code, and if there is, sealing the box should be prohibited.

  8. There is a function of changing registration information in the actual running module.

    At first, I didn’t quite understand the meaning of this function. If you pay attention when using the APP, you will find that the selected collection point starts to load data from the area you selected. If you change the registration information, it will switch to other areas.

    Because the APP download link given to you in the first chapter is from Shanghai, why do you want to do this?

    The resident population of Shanghai is 24 million, plus the non-resident population, which may exceed 30 million. During large-scale collection, the collection work is generally completed within 6 hours. On average, 80,000 samples are collected per minute. The second is nearly 1400 samples.

    Collecting a sample requires 3-5 operations on the interface, and about 3-5 data operations on the background interface. Therefore, the average concurrency on the server side is around 7000, and the peak concurrency may be higher. This amount of concurrency is not too large, but it is not a lot. It is entirely possible to divide into different areas and use different services to support them.

Let's start to realize the development of these functions step by step.

Prepare test data

Because this module is related to other modules, that is to say, some data is generated by other modules before reaching the collection personnel module. What kind of data is there?

  1. Collection point data
  2. Box code data
  3. Test tube code data
  4. Personnel information data

In order to facilitate debugging when writing programs, we still need to create some test data, and the data of the collection points should be bound to the administrative division data.

To create test data, you can enter it manually, or write a script to generate it. Here I use a database stored procedure to write a script to generate test data.

-- 创建采集点测试数据
-- 创建采集点的是在行政区划中第一个村、社区下创建3个采集点,
-- 所以要先查询出area表中的数据,然后用游标提取出每个村、社区的内容,再生成数据,往point表中添加数据。
-- 为便于区分采集点,采集点名称取村、社区名,在后面加上随机数字。
DROP procedure  IF EXISTS generate_points;

CREATE procedure  generate_points()
BEGIN
	DECLARE _areaId bigint;
	DECLARE _pointName varchar(50);
	DECLARE _areaName varchar(30);
	DECLARE var_done int DEFAULT FALSE;
	-- area表中是全国的行政区划,所以不能全查,而是做了一个区的限定。
	DECLARE cursor_area CURSOR FOR select areaId,name from area where areacode = 410307 and level=5;
-- 游标结束时会设置var_done为true,后续可以使用var_done来判断游标是否结束
	DECLARE CONTINUE HANDLER FOR NOT FOUND SET var_done=TRUE;
	open cursor_area;
		select_loop:LOOP
			FETCH cursor_area INTO _areaId,_areaName;
			set _areaName=replace(_areaName,'村民委员会','');
			
-- 			每个村随机生成3个采集点
			SET _pointName = concat(_areaName,'采集点',CEILING(RAND()*50));
			insert into point (pointName,areaCode)
			values (_pointName,_areaId);
			
			SET _pointName = concat(_areaName,'采集点',CEILING(RAND()*50));
			insert into point (pointName,areaCode)
				values (_pointName,_areaId);
			
			SET _pointName = concat(_areaName,'采集点',CEILING(RAND()*50));
			insert into point (pointName,areaCode)
				values (_pointName,_areaId);
			IF var_done THEN
				LEAVE select_loop;
			END IF;
		END LOOP;
	CLOSE cursor_area;
End;
call generate_points();


-- 创建采集箱测试数据,boxCode 从10001开始,创建100个
-- 箱码数据boxCode一般是连续的,并且是不能够重复的,创建测试数据的时候要注意这个问题。
-- 下面的存储过程有两个入参,一个是开始编码,一个是结束编码。
DROP procedure  IF EXISTS generate_boxs;

CREATE procedure  generate_boxs(IN beginCode bigint,in endCode int)
BEGIN
		select_loop:LOOP
			IF beginCode <=endCode THEN
			-- 箱码要初始status数据为0,表示箱码已打印。其它信息在开箱的时候再更新
				insert into box (boxCode,`status`)
					values(beginCode,0);
				set beginCode = beginCode+1;
			else 
			
				LEAVE select_loop;
			end if;
		END LOOP;
END;
call generate_boxs(100001,100100);




-- 创建试管码测试数据
-- 规则与箱码的生成一样
DROP procedure  IF EXISTS generate_testtubes;

CREATE procedure  generate_testtubes(IN beginCode bigint,in endCode bigint)
BEGIN
		select_loop:LOOP
			IF beginCode <=endCode THEN
			-- 试管码要初始status数据为0,表示试管码已打印。其它信息在开管的时候再更新
				insert into testtube (testTubeCode,`status`)
					values(beginCode,0);
				set beginCode = beginCode+1;
			else 
				LEAVE select_loop;
			end if;
		END LOOP;
END;
call generate_testtubes(20221001000001,20221001000501);


-- 创建人员信息测试数据
-- 为了在测试的时候方便区分,第个人的名字后面加上了一个编号。
DROP procedure  IF EXISTS generate_peoples;

CREATE procedure  generate_peoples(IN beginIdcardCode bigint,in endIdcardCode bigint)
BEGIN
	declare _index int;
	set _index = 1;
		select_loop:LOOP
			IF beginIdcardCode <=endIdcardCode THEN
				insert into people (idcard,name,tel)
					values(beginIdcardCode,concat('张三',_index),(18700010000+_index));
				set beginIdcardCode = beginIdcardCode+1;
				set _index =_index+1;
			else 
				LEAVE select_loop;
			end if;
		END LOOP;
END;
call generate_peoples(280103199901020001,280103199901020100);

log in Register

Therefore, the most common functions of the software system, when you need to pay attention, the password in the normal system must be encrypted. The most basic and common encryption method is MD5 encryption, but its strength is relatively weak and relatively easy to be cracked. For a more advanced level, you can use the method of adding random salt. This article uses the most basic encryption method.


@RestController
@RequestMapping("/collector")
public class CollectorController {
    
    
    @Autowired
    ICollectorService collectorService;

    @PostMapping("login")
    public ResultModel<Collector> login(@RequestBody @Valid LoginModel model) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
    
    
        Collector collector = collectorService.login(model);
        //虽然采用了token的登录验证方式,但还是要在session中存储一下,取当前登录用户时就可以直接从session取,减少sql查询请求
        SessionUtil.setCurrentUser(collector);
        return ResultModel.success(collector);
    }
}

The parameter type of the login interface is LoginModel, which is a separate definition and BO class, and is placed in the pojo.bo package of the personnel collection module.

There are also custom validation rules defined on the fields of the class. It should be noted that in order for the validation rules to take effect, the @Valid annotation needs to be added in front of the controller parameter.

package com.hawkon.collector.pojo.bo;

import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

@Data
public class LoginModel {
    
    
    @NotEmpty(message = "手机号不能为空")
    @Size(min = 11, max = 11,message = "手机号必须是11位")
    private String tel;
    @NotEmpty(message = "密码不能为空")
    @Size(min = 6,message = "密码至少6位 ")
    private String password;
}
@Service
public class CollectorService implements ICollectorService {
    
    
    @Autowired
    CollectorDao collectorDao;

    @Override
    public Collector login(LoginModel model) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
    
    
        String md5Password = Md5Util.encode(model.getPassword());
      	//数据库中存储的是加密的密码,所以要先把输入的密码加密再进行查询
        Collector collector = collectorDao.login(model.getTel(), md5Password);
        if (collector == null) {
    
    
            throw new BusinessException("用户名或密码不正确", ResultCodeEnum.LOGIN_ERROR);
        }
        String token = getToken(collector);
        //把token存到cokkie中,并设置过期时间,一天
        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setMaxAge(7 * 24 * 60 * 60);
        Global.response.addCookie(cookie);
        //返回前端之前要把密文的密码清除掉。
        collector.setPassword(null);
        return collector;
    }
  

    /**
     * 根据用户信息生成token
     * @param user
     * @return
     */
    public String getToken(Collector user) {
    
    
        Date start = new Date();
        //有效期设置为7天是为开发阶段方便,实际项目应用中一天足够。
        long currentTime = System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000;//7天有效时间
        Date end = new Date(currentTime);
        String token = "";

        token = JWT.create()
          .withAudience(user.getCollectorId().toString())
          .withIssuedAt(start)
          .withExpiresAt(end)
          .sign(Algorithm.HMAC256(user.getPassword()));
        return token;
    }
}
//MD5加密工具类
public class Md5Util {
    
    
    public static String encode(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
    
    
        //确定计算方法
        MessageDigest md5=MessageDigest.getInstance("MD5");
        BASE64Encoder base64en = new BASE64Encoder();
        //加密后的字符串
        String newstr=base64en.encode(md5.digest(str.getBytes("utf-8")));
        return newstr;
    }
}

When registering, it is necessary to verify whether the phone number and ID number have been registered. Also note that the default password is the last 6 digits of the ID card, and the password should also be encrypted.

From a practical point of view, the password of this APP is relatively random, just get the last 6 digits of the ID card, and the password strength seems to be very poor. Yes, the involvement of such a cryptographic mechanism seems inconceivable in any Internet project. However, for this project, the password strength is too harsh and it is not convenient to use, and the password cracking will not bring too serious consequences. So it is acceptable to do so from the perspective of practical use.

@Override
public void register(Collector model) throws Exception {
    
    
    Collector modelByTel = collectorDao.getCollectorByTel(model.getTel());
    if (modelByTel != null) {
    
    
        throw new BusinessException("电话号码已注册,请直接登录", ResultCodeEnum.REGISTER_ERROR);
    }
    Collector modelByIdcard = collectorDao.getCollectorByIdCard(model.getIdcard());
    if (modelByIdcard != null) {
    
    
        throw new BusinessException("身份证号已注册", ResultCodeEnum.REGISTER_ERROR);
    }
    //取身份证后6位进行加密
    String md5String = Md5Util.encode(model.getIdcard().substring(model.getIdcard().length() - 6));
    model.setPassword(md5String);
    model.setCollectorType(CollectorType.Volunteer.getCode());
    collectorDao.register(model);
}

Login.vue page

<script setup>
	import {
		ref
	} from 'vue';
	import {
		Toast
	} from 'vant';
	import {
		RouterLink,useRouter
	} from 'vue-router'
	//引用封装过后的axios组件
	import api from '@/common/api.js';
	const router = useRouter();

	const loginForm = ref({
		tel: '18638898990',
		password: '156011'
	});
	const now = new Date();
	const onSubmit = (values) => {
		api.post("/collector/login", loginForm.value)
			.then(res => {
				//代码到这里一定是登录成功,因为失败的时候会被api.js中的拦截器处理掉。
				//成功的时候把返回的数据保存在sessionStorage中,因为sessionStorage只能保存String,所以要用JSON.stringify转换一下。
				window.sessionStorage["user"] = JSON.stringify(res.data);
				//跳转路由
				router.push("/SelectPoint");
			})
			.catch(res => {
				console.log("错误", res)
			})
	};
	
</script>

<template>
	<van-row>
		<van-col span="24">
			<h2 style="text-align: center;">全场景疫情病原体检测信息系统</h2>
		</van-col>
		<van-col span="24">
			<van-form @submit="onSubmit">
				<van-cell-group inset>
					<van-field v-model="loginForm.tel" name="tel" label="手机号" placeholder="请输入手机号"
						:rules="[{ required: true, message: '请填写手机号' }]" />
					<van-field v-model="loginForm.password" type="password" name="password" label="密码"
						placeholder="默认密码为身份证后6位" :rules="[{ required: true, message: '请填写密码' }]" />
				</van-cell-group>
				<div style="margin: 16px;">
					<van-button round block type="primary" native-type="submit">
						提交
					</van-button>
				</div>
			</van-form>
		</van-col>
		<van-col span="12" style="padding-left: 1em;">
			<RouterLink to="/Register">注册</RouterLink>
		</van-col>
		<van-col span="12" style="text-align: right;padding-right: 1em">
			<RouterLink to="/Forget">忘记密码</RouterLink>
		</van-col>
	</van-row>
</template>

Register.vue page

The multi-level cascading of the registration page is more troublesome. I considered returning to the front end once and filtering it by the front end, but the amount of data directly from the administrative division to the fifth level is still quite large, so I gave up.

<script setup>
	import {
		ref
	} from 'vue';
	import {
		useRouter
	} from "vue-router";
	import api from "../common/api.js";
	const router = useRouter();
	const onClickLeft = () => {
		router.push("/");
	}
	const registerForm = ref({});

	//重置registerForm上面的指定级别的行政区划代码
	const resetArea = (props) => {
		if (props) {
			props.forEach(key => registerForm.value[key] = null);
		}
	}
	//选择省
	//vant的固定用法,控制显示省的选择组件。
	const showProvincePicker = ref(false);

	const provinces = ref([]);
	api.post("/area/getProvinces")
		.then(res => {
			provinces.value = res.data;
		})
		
	//选中省的时候要重置下面的市、县区等。
	const confirmProvince = (value) => {
		registerForm.value.provinceCode = value.provinceCode;
		registerForm.value.provinceName = value.name;
		showProvincePicker.value = false;
		//选中之后重新加载市的清单
		getCitiesByProvinceCode();
		resetArea(["cityCode","cityName","areaCode","areaName","streetCode","streetName","committeeCode","committeName"]);
	}
	const areaFieldName = {
		text: 'name'
	}


	const cities = ref([]);
	const getCitiesByProvinceCode = () => {
		api.post("/area/getCitiesByProvinceCode", {
				provinceCode: registerForm.value.provinceCode
			})
			.then(res => {
				cities.value = res.data;
			})
	}
	const showCityPicker = ref(false);
	const confirmCity = (value) => {
		registerForm.value.cityCode = value.cityCode;
		registerForm.value.cityName = value.name;

		showCityPicker.value = false;
		getAreasByCityCode();
		resetArea(["areaCode","areaName","streetCode","streetName","committeeCode","committeName"]);
	}

	//区县选择器
	const areas = ref([]);
	const getAreasByCityCode = () => {
		api.post("/area/getAreasByCityCode", {
				cityCode: registerForm.value.cityCode
			})
			.then(res => {
				areas.value = res.data;
			})
	}
	const showAreaPicker = ref(false);
	const confirmArea = (value) => {
		registerForm.value.areaCode = value.areaCode;
		registerForm.value.areaName = value.name;
		showAreaPicker.value = false;
		getStreetsByAreaCode();
		resetArea(["streetCode","streetName","committeeCode","committeName"]);
	}

	//街道/乡镇选择器
	const streets = ref([]);
	const getStreetsByAreaCode = () => {
		api.post("/area/getStreetsByAreaCode", {
				areaCode: registerForm.value.areaCode
			})
			.then(res => {
				streets.value = res.data;
			})
	}
	const showStreetPicker = ref(false);
	const confirmStreet = (value) => {
		registerForm.value.streetCode = value.streetCode;
		registerForm.value.streetName = value.name;
		//最终提交的行政区划ID,要么是街道,要么是村、社区,需要保存他们的areaId
		registerForm.value.areaId =value.areaId;
		showStreetPicker.value = false;
		getCommitteesByStreetCode();
		resetArea(["committeeCode", "committeeName"]);
	}


	//村/社区选择器
	const committees = ref([]);
	const getCommitteesByStreetCode = () => {
		api.post("/area/getCommitteesByStreetCode", {
				streetCode: registerForm.value.streetCode
			})
			.then(res => {
				committees.value = res.data;
			})
	}
	const showCommitteePicker = ref(false);
	const confirmCommittee = (value) => {
		registerForm.value.committeeCode = value.committeeCode;
		registerForm.value.committeeName = value.name;
		//最终提交的行政区划ID,要么是街道,要么是村、社区,需要保存他们的areaId
		registerForm.value.areaId =value.areaId;
		showCommitteePicker.value = false;
	}


	//表单验证
	const repeatValidator = (val) => {
		return val == registerForm.value.tel;
	}
	const register = ()=>{
		var registerModel = {
			name:registerForm.value.name,
			tel:registerForm.value.tel,
			idcard:registerForm.value.idcard,
			//采集人员注册时可以选择街道,也可以选择到村/社区,无论哪一种,最后取的都是areaId
			areaId:registerForm.value.areaId
		}
		api.post("/collector/register",registerModel)
		.then(res=>{
			router.push("/");
		})
	}
</script>

<template>
	<van-nav-bar title="注册" left-text="返回" left-arrow @click-left="onClickLeft" />
	<van-form style="padding: 0.5rem;" @submit="register">
		<h3>第一步:填写身份信息</h3>
		<van-cell-group inset>
			<van-field required v-model="registerForm.name" name="name" label="姓名" placeholder="姓名"
				:rules="[{  required: true, message: '请输入姓名' }]" />
			<van-field required v-model="registerForm.idcard" name="idcardPattern" label="身份证号" placeholder="身份证号"
				:rules="[{ pattern:/^([1-6][1-9]|50)\d{4}(18|19|20)\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入18位身份证号',trigger:'onBlur' }]" />
			<van-field required v-model="registerForm.tel" name="tel" placeholder="手机号" label="手机号"
				:rules="[{ pattern:/^1(3[0-9]|4[5,7]|5[0,1,2,3,5,6,7,8,9]|6[2,5,6,7]|7[0,1,7,8,6]|8[0-9]|9[1,8,9])\d{8}$/, message: '请输入正确的手机号',trigger:'onBlur' }]" />
			<van-field required v-model="registerForm.telAgain" name="tel" label="确认手机号" placeholder="确认手机号"
				:rules="[{ validator:repeatValidator,trigger:'onBlur', message: '两次手机号不一致'}]" />
		</van-cell-group>
		<h3>第二步:填写所属机构</h3>
		<van-cell-group inset>
			<van-field required v-model="registerForm.provinceName" name="provinceCode" label="省" is-link readonly
				placeholder="点击选择省" @click="showProvincePicker=true" :rules="[{  required: true, message: '请选择省' }]" />
			<van-popup required v-model:show="showProvincePicker" position="bottom">
				<van-picker :columns="provinces" :columns-field-names="areaFieldName" @confirm="confirmProvince"
					@cancel="showProvincePicker=false" />
			</van-popup>
			<van-field required v-model="registerForm.cityName" name="cityCode" label="市" placeholder="请选择市" is-link readonly
				@click="showCityPicker=true" :rules="[{  required: true, message: '请选择市' }]" />
			<van-popup required v-model:show="showCityPicker" position="bottom">
				<van-picker :columns="cities" :columns-field-names="areaFieldName" @confirm="confirmCity"
					@cancel="showCityPicker=false" />
			</van-popup>
			<van-field required v-model="registerForm.areaName" name="areaCode" :rules="[{  required: true, message: '请选择区/县' }]"
				placeholder="区/县" label="区/县" is-link readonly @click="showAreaPicker=true" />
			<van-popup v-model:show="showAreaPicker" position="bottom">
				<van-picker :columns="areas" :columns-field-names="areaFieldName" @confirm="confirmArea"
					@cancel="showAreaPicker=false" />
			</van-popup>
			<van-field required v-model="registerForm.streetName" name="streetCode" label="街道/乡镇" placeholder="选择街道/乡镇"
				:rules="[{  required: true, message: '请选择街道/乡镇' }]" is-link readonly @click="showStreetPicker=true" />
			<van-popup v-model:show="showStreetPicker" position="bottom">
				<van-picker :columns="streets" :columns-field-names="areaFieldName" @confirm="confirmStreet"
					@cancel="showStreetPicker=false" />
			</van-popup>
			<van-field  v-model="registerForm.committeeName" name="committeeCode" label="村/社区" placeholder="选择村/社区"
				is-link readonly @click="showCommitteePicker=true" />
			<van-popup v-model:show="showCommitteePicker" position="bottom">
				<van-picker :columns="committees" :columns-field-names="areaFieldName" @confirm="confirmCommittee"
					@cancel="showCommitteePicker=false" />
			</van-popup>
		</van-cell-group>
		<div style="margin: 16px;">
			<van-button round block type="primary" native-type="submit">
				提交
			</van-button>
		</div>
	</van-form>
</template>
<style>
	h3 {
		margin-top: 1rem;
	}
</style>

Select collection point

This page is a bit difficult. There is a search function on the page. When searching, the collection points are filtered, but when it is displayed, it is not only necessary to filter the collection points, but also to filter the corresponding counties, streets, villages and communities, so as to quickly selected.

This effect can be done on the front end or on the back end. In terms of project usage, filtering on the front end will put less pressure on the server.

Therefore, the backend directly returns all the collection points, and the administrative division data displays the administrative division data of the corresponding district through the administrative division data of the registered collectors.

insert image description here

Backend code:


//controller层
    /**
     * 获取区下面的所有行政区划,由前端来处理
     * @param model
     * @return
     */
    @PostMapping("getAllAreaByAreaCode")
    public ResultModel<List<Area>> getAllAreaByAreaCode(@RequestBody Collector model){
    
    
        //前端直接传回当前用户的areaid,是直接到社区的,这里要转换为区的编码,并把区下面的所有数据全部取回;
        Long areaId = model.getAreaId();
        Long areaCode = areaId/1000000;
        List<Area> areas = areaService.getAreasByAreaCode(areaCode);
        return ResultModel.success(areas);
    }
//service层
    @Override
    public List<Area> getAreasByAreaCode(Long areaCode) {
    
    
        List<Area> areas = areaDao.getAreasByAreaCode(areaCode);
        return areas;
    }
//获取采集点
//constroller层
@PostMapping("getPoints")
public ResultModel<List<Point>> getPoints() throws BusinessException {
    
    
    List<Point> provinces = pointService.getPointsByCurrentUser();
    return ResultModel.success(provinces);
}
//service层
@Autowired
PointDao pointDao;
@Override
public List<Point> getPointsByCurrentUser() throws BusinessException {
    
    
    Collector currentUser = SessionUtil.getCurrentUser();
    //因为collector中存的areaId是12位的,可以精确到村社区,
    //但是选择采集点是从区开始的,只列出采集人员所在区的采集点。
    //采集点上的areaId也是精确到村社区一层的,而查询的时候是需要将区下面的所有采集点查询出来,
    //所以查询采集点的时候先取出当前采集人员绑定的areaId,再换算成区的areaCode,
    // 例如,采集人员的areaId是410325108204,区的id应是410325,所以用areaId/1000000即可得到区的id
    //执行查询的时候,采集点中的areaId存的也是到村/社区的,在sql语句中可以采用Like运算,
    //前端页面上的搜索功能可以在前商实现,这样可以减少数据查询的请求次数,减少服务器压力。

    Long areaCode = currentUser.getAreaId()/1000000;
    return pointDao.getPointsByAreaCode(areaCode);
}

SQL statements:


    <select id="getAreasByAreaCode" resultType="com.hawkon.common.pojo.vo.AreaTree">
        select *
        from area
        where areaCode = #{areaCode}
    </select>
	
    <select id="getPointsByAreaCode" resultType="com.hawkon.common.pojo.Point">
        select *
        from point
        where areaCode like concat(#{areaCode},'%');
    </select>

front-end code

SelectPoint.vue

<script setup>
	import {
		ref
	} from 'vue';
	import {
		useRouter
	} from "vue-router";
	const router = useRouter();
	import {
		Toast
	} from 'vant';
	import api from '@/common/api.js';
	const onClickLeft = () => {
		api.post("/collector/logout")
			.then(res => {
				router.push("/");
			})
	};
	//存放后台取到的所有行政区划数据,数据对象名称和区县的名称重复,这里用al
	const allAreaList = ref([]);
	const allPoint = ref([]);
	//存放过滤后的区县一级数据。
	//areas的结构为 areas里是区的数组,区下包含街道数组、街道下包含村、社区数组,村、社区下包含采集点数组。
	const areas = ref([]);
	//检索的关键字
	const key = ref("");
	const currentPoint = ref({});

	const loadAreaList = () => {
		var collector = JSON.parse(sessionStorage["user"])
		api.post("/area/getAllAreaByAreaCode", {
				areaId: collector.areaId
			})
			.then(res => {
				allAreaList.value = res.data;
				queryTrees();
			})
	}
	api.post("/point/getPoints")
		.then(res => {
			allPoint.value = res.data
			loadAreaList();
		});

	//上面是加载数据,然后是显示数据,因为有搜索功能,所以在显示的数据的时候搜索关键字为空的时候也要一并考虑。
	const queryTrees = () => {
		//定义街道和村两级map,存储搜索结果涉及的街道和村,便于最后过滤。
		//区一级不用过滤。
		let streetCodeMap = {};
		let committeeCodeMap = {};
		//定义采集点map,用areaid做为key,暂时存储一下,在方法的最后方便往commmittee上添加,这样不需要再循环一次了。
		let pointsMap = {};
		let searched = key.value?true:false;

		//定义个数组变量,不要直接操作points.value,会触发监听器。
		for (let i = 0; i < allPoint.value.length; i++) {
			let item = allPoint.value[i];
			//检索时名字包含或没有检索时往pointsMap里添加
			if (item.pointName.indexOf(key.value) >= 0 || !searched) {
				let areaId = parseInt(item.areaCode);
				if (!(pointsMap[areaId])) {
					//如果关键字还没值,则需要初始化为数组
					pointsMap[areaId] = [];
				}
				pointsMap[areaId].push((item));
				//采集点的areaCode定位的就村,直接把map中对应areaCode设置true
				committeeCodeMap[areaId] = true;
				//街道一级要把areaID最后三位设置为000.
				let streetCode = Math.floor(areaId / 1000) * 1000;
				streetCodeMap[streetCode] = true;
			}
		}

		let arr_areas = [];
		let arr_streets = [];
		let arr_committees = [];
		let areaItem = null;
		let streetItem = null;
		//数据库里读取出来的area都是一层一层按顺序的,可以用areaItem,steetItem两个变量往子项里添加数据。如果不按顺序那就不能这么干了。
		for (var i = 0; i < allAreaList.value.length; i++) {
			let item = allAreaList.value[i];
			if (item.level == 3) {
				//区县
				arr_areas.push(item);
				areaItem = item;
				//初始化街道数组
				areaItem.streets = [];
				areaItem.showChild = searched;
			}
			if (item.level == 4) {
				//乡镇街道,如果搜索条件没有东西或街道在检索结果中
				if (streetCodeMap[item.areaId] || !searched) {
					//如果检索采集点里有该乡镇的areaId,再往数组里添加
					areaItem.streets.push(item);
					streetItem = item;
					//初始化村、社区数组
					streetItem.committees = [];
					streetItem.showChild = searched;
				}
			}
			if (item.level == 5) {
				if (committeeCodeMap[item.areaId] || !searched) {
					//如果检索采集点里有该村、社区的areaId,再往数组里添加
					streetItem.committees.push(item);
					item.points = pointsMap[item.areaId];
					item.showChild = searched;
				}
			}
		}
		areas.value = arr_areas;
	};

	const selectPoint = (point) => {
		if (currentPoint.value) {
			currentPoint.value.selected = false;
		}
		currentPoint.value = point;
		point.selected = true;
	}
	const toBoxPage = () => {
		if (currentPoint.value.pointId) {
			localStorage["currentPoint"] = JSON.stringify(currentPoint.value);
			router.push("/Box")
		} else {
			Toast.fail('请先选择采集点');
		}
	}
</script>

<template>
	<van-nav-bar title="选择采集点" left-text="退出" left-arrow @click-left="onClickLeft" />

	<van-search v-model="key" show-action shape="round" background="#4fc08d" placeholder="请输入采集点" @search="queryTrees">
		<template #action>
			<div @click="queryTrees">搜索</div>
		</template>
	</van-search>
	<div class="point-tree">
		<ul>
			<li v-for="(area) in areas" :key="area.areaId">
				<van-space align="center" @click="area.showChild=!area.showChild" >
					<van-icon :name="!area.showChild?'plus':'minus'" />
					{
   
   {area.name}}
				</van-space>
				<ul v-show="area.showChild">
					<li v-for="(street) in area.streets" :key="street.areaId">
						<van-space align="center" @click="street.showChild=!street.showChild" >
							<van-icon :name="!street.showChild?'plus':'minus'"
								/>
							{
   
   {street.name}}
						</van-space>
						<ul v-show="street.showChild">
							<li v-for="(committee) in street.committees" :key="committee.areaId">
								<van-space align="center" @click="committee.showChild=!committee.showChild" >
									<van-icon :name="!committee.showChild?'plus':'minus'"
										/>
									{
   
   {committee.name}}
								</van-space>
								<ul v-show="committee.showChild">
									<li class="bline" v-for="(point) in committee.points" :key="point.pointId">
										<van-space align="center" @click="selectPoint(point)">
											<van-icon :name="point.selected?'success':''" />
											{
   
   {point.pointName}}
										</van-space>
									</li>
								</ul>
							</li>
						</ul>
					</li>
				</ul>
			</li>
		</ul>
	</div>
	<van-tabbar v-model="active">
		<van-tabbar-item>
			<van-button type="primary" size="normal" @click="toBoxPage">确定</van-button>
		</van-tabbar-item>
	</van-tabbar>
</template>
<style>
	.point-tree {
		margin: 1rem;
	}

	ul {
		font-size: 1.5rem;
	}

	li {
		margin-left: 1rem;
	}

	.bline {
		border-bottom: 1px solid #5093eb;
	}

	li .van-icon-plus {
		font-size: 0.5rem;
	}
</style>

unpack

Just check the box code and check the status of the box code on the back-end interface after unpacking

//Service层
@Override
public Box openBox(Box model) throws BusinessException {
    
    
    if(model.getPointId()==null){
    
    
        throw new BusinessException("参数错误,没有采集点ID", ResultCodeEnum.BUSSINESS_ERROR);
    }
    Box modelDb = boxDao.getBoxByBoxCode(model.getBoxCode());
    if (modelDb == null) {
    
    
        throw new BusinessException("非法箱码", ResultCodeEnum.BUSSINESS_ERROR);
    }
    if(modelDb.getStatus()>1){
    
    
        throw new  BusinessException("转运箱已封箱,请检查箱码", ResultCodeEnum.BUSSINESS_ERROR);
    }
    if(modelDb.getStatus()==1){
    
    
        return modelDb;
    }
    Integer collectorId = SessionUtil.getCurrentUser().getCollectorId();
    model.setCollectorId(collectorId);
    boxDao.openBox(model);
    modelDb.setStatus(1);
    modelDb.setCollectorId(collectorId);
    return modelDb;
}

The front-end page Box.vue, because we need to use the camera to scan the code, we can use the @zxing/library component to complete it. Installation method: npm install @zxing/library.

After installation, you need to change the project to https mode, otherwise the camera will not be able to open.

How to enable HTTPS:

The first step: install the components,npm install @vitejs/plugin-basic-ssl

Step Two: vite.config.jsAdd the code in

//引入类库
import basicSsl from '@vitejs/plugin-basic-ssl'

export default defineConfig({
    
    
	plugins: [
    basicSsl(),//添加插件配置
		vue(),
		Components({
    
    
			resolvers: [VantResolver()],
		}),
	],
  ....

After the configuration is complete, restart the front-end project, and it will be OK when you see the command prompt below. It should be noted that the HTTPS protocol should also be changed when accessing the page.

insert image description here

The @zxing/library component calls the camera scan method as follows:

The label that displays the camera capture screen on the page

		<video ref="video" id="video" class="scan-video" autoplay></video>	

Scan method:

//打开摄像头
const openCamera = () => {
    
    
	codeReader.value.getVideoInputDevices().then((videoInputDevices) => {
    
    
		tipMsg.value = "正在调用摄像头...";
		// 因为获取的摄像头有可能是前置有可能是后置,但是一般最后一个会是后置,所以在这做一下处理
		// 默认获取第一个摄像头设备id
		let firstDeviceId = videoInputDevices[0].deviceId;
		if (videoInputDevices.length > 1) {
    
    
			// 获取后置摄像头
			let deviceLength = videoInputDevices.length;
			--deviceLength;
			firstDeviceId = videoInputDevices[deviceLength].deviceId;
		}
		decodeFromInputVideoFunc(firstDeviceId);
	}).catch((err) => {
    
    
		tipMsg.value = JSON.stringify(err);
		console.error(err);
	});
}
//扫描时会不断调用该回调
const decodeFromInputVideoFunc = (firstDeviceId) => {
    
    
	codeReader.value.reset(); // 重置
	codeReader.value.decodeFromInputVideoDeviceContinuously(firstDeviceId, "video", (result, err) => {
    
    
		tipMsg.value = "正在尝试识别...";
		if (result) {
    
    
			// 获取到的是条码内容,然后在这个if里面写业务逻辑即可
			boxCode.value = result.text;
			tipMsg.value = "识别成功:" + boxCode.value;
			//扫码成功直接调用开箱方法
			openBox();
		}
		if (err && !err) {
    
    
			tipMsg.value = JSON.stringify(err);
			console.error(err);
		}
	});
}
const closeCamera = () => {
    
    
	codeReader.value.stopContinuousDecode();
	codeReader.value.reset();
}

Box.vue complete code

<script setup>
	import {
		ref
	} from 'vue';
	import {
		useRouter
	} from "vue-router";
	const router = useRouter();
	import {
		Toast
	} from 'vant';
	import api from '@/common/api.js';
	import {
		BrowserMultiFormatReader
	} from "@zxing/library";
	const logout = () => {
		api.post("/collector/logout")
			.then(res => {
				router.push("/");
			})
	};
	const collector = ref({});
	collector.value = JSON.parse(sessionStorage["user"]);
	const point = ref({});
	try {
		point.value = JSON.parse(localStorage["currentPoint"]);
	} catch {
		Toast.fail('参数错误');
		router.push("/SelectPoint")
	}
	const openBoxVisable = ref(false);
	const scanBoxCode = ref(true);
	const codeReader = ref(null);
	const boxCode = ref("");
	const tipMsg = ref("");
	codeReader.value = new BrowserMultiFormatReader();
	const beginScanner = () => {
		openBoxVisable.value = true;
		scanBoxCode.value = true;
		openCamera();
	}
	const openCamera = () => {
		codeReader.value.getVideoInputDevices().then((videoInputDevices) => {
			tipMsg.value = "正在调用摄像头...";
			// 因为获取的摄像头有可能是前置有可能是后置,但是一般最后一个会是后置,所以在这做一下处理
			// 默认获取第一个摄像头设备id
			let firstDeviceId = videoInputDevices[0].deviceId;
			if (videoInputDevices.length > 1) {
				// 获取后置摄像头
				let deviceLength = videoInputDevices.length;
				--deviceLength;
				firstDeviceId = videoInputDevices[deviceLength].deviceId;
			}
			decodeFromInputVideoFunc(firstDeviceId);
		}).catch((err) => {
			tipMsg.value = JSON.stringify(err);
			console.error(err);
		});
	}
	const decodeFromInputVideoFunc = (firstDeviceId) => {
		codeReader.value.reset(); // 重置
		codeReader.value.decodeFromInputVideoDeviceContinuously(firstDeviceId, "video", (result, err) => {
			tipMsg.value = "正在尝试识别...";
			if (result) {
				// 获取到的是二维码内容,然后在这个if里面写业务逻辑即可
				boxCode.value = result.text;
				tipMsg.value = "识别成功:" + boxCode.value;
				console.log(boxCode.value)
				openBox();
			}
			if (err && !err) {
				tipMsg.value = JSON.stringify(err);
				console.error(err);
			}
		});
	}
	const closeCamera = () => {
		codeReader.value.stopContinuousDecode();
		codeReader.value.reset();
	}
	const endScanner = () => {
		openBoxVisable.value = false;
		closeCamera();
	}
	const switchScanner = () => {
		scanBoxCode.value = !scanBoxCode.value;
		if (scanBoxCode.value) {
			openCamera();
		} else {
			closeCamera();
		}
	}
	const openBox = () => {
		api.post("/box/openBox", {
				boxCode: boxCode.value,
				pointId: point.value.pointId
			})
			.then(res => {
				console.log(res)
				if (res.code == 0) {
					sessionStorage["currentBox"] = JSON.stringify(res.data);
					router.push("/TesttubeList");
				}
			})
			.catch(res => {
				tipMsg.value = res.errMsg;
			})
	}
	const toBoxList=()=>{
		router.push("/BoxList")
	}
</script>

<template>
	<van-nav-bar title="全场景疫情病原体检测信息系统" right-arrow @click-right="logout">
		<template #right>
			<van-icon size="18" class-prefix="iconfont i-tuichu" name="extra" />
		</template>
	</van-nav-bar>
	<van-row>
		<van-col span="24" style="padding-top: 3rem;">
			<h1 class="t-center">{
   
   {collector.name}},您好</h1>
			<h2 class="t-center">{
   
   {point.pointName}}</h2>
		</van-col>
	</van-row>
	<van-row class="big-icon">
		<van-col span="12" style="height: 7rem;" class="t-center" @click="beginScanner">
			<van-icon size="6rem" class-prefix="iconfont i-ziyuan92" name="extra" />
			<h1>开箱</h1>
		</van-col>
		<van-col span="12" style="height: 7rem;" class="t-center" @click="toBoxList">
			<van-icon size="6rem" class-prefix="iconfont i-kaixiangyanhuo" name="extra" />
			<h1>列表</h1>
		</van-col>
	</van-row>
	<van-list>
		<van-cell title="按试管查转运箱" is-link to="SearchBox"/>
		<van-cell title="变更注册信息" is-link to="ChangeInfo"/>
	</van-list>
	<van-list>
		<van-cell title="修改密码" is-link to="ChangePwd"/>
	</van-list>

	<van-overlay :show="openBoxVisable" class="scanner">
		<div v-if="scanBoxCode">
			<video ref="video" id="video" class="scan-video" autoplay></video>
			<div>{
   
   {tipMsg}}</div>
		</div>
		<div v-else>
			<div class="scan-video"></div>
			<van-cell-group inset>
				<van-row class="padding">
					<van-col span="24">
						<van-field v-model="boxCode" label="输入箱码" placeholder="请输入箱码" />
					</van-col>
				</van-row>
				<van-row class="padding">
					<van-col span="24">
						<van-button round block type="primary" @click="openBox">确定开箱</van-button>
					</van-col>
				</van-row>
			</van-cell-group>
		</div>
		<van-cell-group inset style="margin-top:1rem">
			<van-row class="padding">
				<van-col span="24">
					<van-button round block type="primary" @click="switchScanner">{
   
   {scanBoxCode?'手动输入':'扫描'}}
					</van-button>
				</van-col>
			</van-row>
			<van-row class="padding">
				<van-col span="24">
					<van-button round block type="primary" @click="endScanner">取消</van-button>
				</van-col>
			</van-row>
		</van-cell-group>
	</van-overlay>

</template>
<style>
	.t-center {
		text-align: center;
	}

	.big-icon {
		margin-top: 2rem;
		background-color: #b8d6ef;
		height: 13rem;
	}

	.scan-video {
		width: 100%;
		height: 50vh;
	}

	.scanner {
		color: #fff;
	}

	.padding {
		padding: 1rem;
	}
</style>
	

insert image description here

Shipping box list page

This page displays a list of transfer boxes that have not yet been transferred. When there is no query, pay attention to filter the status of the transfer box.

insert image description here

Test tube list, open tube

The operation of opening the tube also needs to call the camera, which has been done before, so there is no difficulty here. Just change CV+.

insert image description here

After scanning the code, jumping to the manual input interface is not to open the tube directly, but to choose whether the collection type is single collection or mixed collection.

insert image description here

TestTubeList.vue

<script setup>
	import {
		ref
	} from 'vue';
	import {
		useRouter
	} from "vue-router";
	const router = useRouter();
	import {
		Toast,Dialog
	} from 'vant';
	import api from '@/common/api.js';
	import 'vant/es/dialog/style';
	const logout = () => {
		api.post("/collector/logout")
			.then(res => {
				router.push("/");
			})
	};
	if (!(sessionStorage["currentBox"])) {
		Toast.fail('非法箱码');
		router.push("/box");
	}
	var _box;
	try {
		_box = JSON.parse(sessionStorage["currentBox"]);
	} catch {
		Toast.fail('非法箱码');
		router.push("/box");
	}
	const box = ref(_box)

	const back = () => {
		router.push("/Box");
	}

	//读取试管列表
	const testtubeList = ref([]);
	const getTesttubeList = () => {
		api.post("/testtube/getTestTubeListByBoxId", {
				boxId: box.value.boxId
			})
			.then(res => {
				testtubeList.value = res.data;
			})
	}
	getTesttubeList();
	//开箱操作
	import {
		BrowserMultiFormatReader
	} from "@zxing/library";

	const showOpenTestTubeVisable = ref(false);
	const scanTestTubeCode = ref(true);
	const codeReader = ref(null);
	const testTubeCode = ref("");
	const tipMsg = ref("");
	//采集类型,默认为10人混采
	const collectType = ref("10");
	codeReader.value = new BrowserMultiFormatReader();
	const beginScanner = () => {
		showOpenTestTubeVisable.value = true;
		scanTestTubeCode.value = true;
		openCamera();
	}
	const openCamera = () => {
		codeReader.value.getVideoInputDevices().then((videoInputDevices) => {
			tipMsg.value = "正在调用摄像头...";
			// 因为获取的摄像头有可能是前置有可能是后置,但是一般最后一个会是后置,所以在这做一下处理
			// 默认获取第一个摄像头设备id
			let firstDeviceId = videoInputDevices[0].deviceId;
			if (videoInputDevices.length > 1) {
				// 获取后置摄像头
				let deviceLength = videoInputDevices.length;
				--deviceLength;
				firstDeviceId = videoInputDevices[deviceLength].deviceId;
			}
			decodeFromInputVideoFunc(firstDeviceId);
		}).catch((err) => {
			tipMsg.value = JSON.stringify(err);
			console.error(err);
		});
	}
	const decodeFromInputVideoFunc = (firstDeviceId) => {
		codeReader.value.reset(); // 重置
		codeReader.value.decodeFromInputVideoDeviceContinuously(firstDeviceId, "video", (result, err) => {
			tipMsg.value = "正在尝试识别...";
			if (result) {
				// 获取到的是二维码内容,然后在这个if里面写业务逻辑即可
				testTubeCode.value = result.text;
				tipMsg.value = "识别成功:" + testTubeCode.value;
				switchScanner();
			}
			if (err && !err) {
				tipMsg.value = JSON.stringify(err);
				console.error(err);
			}
		});
	}
	const closeCamera = () => {
		codeReader.value.stopContinuousDecode();
		codeReader.value.reset();
	}
	const endScanner = () => {
		showOpenTestTubeVisable.value = false;
		closeCamera();
	}
	const switchScanner = () => {
		scanTestTubeCode.value = !scanTestTubeCode.value;
		if (scanTestTubeCode.value) {
			openCamera();
		} else {
			closeCamera();
		}
	}
	const openTestTube = () => {
		api.post("/testtube/openTestTube", {
				testTubeCode: testTubeCode.value,
				boxId: box.value.boxId,
				collectType: collectType.value
			})
			.then(res => {
				console.log(res)
				if (res.code == 0) {
					toTestTube(res.data);
				}
			})
			.catch(res => {
				tipMsg.value = res.errMsg;
			})
	}
	//跳转到试管页面
	const toTestTube = (testTube) => {
		router.push("/TestTube/" + testTube.testTubeId);
	}
	//封箱
	const closeBox = ()=>{
		Dialog.confirm({
				title: '操作提醒',
				message: '确认要封箱吗?',
			})
			.then(() => {
				api.post("/box/closeBox",{boxId:box.value.boxId})
				.then(()=>{
					router.push("/Box");
				})
			})
	}
</script>

<template>
	<van-nav-bar :title="'试管列表,箱码:'+box.boxCode+(box.status==2?'已封箱':'')" right-text="刷新" @click-right="getTesttubeList"
		left-arrow @click-left="back">
	</van-nav-bar>

	<van-row class="button-group">
		<van-col span="12" class="padding1rem" @click="beginScanner">
			<van-button round block type="primary">
				开管
			</van-button>
		</van-col>
		<van-col span="12" class="padding1rem" @click="closeBox">
			<van-button round block type="danger">
				封箱
			</van-button>
		</van-col>
	</van-row>
	<van-row>
		<van-col span="24" style="padding-top: 3rem;">
			<van-list>
				<van-cell v-for="item in testtubeList" :key="item" is-link @click="toTestTube(item)">
					<template #title>
						<span class="custom-title">{
   
   {item.testTubeCode}}</span>
						<van-tag size="large" v-if="item.status==1" type="success">检测中</van-tag>
						<van-tag size="large" v-if="item.status==2" type="danger">已封管</van-tag>
					</template>
				</van-cell>
			</van-list>
		</van-col>
	</van-row>

	<van-overlay :show="showOpenTestTubeVisable" class="scanner">
		<div v-if="scanTestTubeCode">
			<video ref="video" id="video" class="scan-video" autoplay></video>
			<div>{
   
   {tipMsg}}</div>
		</div>
		<div v-else>
			<div class="scan-video"></div>
			<van-cell-group inset>
				<van-form style="padding: 0.5rem;" @submit="openTestTube">
					<van-row class="padding">
						<van-col span="24">
							<van-field required v-model="testTubeCode" label="输入试管码" placeholder="请输入试管码"
								:rules="[{  required: true, message: '输入试管码' }]" />
						</van-col>
						<van-col span="24">
							{
   
   {collectType}}
							<van-field required name="checkboxGroup" label="采集类型"
							:rules="[{  required: true, message: '请选择采集类型' }]">
								<template #input>
									<van-radio-group v-model="collectType" direction="horizontal">
										<van-radio name="1" >单采</van-radio>
										<van-radio name="10">10人混采</van-radio>
										<van-radio name="20">20人混采</van-radio>
									</van-radio-group>
								</template>
							</van-field>
						</van-col>
					</van-row>
					<van-row class="padding">
						<van-col span="24">
							<van-button round block type="primary" native-type="submit">确定开管</van-button>
						</van-col>
					</van-row>
				</van-form>
			</van-cell-group>
		</div>
		<van-cell-group inset style="margin-top:1rem">
			<van-row class="padding">
				<van-col span="24">
					<van-button round block type="primary" @click="switchScanner">{
   
   {scanTestTubeCode?'手动输入':'扫描'}}
					</van-button>
				</van-col>
			</van-row>
			<van-row class="padding">
				<van-col span="24">
					<van-button round block type="primary" @click="endScanner">取消</van-button>
				</van-col>
			</van-row>
		</van-cell-group>
	</van-overlay>
</template>
<style>
	.padding1rem {
		padding: 1rem
	}

	.button-group {
		font-size: 2rem;
		margin-right: 1rem;
	}

	.scan-video {
		width: 100%;
		height: 50vh;
	}

	.scanner {
		color: #fff;
	}

	.padding {
		padding: 1rem;
	}
</style>

At the back end, the controller page of the test tube, pay attention to parameter verification and status verification

package com.hawkon.collector.service.impl;

import com.hawkon.collector.dao.BoxDao;
import com.hawkon.collector.dao.TestTubeDao;
import com.hawkon.collector.service.ITestTubeService;
import com.hawkon.common.enums.ResultCodeEnum;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.pojo.Box;
import com.hawkon.common.pojo.TestTube;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TestTubeServiceImpl implements ITestTubeService {
    
    
    @Autowired
    TestTubeDao testTubeDao;
    @Autowired
    BoxDao boxDao;

    @Override
    public List<TestTube> getTestTubeListByBoxId(Integer boxId) throws BusinessException {
    
    
        Box box = boxDao.getBoxByBoxId(boxId);
        if (box == null) {
    
    
            throw new BusinessException("箱码不存在", ResultCodeEnum.BUSSINESS_ERROR);
        }
        if (box.getStatus() == 0 || box.getStatus() > 2) {
    
    
            throw new BusinessException("箱码状态异常", ResultCodeEnum.BUSSINESS_ERROR);
        }
        List<TestTube> list = testTubeDao.getTestTubeListByBoxId(boxId);
        return list;
    }

    @Override
    public TestTube openTestTube(TestTube model) throws BusinessException {
    
    
        if(model.getBoxId()==null){
    
    
            throw new BusinessException("参数错误,没有箱码ID", ResultCodeEnum.BUSSINESS_ERROR);
        }
        TestTube modelDb = testTubeDao.getTestTubeByCode(model.getTestTubeCode());
        if (modelDb == null) {
    
    
            throw new BusinessException("非法试管码", ResultCodeEnum.BUSSINESS_ERROR);
        }
        if(modelDb.getStatus()>1){
    
    
            throw new  BusinessException("试管已封管,请检查试管码", ResultCodeEnum.BUSSINESS_ERROR);
        }
        if(modelDb.getStatus()==1){
    
    
            return modelDb;
        }
        testTubeDao.openTestTube(model);
        modelDb.setStatus(1);
        return modelDb;
    }

    @Override
    public void closeTestTube(TestTube model) throws BusinessException {
    
    
        testTubeDao.closeTestTube(model);
    }
}

Effect picture of test tube list page

insert image description here

Test tube page, add sampler

The test tube page displays the entered sample information by default, and supports three entry methods: ID card scanning, nucleic acid code scanning, and manual entry.

We implement the latter two here. It should be noted that after scanning the ID card and scanning the nucleic acid code, you must jump to the manual input interface to confirm the information and click submit to complete the addition.

insert image description here

manual input

insert image description here

After clicking the sample information on the list page, you can modify the sample information

insert image description here

TestTube.vue

<script setup>
	import {
		ref
	} from 'vue';
	import {
		useRouter,
		useRoute
	} from "vue-router";
	const router = useRouter();
	const route = useRoute();
	import {
		Toast
	} from 'vant';
	import api from '@/common/api.js';
	const back = () => {
		router.push("/TestTubeList");
	}
	const testTubeId = ref(parseInt(route.params.testTubeId));

	const sampleList = ref([]);
	const getSampleList = () => {
		api.post("/sample/getSampleByTestTubeId", {
			testTubeId: testTubeId.value
		}).then(res => {
			sampleList.value = res.data;
		})
	}
	getSampleList();

	const toSample = (sample) => {
		router.push({
			name: "Sample",
			query: sample
		});
	}

	const people = ref({});

	//扫核酸码
	import {
		BrowserMultiFormatReader
	} from "@zxing/library";
	const showCodeScanner = ref(false);
	const tipMsg = ref("");
	const acidCode = ref("");
	const codeReader = ref(null);
	codeReader.value = new BrowserMultiFormatReader();
	const openCodeScanner = () => {
		closeEditor();
		showCodeScanner.value = true;
		openCamera();
	}
	const closeCodeScanner = () => {
		showCodeScanner.value = false;
		closeCamera();
		active.value = "none";
	}
	const openCamera = () => {
		codeReader.value.getVideoInputDevices().then((videoInputDevices) => {
			tipMsg.value = "正在调用摄像头...";
			// 因为获取的摄像头有可能是前置有可能是后置,但是一般最后一个会是后置,所以在这做一下处理
			// 默认获取第一个摄像头设备id
			let firstDeviceId = videoInputDevices[0].deviceId;
			if (videoInputDevices.length > 1) {
				// 获取后置摄像头
				let deviceLength = videoInputDevices.length;
				--deviceLength;
				firstDeviceId = videoInputDevices[deviceLength].deviceId;
			}
			decodeFromInputVideoFunc(firstDeviceId);
		}).catch((err) => {
			tipMsg.value = JSON.stringify(err);
			console.error(err);
		});
	}
	const decodeFromInputVideoFunc = (firstDeviceId) => {
		codeReader.value.reset(); // 重置
		codeReader.value.decodeFromInputVideoDeviceContinuously(firstDeviceId, "video", (result, err) => {
			tipMsg.value = "正在尝试识别...";
			if (result) {
				console.log(result);
				acidCode.value = result.text;
				tipMsg.value = "识别成功:" + acidCode.value;
				closeCodeScanner();
				getPeopleInfoByAcidCode(acidCode.value);
			}
			if (err && !err) {
				tipMsg.value = JSON.stringify(err);
				console.error(err);
			}
		});
	}
	const closeCamera = () => {
		codeReader.value.stopContinuousDecode();
		codeReader.value.reset();
	}
	const getPeopleInfoByAcidCode = (acidCode) => {
		api.post("/people/getPeopleInfoById", {
				peopleId: acidCode
			})
			.then(res => {
				people.value = res.data;
			})
	};
	const getPeopleByIdcard = () => {
		if (people.value.idcard && people.value.idcard.length == 18) {
			api.post("/people/getPeopleByIdcard", {
					idcard: people.value.idcard
				})
				.then(res => {
					if (res.data) {
						people.value = res.data;
					}
				})
		}
	};
	//手工录入
	const showEditor = ref(false);
	const openEditor = () => {
		closeCodeScanner();
		showEditor.value = true;
		people.value = {
			idcardType: "身份证"
		}
	}
	const closeEditor = () => {
		showEditor.value = false;
	}
	const addSample = () => {
		api.post("/sample/addSample", {
				testTubeId: testTubeId.value,
				name: people.value.name,
				idcardType: people.value.idcardType,
				idcard: people.value.idcard,
				tel: people.value.tel,
				address: people.value.address
			})
			.then(res => {
				closeEditor();
				getSampleList();
			})
	}
	const tabbarThemeVars = ref({
		tabbarHeight: "6rem"
	})
	const active = ref("none");
	const openIdcardScanner = () => {
		Toast.fail("功能暂未实现")
	}
	const beginCloseTestTube = () => {
		if (sampleList.value.length == 0) {
			Dialog.confirm({
					title: '操作提醒',
					message: '现在试管中样品是空的,确认要封管吗?',
				})
				.then(() => {
					closeTestTube();
				})
				.catch(() => {
					// on cancel
				});
		} else {
			Dialog.confirm({
					title: '操作提醒',
					message: '确认要封管吗?',
				})
				.then(() => {
					closeTestTube();
				})
				.catch(() => {
					// on cancel
				});
		}
	}
	const closeTestTube = () => {
		api.post("/testtube/closeTestTube", {
				testTubeId: testTubeId.value
			})
			.then(res => {
				router.push("/TestTubeList")
			})
	}

	import 'vant/es/dialog/style';

	import {
		Dialog
	} from 'vant';
</script>

<template>
	<van-nav-bar title="试管" left-arrow @click-left="back" right-text="刷新" @click-right="getSampleList">
	</van-nav-bar>

	<van-row class="padding">
		<van-col span="22" offset="2">
			<van-button round block type="danger" @click="beginCloseTestTube">
				封管
			</van-button>
		</van-col>
	</van-row>
	<van-row>
		<van-col span="24">
			<h1 style="padding:0 0 1rem 1rem">样本数量:{
   
   {sampleList.length}}</h1>
		</van-col>
		<van-col span="24">
			<van-list>
				<van-cell v-for="item in sampleList" :key="item" is-link @click="toSample(item)">
					<!-- 使用 title 插槽来自定义标题 -->
					<template #title>
						<span class="custom-title">{
   
   {item.name}}</span>
						<span>{
   
   {item.idcard}}</span>
					</template>
				</van-cell>
			</van-list>
		</van-col>
	</van-row>
	<van-overlay :show="showCodeScanner" class="scanner">
		<video ref="video" id="video" class="scan-video" autoplay></video>
		<div>{
   
   {tipMsg}}</div>
	</van-overlay>

	<van-overlay :show="showEditor" class="scanner">
		<van-form @submit="addSample">
			<van-cell-group inset class="editor">
				<van-field required v-model="people.idcardType" name="idcardType" label="证件类型" placeholder="证件类型"
					:rules="[{ required: true, message: '请填写证件类型' }]">
					<template #input>
						<van-radio-group v-model="people.idcardType" direction="horizontal">
							<van-radio name="身份证">身份证</van-radio>
							<van-radio name="护照">护照</van-radio>
						</van-radio-group>
					</template>
				</van-field>
				<van-field @blur="getPeopleByIdcard" required v-model="people.idcard" name="idcard" label="身份证"
					placeholder="身份证"
					:rules="[{ pattern:/^([1-6][1-9]|50)\d{4}(18|19|20)\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入18位身份证号',trigger:'onBlur' }]" />
				<van-field required v-model="people.name" name="name" label="姓名" placeholder="姓名"
					:rules="[{ required: true, message: '请填写姓名' }]" />
				<van-field v-model="people.tel" name="tel" label="电话" placeholder="电话" />
				<van-field v-model="people.address" name="address" label="地址" placeholder="地址" />
				<div>
					<van-row class="padding">
						<van-col span="10" offset="2">
							<van-button round block type="primary" native-type="submit">
								提交
							</van-button>
						</van-col>
						<van-col span="10" offset="2">
							<van-button round block type="success" @click="closeEditor">
								取消
							</van-button>
						</van-col>
					</van-row>
				</div>
			</van-cell-group>
		</van-form>
	</van-overlay>
	<van-config-provider :theme-vars="tabbarThemeVars">
		<van-tabbar v-model="active">
			<van-tabbar-item @click="openIdcardScanner" name="sannIdcard">
				<span>扫描身份证</span>
				<template #icon="props">
					<van-icon class-prefix="iconfont i-ziyuan92" name="extra" size="3rem" />
				</template>
			</van-tabbar-item>
			<van-tabbar-item @click="openCodeScanner" name="sannCode">
				<span>扫核酸码</span>
				<template #icon="props">
					<van-icon class-prefix="iconfont i-saoma" name="extra" size="3rem" />
				</template>
			</van-tabbar-item>
			<van-tabbar-item @click="openEditor" name="editor">
				<span>手动录入</span>
				<template #icon="props">
					<van-icon name="edit" size="3rem" />
				</template>
			</van-tabbar-item>
		</van-tabbar>

	</van-config-provider>
</template>
<style>
	.custom-title {
		font-size: 2rem;
		margin-right: 1rem;
	}

	.scan-video {
		width: 100%;
		height: 60vh;
	}

	.scanner {
		color: #fff;
	}

	.padding {
		padding: 1rem;
	}

	.editor {
		height: 60vh;
		margin-top: 10vh;
		padding: 1rem;
	}
</style>

backend code.

package com.hawkon.collector.controller;

import com.hawkon.collector.service.ISampleService;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.pojo.ResultModel;
import com.hawkon.common.pojo.Sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/sample")
public class SampleController {
    
    

    @Autowired
    ISampleService sampleService;

    @PostMapping("getSampleByTestTubeId")
    public ResultModel<List<Sample>> getSampleByTestTubeId(@RequestBody Sample model) throws BusinessException {
    
    
        List<Sample> list = sampleService.getSampleByTestTubeId(model.getTestTubeId());
        return ResultModel.success(list);
    }
    @PostMapping("addSample")
    public ResultModel<Object> addSample(@RequestBody @Valid Sample model) throws BusinessException {
    
    
        sampleService.addSample(model);
        return ResultModel.success(null);
    }
    @PostMapping("updateSample")
    public ResultModel<Object> updateSample(@RequestBody @Valid Sample model) throws BusinessException {
    
    
        sampleService.updateSample(model);
        return ResultModel.success(null);
    }
    @PostMapping("deleteSample")
    public ResultModel<Object> deleteSample(@RequestBody @Valid Sample model) throws BusinessException {
    
    
        sampleService.deleteSample(model);
        return ResultModel.success(null);
    }
}

package com.hawkon.collector.service.impl;

import com.hawkon.collector.dao.PeopleDao;
import com.hawkon.collector.dao.SampleDao;
import com.hawkon.collector.service.ISampleService;
import com.hawkon.common.enums.ResultCodeEnum;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.pojo.People;
import com.hawkon.common.pojo.Sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class SampleServiceImpl implements ISampleService {
    
    
    @Autowired
    SampleDao sampleDao;

    @Autowired
    PeopleDao peopleDao;

    @Override
    public List<Sample> getSampleByTestTubeId(Integer testTubeId) throws BusinessException {
    
    
        return sampleDao.getSampleByTestTubeId(testTubeId);
    }

    @Override
    public void addSample(Sample model) throws BusinessException {
    
    
        People people = peopleDao.getPeopleByIdcard(model.getIdcard());
        if(people==null){
    
    
            peopleDao.insertPeopleFromSample(model);
        }
        int count = sampleDao.checkSample(model);
        if(count>0){
    
    
            throw new BusinessException("试管内身份证号重复,请核对身份证", ResultCodeEnum.BUSSINESS_ERROR);
        }
        sampleDao.addSample(model);
    }

    @Override
    public void updateSample(Sample model) throws BusinessException {
    
    
        People people = peopleDao.getPeopleByIdcard(model.getIdcard());
        if(people==null){
    
    
            peopleDao.insertPeopleFromSample(model);
        }
        int count = sampleDao.checkSample(model);
        if(count>0){
    
    
            throw new BusinessException("试管内身份证号重复,请核对身份证", ResultCodeEnum.BUSSINESS_ERROR);
        }
        sampleDao.updateSample(model);
    }

    @Override
    public void deleteSample(Sample model) {
    
    
        sampleDao.deleteSample(model);
    }
}

mybatis file

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.hawkon.collector.dao.SampleDao">
    <select id="getSampleByTestTubeId" resultType="com.hawkon.common.pojo.Sample">
        select *
        from sample
        where testTubeId = #{testTubeId} ;
    </select>
    <insert id="addSample">
        insert into sample
            (testTubeId, name, idcard, idcardType, tel, address, collectTime)
        values (#{testTubeId}, #{ name}, #{idcard}, #{idcardType}, #{tel}, #{address}, now())
    </insert>
    <select id="checkSample" resultType="int">
        select count(0)
        from sample
        where testTubeId = #{testTubeId}
        and idcard = #{idcard}
        <if test="sampleId!=null">
            and sampleId &lt;&gt;#{sampleId}
        </if>
    </select>
    <update id="updateSample">
        update sample
        set name       = #{name}
          , idcard     = #{idcard}
          , idcardType = #{idcardType}
          , tel        = #{tel}
          , address    = #{address}
        where sampleId = #{sampleId}
    </update>
    <delete id="deleteSample">
        delete from sample where sampleId = #{sampleId}
    </delete>
</mapper>

Sealing, sealing

There are basically no restrictions on the tube sealing operation. Pay attention to the prompts on the front-end interface, and do not seal the tube by clicking the tube sealing button by mistake.

The sealing operation needs to do a good job of status verification.


    @Override
    public void closeBox(Box model) throws BusinessException {
    
    
        model = boxDao.getBoxByBoxId(model.getBoxId());
        if(!model.getStatus().equals(1)){
    
    
            throw new BusinessException("箱码状态异常,无法封箱",ResultCodeEnum.BUSSINESS_ERROR);
        }
        int count = boxDao.getOpenedTestTubeCount(model.getBoxId());
        if(count>0){
    
    
            throw new BusinessException("有未封管试管,请封管后再封箱",ResultCodeEnum.BUSSINESS_ERROR);
        }
        boxDao.closeBox(model.getBoxId());
    }

Accessibility

Change registration information

Note that this page needs to load the bound administrative divisions when it is opened.

insert image description here

If the idea of ​​this function is not clear, it is easy to write in a mess. The idea of ​​this function is as follows.

Step 1: Calculate the IDs of provinces, cities, districts and counties based on the areaId of the collector


	let userAreaId = userInfo.areaId;
	//计算出已绑定的省、市、区行政区划码
	let provinceCode = Math.floor(userAreaId / 10000000000);
	let cityCode = Math.floor(userAreaId / 100000000);
	let areaCode = Math.floor(userAreaId / 1000000);
	let streetCode = Math.floor(userAreaId / 1000);
	let committeeCode = userAreaId;
	const registerForm = ref({
    
    
		provinceCode,
		cityCode,
		areaCode,
		streetCode,
		committeeCode,
		areaId: userAreaId
	});

Step 2: Read the list of provinces, cities, districts, streets, villages and communities.

const getAreasByCityCode = (callBack) => {
    
    
	api.post("/area/getAreasByCityCode", {
    
    
			cityCode: registerForm.value.cityCode
		})
		.then(res => {
    
    
			areas.value = res.data;
			if (callBack) {
    
    
				callBack();
			}
		})
}

Step 3: Calculate the division name corresponding to the administrative division. This step must be executed after the data is obtained in the second step, and the method of the second step is not only used when loading, but also after the selected administrative division is changed on the page. So the second step method defines a callback parameter.


	const setStreet = () => {
    
    
		for (var i = 0; i < streets.value.length; i++) {
    
    
			let p = streets.value[i]
			if (p.streetCode == registerForm.value.streetCode) {
    
    
				registerForm.value.streetCode = p.streetCode;
				registerForm.value.streetName = p.name;
			}
		}
	}

Step 4: Call the loading of 4 sets of data together at the end of page initialization.


	getCitiesByProvinceCode(setCity)
	getAreasByCityCode(setArea);
	getStreetsByAreaCode(setStreet);
	getCommitteesByStreetCode(setCommittee)

It seems that this function is a bit complicated to implement. Some people will say that it is not enough to save these codes and names directly in the collector table. That's right, this is indeed a bit simpler. The reason why we use a relatively complicated method here is that we hope that the students who follow the practice can practice the idea of ​​​​solving the problem through this scene.

ChangeInfo.vue

<script setup>
	import {
		ref
	} from 'vue';
	import {
		useRouter
	} from "vue-router";
	import api from "../common/api.js";
	const router = useRouter();
	const back = () => {
		router.push("/");
	}
	const userInfo = JSON.parse(sessionStorage.user);
	let userAreaId = userInfo.areaId;
	let provinceCode = Math.floor(userAreaId / 10000000000);
	let cityCode = Math.floor(userAreaId / 100000000);
	let areaCode = Math.floor(userAreaId / 1000000);
	let streetCode = Math.floor(userAreaId / 1000);
	let committeeCode = userAreaId;
	const registerForm = ref({
		provinceCode,
		cityCode,
		areaCode,
		streetCode,
		committeeCode,
		areaId: userAreaId
	});

	const resetArea = (props) => {
		if (props) {
			props.forEach(key => registerForm.value[key] = null);
		}
	}
	//选择省
	const showProvincePicker = ref(false);

	const provinces = ref([]);
	api.post("/area/getProvinces")
		.then(res => {
			provinces.value = res.data;
			setProvince();
		})
	const setProvince = () => {
		for (var i = 0; i < provinces.value.length; i++) {
			let p = provinces.value[i]
			if (p.provinceCode == registerForm.value.provinceCode) {
				registerForm.value.provinceCode = p.provinceCode,
					registerForm.value.provinceName = p.name;
			}
		}
	}
	const setCity = () => {
		for (var i = 0; i < cities.value.length; i++) {
			let p = cities.value[i]
			if (p.cityCode == registerForm.value.cityCode) {
				registerForm.value.cityCode = p.cityCode;
				registerForm.value.cityName = p.name;
			}
		}
	}
	const setArea = () => {
		for (var i = 0; i < areas.value.length; i++) {
			let p = areas.value[i]
			if (p.areaCode == registerForm.value.areaCode) {
				registerForm.value.areaCode = p.areaCode;
				registerForm.value.areaName = p.name;
			}
		}

	}
	const setStreet = () => {
		for (var i = 0; i < streets.value.length; i++) {
			let p = streets.value[i]
			if (p.streetCode == registerForm.value.streetCode) {
				registerForm.value.streetCode = p.streetCode;
				registerForm.value.streetName = p.name;
			}
		}
	}
	const setCommittee = () => {
		for (var i = 0; i < committees.value.length; i++) {
			let p = committees.value[i]
			if (p.committeeCode == registerForm.value.committeeCode) {
				registerForm.value.committeeCode = p.committeeCode;
				registerForm.value.committeeName = p.name;
			}
		}
	}
	const confirmProvince = (value) => {
		registerForm.value.provinceCode = value.provinceCode;
		registerForm.value.provinceName = value.name;
		showProvincePicker.value = false;
		getCitiesByProvinceCode();
		resetArea(["cityCode", "cityName", "areaCode", "areaName", "streetCode", "streetName", "committeeCode",
			"committeName"
		]);
	}
	const areaFieldName = {
		text: 'name'
	}


	const cities = ref([]);
	const getCitiesByProvinceCode = (callBack) => {
		api.post("/area/getCitiesByProvinceCode", {
				provinceCode: registerForm.value.provinceCode
			})
			.then(res => {
				cities.value = res.data;
				if (callBack) {
					callBack();
				}
			})
	}
	const showCityPicker = ref(false);
	const confirmCity = (value) => {
		registerForm.value.cityCode = value.cityCode;
		registerForm.value.cityName = value.name;

		showCityPicker.value = false;
		getAreasByCityCode();
		resetArea(["areaCode", "areaName", "streetCode", "streetName", "committeeCode", "committeeName"]);
	}

	//区县选择器
	const areas = ref([]);
	const getAreasByCityCode = (callBack) => {
		api.post("/area/getAreasByCityCode", {
				cityCode: registerForm.value.cityCode
			})
			.then(res => {
				areas.value = res.data;
				if (callBack) {
					callBack();
				}
			})
	}
	const showAreaPicker = ref(false);
	const confirmArea = (value) => {
		registerForm.value.areaCode = value.areaCode;
		registerForm.value.areaName = value.name;
		showAreaPicker.value = false;
		getStreetsByAreaCode();
		resetArea(["streetCode", "streetName", "committeeCode", "committeeName"]);
	}

	//街道/乡镇选择器
	const streets = ref([]);
	const getStreetsByAreaCode = (callBack) => {
		api.post("/area/getStreetsByAreaCode", {
				areaCode: registerForm.value.areaCode
			})
			.then(res => {
				streets.value = res.data;
				if (callBack) {
					callBack();
				}
			})
	}
	const showStreetPicker = ref(false);
	const confirmStreet = (value) => {
		registerForm.value.streetCode = value.streetCode;
		registerForm.value.streetName = value.name;
		registerForm.value.areaId =value.areaId;
		showStreetPicker.value = false;
		getCommitteesByStreetCode();
		resetArea(["committeeCode", "committeeName"]);
	}


	//村/社区选择器
	const committees = ref([]);
	const getCommitteesByStreetCode = (callBack) => {

		api.post("/area/getCommitteesByStreetCode", {
				streetCode: registerForm.value.streetCode
			})
			.then(res => {
				committees.value = res.data;
				if (callBack) {
					callBack();
				}
			})
	}
	const showCommitteePicker = ref(false);
	const confirmCommittee = (value) => {
		registerForm.value.committeeCode = value.committeeCode;
		registerForm.value.committeeName = value.name;
		registerForm.value.areaId =value.areaId;
		showCommitteePicker.value = false;
	}


	//表单验证
	const repeatValidator = (val) => {
		return val == registerForm.value.tel;
	}
	const changeInfo = () => {
		var registerModel = {
			areaId: registerForm.value.areaId
		}
		console.log(registerForm.value);
		api.post("/collector/changeInfo", registerModel)
			.then(res => {})
	}
	getCitiesByProvinceCode(setCity)
	getAreasByCityCode(setArea);
	getStreetsByAreaCode(setStreet);
	getCommitteesByStreetCode(setCommittee)
</script>

<template>
	<van-nav-bar title="变更注册信息" left-text="返回" left-arrow @click-left="back" />
	<van-form style="padding: 0.5rem;" @submit="changeInfo">
		<van-cell-group inset>
			<van-field required v-model="registerForm.provinceName" name="provinceCode" label="省" is-link readonly
				placeholder="点击选择省" @click="showProvincePicker=true" :rules="[{  required: true, message: '请选择省' }]" />
			<van-popup required v-model:show="showProvincePicker" position="bottom">
				<van-picker :columns="provinces" :columns-field-names="areaFieldName" @confirm="confirmProvince"
					@cancel="showProvincePicker=false" />
			</van-popup>
			<van-field required v-model="registerForm.cityName" name="cityCode" label="市" placeholder="请选择市" is-link
				readonly @click="showCityPicker=true" :rules="[{  required: true, message: '请选择市' }]" />
			<van-popup required v-model:show="showCityPicker" position="bottom">
				<van-picker :columns="cities" :columns-field-names="areaFieldName" @confirm="confirmCity"
					@cancel="showCityPicker=false" />
			</van-popup>
			<van-field required v-model="registerForm.areaName" name="areaCode"
				:rules="[{  required: true, message: '请选择区/县' }]" placeholder="区/县" label="区/县" is-link readonly
				@click="showAreaPicker=true" />
			<van-popup v-model:show="showAreaPicker" position="bottom">
				<van-picker :columns="areas" :columns-field-names="areaFieldName" @confirm="confirmArea"
					@cancel="showAreaPicker=false" />
			</van-popup>
			<van-field required v-model="registerForm.streetName" name="streetCode" label="街道/乡镇" placeholder="选择街道/乡镇"
				:rules="[{  required: true, message: '请选择街道/乡镇' }]" is-link readonly @click="showStreetPicker=true" />
			<van-popup v-model:show="showStreetPicker" position="bottom">
				<van-picker :columns="streets" :columns-field-names="areaFieldName" @confirm="confirmStreet"
					@cancel="showStreetPicker=false" />
			</van-popup>
			<van-field v-model="registerForm.committeeName" name="committeeCode" label="村/社区" placeholder="选择村/社区"
				is-link readonly @click="showCommitteePicker=true" />
			<van-popup v-model:show="showCommitteePicker" position="bottom">
				<van-picker :columns="committees" :columns-field-names="areaFieldName" @confirm="confirmCommittee"
					@cancel="showCommitteePicker=false" />
			</van-popup>
		</van-cell-group>
		<div style="margin: 16px;">
			<van-button round block type="primary" native-type="submit">
				提交
			</van-button>
		</div>
	</van-form>
</template>
<style>
	h3 {
		margin-top: 1rem;
	}
</style>

Test tube check code

To be honest, I don’t quite understand why there is such a function in the APP. From the perspective of practice, this function is the only function that needs to use multi-table connection in the collector module. Let’s do it.

insert image description here

The front-end and back-end codes will not be posted. The SQL code is as follows:


    <select id="searchBoxByTestTubeCode" resultType="com.hawkon.collector.pojo.vo.BoxVO">
        select b.*,c.name as collector
             ,tf.name as transfer
             ,r.name as reciever
             ,u.name as uploader
             ,(select count(0) from testTube where testTube.boxId = b.boxId) as testTubeCount
             ,(select count(0) from testTube tt inner join sample s on tt.testTubeId = s.testTubeId where tt.boxId = b.boxId) as peopleCount
        from box b left join testtube t on b.boxId = t.boxId
                   left join collector c on b.collectorId = c.collectorId
                   left join transfer tf on b.transferId = tf.transferId
                   left join reciever r on b.recieverId = r.recieverId
                   left join uploader u on b.uploaderId = u.uploaderId
        where t.testTubeCode = #{testTubeCode}
    </select>

End

The functions of the collection personnel module are almost the same, and all the codes are not posted in the article, and the students who follow along can complete other modules by themselves.

If there are students who are following along and have questions and want to communicate, they can go to my WeChat public account (Yao Sir interview room) to reply "nucleic acid testing" and get a way to discuss the project with me.

Other articles in this series:

Chapter 1 Reverse Engineering

Chapter Two

Chapter Three Sharp Weapons

Guess you like

Origin blog.csdn.net/aley/article/details/128115624