Files

903 lines
19 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>