最新版本医废小程序代码,包含医废收集、一键入库/出库、周转箱入库/出库

This commit is contained in:
2026-05-12 01:25:59 +08:00
commit 6c5847b2e8
70 changed files with 23577 additions and 0 deletions
+902
View File
@@ -0,0 +1,902 @@
<template>
<view>
<scroll-view scroll-y class="page">
<cu-custom bgColor="bg-blue" :isBack="true">
<block slot="backText">返回</block>
<block slot="content">箱袋入库</block>
</cu-custom>
<view class="form-container">
<!-- 扫描箱子 -->
<view class="scan-section">
<view class="scan-label">箱子编码</view>
<view class="scan-box-wrapper">
<view class="scan-input-box" :class="{'has-value': boxCode}">
<text v-if="boxCode" class="scan-value">{{boxCode}}</text>
<text v-else class="scan-placeholder">请先扫描箱子二维码</text>
</view>
<button class="scan-btn" @click="scanBox">
<text class="cuIcon-scan"></text>
<text class="btn-text">扫描箱子</text>
</button>
</view>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 扫描医废袋 -->
<view class="scan-section">
<view class="scan-header">
<view class="scan-label">医废袋列表</view>
<button class="scan-btn small" @click="scanBag" :disabled="!boxCode">
<text class="cuIcon-scan"></text>
<text class="btn-text">扫描医废袋</text>
</button>
</view>
<!-- 医废袋列表 -->
<view class="bag-list">
<view v-if="bagList.length === 0" class="empty-tip">
<text class="cuIcon-info"></text>
<text>请扫描医废袋二维码添加</text>
</view>
<view class="bag-item" v-for="(item, index) in bagList" :key="index">
<view class="bag-info">
<view class="bag-code">{{item.code}}</view>
<view class="bag-detail">
<text class="bag-type" :class="'type-' + item.typeCode">{{item.type}}</text>
<text class="bag-weight">{{item.weight}}kg</text>
</view>
</view>
<view class="bag-delete" @click="removeBag(index)">
<text class="cuIcon-delete"></text>
</view>
</view>
</view>
</view>
<!-- 统计信息 -->
<view class="stats-section" v-if="bagList.length > 0">
<view class="stats-title">统计信息</view>
<view class="stats-content">
<view class="stats-item">
<text class="stats-label">袋数</text>
<text class="stats-value">{{bagList.length}}</text>
</view>
<view class="stats-item">
<text class="stats-label">总重</text>
<text class="stats-value">{{totalWeight}}kg</text>
</view>
</view>
<view class="type-stats">
<view class="type-item" v-for="(stat, type) in typeStats" :key="type">
<text class="type-name">{{type}}:</text>
<text class="type-count">{{stat.count}}</text>
<text class="type-weight">{{stat.weight}}kg</text>
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="footer">
<button class="submit-btn" :disabled="!boxCode || bagList.length === 0" @click="submitStorage">
箱袋入库
</button>
</view>
<view class="cu-tabbar-height"></view>
</scroll-view>
<!-- 入库确认弹窗 -->
<view class="modal" v-if="showModal" @click.self="closeModal">
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">入库确认</text>
<text class="modal-close" @click="closeModal"></text>
</view>
<view class="modal-body">
<view class="confirm-section">
<view class="confirm-label">箱子编码</view>
<view class="confirm-value highlight">{{boxCode}}</view>
</view>
<view class="confirm-section">
<view class="confirm-label">医废袋数量</view>
<view class="confirm-value">{{bagList.length}}</view>
</view>
<view class="confirm-section">
<view class="confirm-label">总重量</view>
<view class="confirm-value highlight">{{totalWeight}}kg</view>
</view>
<view class="confirm-section type-detail">
<view class="confirm-label">类型明细</view>
<view class="type-list">
<view class="type-row" v-for="(stat, type) in typeStats" :key="type">
<text class="type-name">{{type}}</text>
<text class="type-info">{{stat.count}} / {{stat.weight}}kg</text>
</view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn-cancel" @click="closeModal">取消</button>
<button class="btn-confirm" @click="confirmStorage">确认入库</button>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
boxCode: '',
bagList: [],
showModal: false,
isScanning: false, // 防抖
scanCodeBuffer: '',
scanTimer: null,
typeMap: {
'感染性废物': { code: 'infectious', color: '#e54d42' },
'损伤性废物': { code: 'injury', color: '#f37b1d' },
'病理性废物': { code: 'pathology', color: '#8dc63f' },
'化学性废物': { code: 'chemical', color: '#6739b6' },
'药物性废物': { code: 'pharmaceutical', color: '#279cff' }
}
}
},
computed: {
totalWeight() {
return this.bagList.reduce((sum, item) => sum + parseFloat(item.weight || 0), 0).toFixed(2)
},
typeStats() {
const stats = {}
this.bagList.forEach(item => {
if (!stats[item.type]) {
stats[item.type] = { count: 0, weight: 0 }
}
stats[item.type].count++
stats[item.type].weight += parseFloat(item.weight || 0)
})
// 格式化重量
for (let key in stats) {
stats[key].weight = stats[key].weight.toFixed(2)
}
return stats
}
},
onShow() {
this.initScanner()
},
onHide() {
this.destroyScanner()
},
onUnload() {
this.destroyScanner()
},
methods: {
// 初始化:同时开 广播+键盘监听(双保险)
initScanner() {
// #ifdef APP-PLUS
this.initBroadcast()
this.initKeyboard()
// #endif
},
// 1. 广播模式(scan.rcv.message / barcodeData
initBroadcast() {
try {
const main = plus.android.runtimeMainActivity()
const IntentFilter = plus.android.importClass('android.content.IntentFilter')
const filter = new IntentFilter()
// 注册多个常见的扫码广播 action(含 PDA 实际使用的 scan.rcv.message
const actions = [
'scan.rcv.message', // PDA 实际广播名
'com.android.scan.action.SCAN_RESULT',
'android.intent.action.DECODEDATA',
'com.barcode.send',
'com.ge.android.scanner.BARCODE_DATA'
]
actions.forEach(action => {
try {
filter.addAction(action)
} catch(e) {}
})
filter.addCategory('android.intent.category.DEFAULT')
this.scanReceiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
onReceive: (context, intent) => {
plus.android.importClass(intent)
// 尝试多种 extra 名称(含 PDA 实际键值名 barcodeData
const extras = [
'barcodeData', // PDA 实际键值名
'data', 'BARCODE', 'barcode', 'scandata', 'code', 'content', 'string'
]
let code = null
for (let key of extras) {
try {
const val = intent.getStringExtra(key)
if (val && val.trim()) {
code = val.trim()
break
}
} catch(e) {}
}
if (code) {
this.handlePdaScan(code)
}
}
})
main.registerReceiver(this.scanReceiver, filter)
} catch (e) {}
},
// 2. 键盘模式(兜底)
initKeyboard() {
document.addEventListener('keydown', this.onKeyDown, true)
window.addEventListener('keydown', this.onKeyDown, true)
},
// 键盘事件处理
onKeyDown(e) {
if (this.scanTimer) clearTimeout(this.scanTimer)
if (e.keyCode === 13) {
const code = this.scanCodeBuffer.trim()
this.scanCodeBuffer = ''
if (code) {
this.handlePdaScan(code)
}
return
}
const char = e.key || String.fromCharCode(e.which)
if (char?.length === 1) this.scanCodeBuffer += char
this.scanTimer = setTimeout(() => { this.scanCodeBuffer = '' }, 200)
},
// 销毁监听
destroyScanner() {
// #ifdef APP-PLUS
try {
if (this.scanReceiver) {
const main = plus.android.runtimeMainActivity()
main.unregisterReceiver(this.scanReceiver)
this.scanReceiver = null
}
} catch (e) {}
document.removeEventListener('keydown', this.onKeyDown, true)
window.removeEventListener('keydown', this.onKeyDown, true)
// #endif
if (this.scanTimer) clearTimeout(this.scanTimer)
this.scanCodeBuffer = ''
},
// 统一处理PDA扫码结果(防抖+去重)
handlePdaScan(code) {
if (!code || this.isScanning) return
this.isScanning = true
setTimeout(() => { this.isScanning = false }, 500)
// 根据当前模式决定是扫码还是扫袋
if (!this.boxCode) {
this.verifyBoxCode(code)
} else {
if (this.bagList.some(item => item.code === code)) {
uni.showToast({ title: '该医废袋已扫描', icon: 'none' })
return
}
this.getBagInfo(code)
}
},
// 扫描箱子(按钮点击方式)
scanBox() {
uni.scanCode({
success: (res) => {
const code = res.result
this.verifyBoxCode(code)
},
fail: () => {
uni.showToast({
title: '扫描失败',
icon: 'none'
})
}
})
},
// 验证箱子编码(调用接口)
verifyBoxCode(code) {
uni.request({
url: 'https://lekapi.opmonitor.com/?c=app_api&a=verifyBox',
data: { code: code},
success: (res) => {
console.log(res.data)
if (res.data.data == 'success') {
this.boxCode = code
uni.showToast({
title: '箱子扫描成功',
icon: 'success'
})
} else {
uni.showModal({
title: '提示',
content: '箱子编码已被使用或者无效的箱子编码',
showCancel: false
})
}
}
})
},
// 扫描医废袋(按钮点击方式)
scanBag() {
if (!this.boxCode) {
uni.showToast({
title: '请先扫描箱子',
icon: 'none'
})
return
}
uni.scanCode({
success: (res) => {
const code = res.result
// 检查是否已扫描
if (this.bagList.some(item => item.code === code)) {
uni.showToast({
title: '该医废袋已扫描',
icon: 'none'
})
return
}
this.getBagInfo(code)
},
fail: () => {
uni.showToast({
title: '扫描失败',
icon: 'none'
})
}
})
},
// 获取医废袋信息(调用接口)
getBagInfo(code) {
uni.request({
url: 'https://lekapi.opmonitor.com/?c=app_api&a=getBagInfo',
data: { code: code },
success: (res) => {
console.log(res.data)
if (res.data.err_msg == 'success') {
const data = res.data.data
this.bagList.push({
code: code,
type: data.type,
typeCode: this.typeMap[data.type]?.code || 'other',
weight: data.weight
})
uni.showToast({
title: '添加成功',
icon: 'success'
})
} else {
uni.showModal({
title: '提示',
content: '医废袋编码不存在或非收集状态',
showCancel: false
})
}
}
})
},
// 删除医废袋
removeBag(index) {
uni.showModal({
title: '提示',
content: '确定删除该医废袋?',
success: (res) => {
if (res.confirm) {
this.bagList.splice(index, 1)
}
}
})
},
// 提交入库
submitStorage() {
if (!this.boxCode) {
uni.showToast({
title: '请先扫描箱子',
icon: 'none'
})
return
}
if (this.bagList.length === 0) {
uni.showToast({
title: '请扫描医废袋',
icon: 'none'
})
return
}
this.showModal = true
},
// 关闭弹窗
closeModal() {
this.showModal = false
},
// 确认入库
confirmStorage() {
// 获取登录信息
const hospital = uni.getStorageSync('hospital') || ''
const account = uni.getStorageSync('account') || ''
// TODO: 调用入库接口
const params = {
boxCode: this.boxCode,
hospital: hospital,
account: account,
bagList: this.bagList,
totalWeight: this.totalWeight,
typeStats: this.typeStats
}
console.log('入库参数:', params)
uni.request({
url: 'https://lekapi.opmonitor.com/?c=app_api&a=boxBagIn',
method: 'POST',
data: params,
success: (res) => {
console.log(res.data)
if (res.data.data.code === 200) {
uni.showToast({
title: '入库成功',
icon: 'success'
})
this.resetData()
} else {
uni.showModal({
title: '提示',
content: '入库失败',
showCancel: false
})
}
},
fail: () => {
uni.showToast({
title: '网络错误',
icon: 'none'
})
}
})
},
// 重置数据
resetData() {
this.boxCode = ''
this.bagList = []
this.showModal = false
}
}
}
</script>
<style>
.page {
width: 100vw;
height: 100vh;
background: #f5f5f5;
}
.form-container {
padding: 20rpx;
}
.scan-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.scan-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
}
.scan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.scan-box-wrapper {
display: flex;
align-items: center;
gap: 20rpx;
}
.scan-input-box {
flex: 1;
height: 88rpx;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
}
.scan-input-box.has-value {
background: #e6f7ff;
border: 2rpx solid #1890ff;
}
.scan-placeholder {
font-size: 28rpx;
color: #999;
}
.scan-value {
font-size: 28rpx;
color: #1890ff;
font-weight: 500;
}
.scan-btn {
height: 88rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
border: none;
border-radius: 12rpx;
padding: 0 30rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
.scan-btn.small {
height: 64rpx;
padding: 0 20rpx;
font-size: 26rpx;
}
.scan-btn[disabled] {
background: #ccc;
box-shadow: none;
}
.scan-btn::after {
border: none;
}
.scan-btn text {
font-size: 32rpx;
}
.btn-text {
font-size: 28rpx;
}
.divider {
height: 2rpx;
background: #e8e8e8;
margin: 10rpx 0;
}
.bag-list {
min-height: 200rpx;
}
.empty-tip {
display: flex;
align-items: center;
justify-content: center;
padding: 60rpx 0;
color: #999;
font-size: 28rpx;
}
.empty-tip text {
margin-right: 8rpx;
font-size: 32rpx;
}
.bag-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
.bag-info {
flex: 1;
}
.bag-code {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.bag-detail {
display: flex;
align-items: center;
gap: 20rpx;
}
.bag-type {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
color: #fff;
}
.type-infectious {
background: #e54d42;
}
.type-injury {
background: #f37b1d;
}
.type-pathology {
background: #8dc63f;
}
.type-chemical {
background: #6739b6;
}
.type-pharmaceutical {
background: #279cff;
}
.type-other {
background: #8799a3;
}
.bag-weight {
font-size: 24rpx;
color: #666;
}
.bag-delete {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e54d42;
}
.bag-delete text {
font-size: 36rpx;
}
.stats-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.stats-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
}
.stats-content {
display: flex;
gap: 40rpx;
margin-bottom: 20rpx;
}
.stats-item {
display: flex;
align-items: center;
}
.stats-label {
font-size: 26rpx;
color: #666;
}
.stats-value {
font-size: 32rpx;
color: #1890ff;
font-weight: 600;
}
.type-stats {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.type-item {
background: #f5f5f5;
padding: 12rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
}
.type-item .type-name {
color: #666;
margin-right: 8rpx;
}
.type-item .type-count {
color: #1890ff;
margin-right: 8rpx;
}
.type-item .type-weight {
color: #52c41a;
}
.footer {
padding: 20rpx 40rpx 40rpx;
}
.submit-btn {
height: 96rpx;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: #fff;
border: none;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 500;
box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.3);
}
.submit-btn[disabled] {
background: #ccc;
box-shadow: none;
}
.submit-btn::after {
border: none;
}
/* 弹窗样式 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 80%;
max-width: 600rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.modal-close {
font-size: 36rpx;
color: #999;
padding: 10rpx;
}
.modal-body {
padding: 30rpx;
}
.confirm-section {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.confirm-section.type-detail {
flex-direction: column;
align-items: flex-start;
margin-top: 30rpx;
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.confirm-label {
font-size: 28rpx;
color: #666;
width: 160rpx;
}
.confirm-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.confirm-value.highlight {
color: #1890ff;
font-size: 32rpx;
}
.type-list {
width: 100%;
margin-top: 16rpx;
}
.type-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 12rpx;
}
.type-row .type-name {
font-size: 28rpx;
color: #333;
}
.type-row .type-info {
font-size: 26rpx;
color: #666;
}
.modal-footer {
display: flex;
padding: 20rpx 30rpx 30rpx;
gap: 20rpx;
}
.btn-cancel,
.btn-confirm {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
border: none;
}
.btn-cancel {
background: #f5f5f5;
color: #666;
}
.btn-confirm {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
}
.btn-cancel::after,
.btn-confirm::after {
border: none;
}
</style>