feat(V37): 药品追溯码管理模块 — 2026新规+三甲要求

后端:
- DrugTraceController: 5大功能(追溯码管理/批次管理/扫码记录/预警管理/统计)
- 追溯码验证接口: 验证追溯码有效性+过期检测
- 全链路追溯接口: 追踪药品从入库到发药的全流程
- 预警管理: 近效期/过期/召回/异常扫码自动预警
- 统计概览: 追溯码数量/批次状态/预警统计/近效期/过期统计

数据库:
- V36: drug_trace_code/drug_trace_batch/drug_trace_scan/drug_trace_alert 4张表

前端: 4个完整页面(追溯码管理/批次管理/扫码记录/预警管理)

测试: 14/14 API接口全部通过
This commit is contained in:
2026-06-07 12:35:47 +08:00
parent bfa33f6efe
commit 90ed481649
23 changed files with 1050 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
package com.healthlink.his.web.drugtrace.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.drugtrace.domain.*;
import com.healthlink.his.drugtrace.service.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* 药品追溯码管理 Controller — 2026新规+三甲要求
*
* 业务说明:
* 1. 追溯码注册: 药品入库时注册追溯码,关联药品基本信息
* 2. 扫码出入库: 入库/出库/发药时扫码验证,确保药品来源可查
* 3. 全流程追溯: 从采购→入库→调拨→发药→使用的全链路追踪
* 4. 预警管理: 近效期/过期/批次召回/异常扫码自动预警
*
* 调用关系:
* DrugTraceController → IDrugTraceCodeService → 追溯码CRUD+验证
* → IDrugTraceBatchService → 批次管理
* → IDrugTraceScanService → 扫码记录
* → IDrugTraceAlertService → 预警管理
*/
@RestController
@RequestMapping("/drugtrace")
@Slf4j
@AllArgsConstructor
public class DrugTraceController {
private final IDrugTraceCodeService codeService;
private final IDrugTraceBatchService batchService;
private final IDrugTraceScanService scanService;
private final IDrugTraceAlertService alertService;
// ==================== 1. 追溯码管理 ====================
@GetMapping("/code/page")
public R<?> getCodePage(
@RequestParam(value = "drugName", required = false) String drugName,
@RequestParam(value = "batchNo", required = false) String batchNo,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<DrugTraceCode> w = new LambdaQueryWrapper<>();
w.like(StringUtils.hasText(drugName), DrugTraceCode::getDrugName, drugName)
.eq(StringUtils.hasText(batchNo), DrugTraceCode::getBatchNo, batchNo)
.eq(StringUtils.hasText(status), DrugTraceCode::getStatus, status)
.orderByDesc(DrugTraceCode::getCreateTime);
return R.ok(codeService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/code/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addCode(@RequestBody DrugTraceCode code) {
code.setCreateTime(new Date());
code.setStatus("ACTIVE");
codeService.save(code);
log.info("追溯码注册: {} {} {}", code.getDrugName(), code.getBatchNo(), code.getTraceCode());
return R.ok(code);
}
@PutMapping("/code/{id}")
@Transactional(rollbackFor = Exception.class)
public R<?> updateCode(@PathVariable Long id, @RequestBody DrugTraceCode data) {
DrugTraceCode code = codeService.getById(id);
if (code != null) {
data.setId(id);
codeService.updateById(data);
}
return R.ok();
}
@DeleteMapping("/code/delete/{id}")
@Transactional(rollbackFor = Exception.class)
public R<?> deleteCode(@PathVariable Long id) {
codeService.removeById(id);
return R.ok();
}
@GetMapping("/code/verify/{traceCode}")
public R<?> verifyTraceCode(@PathVariable String traceCode) {
LambdaQueryWrapper<DrugTraceCode> w = new LambdaQueryWrapper<>();
w.eq(DrugTraceCode::getTraceCode, traceCode);
DrugTraceCode code = codeService.getOne(w);
if (code == null) {
return R.ok(Map.of("valid", false, "message", "追溯码不存在"));
}
Map<String, Object> result = new HashMap<>();
result.put("valid", true);
result.put("drugName", code.getDrugName());
result.put("batchNo", code.getBatchNo());
result.put("manufacturer", code.getManufacturer());
result.put("expiryDate", code.getExpiryDate());
result.put("status", code.getStatus());
boolean expired = code.getExpiryDate() != null && code.getExpiryDate().before(new Date());
result.put("expired", expired);
if (expired) {
result.put("warning", "该药品已过期!");
}
return R.ok(result);
}
// ==================== 2. 批次管理 ====================
@GetMapping("/batch/page")
public R<?> getBatchPage(
@RequestParam(value = "batchNo", required = false) String batchNo,
@RequestParam(value = "drugName", required = false) String drugName,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<DrugTraceBatch> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(batchNo), DrugTraceBatch::getBatchNo, batchNo)
.like(StringUtils.hasText(drugName), DrugTraceBatch::getDrugName, drugName)
.eq(StringUtils.hasText(status), DrugTraceBatch::getStatus, status)
.orderByDesc(DrugTraceBatch::getCreateTime);
return R.ok(batchService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/batch/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addBatch(@RequestBody DrugTraceBatch batch) {
batch.setCreateTime(new Date());
batch.setStatus("PENDING");
batch.setReceivedQuantity(0);
batchService.save(batch);
log.info("追溯批次创建: {} 药品: {} 数量: {}", batch.getBatchNo(), batch.getDrugName(), batch.getQuantity());
return R.ok(batch);
}
@PutMapping("/batch/{id}/receive")
@Transactional(rollbackFor = Exception.class)
public R<?> receiveBatch(@PathVariable Long id, @RequestBody Map<String, Object> data) {
DrugTraceBatch batch = batchService.getById(id);
if (batch == null) return R.ok(Map.of("success", false, "message", "批次不存在"));
Integer receivedQty = (Integer) data.get("receivedQuantity");
batch.setReceivedQuantity(receivedQty != null ? receivedQty : batch.getQuantity());
batch.setReceiveTime(new Date());
batch.setReceiverId(Long.valueOf(String.valueOf(data.getOrDefault("receiverId", 0))));
batch.setReceiverName((String) data.getOrDefault("receiverName", ""));
batch.setStatus("RECEIVED");
batchService.updateById(batch);
log.info("批次收货: {} 实收: {}/{}", batch.getBatchNo(), batch.getReceivedQuantity(), batch.getQuantity());
return R.ok(batch);
}
@DeleteMapping("/batch/delete/{id}")
@Transactional(rollbackFor = Exception.class)
public R<?> deleteBatch(@PathVariable Long id) {
batchService.removeById(id);
return R.ok();
}
// ==================== 3. 扫码记录 ====================
@GetMapping("/scan/page")
public R<?> getScanPage(
@RequestParam(value = "scanType", required = false) String scanType,
@RequestParam(value = "drugName", required = false) String drugName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<DrugTraceScan> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(scanType), DrugTraceScan::getScanType, scanType)
.like(StringUtils.hasText(drugName), DrugTraceScan::getDrugName, drugName)
.orderByDesc(DrugTraceScan::getScanTime);
return R.ok(scanService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/scan/record")
@Transactional(rollbackFor = Exception.class)
public R<?> recordScan(@RequestBody DrugTraceScan scan) {
scan.setScanTime(new Date());
scan.setScanResult("SUCCESS");
scanService.save(scan);
log.info("扫码记录: {} 类型: {} 结果: {}", scan.getTraceCode(), scan.getScanType(), scan.getScanResult());
return R.ok(scan);
}
@GetMapping("/scan/trace/{traceCode}")
public R<?> traceByCode(@PathVariable String traceCode) {
LambdaQueryWrapper<DrugTraceScan> w = new LambdaQueryWrapper<>();
w.eq(DrugTraceScan::getTraceCode, traceCode).orderByAsc(DrugTraceScan::getScanTime);
List<DrugTraceScan> scans = scanService.list(w);
Map<String, Object> result = new HashMap<>();
result.put("traceCode", traceCode);
result.put("scanCount", scans.size());
result.put("scans", scans);
result.put("firstScan", scans.isEmpty() ? null : scans.get(0));
result.put("lastScan", scans.isEmpty() ? null : scans.get(scans.size() - 1));
List<String> locations = scans.stream().map(DrugTraceScan::getScanLocation).filter(Objects::nonNull).distinct().collect(Collectors.toList());
result.put("locations", locations);
return R.ok(result);
}
// ==================== 4. 预警管理 ====================
@GetMapping("/alert/page")
public R<?> getAlertPage(
@RequestParam(value = "alertType", required = false) String alertType,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<DrugTraceAlert> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(alertType), DrugTraceAlert::getAlertType, alertType)
.eq(StringUtils.hasText(status), DrugTraceAlert::getStatus, status)
.orderByDesc(DrugTraceAlert::getAlertTime);
return R.ok(alertService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/alert/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addAlert(@RequestBody DrugTraceAlert alert) {
alert.setAlertTime(new Date());
alert.setStatus("PENDING");
alertService.save(alert);
log.info("追溯预警: {} 级别: {} 内容: {}", alert.getAlertType(), alert.getAlertLevel(), alert.getAlertContent());
return R.ok(alert);
}
@PutMapping("/alert/{id}/handle")
@Transactional(rollbackFor = Exception.class)
public R<?> handleAlert(@PathVariable Long id, @RequestBody Map<String, String> data) {
DrugTraceAlert alert = alertService.getById(id);
if (alert != null) {
alert.setHandlerName(data.get("handlerName"));
alert.setHandleTime(new Date());
alert.setHandleResult(data.get("handleResult"));
alert.setStatus("HANDLED");
alertService.updateById(alert);
}
return R.ok();
}
@DeleteMapping("/alert/delete/{id}")
@Transactional(rollbackFor = Exception.class)
public R<?> deleteAlert(@PathVariable Long id) {
alertService.removeById(id);
return R.ok();
}
// ==================== 5. 统计 ====================
@GetMapping("/statistics")
public R<?> getStatistics() {
Map<String, Object> stats = new HashMap<>();
stats.put("totalCodes", codeService.count());
stats.put("activeCodes", codeService.count(new LambdaQueryWrapper<DrugTraceCode>().eq(DrugTraceCode::getStatus, "ACTIVE")));
stats.put("totalBatches", batchService.count());
stats.put("pendingBatches", batchService.count(new LambdaQueryWrapper<DrugTraceBatch>().eq(DrugTraceBatch::getStatus, "PENDING")));
stats.put("totalScans", scanService.count());
stats.put("pendingAlerts", alertService.count(new LambdaQueryWrapper<DrugTraceAlert>().eq(DrugTraceAlert::getStatus, "PENDING")));
// 近效期预警(30天内过期)
Date thirtyDaysLater = new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000);
Date now = new Date();
stats.put("expiringSoon", codeService.count(new LambdaQueryWrapper<DrugTraceCode>()
.ge(DrugTraceCode::getExpiryDate, now)
.le(DrugTraceCode::getExpiryDate, thirtyDaysLater)));
stats.put("expired", codeService.count(new LambdaQueryWrapper<DrugTraceCode>()
.lt(DrugTraceCode::getExpiryDate, now)));
return R.ok(stats);
}
}

View File

@@ -0,0 +1,120 @@
-- ==========================================
-- V36: 药品追溯码管理模块
-- 三甲要求: 2026年新规药品全流程追溯
-- ==========================================
-- 1. 药品追溯码主表
CREATE TABLE IF NOT EXISTS drug_trace_code (
id BIGSERIAL PRIMARY KEY,
drug_code VARCHAR(50) NOT NULL,
drug_name VARCHAR(200) NOT NULL,
generic_name VARCHAR(200),
specification VARCHAR(200),
manufacturer VARCHAR(200),
batch_no VARCHAR(100) NOT NULL,
trace_code VARCHAR(200) NOT NULL,
production_date TIMESTAMP,
expiry_date TIMESTAMP,
approval_number VARCHAR(100),
dosage_form VARCHAR(50),
unit VARCHAR(20),
barcode VARCHAR(200),
qr_code VARCHAR(500),
status VARCHAR(20) DEFAULT 'ACTIVE',
delete_flag VARCHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_drug_trace_code_drug ON drug_trace_code(drug_code);
CREATE INDEX IF NOT EXISTS idx_drug_trace_code_batch ON drug_trace_code(batch_no);
CREATE INDEX IF NOT EXISTS idx_drug_trace_code_trace ON drug_trace_code(trace_code);
-- 2. 药品追溯批次信息
CREATE TABLE IF NOT EXISTS drug_trace_batch (
id BIGSERIAL PRIMARY KEY,
batch_no VARCHAR(100) NOT NULL,
drug_code VARCHAR(50) NOT NULL,
drug_name VARCHAR(200),
supplier VARCHAR(200),
supplier_license VARCHAR(100),
purchase_order_no VARCHAR(100),
quantity INTEGER NOT NULL,
received_quantity INTEGER DEFAULT 0,
warehouse_id BIGINT,
warehouse_name VARCHAR(100),
receive_time TIMESTAMP,
receiver_id BIGINT,
receiver_name VARCHAR(64),
status VARCHAR(20) DEFAULT 'PENDING',
delete_flag VARCHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_drug_trace_batch_no ON drug_trace_batch(batch_no);
CREATE INDEX IF NOT EXISTS idx_drug_trace_batch_drug ON drug_trace_batch(drug_code);
-- 3. 药品追溯扫码记录
CREATE TABLE IF NOT EXISTS drug_trace_scan (
id BIGSERIAL PRIMARY KEY,
trace_code VARCHAR(200) NOT NULL,
drug_code VARCHAR(50),
drug_name VARCHAR(200),
batch_no VARCHAR(100),
scan_type VARCHAR(50) NOT NULL,
scan_location VARCHAR(200),
scan_person_id BIGINT,
scan_person_name VARCHAR(64),
scan_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
scan_result VARCHAR(20) DEFAULT 'SUCCESS',
related_order_no VARCHAR(100),
related_order_type VARCHAR(50),
ip_address VARCHAR(50),
device_info VARCHAR(200),
delete_flag VARCHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_drug_trace_scan_code ON drug_trace_scan(trace_code);
CREATE INDEX IF NOT EXISTS idx_drug_trace_scan_time ON drug_trace_scan(scan_time);
CREATE INDEX IF NOT EXISTS idx_drug_trace_scan_type ON drug_trace_scan(scan_type);
-- 4. 药品追溯预警
CREATE TABLE IF NOT EXISTS drug_trace_alert (
id BIGSERIAL PRIMARY KEY,
alert_type VARCHAR(50) NOT NULL,
alert_level VARCHAR(20) NOT NULL,
drug_code VARCHAR(50),
drug_name VARCHAR(200),
batch_no VARCHAR(100),
trace_code VARCHAR(200),
alert_content TEXT NOT NULL,
related_order_no VARCHAR(100),
alert_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
handler_id BIGINT,
handler_name VARCHAR(64),
handle_time TIMESTAMP,
handle_result TEXT,
status VARCHAR(20) DEFAULT 'PENDING',
delete_flag VARCHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_drug_trace_alert_type ON drug_trace_alert(alert_type);
CREATE INDEX IF NOT EXISTS idx_drug_trace_alert_status ON drug_trace_alert(status);
CREATE INDEX IF NOT EXISTS idx_drug_trace_alert_time ON drug_trace_alert(alert_time);

View File

@@ -0,0 +1,46 @@
package com.healthlink.his.drugtrace.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("drug_trace_alert")
public class DrugTraceAlert extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField("alert_type")
private String alertType;
@TableField("alert_level")
private String alertLevel;
@TableField("drug_code")
private String drugCode;
@TableField("drug_name")
private String drugName;
@TableField("batch_no")
private String batchNo;
@TableField("trace_code")
private String traceCode;
@TableField("alert_content")
private String alertContent;
@TableField("related_order_no")
private String relatedOrderNo;
@TableField("alert_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date alertTime;
@TableField("handler_id")
private Long handlerId;
@TableField("handler_name")
private String handlerName;
@TableField("handle_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date handleTime;
@TableField("handle_result")
private String handleResult;
@TableField("status")
private String status;
}

View File

@@ -0,0 +1,45 @@
package com.healthlink.his.drugtrace.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("drug_trace_batch")
public class DrugTraceBatch extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField("batch_no")
private String batchNo;
@TableField("drug_code")
private String drugCode;
@TableField("drug_name")
private String drugName;
@TableField("supplier")
private String supplier;
@TableField("supplier_license")
private String supplierLicense;
@TableField("purchase_order_no")
private String purchaseOrderNo;
@TableField("quantity")
private Integer quantity;
@TableField("received_quantity")
private Integer receivedQuantity;
@TableField("warehouse_id")
private Long warehouseId;
@TableField("warehouse_name")
private String warehouseName;
@TableField("receive_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date receiveTime;
@TableField("receiver_id")
private Long receiverId;
@TableField("receiver_name")
private String receiverName;
@TableField("status")
private String status;
}

View File

@@ -0,0 +1,48 @@
package com.healthlink.his.drugtrace.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("drug_trace_code")
public class DrugTraceCode extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField("drug_code")
private String drugCode;
@TableField("drug_name")
private String drugName;
@TableField("generic_name")
private String genericName;
@TableField("specification")
private String specification;
@TableField("manufacturer")
private String manufacturer;
@TableField("batch_no")
private String batchNo;
@TableField("trace_code")
private String traceCode;
@TableField("production_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date productionDate;
@TableField("expiry_date")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date expiryDate;
@TableField("approval_number")
private String approvalNumber;
@TableField("dosage_form")
private String dosageForm;
@TableField("unit")
private String unit;
@TableField("barcode")
private String barcode;
@TableField("qr_code")
private String qrCode;
@TableField("status")
private String status;
}

View File

@@ -0,0 +1,45 @@
package com.healthlink.his.drugtrace.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("drug_trace_scan")
public class DrugTraceScan extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@TableField("trace_code")
private String traceCode;
@TableField("drug_code")
private String drugCode;
@TableField("drug_name")
private String drugName;
@TableField("batch_no")
private String batchNo;
@TableField("scan_type")
private String scanType;
@TableField("scan_location")
private String scanLocation;
@TableField("scan_person_id")
private Long scanPersonId;
@TableField("scan_person_name")
private String scanPersonName;
@TableField("scan_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date scanTime;
@TableField("scan_result")
private String scanResult;
@TableField("related_order_no")
private String relatedOrderNo;
@TableField("related_order_type")
private String relatedOrderType;
@TableField("ip_address")
private String ipAddress;
@TableField("device_info")
private String deviceInfo;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.drugtrace.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.drugtrace.domain.DrugTraceAlert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DrugTraceAlertMapper extends BaseMapper<DrugTraceAlert> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.drugtrace.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.drugtrace.domain.DrugTraceBatch;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DrugTraceBatchMapper extends BaseMapper<DrugTraceBatch> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.drugtrace.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.drugtrace.domain.DrugTraceCode;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DrugTraceCodeMapper extends BaseMapper<DrugTraceCode> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.drugtrace.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.drugtrace.domain.DrugTraceScan;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DrugTraceScanMapper extends BaseMapper<DrugTraceScan> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.drugtrace.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.drugtrace.domain.DrugTraceAlert;
public interface IDrugTraceAlertService extends IService<DrugTraceAlert> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.drugtrace.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.drugtrace.domain.DrugTraceBatch;
public interface IDrugTraceBatchService extends IService<DrugTraceBatch> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.drugtrace.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.drugtrace.domain.DrugTraceCode;
public interface IDrugTraceCodeService extends IService<DrugTraceCode> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.drugtrace.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.drugtrace.domain.DrugTraceScan;
public interface IDrugTraceScanService extends IService<DrugTraceScan> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.drugtrace.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.drugtrace.domain.DrugTraceAlert;
import com.healthlink.his.drugtrace.mapper.DrugTraceAlertMapper;
import com.healthlink.his.drugtrace.service.IDrugTraceAlertService;
import org.springframework.stereotype.Service;
@Service
public class DrugTraceAlertServiceImpl extends ServiceImpl<DrugTraceAlertMapper, DrugTraceAlert> implements IDrugTraceAlertService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.drugtrace.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.drugtrace.domain.DrugTraceBatch;
import com.healthlink.his.drugtrace.mapper.DrugTraceBatchMapper;
import com.healthlink.his.drugtrace.service.IDrugTraceBatchService;
import org.springframework.stereotype.Service;
@Service
public class DrugTraceBatchServiceImpl extends ServiceImpl<DrugTraceBatchMapper, DrugTraceBatch> implements IDrugTraceBatchService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.drugtrace.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.drugtrace.domain.DrugTraceCode;
import com.healthlink.his.drugtrace.mapper.DrugTraceCodeMapper;
import com.healthlink.his.drugtrace.service.IDrugTraceCodeService;
import org.springframework.stereotype.Service;
@Service
public class DrugTraceCodeServiceImpl extends ServiceImpl<DrugTraceCodeMapper, DrugTraceCode> implements IDrugTraceCodeService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.drugtrace.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.drugtrace.domain.DrugTraceScan;
import com.healthlink.his.drugtrace.mapper.DrugTraceScanMapper;
import com.healthlink.his.drugtrace.service.IDrugTraceScanService;
import org.springframework.stereotype.Service;
@Service
public class DrugTraceScanServiceImpl extends ServiceImpl<DrugTraceScanMapper, DrugTraceScan> implements IDrugTraceScanService {
}

View File

@@ -0,0 +1,74 @@
<template>
<div class="app-container">
<el-row :gutter="20" class="mb8">
<el-col :span="6"><el-card shadow="hover"><el-statistic title="待处理预警" :value="stats.pendingAlerts || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="近效期药品" :value="stats.expiringSoon || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="已过期药品" :value="stats.expired || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="活跃追溯码" :value="stats.activeCodes || 0" /></el-card></el-col>
</el-row>
<el-form :model="queryParams" :inline="true" v-show="showSearch">
<el-form-item label="预警类型">
<el-select v-model="queryParams.alertType" placeholder="全部" clearable>
<el-option label="近效期" value="EXPIRING" /><el-option label="已过期" value="EXPIRED" />
<el-option label="批次召回" value="RECALL" /><el-option label="异常扫码" value="ABNORMAL_SCAN" />
<el-option label="来源不明" value="UNKNOWN_SOURCE" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option label="待处理" value="PENDING" /><el-option label="已处理" value="HANDLED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="预警类型" prop="alertType" width="100">
<template #default="s">
<el-tag :type="s.row.alertType==='EXPIRED'?'danger':s.row.alertType==='RECALL'?'warning':'info'">
{{ {EXPIRING:'近效期',EXPIRED:'已过期',RECALL:'批次召回',ABNORMAL_SCAN:'异常扫码',UNKNOWN_SOURCE:'来源不明'}[s.row.alertType] || s.row.alertType }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="预警级别" prop="alertLevel" width="80">
<template #default="s"><el-tag :type="s.row.alertLevel==='RED'?'danger':s.row.alertLevel==='YELLOW'?'warning':'success'">{{ s.row.alertLevel }}</el-tag></template>
</el-table-column>
<el-table-column label="药品名称" prop="drugName" width="150" />
<el-table-column label="批次号" prop="batchNo" width="120" />
<el-table-column label="预警内容" prop="alertContent" width="250" show-overflow-tooltip />
<el-table-column label="预警时间" prop="alertTime" width="170" />
<el-table-column label="处理人" prop="handlerName" width="100" />
<el-table-column label="状态" prop="status" width="90">
<template #default="s"><el-tag :type="s.row.status==='PENDING'?'danger':'success'">{{ s.row.status === 'PENDING' ? '待处理' : '已处理' }}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="s">
<el-button link type="primary" v-if="s.row.status==='PENDING'" @click="handleProcess(s.row)">处理</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getAlertPage, handleAlert, getStatistics } from '../api'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false); const dataList = ref([]); const total = ref(0); const showSearch = ref(true)
const stats = ref({})
const queryParams = reactive({ alertType: '', status: '', pageNo: 1, pageSize: 20 })
const getList = async () => { loading.value = true; const res = await getAlertPage(queryParams); dataList.value = res.data?.records || []; total.value = res.data?.total || 0; loading.value = false }
const loadStats = async () => { const res = await getStatistics(); stats.value = res.data || {} }
const handleQuery = () => { queryParams.pageNo = 1; getList() }
const resetQuery = () => { queryParams.alertType = ''; queryParams.status = ''; handleQuery() }
const handleProcess = async (row) => {
await ElMessageBox.confirm('确认处理此预警?', '提示', { type: 'info' })
await handleAlert(row.id, { handlerName: '管理员', handleResult: '已核实处理' }); ElMessage.success('处理成功'); getList(); loadStats()
}
onMounted(() => { getList(); loadStats() })
</script>

View File

@@ -0,0 +1,23 @@
import request from '@/utils/request'
export function getCodePage(params) { return request({ url: '/drugtrace/code/page', method: 'get', params }) }
export function addCode(data) { return request({ url: '/drugtrace/code/add', method: 'post', data }) }
export function updateCode(id, data) { return request({ url: '/drugtrace/code/' + id, method: 'put', data }) }
export function deleteCode(id) { return request({ url: '/drugtrace/code/delete/' + id, method: 'delete' }) }
export function verifyTraceCode(traceCode) { return request({ url: '/drugtrace/code/verify/' + traceCode, method: 'get' }) }
export function getBatchPage(params) { return request({ url: '/drugtrace/batch/page', method: 'get', params }) }
export function addBatch(data) { return request({ url: '/drugtrace/batch/add', method: 'post', data }) }
export function receiveBatch(id, data) { return request({ url: '/drugtrace/batch/' + id + '/receive', method: 'put', data }) }
export function deleteBatch(id) { return request({ url: '/drugtrace/batch/delete/' + id, method: 'delete' }) }
export function getScanPage(params) { return request({ url: '/drugtrace/scan/page', method: 'get', params }) }
export function recordScan(data) { return request({ url: '/drugtrace/scan/record', method: 'post', data }) }
export function traceByCode(traceCode) { return request({ url: '/drugtrace/scan/trace/' + traceCode, method: 'get' }) }
export function getAlertPage(params) { return request({ url: '/drugtrace/alert/page', method: 'get', params }) }
export function addAlert(data) { return request({ url: '/drugtrace/alert/add', method: 'post', data }) }
export function handleAlert(id, data) { return request({ url: '/drugtrace/alert/' + id + '/handle', method: 'put', data }) }
export function deleteAlert(id) { return request({ url: '/drugtrace/alert/delete/' + id, method: 'delete' }) }
export function getStatistics() { return request({ url: '/drugtrace/statistics', method: 'get' }) }

View File

@@ -0,0 +1,87 @@
<template>
<div class="app-container">
<el-form :model="queryParams" :inline="true" v-show="showSearch">
<el-form-item label="批次号"><el-input v-model="queryParams.batchNo" placeholder="批次号" clearable /></el-form-item>
<el-form-item label="药品名称"><el-input v-model="queryParams.drugName" placeholder="药品名称" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option label="待收货" value="PENDING" /><el-option label="已收货" value="RECEIVED" /><el-option label="已完成" value="COMPLETED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<el-button type="primary" icon="Plus" @click="handleAdd">新增批次</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="批次号" prop="batchNo" width="140" />
<el-table-column label="药品名称" prop="drugName" width="150" />
<el-table-column label="供应商" prop="supplier" width="150" show-overflow-tooltip />
<el-table-column label="采购单号" prop="purchaseOrderNo" width="130" />
<el-table-column label="数量" prop="quantity" width="80" />
<el-table-column label="实收" prop="receivedQuantity" width="80" />
<el-table-column label="收货仓库" prop="warehouseName" width="120" />
<el-table-column label="收货人" prop="receiverName" width="100" />
<el-table-column label="收货时间" prop="receiveTime" width="170" />
<el-table-column label="状态" prop="status" width="90">
<template #default="s">
<el-tag :type="s.row.status==='PENDING'?'warning':s.row.status==='RECEIVED'?'success':'info'">
{{ {PENDING:'待收货',RECEIVED:'已收货',COMPLETED:'已完成'}[s.row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="s">
<el-button link type="primary" v-if="s.row.status==='PENDING'" @click="handleReceive(s.row)">收货</el-button>
<el-button link type="primary" @click="handleEdit(s.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="550px">
<el-form :model="formData" label-width="100px">
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="批次号"><el-input v-model="formData.batchNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="药品编码"><el-input v-model="formData.drugCode" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="药品名称"><el-input v-model="formData.drugName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="供应商"><el-input v-model="formData.supplier" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="供应商许可"><el-input v-model="formData.supplierLicense" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="采购单号"><el-input v-model="formData.purchaseOrderNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="数量"><el-input-number v-model="formData.quantity" :min="1" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="仓库"><el-input v-model="formData.warehouseName" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getBatchPage, addBatch, receiveBatch, deleteBatch } from '../api'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false); const dataList = ref([]); const total = ref(0); const showSearch = ref(true)
const dialogVisible = ref(false); const dialogTitle = ref('新增批次')
const queryParams = reactive({ batchNo: '', drugName: '', status: '', pageNo: 1, pageSize: 20 })
const formData = ref({})
const getList = async () => { loading.value = true; const res = await getBatchPage(queryParams); dataList.value = res.data?.records || []; total.value = res.data?.total || 0; loading.value = false }
const handleQuery = () => { queryParams.pageNo = 1; getList() }
const resetQuery = () => { queryParams.batchNo = ''; queryParams.drugName = ''; queryParams.status = ''; handleQuery() }
const handleAdd = () => { formData.value = {}; dialogTitle.value = '新增批次'; dialogVisible.value = true }
const handleEdit = (row) => { formData.value = { ...row }; dialogTitle.value = '编辑批次'; dialogVisible.value = true }
const submitForm = async () => { await addBatch(formData.value); ElMessage.success('操作成功'); dialogVisible.value = false; getList() }
const handleReceive = async (row) => {
await ElMessageBox.confirm('确认收货?', '提示', { type: 'info' })
await receiveBatch(row.id, { receivedQuantity: row.quantity, receiverName: '管理员' }); ElMessage.success('收货成功'); getList()
}
const handleDelete = async (row) => { await ElMessageBox.confirm('确认删除?', '提示', { type: 'warning' }); await deleteBatch(row.id); ElMessage.success('删除成功'); getList() }
onMounted(() => getList())
</script>

View File

@@ -0,0 +1,106 @@
<template>
<div class="app-container">
<el-form :model="queryParams" :inline="true" v-show="showSearch">
<el-form-item label="药品名称">
<el-input v-model="queryParams.drugName" placeholder="药品名称" clearable />
</el-form-item>
<el-form-item label="批次号">
<el-input v-model="queryParams.batchNo" placeholder="批次号" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option label="有效" value="ACTIVE" />
<el-option label="停用" value="INACTIVE" />
<el-option label="召回" value="RECALLED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="药品名称" prop="drugName" width="150" />
<el-table-column label="通用名" prop="genericName" width="120" />
<el-table-column label="规格" prop="specification" width="120" />
<el-table-column label="批次号" prop="batchNo" width="120" />
<el-table-column label="追溯码" prop="traceCode" width="200" show-overflow-tooltip />
<el-table-column label="生产企业" prop="manufacturer" width="150" show-overflow-tooltip />
<el-table-column label="有效期至" prop="expiryDate" width="120" />
<el-table-column label="状态" prop="status" width="80">
<template #default="s">
<el-tag :type="s.row.status==='ACTIVE'?'success':s.row.status==='RECALLED'?'danger':'info'">
{{ {ACTIVE:'有效',INACTIVE:'停用',RECALLED:'召回'}[s.row.status] || s.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="s">
<el-button link type="primary" @click="handleEdit(s.row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form :model="formData" label-width="100px">
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="药品编码"><el-input v-model="formData.drugCode" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="药品名称"><el-input v-model="formData.drugName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="通用名"><el-input v-model="formData.genericName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="规格"><el-input v-model="formData.specification" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="生产企业"><el-input v-model="formData.manufacturer" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="批次号"><el-input v-model="formData.batchNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="追溯码"><el-input v-model="formData.traceCode" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="批准文号"><el-input v-model="formData.approvalNumber" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="剂型"><el-input v-model="formData.dosageForm" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="单位"><el-input v-model="formData.unit" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="生产日期"><el-date-picker v-model="formData.productionDate" type="date" value-format="YYYY-MM-DD" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="有效期至"><el-date-picker v-model="formData.expiryDate" type="date" value-format="YYYY-MM-DD" /></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getCodePage, addCode, updateCode, deleteCode } from '../api'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
const dataList = ref([])
const total = ref(0)
const showSearch = ref(true)
const dialogVisible = ref(false)
const dialogTitle = ref('新增追溯码')
const queryParams = reactive({ drugName: '', batchNo: '', status: '', pageNo: 1, pageSize: 20 })
const formData = ref({})
const getList = async () => {
loading.value = true
const res = await getCodePage(queryParams)
dataList.value = res.data?.records || []
total.value = res.data?.total || 0
loading.value = false
}
const handleQuery = () => { queryParams.pageNo = 1; getList() }
const resetQuery = () => { queryParams.drugName = ''; queryParams.batchNo = ''; queryParams.status = ''; handleQuery() }
const handleAdd = () => { formData.value = {}; dialogTitle.value = '新增追溯码'; dialogVisible.value = true }
const handleEdit = (row) => { formData.value = { ...row }; dialogTitle.value = '编辑追溯码'; dialogVisible.value = true }
const submitForm = async () => {
if (formData.value.id) { await updateCode(formData.value.id, formData.value) } else { await addCode(formData.value) }
ElMessage.success('操作成功'); dialogVisible.value = false; getList()
}
const handleDelete = async (row) => {
await ElMessageBox.confirm('确认删除?', '提示', { type: 'warning' })
await deleteCode(row.id); ElMessage.success('删除成功'); getList()
}
onMounted(() => getList())
</script>

View File

@@ -0,0 +1,78 @@
<template>
<div class="app-container">
<el-row :gutter="20" class="mb8">
<el-col :span="6"><el-card shadow="hover"><el-statistic title="今日扫码" :value="stats.totalScans || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="扫码成功" :value="stats.successScans || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="扫码失败" :value="stats.failedScans || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover">
<el-input v-model="searchCode" placeholder="输入追溯码查询" @keyup.enter="handleVerify">
<template #append><el-button icon="Search" @click="handleVerify" /></template>
</el-input>
</el-card></el-col>
</el-row>
<el-form :model="queryParams" :inline="true" v-show="showSearch">
<el-form-item label="扫码类型">
<el-select v-model="queryParams.scanType" placeholder="全部" clearable>
<el-option label="入库扫码" value="INBOUND" /><el-option label="出库扫码" value="OUTBOUND" />
<el-option label="发药扫码" value="DISPENSING" /><el-option label="退药扫码" value="RETURN" />
<el-option label="盘点扫码" value="STOCKTAKE" />
</el-select>
</el-form-item>
<el-form-item label="药品名称"><el-input v-model="queryParams.drugName" placeholder="药品名称" clearable /></el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="dataList">
<el-table-column label="追溯码" prop="traceCode" width="200" show-overflow-tooltip />
<el-table-column label="药品名称" prop="drugName" width="150" />
<el-table-column label="批次号" prop="batchNo" width="120" />
<el-table-column label="扫码类型" prop="scanType" width="100">
<template #default="s">
<el-tag>{{ {INBOUND:'入库',OUTBOUND:'出库',DISPENSING:'发药',RETURN:'退药',STOCKTAKE:'盘点'}[s.row.scanType] || s.row.scanType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="扫码地点" prop="scanLocation" width="150" />
<el-table-column label="扫码人" prop="scanPersonName" width="100" />
<el-table-column label="扫码时间" prop="scanTime" width="170" />
<el-table-column label="结果" prop="scanResult" width="80">
<template #default="s"><el-tag :type="s.row.scanResult==='SUCCESS'?'success':'danger'">{{ s.row.scanResult === 'SUCCESS' ? '成功' : '失败' }}</el-tag></template>
</el-table-column>
<el-table-column label="关联单号" prop="relatedOrderNo" width="130" show-overflow-tooltip />
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-dialog title="追溯码验证" v-model="verifyVisible" width="500px">
<el-descriptions :column="1" border v-if="verifyResult">
<el-descriptions-item label="追溯码">{{ verifyResult.traceCode }}</el-descriptions-item>
<el-descriptions-item label="药品名称">{{ verifyResult.drugName }}</el-descriptions-item>
<el-descriptions-item label="批次号">{{ verifyResult.batchNo }}</el-descriptions-item>
<el-descriptions-item label="生产企业">{{ verifyResult.manufacturer }}</el-descriptions-item>
<el-descriptions-item label="有效期至">{{ verifyResult.expiryDate }}</el-descriptions-item>
<el-descriptions-item label="状态"><el-tag :type="verifyResult.expired?'danger':'success'">{{ verifyResult.expired ? '已过期' : '正常' }}</el-tag></el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getScanPage, verifyTraceCode, getStatistics } from '../api'
import { ElMessage } from 'element-plus'
const loading = ref(false); const dataList = ref([]); const total = ref(0); const showSearch = ref(true)
const searchCode = ref(''); const verifyVisible = ref(false); const verifyResult = ref(null)
const stats = ref({})
const queryParams = reactive({ scanType: '', drugName: '', pageNo: 1, pageSize: 20 })
const getList = async () => { loading.value = true; const res = await getScanPage(queryParams); dataList.value = res.data?.records || []; total.value = res.data?.total || 0; loading.value = false }
const loadStats = async () => { const res = await getStatistics(); stats.value = res.data || {} }
const handleQuery = () => { queryParams.pageNo = 1; getList() }
const resetQuery = () => { queryParams.scanType = ''; queryParams.drugName = ''; handleQuery() }
const handleVerify = async () => {
if (!searchCode.value) { ElMessage.warning('请输入追溯码'); return }
const res = await verifyTraceCode(searchCode.value); verifyResult.value = res.data; verifyVisible.value = true
}
onMounted(() => { getList(); loadStats() })
</script>