1.1.1
This commit is contained in:
+2
-2
@@ -2,8 +2,8 @@
|
||||
"name" : "危废数据终端",
|
||||
"appid" : "__UNI__F18DD0A",
|
||||
"description" : "",
|
||||
"versionName" : "1.1.0",
|
||||
"versionCode" : 110,
|
||||
"versionName" : "1.1.1",
|
||||
"versionCode" : 111,
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
|
||||
+115
-13
@@ -29,12 +29,15 @@
|
||||
<!-- 名称 -->
|
||||
<text>{{ item.fwmc }}</text>
|
||||
<!-- 代码 -->
|
||||
<text>{{ item.fwdm }}</text>
|
||||
<!-- <text>{{ item.fwdm }}</text> -->
|
||||
<!-- 重量 单位 -->
|
||||
<text>{{ item.csl || item.rkl}}{{ item.jldw }}</text>
|
||||
<!-- 时间 -->
|
||||
<text>{{ item.cssj }}</text>
|
||||
</view>
|
||||
<view class="data-card-text">
|
||||
<text class="light">{{ item.szsbm }}</text>
|
||||
</view>
|
||||
<!-- 选择模式下显示右侧箭头 -->
|
||||
<view class="select-arrow" v-if="isSelectMode">
|
||||
<uni-icons type="right" size="16" color="#cccccc"></uni-icons>
|
||||
@@ -47,6 +50,22 @@
|
||||
<uni-load-more :contentText="{contentdown: '显示更多'}" v-if="dataList.length > 0" :status="loadMoreStatus" @clickLoadMore="handleLoadMore" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 操作选择弹窗:撤销 / 补打 -->
|
||||
<uni-popup ref="actionPopup" type="center">
|
||||
<view class="action-popup" v-if="selectedItem">
|
||||
<!-- <view class="action-popup-title">请选择操作</view> -->
|
||||
<view class="action-popup-info">
|
||||
<text class="action-popup-name">{{ selectedItem.fwmc }}</text>
|
||||
<text class="action-popup-name">{{ selectedItem.fwdm }}</text>
|
||||
<text class="action-popup-code">{{ selectedItem.szsbm }}</text>
|
||||
</view>
|
||||
<view class="action-popup-btns">
|
||||
<button class="btn btn-outline action-btn" @click="handleUndo">撤 销</button>
|
||||
<button class="btn btn-primary action-btn" @click="handleReprint">补 打</button>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -77,7 +96,9 @@ export default {
|
||||
/*手机高度*/
|
||||
phoneHeight: 0,
|
||||
/* 选择模式 */
|
||||
isSelectMode: false,
|
||||
isSelectMode: true,
|
||||
/* 弹窗选中的台账条目 */
|
||||
selectedItem: null,
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
@@ -163,19 +184,45 @@ export default {
|
||||
this.currentPage += 1
|
||||
this.loadData(true)
|
||||
},
|
||||
/** 选择模式下点击数据条目,将数据回传给上一页 */
|
||||
/** 点击列表项:选择模式下回传数据,普通模式弹出操作选择 */
|
||||
handleSelectItem(item) {
|
||||
if (!this.isSelectMode) return
|
||||
const pages = getCurrentPages()
|
||||
const prevPage = pages[pages.length - 2]
|
||||
if (prevPage) {
|
||||
// 将选中项存入上一页,由上一页根据需要提取字段
|
||||
prevPage.$vm.selectedItem = item
|
||||
prevPage.$vm.isSelected = true
|
||||
}
|
||||
uni.navigateBack()
|
||||
// 选择模式:将数据回传给上一页
|
||||
// if (this.isSelectMode) {
|
||||
// const pages = getCurrentPages()
|
||||
// const prevPage = pages[pages.length - 2]
|
||||
// if (prevPage) {
|
||||
// // 将选中项存入上一页,由上一页根据需要提取字段
|
||||
// prevPage.$vm.selectedItem = item
|
||||
// prevPage.$vm.isSelected = true
|
||||
// }
|
||||
// uni.navigateBack()
|
||||
// return
|
||||
// }
|
||||
// 普通模式:弹出撤销/补打选择弹窗
|
||||
this.selectedItem = item
|
||||
this.$refs.actionPopup.open()
|
||||
},
|
||||
handleScan() { uni.showToast({ title: '扫码查询', icon: 'none' }) }
|
||||
/** 撤销:跳转数据撤销页并携带数字识别码 */
|
||||
handleUndo() {
|
||||
const code = this.selectedItem ? this.selectedItem.szsbm : ''
|
||||
this.$refs.actionPopup.close()
|
||||
if (!code) {
|
||||
uni.showToast({ title: '该条数据无数字识别码,无法撤销', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/waste/undo?code=${encodeURIComponent(code)}` })
|
||||
},
|
||||
/** 补打:跳转标签补打页并携带数字识别码 */
|
||||
handleReprint() {
|
||||
const code = this.selectedItem ? this.selectedItem.szsbm : ''
|
||||
this.$refs.actionPopup.close()
|
||||
if (!code) {
|
||||
uni.showToast({ title: '该条数据无数字识别码,无法补打', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/waste/reprint?code=${encodeURIComponent(code)}` })
|
||||
},
|
||||
// handleScan() { uni.showToast({ title: '扫码查询', icon: 'none' }) }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -212,6 +259,9 @@ export default {
|
||||
.data-card-text{
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
.light{
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* 选择模式右侧箭头 */
|
||||
@@ -226,4 +276,56 @@ export default {
|
||||
text-align: center;
|
||||
padding: $spacing-md 0;
|
||||
}
|
||||
|
||||
/* 撤销/补打操作弹窗 */
|
||||
.action-popup {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-lg $spacing-md $spacing-md;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.action-popup-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.action-popup-info {
|
||||
width: 100%;
|
||||
margin-top: $spacing-md;
|
||||
padding: $spacing-base;
|
||||
// background: $bg-primary;
|
||||
// border-radius: $border-radius-sm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.action-popup-name {
|
||||
font-size: $font-size-md;
|
||||
color: $text-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.action-popup-code {
|
||||
margin-top: $spacing-xs;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.action-popup-btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: $spacing-base;
|
||||
margin-top: $spacing-lg;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">说明</text>
|
||||
<text class="value value-desc">{{ scanMode === 'uniapp' ? '使用默认摄像头扫码' : scanMode === 'idata' ? '仅支持 iData 设备内置扫码' : '通用 Android PDA 设备' }}
|
||||
<text class="value value-desc">{{ scanModeDesc }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -130,10 +130,22 @@ export default {
|
||||
scanModeText() {
|
||||
const map = {
|
||||
uniapp: '安卓设备',
|
||||
idata: 'IDATA PDA设备',
|
||||
android: '带扫码PDA设备'
|
||||
idata: 'IDATA设备',
|
||||
android: '通用扫码设备',
|
||||
pda: 'PDA设备'
|
||||
}
|
||||
return map[this.scanMode] || '安卓设备'
|
||||
},
|
||||
|
||||
// 扫码模式说明
|
||||
scanModeDesc() {
|
||||
const map = {
|
||||
uniapp: '使用默认摄像头扫码',
|
||||
idata: '仅支持 iData 设备内置扫码',
|
||||
android: '通用 Android PDA 设备',
|
||||
pda: '带物理扫码键 PDA 设备(hbyapi 广播协议)'
|
||||
}
|
||||
return map[this.scanMode] || '使用默认摄像头扫码'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
@@ -174,8 +186,9 @@ export default {
|
||||
// 使用对象数组,key:value 形式定义选项
|
||||
const modeOptions = [
|
||||
{ key: 'uniapp', label: '安卓设备' },
|
||||
{ key: 'idata', label: 'IDATA PDA设备' },
|
||||
{ key: 'android', label: '带扫码PDA设备' }
|
||||
{ key: 'idata', label: 'IDATA设备' },
|
||||
{ key: 'android', label: '通用扫码设备' },
|
||||
{ key: 'pda', label: 'PDA设备' }
|
||||
]
|
||||
|
||||
const res = await uni.showActionSheet({
|
||||
|
||||
@@ -294,9 +294,10 @@ export default {
|
||||
*/
|
||||
selectCommandType(type) {
|
||||
this.commandType = type
|
||||
printerService.setCommandType(type)
|
||||
// 切换指令类型时同步更新纸张尺寸
|
||||
printerService.setPaperSize(this.paperWidth, this.paperHeight)
|
||||
// 测试页临时切换指令类型,不覆盖正式配置
|
||||
printerService.setCommandTypeTemporary(type)
|
||||
// 切换指令类型时同步更新纸张尺寸(临时生效)
|
||||
printerService.setPaperSizeTemporary(this.paperWidth, this.paperHeight)
|
||||
},
|
||||
/**
|
||||
* 选择打印纸尺寸
|
||||
@@ -304,7 +305,8 @@ export default {
|
||||
selectPaperSize(width, height) {
|
||||
this.paperWidth = width
|
||||
this.paperHeight = height
|
||||
printerService.setPaperSize(width, height)
|
||||
// 测试页临时切换纸张尺寸,不覆盖正式配置
|
||||
printerService.setPaperSizeTemporary(width, height)
|
||||
},
|
||||
/**
|
||||
* 快捷模板
|
||||
@@ -325,29 +327,29 @@ export default {
|
||||
riqi: new Date().toISOString().split('T')[0],
|
||||
zhongliang: '10kg',
|
||||
beizhu: '测试备注',
|
||||
biaoshi: '',
|
||||
biaoshi: [1, 2, 3, 4],
|
||||
qrcode: 'https://www.cswm.org.cn/qr/?code=7G6MOYELnFHZfF1Y0F8e6gjxR2K2pALNJJNhJSuOg17LV6IZfuDWUnF2qC3Rsdejs&type=WFBQ'
|
||||
}
|
||||
// 确保 service 使用当前的 commandType 和 paperSize
|
||||
printerService.setCommandType(this.commandType)
|
||||
printerService.setPaperSize(this.paperWidth, this.paperHeight)
|
||||
// 确保 service 使用当前的 commandType 和 paperSize(临时生效,不覆盖配置)
|
||||
printerService.setCommandTypeTemporary(this.commandType)
|
||||
printerService.setPaperSizeTemporary(this.paperWidth, this.paperHeight)
|
||||
this.printContent = printerService.generateWasteLabel(testData)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'qrcode') {
|
||||
printerService.setCommandType(this.commandType)
|
||||
printerService.setPaperSize(this.paperWidth, this.paperHeight)
|
||||
if (type === 'qrcode') {
|
||||
printerService.setCommandTypeTemporary(this.commandType)
|
||||
printerService.setPaperSizeTemporary(this.paperWidth, this.paperHeight)
|
||||
this.printContent = printerService.generateQRCodeTemplate('https://www.example.com')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'barcode') {
|
||||
printerService.setCommandType(this.commandType)
|
||||
printerService.setPaperSize(this.paperWidth, this.paperHeight)
|
||||
if (type === 'barcode') {
|
||||
printerService.setCommandTypeTemporary(this.commandType)
|
||||
printerService.setPaperSizeTemporary(this.paperWidth, this.paperHeight)
|
||||
this.printContent = printerService.generateBarcodeTemplate('1234567890')
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 清空打印内容
|
||||
@@ -367,9 +369,9 @@ export default {
|
||||
|
||||
this.isPrinting = true
|
||||
try {
|
||||
// 确保打印前同步配置
|
||||
printerService.setCommandType(this.commandType)
|
||||
printerService.setPaperSize(this.paperWidth, this.paperHeight)
|
||||
// 确保打印前同步配置(测试页临时生效,不覆盖正式配置)
|
||||
printerService.setCommandTypeTemporary(this.commandType)
|
||||
printerService.setPaperSizeTemporary(this.paperWidth, this.paperHeight)
|
||||
await printerService.printText(this.printContent)
|
||||
uni.showToast({ title: '打印成功', icon: 'success' })
|
||||
} catch (err) {
|
||||
|
||||
+42
-15
@@ -35,20 +35,37 @@
|
||||
<view class="data-list" v-if="selectedTypeCorp">
|
||||
<view class="data-card">
|
||||
<view class="data-card-header"><text class="data-card-title">已选处置公司</text></view>
|
||||
<view class="data-card-row"><text>处置公司:</text><text class="val">{{ selectedTypeCorp.jydwmc || '-' }}</text></view>
|
||||
<view class="data-card-row"><text>废物名称:</text><text class="val">{{ selectedTypeCorp.fwmc || '-' }}</text></view>
|
||||
<view class="data-card-row"><text>废物代码:</text><text class="val">{{ selectedTypeCorp.fwdm || '-' }}</text></view>
|
||||
<view class="data-card-row">
|
||||
<view class="label">处置公司:</view>
|
||||
<text class="val">{{ selectedTypeCorp.jydwmc || '-' }}</text></view>
|
||||
<view class="data-card-row">
|
||||
<view class="label">废物名称:</view>
|
||||
<text class="val">{{ selectedTypeCorp.fwmc || '-' }}</text></view>
|
||||
<view class="data-card-row">
|
||||
<view class="label">废物代码:</view>
|
||||
<text class="val">{{ selectedTypeCorp.fwdm || '-' }}</text></view>
|
||||
</view>
|
||||
<!-- type_corp_info 返回的信息 -->
|
||||
<view class="data-card" v-if="typeCorpInfoData.summary_lines && typeCorpInfoData.summary_lines.length">
|
||||
<view class="data-card">
|
||||
<view class="data-card-header" @click="summaryExpanded = !summaryExpanded">
|
||||
<text class="data-card-title">汇总信息</text>
|
||||
<text class="data-card-toggle">{{ summaryExpanded ? '收起 ▲' : '更多 ▼' }}</text>
|
||||
</view>
|
||||
<view class="data-card-body" :class="{ collapsed: !summaryExpanded }">
|
||||
<view class="data-card-row" v-for="(line, i) in typeCorpInfoData.summary_lines" :key="'s'+i">
|
||||
<text class="val summary-text">{{ line }}</text>
|
||||
<view class="data-card-body">
|
||||
<view class="data-card-row">
|
||||
<text class="val summary-text">总条数: {{ typeCorpInfoData.total_items }} 总重量: {{ typeCorpInfoData.total_weight }}千克</text>
|
||||
</view>
|
||||
<template v-if="summaryExpanded">
|
||||
<view class="data-card-row" v-for="(group, gi) in typeCorpInfoData.waste_groups" :key="'g'+gi">
|
||||
<text>
|
||||
{{ group.waste_name }}
|
||||
{{ group.waste_count }}件
|
||||
</text>
|
||||
<text class="val">
|
||||
{{ group.total_weight }} {{ group.rows[0].unit }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<view class="data-card" v-if="typeCorpInfoData.receiver_company || typeCorpInfoData.transport_company">
|
||||
@@ -87,16 +104,19 @@
|
||||
</view>
|
||||
<view class="data-card-row">
|
||||
<text>转运数量:</text>
|
||||
<text class="val">{{ item.zysl }}</text><text class="val">{{ item.jldw || '' }}</text>
|
||||
<text class="val">{{ item.zysl }}{{ item.jldw || '' }}</text>
|
||||
</view>
|
||||
<view class="data-card-row">
|
||||
<text>接收数量:</text><text class="val">{{ item.jssl }}</text><text class="val">{{ item.jldw || '' }}</text>
|
||||
<text>接收数量:</text>
|
||||
<text class="val">{{ item.jssl }}{{ item.jldw || '' }}</text>
|
||||
</view>
|
||||
<view class="data-card-row">
|
||||
<text>危废运出时间:</text><text class="val">{{ item.wfycsj || '-' }}</text>
|
||||
<text>危废运出时间:</text>
|
||||
<text class="val">{{ item.wfycsj || '-' }}</text>
|
||||
</view>
|
||||
<view class="data-card-row">
|
||||
<text>办结时间:</text><text class="val">{{ item.bjsj || '-' }}</text>
|
||||
<text>办结时间:</text>
|
||||
<text class="val">{{ item.bjsj || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -105,6 +125,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { api } from '@/api/index'
|
||||
export default {
|
||||
data() {
|
||||
@@ -211,8 +232,10 @@ export default {
|
||||
} else {
|
||||
this.typeCorpInfoData = {}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('[联单] typeCorpInfo 加载失败:', e)
|
||||
// console.error('[联单] typeCorpInfo 加载失败:', e)
|
||||
uni.showToast({ title: '加载处置公司详情失败,'+e.message, icon: 'none' })
|
||||
this.typeCorpInfoData = {}
|
||||
}
|
||||
},
|
||||
@@ -331,9 +354,6 @@ export default {
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
.data-card-body.collapsed .data-card-row:nth-child(n+4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.data-card-tag {
|
||||
padding: 2px 8px;
|
||||
@@ -350,4 +370,11 @@ export default {
|
||||
border: 1px solid #ffa39e;
|
||||
}
|
||||
}
|
||||
|
||||
.data-card-row {
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+40
-3
@@ -82,6 +82,13 @@ export default {
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 从台账详情跳转:携带数字识别码,自动查询
|
||||
if (options && options.code) {
|
||||
this.form.code = decodeURIComponent(options.code)
|
||||
this.handleQuery()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 扫码结果回调:填充数字识别码 -> 自动查询 */
|
||||
onBarcodeScanned(barcode) {
|
||||
@@ -119,13 +126,17 @@ export default {
|
||||
uni.showToast({ title: '打印中...', icon: 'none' })
|
||||
try {
|
||||
await printerService.init()
|
||||
await printerService.autoConnect()
|
||||
const connected = await printerService.autoConnect()
|
||||
if (!connected) {
|
||||
// 真实失败原因已在 printerService 内 console.error 输出
|
||||
throw new Error('打印机连接失败,请检查设备蓝牙是否开启或已配对')
|
||||
}
|
||||
await printerService.printWasteLabel(printData)
|
||||
await this.handleDisconnect()
|
||||
uni.showToast({ title: '打印成功', icon: 'success' })
|
||||
} catch (error) {
|
||||
console.error('[补打] 打印失败:', error)
|
||||
uni.showToast({ title: '打印失败: ' + error, icon: 'none' })
|
||||
uni.showToast({ title: '打印失败: ' + error.message, icon: 'none' })
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
@@ -142,6 +153,32 @@ export default {
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
/**
|
||||
* 解析危废危险特性,返回标识点位置编号数组
|
||||
* 固定映射:1=腐蚀性(左上)、2=毒性(右上)、3=易燃性(左下)、4=反应性(右下)
|
||||
* 感染性不占位
|
||||
* @param {Object} data 危废信息对象(危险特性字段 hazardous_properties 或 wxtx,逗号分隔)
|
||||
* @returns {number[]} 位置编号数组,如 [1,2,3,4]
|
||||
*/
|
||||
processBiaoshi(data) {
|
||||
const raw = (data && (data.hazardous_properties || data.wxtx)) || '';
|
||||
if (!raw) return [];
|
||||
const properties = raw.split(',').map(s => s.trim()).filter(Boolean);
|
||||
const PROPERTY_TO_POSITION = {
|
||||
'腐蚀性': 1,
|
||||
'毒性': 2,
|
||||
'易燃性': 3,
|
||||
'反应性': 4
|
||||
};
|
||||
const positions = [];
|
||||
properties.forEach(prop => {
|
||||
const pos = PROPERTY_TO_POSITION[prop];
|
||||
if (pos !== undefined && !positions.includes(pos)) {
|
||||
positions.push(pos);
|
||||
}
|
||||
});
|
||||
return positions;
|
||||
},
|
||||
printData() {
|
||||
const printData = {
|
||||
'mingcheng': this.previewData.fwmc || '', // 危废名称
|
||||
@@ -157,7 +194,7 @@ export default {
|
||||
'riqi': (this.previewData.cssj || '').split(' ')[0], // 产生时间(仅日期)
|
||||
'zhongliang': (this.previewData.csl || '') + ' ' + (this.previewData.jldw || ''), // 重量
|
||||
'beizhu': this.previewData.wxtx || '-', // 备注 危险特征
|
||||
'biaoshi': '', // 标识
|
||||
'biaoshi': this.processBiaoshi(this.previewData), // 标识(危险特性点位)
|
||||
'qrcode': this.previewData.wxfwbqewm || '', // 二维码
|
||||
'lianxidianhua': this.previewData.lxrsj || '', // 联系电话
|
||||
'rongqileixing': this.previewData.rqlx || '', // 容器类型
|
||||
|
||||
@@ -57,6 +57,13 @@ export default {
|
||||
recordData: {}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 从台账详情跳转:携带数字识别码,自动查询
|
||||
if (options && options.code) {
|
||||
this.form.code = decodeURIComponent(options.code)
|
||||
this.handleQuery()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canUndo() {
|
||||
return this.recordData.list && this.recordData.list.length && this.form.reason
|
||||
|
||||
@@ -341,7 +341,6 @@ export default {
|
||||
|
||||
if (res.code == 1000) {
|
||||
uni.showToast({ title: '登记成功', icon: 'success' })
|
||||
// const data = res.data
|
||||
const hwInfo = this.hwInfo
|
||||
const result = res.data
|
||||
const printData = {
|
||||
@@ -353,12 +352,12 @@ export default {
|
||||
'youhaichengfen': hwInfo.hazardous_components || '-', // 有害成分
|
||||
'zhuyi': hwInfo.main_properties || '-', // 注意事项
|
||||
'shibiema': result.digital_id, // 数字识别码
|
||||
'danwei': hwInfo.unit, // 单位
|
||||
'danwei': hwInfo.enterprise_name || '-', // 单位名称
|
||||
'lianxi': '', // 联系人
|
||||
'riqi': getDate(), // 日期
|
||||
'zhongliang': submitData.weight, // 重量
|
||||
'zhongliang': submitData.weight + hwInfo.unit, // 重量
|
||||
'beizhu': result.produce_ledger_no || '-', // 备注 台账编号,包装类型
|
||||
'biaoshi': '', // 标识
|
||||
'biaoshi': this.processBiaoshi(hwInfo), // 标识
|
||||
'qrcode': result.label_qrcode // 二维码
|
||||
}
|
||||
this.currentPrintData = printData
|
||||
@@ -367,6 +366,32 @@ export default {
|
||||
})
|
||||
|
||||
},
|
||||
/**
|
||||
* 解析危废危险特性,返回标识点位置编号数组
|
||||
* 固定映射:1=腐蚀性(左上)、2=毒性(右上)、3=易燃性(左下)、4=反应性(右下)
|
||||
* 感染性不占位
|
||||
* @param {Object} hwInfo 危废信息对象(接口返回 hazardous_properties 逗号分隔字符串)
|
||||
* @returns {number[]} 位置编号数组,如 [1,2,3,4]
|
||||
*/
|
||||
processBiaoshi(hwInfo) {
|
||||
const raw = (hwInfo && hwInfo.hazardous_properties) || '';
|
||||
if (!raw) return [];
|
||||
const properties = raw.split(',').map(s => s.trim()).filter(Boolean);
|
||||
const PROPERTY_TO_POSITION = {
|
||||
'腐蚀性': 1,
|
||||
'毒性': 2,
|
||||
'易燃性': 3,
|
||||
'反应性': 4
|
||||
};
|
||||
const positions = [];
|
||||
properties.forEach(prop => {
|
||||
const pos = PROPERTY_TO_POSITION[prop];
|
||||
if (pos !== undefined && !positions.includes(pos)) {
|
||||
positions.push(pos);
|
||||
}
|
||||
});
|
||||
return positions;
|
||||
},
|
||||
async doPrint() {
|
||||
|
||||
// const printData = {
|
||||
|
||||
@@ -125,9 +125,13 @@ export default {
|
||||
// 更新纸张尺寸
|
||||
async updatePaperSize({ dispatch }, { width, height }) {
|
||||
try {
|
||||
const presetKey = `${width}x${height}`;
|
||||
// 预设尺寸同步记录 currentPreset,保证补打/生成页布局正确
|
||||
const currentPreset = ['100x100', '150x150', '200x200'].includes(presetKey) ? presetKey : null;
|
||||
await dispatch('savePrinterConfig', {
|
||||
paperWidth: width,
|
||||
paperHeight: height,
|
||||
currentPreset,
|
||||
lastUpdated: new Date().toISOString()
|
||||
});
|
||||
return true;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* 扫码模式配置 Store 模块
|
||||
* 管理 安卓设备 / IDATA PDA / 带扫码PDA 三种模式
|
||||
* 管理 安卓设备 / IDATA PDA / 带扫码PDA / 带物理扫码键PDA 四种模式
|
||||
* 默认:uniapp(安卓设备)
|
||||
*/
|
||||
export default {
|
||||
namespaced: true,
|
||||
|
||||
state: {
|
||||
// 扫码模式:'uniapp' | 'idata' | 'android'
|
||||
// 扫码模式:'uniapp' | 'idata' | 'android' | 'pda'
|
||||
scanMode: 'uniapp'
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
initScanMode({ commit }) {
|
||||
try {
|
||||
const saved = uni.getStorageSync('scanMode')
|
||||
if (saved && ['uniapp', 'idata', 'android'].includes(saved)) {
|
||||
if (saved && ['uniapp', 'idata', 'android', 'pda'].includes(saved)) {
|
||||
commit('SET_SCAN_MODE', saved)
|
||||
console.log('[Scan Store] 恢复扫码模式:', saved)
|
||||
} else {
|
||||
@@ -39,10 +39,10 @@ export default {
|
||||
|
||||
/**
|
||||
* 设置扫码模式并持久化
|
||||
* @param {string} mode - 'uniapp' | 'idata' | 'android'
|
||||
* @param {string} mode - 'uniapp' | 'idata' | 'android' | 'pda'
|
||||
*/
|
||||
setScanMode({ commit }, mode) {
|
||||
if (!['uniapp', 'idata', 'android'].includes(mode)) {
|
||||
if (!['uniapp', 'idata', 'android', 'pda'].includes(mode)) {
|
||||
console.error('[Scan Store] 无效的模式:', mode)
|
||||
return
|
||||
}
|
||||
@@ -56,6 +56,7 @@ export default {
|
||||
scanMode: state => state.scanMode,
|
||||
isUniapp: state => state.scanMode === 'uniapp',
|
||||
isIdata: state => state.scanMode === 'idata',
|
||||
isAndroid: state => state.scanMode === 'android'
|
||||
isAndroid: state => state.scanMode === 'android',
|
||||
isPda: state => state.scanMode === 'pda'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,22 +49,30 @@ const PAPER_PRESETS = {
|
||||
pageWidth: 800, // 100mm @200dpi = 800 点
|
||||
gapSense: true, // 自动检测标签间隙,避免固定 GAP 值不匹配
|
||||
fields: [
|
||||
{ x: 192, y: 100 }, // 名称
|
||||
{ x: 192, y: 150 }, // 类别
|
||||
{ x: 192, y: 200 }, // 代码
|
||||
{ x: 480, y: 200 }, // 形态
|
||||
{ x: 192, y: 250 }, // 主要成分
|
||||
{ x: 192, y: 350 }, // 有害成分
|
||||
{ x: 192, y: 448 }, // 注意
|
||||
{ x: 216, y: 536 }, // 识别码
|
||||
{ x: 256, y: 584 }, // 单位
|
||||
{ x: 288, y: 632 }, // 联系
|
||||
{ x: 192, y: 680 }, // 日期
|
||||
{ x: 480, y: 680 }, // 重量
|
||||
{ x: 150, y: 728 }, // 备注
|
||||
{ x: 480, y: 448 } // 标识
|
||||
{ x: 192, y: 140 }, // 名称(识别码以上,下2mm)
|
||||
{ x: 192, y: 190 }, // 类别(识别码以上,下2mm)
|
||||
{ x: 192, y: 240 }, // 代码(识别码以上,下2mm)
|
||||
{ x: 480, y: 240 }, // 形态(识别码以上,下2mm)
|
||||
{ x: 192, y: 290 }, // 主要成分(识别码以上,下2mm)
|
||||
{ x: 192, y: 390 }, // 有害成分(识别码以上,下2mm)
|
||||
{ x: 192, y: 488 }, // 注意(识别码以上,下2mm)
|
||||
{ x: 216, y: 576 }, // 识别码(含,下2mm)
|
||||
{ x: 256, y: 624 }, // 单位(识别码以下,下1mm)
|
||||
{ x: 288, y: 672 }, // 联系(识别码以下,下1mm)
|
||||
{ x: 192, y: 720 }, // 日期(识别码以下,下1mm)
|
||||
{ x: 480, y: 720 }, // 重量(识别码以下,下1mm)
|
||||
{ x: 150, y: 768 } // 备注(识别码以下,下1mm)
|
||||
],
|
||||
qrCode: { x: 624, y: 584, size: 4 }
|
||||
// 标识点:正方形四角布局(1=左上、2=右上、3=左下、4=右下),无中心点
|
||||
// 由 data.biaoshi 传入位置编号数组驱动,使用 √ 字符绘制
|
||||
biaoshiPositions: {
|
||||
1: { x: 608, y: 250 }, // 向左 2mm(16点)
|
||||
2: { x: 704, y: 250 },
|
||||
3: { x: 608, y: 410 }, // 向左 2mm(16点),向下 2mm(40点)
|
||||
4: { x: 704, y: 410 } // 向下 2mm(40点)
|
||||
},
|
||||
biaoshiMag: [2, 2],
|
||||
qrCode: { x: 604, y: 624, size: 4 } // 向左 1mm(20点),向上 1mm(20点)
|
||||
},
|
||||
tspl: {
|
||||
startX: 20,
|
||||
@@ -91,14 +99,21 @@ const PAPER_PRESETS = {
|
||||
{ x: 288, y: 399, mag: [2, 2], bold: 0.5, font: 0 }, // 主要成分(150:375+24,加粗)
|
||||
{ x: 288, y: 549, mag: [2, 2], bold: 0.5, font: 0 }, // 有害成分(150:525+24,加粗)
|
||||
{ x: 288, y: 696, mag: [2, 2], bold: 0.0, font: 1 }, // 注意(150:672+24)
|
||||
{ x: 308, y: 836, mag: [2, 2], bold: 0.0, font: 1 }, // 识别码(注意以下:x-16 左2mm, y+8 下1mm)
|
||||
{ x: 308, y: 836, mag: [1, 1], bold: 0.0, font: 1 }, // 识别码(注意以下:x-16 左2mm, y+8 下1mm)
|
||||
{ x: 368, y: 908, mag: [2, 2], bold: 0.0, font: 1 }, // 单位(注意以下)
|
||||
{ x: 416, y: 980, mag: [2, 2], bold: 0.0, font: 1 }, // 联系(注意以下)
|
||||
{ x: 272, y: 1052, mag: [2, 2], bold: 0.5, font: 0 }, // 日期(注意以下,加粗)
|
||||
{ x: 704, y: 1052, mag: [2, 2], bold: 0.0, font: 1 }, // 重量(注意以下)
|
||||
{ x: 209, y: 1124, mag: [2, 2], bold: 0.0, font: 1 }, // 备注(注意以下)
|
||||
{ x: 720, y: 549, mag: [4, 4], bold: 0.0, font: 1 } // 标识(150:525+24,4 倍放大)
|
||||
{ x: 209, y: 1124, mag: [2, 2], bold: 0.0, font: 1 } // 备注(注意以下)
|
||||
],
|
||||
// 标识点:正方形四角布局(1=左上、2=右上、3=左下、4=右下),无中心点
|
||||
biaoshiPositions: {
|
||||
1: { x: 904, y: 399 }, // 左上(累计左移 4mm / 32点)
|
||||
2: { x: 1064, y: 399 }, // 右上(累计左移 4mm / 32点)
|
||||
3: { x: 904, y: 551 }, // 左下(累计左移 4mm + 下移 4mm / 各32点)
|
||||
4: { x: 1064, y: 551 } // 右下(累计左移 4mm + 下移 4mm / 各32点)
|
||||
},
|
||||
biaoshiMag: [4, 4],
|
||||
qrCode: { x: 936, y: 900, size: 4 } // 位置用 150 原坐标(876+24),size 4 不越界
|
||||
},
|
||||
tspl: {
|
||||
@@ -128,9 +143,16 @@ const PAPER_PRESETS = {
|
||||
{ x: 552, y: 1328, mag: [2, 2], bold: 0.0, font: 1 }, // 联系
|
||||
{ x: 352, y: 1432, mag: [2, 2], bold: 0.5, font: 0 }, // 日期(加粗)
|
||||
{ x: 912, y: 1432, mag: [2, 2], bold: 0.0, font: 1 }, // 重量
|
||||
{ x: 240, y: 1528, mag: [2, 2], bold: 0.0, font: 1 }, // 备注
|
||||
{ x: 1432, y: 544, mag: [4, 4], bold: 0.0, font: 1 } // 标识(√ 勾选,4 倍放大)
|
||||
{ x: 240, y: 1528, mag: [2, 2], bold: 0.0, font: 1 } // 备注
|
||||
],
|
||||
// 标识点:正方形四角布局(1=左上、2=右上、3=左下、4=右下),无中心点
|
||||
biaoshiPositions: {
|
||||
1: { x: 1220, y: 544 },
|
||||
2: { x: 1460, y: 544 },
|
||||
3: { x: 1220, y: 724 },
|
||||
4: { x: 1460, y: 724 }
|
||||
},
|
||||
biaoshiMag: [4, 4],
|
||||
qrCode: { x: 1220, y: 1224, size: 8, prefix: 'BARCODE QR' }
|
||||
},
|
||||
tspl: {
|
||||
@@ -190,6 +212,13 @@ class BluetoothPrinterService {
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
// 10001 表示蓝牙适配器已打开,视为成功(部分机型重复 open 会返回该码)
|
||||
if (err && err.errCode === 10001) {
|
||||
this.adapterOpen = true;
|
||||
this._registerConnectionListener();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
console.error('蓝牙初始化失败', err);
|
||||
reject(err);
|
||||
}
|
||||
@@ -328,122 +357,90 @@ class BluetoothPrinterService {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _connect(deviceId, savedServiceId = '', savedWriteCharId = '') {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// 首先尝试使用已保存的服务配置
|
||||
if (savedServiceId && savedWriteCharId) {
|
||||
try {
|
||||
console.log('尝试使用已保存的服务配置:', { savedServiceId, savedWriteCharId });
|
||||
|
||||
// 建立连接
|
||||
await new Promise((res, rej) => {
|
||||
uni.createBLEConnection({
|
||||
deviceId,
|
||||
timeout: 15000,
|
||||
success: res,
|
||||
fail: rej
|
||||
});
|
||||
});
|
||||
|
||||
// MTU 协商(蓝牙 5.0 理论最大 512,实际由设备/系统决定)
|
||||
try {
|
||||
const mtuRes = await new Promise((res) => {
|
||||
uni.setBLEMTU({
|
||||
deviceId,
|
||||
mtu: 512,
|
||||
success: res,
|
||||
fail: () => res({ mtu: 23 })
|
||||
});
|
||||
});
|
||||
this._mtu = (mtuRes.mtu || 512) - 3;
|
||||
console.log(`[打印] MTU 协商: 请求 512, 实际=${mtuRes.mtu}, 写入块=${this._mtu}`);
|
||||
} catch (e) {
|
||||
console.warn('[打印] MTU 协商异常, 使用默认值 20', e);
|
||||
this._mtu = 20;
|
||||
}
|
||||
|
||||
// 等待服务发现(像 configPrinter 一样)
|
||||
await this._delay(800);
|
||||
|
||||
// ✅ 直接使用已保存的配置,不做任何验证
|
||||
// 原因:调用 getBLEDeviceCharacteristics 后该特征值会变得不可写(10007)
|
||||
console.log('使用已保存配置连接成功');
|
||||
this.deviceId = deviceId;
|
||||
this.serviceId = savedServiceId;
|
||||
this.writeCharId = savedWriteCharId;
|
||||
|
||||
// 更新 store
|
||||
this._saveConfig({
|
||||
deviceId,
|
||||
serviceId: savedServiceId,
|
||||
writeCharId: savedWriteCharId
|
||||
});
|
||||
|
||||
resolve();
|
||||
return;
|
||||
} catch (err) {
|
||||
// 连接失败,回退到自动发现
|
||||
console.warn('使用已保存配置连接失败,回退到自动发现:', err);
|
||||
await this._disconnectDevice(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动发现服务逻辑(保底方案)
|
||||
// 建立 BLE 连接(与 configPrinter 一致,只用 deviceId 直连,秒连)
|
||||
await new Promise((res, rej) => {
|
||||
uni.createBLEConnection({
|
||||
deviceId,
|
||||
timeout: 15000,
|
||||
success: async () => {
|
||||
// 等待服务发现
|
||||
await this._delay(800);
|
||||
|
||||
// 获取服务列表
|
||||
const { services } = await this._getServices(deviceId);
|
||||
if (!services || services.length === 0) {
|
||||
reject(new Error('未找到蓝牙服务'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找第一个有可写特征值的服务
|
||||
let foundService = null;
|
||||
let writeChar = null;
|
||||
|
||||
for (const service of services) {
|
||||
try {
|
||||
const { characteristics } = await this._getCharacteristics(deviceId, service.uuid);
|
||||
const writableChar = characteristics.find(c =>
|
||||
c.properties.write || c.properties.writeNoResponse
|
||||
);
|
||||
if (writableChar) {
|
||||
foundService = service;
|
||||
writeChar = writableChar;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 跳过无效服务
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!writeChar) {
|
||||
reject(new Error('未找到可写入的特征值'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.deviceId = deviceId;
|
||||
this.serviceId = foundService.uuid;
|
||||
this.writeCharId = writeChar.uuid;
|
||||
|
||||
// 保存配置
|
||||
this._saveConfig({
|
||||
deviceId,
|
||||
serviceId: foundService.uuid,
|
||||
writeCharId: writeChar.uuid
|
||||
});
|
||||
|
||||
resolve();
|
||||
},
|
||||
fail: reject
|
||||
success: res,
|
||||
fail: rej
|
||||
});
|
||||
});
|
||||
|
||||
// MTU 协商(蓝牙 5.0 理论最大 512,实际由设备/系统决定)
|
||||
try {
|
||||
const mtuRes = await new Promise((res) => {
|
||||
uni.setBLEMTU({
|
||||
deviceId,
|
||||
mtu: 512,
|
||||
success: res,
|
||||
fail: () => res({ mtu: 23 })
|
||||
});
|
||||
});
|
||||
this._mtu = (mtuRes.mtu || 512) - 3;
|
||||
console.log(`[打印] MTU 协商: 请求 512, 实际=${mtuRes.mtu}, 写入块=${this._mtu}`);
|
||||
} catch (e) {
|
||||
console.warn('[打印] MTU 协商异常, 使用默认值 20', e);
|
||||
this._mtu = 20;
|
||||
}
|
||||
|
||||
// 等待服务发现(像 configPrinter 一样)
|
||||
await this._delay(800);
|
||||
|
||||
// ✅ 直接使用已保存的配置,不做任何验证
|
||||
// 原因:调用 getBLEDeviceCharacteristics 后该特征值会变得不可写(10007)
|
||||
// 若未提供已保存配置,回退到自动发现
|
||||
if (savedServiceId && savedWriteCharId) {
|
||||
console.log('使用已保存配置连接成功:', { savedServiceId, savedWriteCharId });
|
||||
this.deviceId = deviceId;
|
||||
this.serviceId = savedServiceId;
|
||||
this.writeCharId = savedWriteCharId;
|
||||
|
||||
this._saveConfig({
|
||||
deviceId,
|
||||
serviceId: savedServiceId,
|
||||
writeCharId: savedWriteCharId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('未提供已保存配置,回退到自动发现');
|
||||
const { services } = await this._getServices(deviceId);
|
||||
if (!services || services.length === 0) {
|
||||
throw new Error('未找到蓝牙服务');
|
||||
}
|
||||
|
||||
let foundService = null;
|
||||
let writeChar = null;
|
||||
for (const service of services) {
|
||||
try {
|
||||
const { characteristics } = await this._getCharacteristics(deviceId, service.uuid);
|
||||
const writableChar = characteristics.find(c =>
|
||||
c.properties.write || c.properties.writeNoResponse
|
||||
);
|
||||
if (writableChar) {
|
||||
foundService = service;
|
||||
writeChar = writableChar;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!writeChar) {
|
||||
throw new Error('未找到可写入的特征值');
|
||||
}
|
||||
|
||||
this.deviceId = deviceId;
|
||||
this.serviceId = foundService.uuid;
|
||||
this.writeCharId = writeChar.uuid;
|
||||
|
||||
this._saveConfig({
|
||||
deviceId,
|
||||
serviceId: foundService.uuid,
|
||||
writeCharId: writeChar.uuid
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -567,12 +564,7 @@ class BluetoothPrinterService {
|
||||
try {
|
||||
await this._writeData(blockBuffer);
|
||||
} catch (err) {
|
||||
const isLastBlock = i >= lines.length;
|
||||
if (isLastBlock) {
|
||||
// 尾块容错:与 _writeData 保持一致,最后一个块写入失败不抛异常
|
||||
console.warn(`[打印] QR 块(最后一个块)发送失败,视为尾包处理:`, err);
|
||||
return;
|
||||
}
|
||||
// QR 块是打印指令的组成数据,发送失败会导致整页数据不完整,必须报错
|
||||
console.error(`[打印] QR 块发送失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
@@ -580,18 +572,35 @@ class BluetoothPrinterService {
|
||||
// 普通行,逐行发送
|
||||
const lineStr = line + '\n';
|
||||
const buffer = this._stringToBuffer(lineStr);
|
||||
const isCriticalLine = trimmed === 'PRINT' || trimmed === 'FORM';
|
||||
|
||||
try {
|
||||
await this._writeData(buffer);
|
||||
} catch (err) {
|
||||
const isLastLine = i === lines.length - 1;
|
||||
if (isLastLine) {
|
||||
// 尾行容错:与 _writeData 保持一致,最后一行写入失败不抛异常
|
||||
console.warn(`[打印] 第 ${i + 1} 行(最后一行)发送失败,视为尾包处理:`, err);
|
||||
return;
|
||||
if (isCriticalLine) {
|
||||
// 关键打印指令:最多重试 3 次,不容忍失败
|
||||
let delivered = false;
|
||||
for (let retry = 0; retry < 3; retry++) {
|
||||
try {
|
||||
await this._writeData(buffer);
|
||||
delivered = true;
|
||||
break;
|
||||
} catch (writeErr) {
|
||||
if (retry < 2) {
|
||||
const wait = 300 + retry * 300; // 300, 600ms
|
||||
console.warn(`[打印] 关键行 "${trimmed}" 写入失败,${wait}ms后重试(${retry + 1}/3):`, writeErr.message || writeErr);
|
||||
await this._delay(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!delivered) {
|
||||
throw new Error(`关键打印指令 "${trimmed}" 发送失败,打印机未收到执行命令,请重试打印`);
|
||||
}
|
||||
} else {
|
||||
// 普通行:任意一行发送失败都视为打印数据不完整,必须报错
|
||||
try {
|
||||
await this._writeData(buffer);
|
||||
} catch (err) {
|
||||
console.error(`[打印] 第 ${i + 1} 行发送失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
console.error(`[打印] 第 ${i + 1} 行发送失败:`, err);
|
||||
throw err;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
@@ -611,6 +620,9 @@ class BluetoothPrinterService {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async printWasteLabel(data) {
|
||||
// 正式打印入口:强制从 store 加载正式配置(含纸张尺寸/currentPreset/指令类型),
|
||||
// 避免打印测试页的临时设置(纸张/指令类型仅在内存生效)泄漏到正式打印
|
||||
this._loadConfig();
|
||||
const command = this.generateWasteLabel(data);
|
||||
return this.printText(command);
|
||||
}
|
||||
@@ -672,14 +684,14 @@ class BluetoothPrinterService {
|
||||
'mingcheng', 'leibie', 'daima', 'xingtai',
|
||||
'zhuyaochengfen', 'youhaichengfen', 'zhuyi',
|
||||
'shibiema', 'danwei', 'lianxi', 'riqi',
|
||||
'zhongliang', 'beizhu', 'biaoshi'
|
||||
'zhongliang', 'beizhu'
|
||||
];
|
||||
|
||||
const fieldLabels = {
|
||||
mingcheng: '名称', leibie: '类别', daima: '代码', xingtai: '形态',
|
||||
zhuyaochengfen: '主要成分', youhaichengfen: '有害成分', zhuyi: '注意',
|
||||
shibiema: '识别码', danwei: '单位', lianxi: '联系', riqi: '日期',
|
||||
zhongliang: '重量', beizhu: '备注', biaoshi: '●'
|
||||
zhongliang: '重量', beizhu: '备注'
|
||||
};
|
||||
|
||||
// 优先使用预设布局,否则使用默认配置
|
||||
@@ -688,8 +700,7 @@ class BluetoothPrinterService {
|
||||
{ x: 192, y: 100 }, { x: 192, y: 150 }, { x: 192, y: 200 }, { x: 480, y: 200 },
|
||||
{ x: 192, y: 250 }, { x: 192, y: 350 }, { x: 192, y: 448 },
|
||||
{ x: 216, y: 536 }, { x: 256, y: 584 }, { x: 288, y: 632 },
|
||||
{ x: 192, y: 680 }, { x: 480, y: 680 }, { x: 150, y: 728 },
|
||||
{ x: 480, y: 448 }
|
||||
{ x: 192, y: 680 }, { x: 480, y: 680 }, { x: 150, y: 728 }
|
||||
];
|
||||
|
||||
const labelHeight = (layout && layout.labelHeight) || 800;
|
||||
@@ -716,6 +727,9 @@ class BluetoothPrinterService {
|
||||
// (100x100 标签字段无这些配置,避免输出多余的 SETMAG/SETBOLD)
|
||||
if (f.mag) {
|
||||
commands.push(`SETMAG ${f.mag[0]} ${f.mag[1]}`);
|
||||
} else {
|
||||
// 无 mag 时显式复位成 1x1,避免继承上一个 T(如标识点的 2x2)的放大状态
|
||||
commands.push(`SETMAG 1 1`);
|
||||
}
|
||||
if (f.bold != null) {
|
||||
commands.push(`SETBOLD ${f.bold}`);
|
||||
@@ -725,6 +739,22 @@ class BluetoothPrinterService {
|
||||
}
|
||||
});
|
||||
|
||||
// 标识点:根据 data.biaoshi 传入的位置编号数组,在正方形四角绘制 √
|
||||
const biaoshiArr = Array.isArray(data.biaoshi) ? data.biaoshi : [];
|
||||
if (biaoshiArr.length > 0) {
|
||||
const positions = (layout && layout.biaoshiPositions) || {};
|
||||
const mag = (layout && layout.biaoshiMag) || [2, 2];
|
||||
commands.push(`SETMAG ${mag[0]} ${mag[1]}`);
|
||||
biaoshiArr.forEach(posNum => {
|
||||
const pos = positions[posNum];
|
||||
if (pos) {
|
||||
commands.push(`T 1 0 ${pos.x} ${pos.y} √`);
|
||||
}
|
||||
});
|
||||
// 标识 √ 画完后复位字号,避免影响后续二维码/字段(SETMAG 为累积状态)
|
||||
commands.push(`SETMAG 1 1`);
|
||||
}
|
||||
|
||||
if (data.qrcode) {
|
||||
const qrc = (layout && layout.qrCode) || { x: 624, y: 584, size: 4, prefix: 'B QR' };
|
||||
const qrPrefix = qrc.prefix || 'B QR';
|
||||
@@ -870,6 +900,33 @@ PRINT`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时设置纸张尺寸(仅内存生效,不保存配置)
|
||||
* 用于打印测试页面,不影响正式配置与 store
|
||||
* @param {number} width - 宽度(mm)
|
||||
* @param {number} height - 高度(mm)
|
||||
*/
|
||||
setPaperSizeTemporary(width, height) {
|
||||
this.paperWidth = width;
|
||||
this.paperHeight = height;
|
||||
|
||||
const presetKey = `${width}x${height}`;
|
||||
if (PAPER_PRESETS[presetKey]) {
|
||||
this.currentPreset = presetKey;
|
||||
} else {
|
||||
this.currentPreset = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时设置指令类型(仅内存生效,不保存配置)
|
||||
* 用于打印测试页面,不影响正式配置与 store
|
||||
* @param {'CPCL' | 'TSPL'} type
|
||||
*/
|
||||
setCommandTypeTemporary(type) {
|
||||
this.commandType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的预设纸张尺寸
|
||||
* @returns {Array<{key: string, label: string, width: number, height: number}>}
|
||||
@@ -912,6 +969,13 @@ PRINT`;
|
||||
* @returns {object|null}
|
||||
*/
|
||||
_getCurrentLayout() {
|
||||
// 一致性修正:currentPreset 与当前纸张尺寸不符时,按尺寸重新推导
|
||||
// (配置页只保存宽高、未保存 currentPreset 时也能正确选布局)
|
||||
const presetKey = `${this.paperWidth}x${this.paperHeight}`;
|
||||
if (this.currentPreset !== presetKey && PAPER_PRESETS[presetKey]) {
|
||||
this.currentPreset = presetKey;
|
||||
}
|
||||
|
||||
if (!this.currentPreset || !PAPER_PRESETS[this.currentPreset]) {
|
||||
return null;
|
||||
}
|
||||
@@ -1003,7 +1067,7 @@ PRINT`;
|
||||
* 3. 失败时切换 writeType 重试(null → writeNoResponse → write)
|
||||
* 4. 失败时增加延迟(BLE 协议栈繁忙)后再次重试
|
||||
*/
|
||||
async _writeData(buffer, attempt = 0) {
|
||||
async _writeData(buffer, attempt = 0, _lastError = null) {
|
||||
const dataLen = buffer.byteLength;
|
||||
const mtu = this._mtu || 20;
|
||||
|
||||
@@ -1017,11 +1081,13 @@ PRINT`;
|
||||
const maxAttempts = 3; // 总共最多重试 3 次
|
||||
|
||||
if (attempt >= maxAttempts) {
|
||||
// 尾包/单包容错:数据已基本送达(最后一次 writeBLECharacteristicValue 触发 errCode=10007
|
||||
// 通常是打印机刚处理完上一批数据时协议栈返回 property not support,并非真正写入失败)。
|
||||
// 此处不再抛异常,静默视为成功以保证整个打印流程不中断。
|
||||
console.warn(`[打印] 写入重试 ${maxAttempts} 次仍失败,视为尾包/打印机忙导致,数据可能已送达`);
|
||||
return { success: true, silent: true, reason: 'max attempts reached, treat as success (tail-packet tolerance)' };
|
||||
const errCode = _lastError ? _lastError.errCode : 'unknown';
|
||||
const errMsg = _lastError ? _lastError.errMsg : '';
|
||||
// 三种 writeType(null/writeNoResponse/write) 全部尝试失败 = 数据未送达。
|
||||
// 10007 多为"特征值不支持该写入方式/属性不支持",连续失败说明特征值
|
||||
// 不可写或 serviceId/writeCharId 配置错误,绝不能静默视为成功,否则会
|
||||
// 误报"打印成功"而打印机实际无反应。
|
||||
throw new Error(`蓝牙写入失败(已重试${maxAttempts}次): errCode=${errCode}, errMsg=${errMsg},请重试或重新配对打印机`);
|
||||
}
|
||||
|
||||
// 选择本次尝试的 writeType
|
||||
@@ -1056,7 +1122,7 @@ PRINT`;
|
||||
// 延迟后重试(递增延迟,给 BLE 协议栈恢复时间)
|
||||
const delay = 100 + attempt * 150; // 100ms, 250ms, 400ms
|
||||
setTimeout(() => {
|
||||
this._writeData(buffer, attempt + 1).then(resolve).catch(reject);
|
||||
this._writeData(buffer, attempt + 1, err).then(resolve).catch(reject);
|
||||
}, delay);
|
||||
}
|
||||
});
|
||||
@@ -1220,9 +1286,8 @@ PRINT`;
|
||||
if (delivered) {
|
||||
continue;
|
||||
}
|
||||
// 5 次重试全部失败:仍按原策略静默成功(避免上层误报),但通过 console.error 醒目提示数据可能不完整
|
||||
console.error(`【打印数据可能不完整】尾包 5 次重试均失败(offset=${offset}, size=${end - offset}B),末尾 ${end - offset} 字节可能丢失,请检查打印结果!`);
|
||||
return;
|
||||
// 5 次重试(含强制 write 模式)全部失败:数据确实未送达,必须报错
|
||||
throw new Error(`尾包发送失败,打印数据不完整(末尾 ${end - offset} 字节可能丢失),请重试打印`);
|
||||
}
|
||||
console.error(`分片发送失败 at offset ${offset}:`, err);
|
||||
throw err;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* PDA 设备扫码实现(hbyapi 广播协议,基于 QS5502 开发 demo)
|
||||
*
|
||||
* 设备自身带有物理扫码按键,按下即触发扫描头;
|
||||
* 本模块负责「监听扫码结果广播」+「页面扫码按钮主动触发」。
|
||||
*
|
||||
* 协议(来自 QS5502 demo MainActivity.java):
|
||||
* 触发扫描:发送广播 hbyapi.intent.key_scan_down(模拟按键按下)
|
||||
* 接收结果:监听广播 com.qs.scancode,从 extra 键 data 获取扫码字符串
|
||||
*
|
||||
* 用法(与 scan-*.js 同构):
|
||||
* const impl = new ScanPdaImpl()
|
||||
* impl.init()
|
||||
* impl.register((code) => {...})
|
||||
* impl.startScan() // 主动触发(物理键按下同样会回传,无需手动调用)
|
||||
*/
|
||||
|
||||
class ScanPdaImpl {
|
||||
constructor() {
|
||||
this._main = null
|
||||
this._receiver = null
|
||||
this._filter = null
|
||||
this._callback = null
|
||||
this._isScanning = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化:获取主 Activity 引用
|
||||
*/
|
||||
init() {
|
||||
this._main = plus.android.runtimeMainActivity()
|
||||
if (!this._main) {
|
||||
throw new Error('[Scan-PDA] 获取主 Activity 失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册扫码结果回调(注册 com.qs.scancode 广播接收器)
|
||||
*/
|
||||
register(callback) {
|
||||
this._callback = callback
|
||||
if (this._receiver) return
|
||||
|
||||
var self = this
|
||||
var IntentFilter = plus.android.importClass('android.content.IntentFilter')
|
||||
this._filter = new IntentFilter()
|
||||
this._filter.addAction('com.qs.scancode')
|
||||
|
||||
this._receiver = plus.android.implements(
|
||||
'io.dcloud.feature.internal.reflect.BroadcastReceiver',
|
||||
{
|
||||
onReceive: function (context, intent) {
|
||||
plus.android.importClass(intent)
|
||||
var codeStr = intent.getStringExtra('data')
|
||||
if (!codeStr) return
|
||||
console.log('[Scan-PDA] 扫码结果:', codeStr)
|
||||
self._isScanning = false
|
||||
if (self._callback) {
|
||||
self._callback(codeStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
this._main.registerReceiver(this._receiver, this._filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销监听(页面卸载时调用)
|
||||
*/
|
||||
unregister() {
|
||||
if (this._receiver && this._main) {
|
||||
this._main.unregisterReceiver(this._receiver)
|
||||
this._receiver = null
|
||||
}
|
||||
this._callback = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始扫描:发送 key_scan_down 广播主动触发扫描头
|
||||
* 设备物理扫码键按下时同样会回传结果,无需手动调用此方法
|
||||
*/
|
||||
startScan() {
|
||||
if (!this._main) return
|
||||
var Intent = plus.android.importClass('android.content.Intent')
|
||||
var intent = new Intent()
|
||||
intent.setAction('hbyapi.intent.key_scan_down')
|
||||
this._main.sendBroadcast(intent)
|
||||
this._isScanning = true
|
||||
console.log('[Scan-PDA] 已触发扫描广播')
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止扫描
|
||||
* demo 仅演示 key_scan_down;部分同协议机型支持 key_scan_up 停止,
|
||||
* 这里 try-catch 发送,无副作用
|
||||
*/
|
||||
stopScan() {
|
||||
this._isScanning = false
|
||||
if (!this._main) return
|
||||
try {
|
||||
var Intent = plus.android.importClass('android.content.Intent')
|
||||
var intent = new Intent()
|
||||
intent.setAction('hbyapi.intent.key_scan_up')
|
||||
this._main.sendBroadcast(intent)
|
||||
} catch (e) {
|
||||
console.log('[Scan-PDA] 发送停止广播失败(可忽略):', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
isScanning() {
|
||||
return this._isScanning
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全销毁(应用退出时调用)
|
||||
*/
|
||||
destroy() {
|
||||
this.unregister()
|
||||
this._main = null
|
||||
this._filter = null
|
||||
}
|
||||
}
|
||||
|
||||
export default ScanPdaImpl
|
||||
+11
-6
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* 统一扫码模块
|
||||
* 支持 安卓设备 / iData 专用 / 通用 Android 手动配置
|
||||
* 支持 安卓设备 / iData 专用 / 通用 Android PDA / 带物理扫码键 PDA(hbyapi 广播)
|
||||
*
|
||||
* === 使用方式 ===
|
||||
*
|
||||
* import scan from '@/utils/scan.js'
|
||||
*
|
||||
* onLoad() {
|
||||
* scan.setMode('uniapp') // 'uniapp' | 'idata' | 'android'(默认 uniapp)
|
||||
* scan.setMode('uniapp') // 'uniapp' | 'idata' | 'android' | 'pda'(默认 uniapp)
|
||||
* scan.init()
|
||||
* scan.register((codeStr) => {
|
||||
* console.log('扫码结果:', codeStr)
|
||||
@@ -18,7 +18,7 @@
|
||||
* }
|
||||
*
|
||||
* === API ===
|
||||
* scan.setMode(mode) - 设置模式:'uniapp' | 'idata' | 'android'
|
||||
* scan.setMode(mode) - 设置模式:'uniapp' | 'idata' | 'android' | 'pda'
|
||||
* scan.init() - 初始化扫码模块
|
||||
* scan.register(cb) - 注册回调:cb(codeStr)
|
||||
* scan.unregister() - 注销回调(页面级)
|
||||
@@ -32,7 +32,8 @@
|
||||
var MODE = {
|
||||
UNIAPP: 'uniapp',
|
||||
IDATA: 'idata',
|
||||
ANDROID: 'android'
|
||||
ANDROID: 'android',
|
||||
PDA: 'pda'
|
||||
}
|
||||
|
||||
// 当前模式,默认 uniapp(安卓设备)
|
||||
@@ -46,9 +47,9 @@ var impl = null
|
||||
* @param {'uniapp'|'idata'|'android'} mode
|
||||
*/
|
||||
function setMode(mode) {
|
||||
var validModes = [MODE.UNIAPP, MODE.IDATA, MODE.ANDROID]
|
||||
var validModes = [MODE.UNIAPP, MODE.IDATA, MODE.ANDROID, MODE.PDA]
|
||||
if (validModes.indexOf(mode) === -1) {
|
||||
console.error('[Scan] 不支持的模式: ' + mode + ',可选: uniapp, idata, android')
|
||||
console.error('[Scan] 不支持的模式: ' + mode + ',可选: uniapp, idata, android, pda')
|
||||
return
|
||||
}
|
||||
// 如果已初始化,先销毁再重新设置模式
|
||||
@@ -80,6 +81,10 @@ function init() {
|
||||
}
|
||||
var ScanIdataImpl = require('./scan-idata.js').default
|
||||
impl = new ScanIdataImpl(iDataModule)
|
||||
} else if (currentMode === MODE.PDA) {
|
||||
// 带物理扫码键 PDA 设备:hbyapi 广播协议(发送 key_scan_down 触发,监听 com.qs.scancode 接收)
|
||||
var ScanPdaImpl = require('./scan-pda.js').default
|
||||
impl = new ScanPdaImpl()
|
||||
} else {
|
||||
// 带扫码PDA设备:使用 Android 系统扫码 API
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user