[Proyecto de combate: plataforma de detección de ácido nucleico] Carga del capítulo 4

Project Combat: Plataforma de detección de ácido nucleico Capítulo 4 Carga

Resumen: En la guerra, las personas más peligrosas siempre están al frente. En la batalla contra la nueva corona, la primera línea es el personal médico y los trabajadores de prevención de epidemias.

Como importante arma de vanguardia, la APP de los colectores de la plataforma de detección de ácidos nucleicos debe ser fácil de usar y práctica para solucionar el problema.

Objetivos de este capítulo

Completa la APP de coleccionista

Tecnología utilizada:

  • procedimiento almacenado
  • cursor
  • Código de barras, componente de escaneo de código QR

dificultad

  • Seleccione el efecto de filtro de búsqueda de la página del punto de recogida
  • Selección en cascada de la división administrativa y visualización de datos predeterminada

descripción general

En la guerra, las personas más peligrosas siempre están al frente. En la guerra contra la nueva corona, es el personal médico y los trabajadores de prevención de epidemias quienes están al frente.

Como arma importante para la vanguardia de la plataforma de detección de ácidos nucleicos, la aplicación para los recolectores debe ser fácil de usar y práctica para convertirse en un arma afilada para las tropas de recolección de ácidos nucleicos.

Aunque estamos haciendo simulación y no optimizando el rendimiento, el diseño de la lógica empresarial debe ser lo más práctico y fácil de usar posible. Asegurar también la exactitud de los datos.

Revisemos nuevamente el proceso de recolección de ácido nucleico, esta vez agregamos una lista de instrucciones de lógica de negocios al final del diagrama de flujo.

inserte la descripción de la imagen aquí

Aunque el módulo de recolección de personal es un módulo independiente, aún necesita trabajar con otros módulos, y los puntos de coordinación son los siguientes:

  1. El código de la caja y el código de barras del tubo de ensayo deben generarse con anticipación, y el código de la caja y el código del tubo de ensayo también deben generarse con anticipación.
  2. El código de ácido nucleico es un código bidimensional generado por usuarios comunes después de completar los datos.
  3. Una vez sellada la caja, el personal de transbordo continuará con las operaciones posteriores

En términos generales, se debe prestar atención a los siguientes puntos en las funciones del módulo de recopilación de personal, y se debe prestar atención al codificar la página de función.

  1. Durante las operaciones de desembalaje y apertura de tubos no se crean datos, pero se modifica el estado correspondiente al código de caja y código de probeta.

  2. Al agregar información de la muestra (persona analizada), hay tres formas de agregar: escanear la tarjeta de identificación, escanear el código de ácido nucleico e ingresar manualmente.

    1. El escaneo de la tarjeta de identificación involucra el módulo de identificación de la tarjeta de identificación, que puede llamar a la interfaz de IA proporcionada por el fabricante y, finalmente, devolver los datos de texto, que no se implementarán por el momento.
    2. Al escanear el código de ácido nucleico, los datos en el código de ácido nucleico deben ser la información de identificación de los usuarios comunes, que es solo un número, y este número debe corresponder a la identificación de personas. Por lo tanto, una vez que se completa el escaneo, es necesario extraer la información del personal en segundo plano.
    3. Al ingresar manualmente, si ya hay datos en la base de datos de personas, la información del personal se puede extraer automáticamente después de completar la entrada de la tarjeta de identificación; al enviar la información guardada, es necesario verificar si la información de la persona ingresada está en las personas, si no, debe estar en las personas Agregar información del personal, para que pueda ingresar manualmente la información la próxima vez.
  3. Al agregar muestras, se debe verificar la cantidad de tubos de ensayo y la interfaz frontal debe recordar la cantidad de muestras restantes. Cuando se excede la cantidad de tubos de ensayo, se debe dar un recordatorio. Para la comodidad de los recolectores, un pequeño También debe permitirse una cantidad excesiva de recaudación. Por lo tanto, no se imponen restricciones obligatorias al agregar muestras en el back-end.

  4. Al agregar muestras, es necesario verificar la doble entrada de la información de la tarjeta de identificación.

  5. Las muestras permiten la modificación y eliminación

  6. El tubo de ensayo debería permitir la eliminación, pero la eliminación no elimina los datos, sino que restaura el estado del tubo de ensayo.

  7. Al sellar la caja, verifique si hay un código de tubo de ensayo sin sellar y, si lo hay, debe prohibirse sellar la caja.

  8. Existe una función para cambiar la información de registro en el módulo en ejecución real.

    Al principio, no entendí muy bien el significado de esta función. Si presta atención al usar la aplicación, encontrará que el punto de recolección seleccionado comienza a cargar datos del área que seleccionó. Si cambia la información de registro, cambiará a otras áreas.

    Debido a que el enlace de descarga de la aplicación que se le proporcionó en el primer capítulo es de Shanghái, ¿por qué quiere hacer esto?

    La población residente de Shanghái es de 24 millones, más la población no residente, que puede superar los 30 millones. Durante la recolección a gran escala, el trabajo de recolección generalmente se completa en 6 horas. En promedio, se recolectan 80,000 muestras por minuto. El segundo es casi 1400 muestras.

    La recolección de una muestra requiere de 3 a 5 operaciones en la interfaz y de 3 a 5 operaciones de datos en la interfaz de fondo. Por lo tanto, la simultaneidad promedio en el lado del servidor es de alrededor de 7000 y la simultaneidad máxima puede ser mayor. Esta cantidad de simultaneidad no es demasiado grande, pero no es mucha. Es completamente posible dividirse en diferentes áreas y usar diferentes servicios para apoyarlas.

Comencemos a darnos cuenta del desarrollo de estas funciones paso a paso.

Preparar datos de prueba

Porque este módulo está relacionado con otros módulos, es decir, algunos datos son generados por otros módulos antes de llegar al módulo de personal de recolección ¿Qué tipo de datos hay?

  1. Datos del punto de recogida
  2. Datos del código de caja
  3. Datos del código del tubo de ensayo
  4. Datos de información del personal

Para facilitar la depuración al escribir programas, aún necesitamos crear algunos datos de prueba, entre los cuales los datos del punto de recopilación deben estar vinculados a los datos de la división administrativa.

Para crear datos de prueba, puede ingresarlos manualmente o escribir un script para generarlos Aquí uso un procedimiento almacenado en la base de datos para escribir un script para generar datos de prueba.

-- 创建采集点测试数据
-- 创建采集点的是在行政区划中第一个村、社区下创建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);

Iniciar sesión Registrarse

Por lo tanto, las funciones más comunes del sistema de software, cuando necesita prestar atención, la contraseña en el sistema normal debe estar encriptada. El método de cifrado más básico y común es el cifrado MD5, pero su fuerza es relativamente débil y relativamente fácil de descifrar. Para un nivel más avanzado, puede usar el método de agregar sal aleatoria.Este artículo usa el método de encriptación más básico.


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

El tipo de parámetro de la interfaz de inicio de sesión es LoginModel, que es una definición separada y una clase BO, y se coloca en el paquete pojo.bo del módulo de recopilación de personal.

También hay reglas de validación personalizadas definidas en los campos de la clase. Cabe señalar que para que las reglas de validación surtan efecto, se debe agregar la anotación @Valid delante del parámetro del controlador.

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

Al registrarse, es necesario verificar si el número de teléfono y el número de identificación se han registrado. También tenga en cuenta que la contraseña predeterminada son los últimos 6 dígitos de la tarjeta de identificación, y la contraseña también debe estar encriptada.

Desde un punto de vista práctico, la contraseña de esta aplicación es relativamente aleatoria, solo obtenga los últimos 6 dígitos de la tarjeta de identificación y la seguridad de la contraseña parece ser muy pobre. Sí, la implicación de un mecanismo criptográfico de este tipo parece inconcebible en cualquier proyecto de Internet. Sin embargo, para este proyecto, la seguridad de la contraseña es demasiado dura y no es conveniente de usar, y el descifrado de la contraseña no traerá consecuencias demasiado graves. Por lo tanto, es aceptable hacerlo desde la perspectiva del uso práctico.

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

Página de inicio de sesión.vue

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

Página de registro.vue

La cascada de varios niveles de la página de registro es más problemática. Consideré volver al front-end una vez y filtrarlo por el front-end, pero la cantidad de datos directamente desde la división administrativa hasta el quinto nivel sigue siendo bastante grande, así que Renunció.

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

Seleccionar punto de recogida

Esta página es un poco difícil. Hay una función de búsqueda en la página. Al buscar, se filtran los puntos de recolección, pero cuando se muestra, no solo es necesario filtrar los puntos de recolección, sino también filtrar los condados correspondientes, calles, pueblos y comunidades, a fin de seleccionar rápidamente.

Este efecto se puede hacer en el front-end o en el back-end.En términos de uso del proyecto, el filtrado en el front-end ejercerá menos presión sobre el servidor.

Por lo tanto, el backend devuelve directamente todos los puntos de recolección y los datos de la división administrativa muestran los datos de la división administrativa del distrito correspondiente a través de los datos de la división administrativa de los recolectores registrados.

inserte la descripción de la imagen aquí

Código de fondo:


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

Sentencias SQL:


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

código de front-end

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>

deshacer

Simplemente verifique el código de la caja y verifique el estado del código de la caja en la interfaz de back-end después de desempacar

//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;
}

La página de inicio Box.vue, debido a que necesitamos usar la cámara para escanear el código, podemos usar el componente @zxing/library para completarlo. Método de instalación: npm install @zxing/library.

Después de la instalación, debe cambiar el proyecto al modo https; de lo contrario, la cámara no podrá abrirse.

Cómo habilitar HTTPS:

El primer paso: instalar los componentes,npm install @vitejs/plugin-basic-ssl

Paso dos: vite.config.jsagregue el código en

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

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

Una vez completada la configuración, reinicie el proyecto front-end y estará bien cuando vea el símbolo del sistema a continuación. Cabe señalar que también se debe cambiar el protocolo HTTPS al acceder a la página.

inserte la descripción de la imagen aquí

El componente @zxing/library llama al método de escaneo de la cámara de la siguiente manera:

La etiqueta que muestra la pantalla de captura de la cámara en la página

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

Método de escaneo:

//打开摄像头
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();
}

Código completo de Box.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';
	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>
	

inserte la descripción de la imagen aquí

Página de lista de cajas de envío

Esta página muestra una lista de cajas de transferencia que aún no han sido transferidas, cuando no haya consulta, preste atención para filtrar el estado de la caja de transferencia.

inserte la descripción de la imagen aquí

Lista de tubos de ensayo, tubo abierto

La operación de abrir el tubo también necesita llamar a la cámara, lo cual ya se ha hecho antes, por lo que aquí no hay dificultad. Solo cambia CV+.

inserte la descripción de la imagen aquí

Después de escanear el código, saltar a la interfaz de entrada manual no es para abrir el tubo directamente, sino para elegir si el tipo de recolección es recolección única o recolección mixta.

inserte la descripción de la imagen aquí

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>

En el extremo posterior, la página del controlador del tubo de ensayo, preste atención a la verificación de parámetros y verificación de estado

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

Imagen de efecto de la página de lista de tubos de ensayo

inserte la descripción de la imagen aquí

Página del tubo de ensayo, agregar muestra

La página del tubo de ensayo muestra la información de muestra ingresada de manera predeterminada y admite tres métodos de ingreso: escaneo de tarjeta de identificación, escaneo de código de ácido nucleico e ingreso manual.

Implementamos los dos últimos aquí. Cabe señalar que después de escanear la tarjeta de identificación y escanear el código de ácido nucleico, debe saltar a la interfaz de entrada manual para confirmar la información y hacer clic en enviar para completar la adición.

inserte la descripción de la imagen aquí

entrada manual

inserte la descripción de la imagen aquí

Después de hacer clic en la información de la muestra en la página de la lista, puede modificar la información de la muestra

inserte la descripción de la imagen aquí

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>

código de fondo.

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

archivo mybatis

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

Sellado, sellado

Básicamente, no hay restricciones en la operación de sellado del tubo. Preste atención a las indicaciones en la interfaz frontal y no selle el tubo haciendo clic en el botón de sellado del tubo por error.

La operación de sellado necesita hacer un buen trabajo de verificación de estado.


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

Accesibilidad

Cambiar información de registro

Tenga en cuenta que esta página debe cargar las divisiones administrativas enlazadas cuando se abre.

inserte la descripción de la imagen aquí

Si la idea de esta función no está clara, es fácil escribir en un lío.La idea de esta función es la siguiente.

Paso 1: Calcule los ID de provincias, ciudades, distritos y condados en función del ID de área del recopilador


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

Paso 2: Lea la lista de provincias, ciudades, distritos, calles, pueblos y comunidades.

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

Paso 3: Calcular el nombre de la división correspondiente a la división administrativa. Este paso debe ejecutarse después de obtener los datos en el segundo paso, y el método del segundo paso no solo se usa al cargar, sino también después de que se cambia la división administrativa seleccionada en la página. Entonces, el método del segundo paso define un parámetro de devolución de llamada.


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

Paso 4: llame a la carga de 4 conjuntos de datos juntos al final de la inicialización de la página.


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

Parece que esta función es un poco complicada de implementar, algunos dirán que no es suficiente guardar estos códigos y nombres directamente en la tabla de recopiladores. Así es, esto sí es un poco más simple, la razón por la que aquí usamos un método relativamente complicado es que esperamos que los estudiantes que siguen la práctica puedan practicar la idea de resolver el problema a través de esta escena.

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>

Código de verificación del tubo de ensayo

Para ser honesto, no entiendo muy bien por qué existe tal función en la aplicación. Desde la perspectiva de la práctica, esta función es la única función que necesita usar la conexión de varias mesas en el módulo colector. Hagámoslo.

inserte la descripción de la imagen aquí

Los códigos front-end y back-end no se publicarán. El código SQL es el siguiente:


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

Fin

Las funciones del módulo de personal de recolección son casi las mismas, y todos los códigos no se publican en el artículo, y los estudiantes que siguen pueden completar otros módulos por sí mismos.

Si hay estudiantes que me están siguiendo y tienen preguntas y quieren comunicarse, pueden ir a mi cuenta pública de WeChat (sala de entrevistas de Yao Sir) para responder "prueba de ácido nucleico" y obtener una forma de discutir el proyecto conmigo.

Otros artículos de esta serie:

Capítulo 1 Ingeniería inversa

Capitulo dos

Capítulo tres Armas afiladas

Supongo que te gusta

Origin blog.csdn.net/aley/article/details/128115624
Recomendado
Clasificación