Files
mwims-new-mini-program/pages/plugin/boxOut.vue
T

810 lines
18 KiB
Vue
Raw 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-header">
<view class="scan-label">周转箱列表</view>
<button class="scan-btn" @click="scanBox">
<text class="cuIcon-scan"></text>
<text class="btn-text">扫描箱子</text>
</button>
</view>
<!-- 周转箱列表 -->
<view class="box-list">
<view v-if="boxList.length === 0" class="empty-tip">
<text class="cuIcon-info"></text>
<text>请扫描周转箱二维码添加</text>
</view>
<view class="box-item" v-for="(item, index) in boxList" :key="index">
<view class="box-info">
<view class="box-code">{{item.code}}</view>
<view class="box-detail">
<text class="box-bags">{{item.bagCount}}</text>
<text class="box-weight">{{item.totalWeight}}kg</text>
</view>
</view>
<view class="box-delete" @click="removeBox(index)">
<text class="cuIcon-delete"></text>
</view>
</view>
</view>
</view>
<!-- 统计信息 -->
<view class="stats-section" v-if="boxList.length > 0">
<view class="stats-title">汇总统计</view>
<view class="stats-content">
<view class="stats-item">
<text class="stats-label">箱子数量</text>
<text class="stats-value">{{boxList.length}}</text>
</view>
<view class="stats-item">
<text class="stats-label">医废总袋数</text>
<text class="stats-value">{{totalBagCount}}</text>
</view>
<view class="stats-item">
<text class="stats-label">医废总重量</text>
<text class="stats-value highlight">{{totalWeight}}kg</text>
</view>
</view>
<view class="type-stats">
<view class="stats-title-sub">分类明细</view>
<view class="type-item" 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="footer">
<button class="submit-btn" :disabled="boxList.length === 0" @click="submitOut">
周转箱出库
</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">{{boxList.length}}</view>
</view>
<view class="confirm-section">
<view class="confirm-label">医废总袋数</view>
<view class="confirm-value">{{totalBagCount}}</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 v-if="Object.keys(typeStats).length === 0" class="no-type-tip">
暂无分类数据
</view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn-cancel" @click="closeModal">取消</button>
<button class="btn-confirm" @click="confirmOut">确认出库</button>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
boxList: [],
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: {
totalBagCount() {
return this.boxList.reduce((sum, item) => sum + (item.bagCount || 0), 0)
},
totalWeight() {
return this.boxList.reduce((sum, item) => sum + parseFloat(item.totalWeight || 0), 0).toFixed(2)
},
typeStats() {
const stats = {}
this.boxList.forEach(box => {
if (box.typeDetail) {
for (let type in box.typeDetail) {
if (!stats[type]) {
stats[type] = { count: 0, weight: 0 }
}
stats[type].count += box.typeDetail[type].count || 0
stats[type].weight += parseFloat(box.typeDetail[type].weight || 0)
}
}
})
// 格式化重量
for (let key in stats) {
stats[key].weight = parseFloat(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.handleScanCode(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.handleScanCode(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 = ''
},
// 统一处理扫码结果(防抖+去重)
handleScanCode(code) {
if (!code || this.isScanning) return
this.isScanning = true
setTimeout(() => { this.isScanning = false }, 500)
const exists = this.boxList.some(item => item.code === code)
if (exists) {
uni.showToast({ title: '该箱子已扫描', icon: 'none' })
return
}
this.getBoxInfo(code)
},
// 按钮扫码(备用)
scanBox() {
uni.scanCode({
success: (res) => this.handleScanCode(res.result),
fail: () => uni.showToast({ title: '扫描失败', icon: 'none' })
})
},
// 获取箱子信息(调用接口验证并获取箱子信息)
getBoxInfo(code) {
uni.showLoading({ title: '验证中...' })
uni.request({
url: 'https://lekapi.opmonitor.com/?c=app_api&a=box_verifyBox',
data: { code: code },
header: {
'Content-type': 'application/json'
},
success: (res) => {
uni.hideLoading()
console.log('箱子验证结果:', res.data)
// 处理 send_result 包装的情况
let resultData = res.data.data
console.log('resultData', resultData)
console.log('bagCount', resultData.bagCount, 'totalWeight', resultData.totalWeight)
// 如果被包装了一层(data里有err_msg),取内层data
if (resultData && resultData.err_msg) {
if (resultData.err_msg !== 'success') {
uni.showModal({
title: '提示',
content: resultData.msg || '周转箱编码不存在',
showCancel: false
})
return
}
resultData = resultData.data
}
// 检查箱子是否可用(available 为 false 或 undefined/null 时不可用)
if (resultData && resultData.available === false) {
uni.showModal({
title: '提示',
content: resultData.message || '该周转箱不可用',
showCancel: false
})
return
}
// 添加箱子到列表
this.boxList.push({
code: code,
bagCount: resultData.bagCount || 0,
totalWeight: resultData.totalWeight || '0',
typeDetail: resultData.typeDetail || {}
})
uni.showToast({
title: '添加成功',
icon: 'success'
})
},
fail: () => {
uni.hideLoading()
uni.showToast({
title: '网络错误',
icon: 'none'
})
}
})
},
// 删除箱子
removeBox(index) {
uni.showModal({
title: '提示',
content: '确定删除该周转箱?',
success: (res) => {
if (res.confirm) {
this.boxList.splice(index, 1)
}
}
})
},
// 提交出库
submitOut() {
if (this.boxList.length === 0) {
uni.showToast({
title: '请先扫描周转箱',
icon: 'none'
})
return
}
this.showModal = true
},
// 关闭弹窗
closeModal() {
this.showModal = false
},
// 确认出库(调用接口)
confirmOut() {
const params = {
boxList: this.boxList.map(box => box.code),
hospital: getApp().globalData.hospital,
account: getApp().globalData.name || '暂存点管理员'
}
console.log('出库参数:', params)
uni.showLoading({ title: '出库中...' })
uni.request({
url: 'https://lekapi.opmonitor.com/?c=app_api&a=boxOut',
method: 'POST',
data: params,
header: {
'Content-type': 'application/json'
},
success: (res) => {
uni.hideLoading()
console.log('出库结果:', res.data)
// 处理 send_result 包装的情况
let resultData = res.data
if (res.data.data && res.data.data.code) {
resultData = res.data.data
}
if (resultData.code === 200) {
uni.showToast({
title: '出库成功',
icon: 'success'
})
this.resetData()
this.showModal = false
} else {
uni.showModal({
title: '提示',
content: resultData.msg || '出库失败',
showCancel: false
})
}
},
fail: () => {
uni.hideLoading()
uni.showToast({
title: '网络错误',
icon: 'none'
})
}
})
},
// 重置数据
resetData() {
this.boxList = []
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-btn {
height: 88rpx;
background: linear-gradient(135deg, #722ed1 0%, #5317ad 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(114, 46, 209, 0.3);
}
.scan-btn::after {
border: none;
}
.scan-btn text {
font-size: 32rpx;
}
.btn-text {
font-size: 28rpx;
}
.box-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;
}
.box-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
.box-info {
flex: 1;
}
.box-code {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.box-detail {
display: flex;
align-items: center;
gap: 20rpx;
}
.box-bags {
font-size: 24rpx;
color: #722ed1;
padding: 4rpx 16rpx;
background: rgba(114, 46, 209, 0.1);
border-radius: 8rpx;
}
.box-weight {
font-size: 24rpx;
color: #52c41a;
}
.box-delete {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e54d42;
}
.box-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-title-sub {
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 2rpx solid #f0f0f0;
}
.stats-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.stats-item {
display: flex;
align-items: center;
}
.stats-label {
font-size: 26rpx;
color: #666;
width: 180rpx;
}
.stats-value {
font-size: 32rpx;
color: #333;
font-weight: 600;
}
.stats-value.highlight {
color: #722ed1;
}
.type-stats {
margin-top: 10rpx;
}
.type-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 12rpx;
}
.type-item .type-name {
font-size: 28rpx;
color: #333;
}
.type-item .type-info {
font-size: 26rpx;
color: #666;
}
.footer {
padding: 20rpx 40rpx 40rpx;
}
.submit-btn {
height: 96rpx;
background: linear-gradient(135deg, #722ed1 0%, #5317ad 100%);
color: #fff;
border: none;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 500;
box-shadow: 0 8rpx 24rpx rgba(114, 46, 209, 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: #722ed1;
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;
}
.no-type-tip {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 20rpx;
}
.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, #722ed1 0%, #5317ad 100%);
color: #fff;
}
.btn-cancel::after,
.btn-confirm::after {
border: none;
}
</style>