feat(scan): 添加手机扫码功能支持

- 在 FabricOutScanBar 组件中添加手机扫码图标和事件处理
- 更新工作台页面的扫码逻辑以支持手机扫码回退
- 在销售拣货详情页添加手机扫码按钮和处理方法
- 重构扫码设置页面以支持扫码方式选择
- 添加手机扫码配置和判断逻辑到扫码配置模块
- 扩展扫码混入模块以支持手机扫码模式和设备检测
- 在多个业务页面中集成手机扫码功能和界面元素
- 更新扫码配置存储和读取逻辑以兼容新配置结构
This commit is contained in:
郭鸿轩 2026-06-23 17:08:17 +08:00
parent 084bb235f8
commit 36c0f9a3a0
12 changed files with 277 additions and 103 deletions

View File

@ -2,8 +2,20 @@ export const STORAGE_KEY = 'pda_scan_config';
export const DICTIONARY_TYPE_SCANNER = 10046;
export const PHONE_SCAN_BRAND = 'phone';
export const PHONE_SCAN_CONFIG = {
brand: PHONE_SCAN_BRAND,
name: '手机扫码',
isPhoneScan: true,
};
const NO_SETUP_HINT_KEY = 'pda_scan_config_hint_shown';
export function isPhoneScanConfig(config) {
return !!(config && (config.isPhoneScan || config.brand === PHONE_SCAN_BRAND));
}
function enrichFromBuiltinConfig(config, builtinConfigs = []) {
if (!config || !builtinConfigs.length) {
return config;
@ -30,8 +42,15 @@ function enrichFromBuiltinConfig(config, builtinConfigs = []) {
export function normalizeConfig(raw, builtinConfigs = []) {
if (!raw || typeof raw !== 'object') return null;
const brand = String(raw.brand || raw.id || raw.dictionary_detail_id || raw.name || '');
const isPhoneScan = raw.isPhoneScan === true || brand === PHONE_SCAN_BRAND;
if (isPhoneScan) {
return { ...PHONE_SCAN_CONFIG };
}
const config = enrichFromBuiltinConfig({
brand: String(raw.brand || raw.id || raw.dictionary_detail_id || raw.name || ''),
brand,
name: raw.name || '',
action: raw.action || raw.code || '',
dataKey: raw.dataKey || raw.data_key || raw.remark || '',
@ -51,10 +70,10 @@ export function mapDictionaryToScannerConfig(raw, builtinConfigs = []) {
return normalizeConfig(raw, builtinConfigs);
}
export function getScanConfig() {
export function getScanConfig(builtinConfigs = []) {
try {
const data = uni.getStorageSync(STORAGE_KEY);
return normalizeConfig(data);
return normalizeConfig(data, builtinConfigs);
} catch (e) {
console.log('读取扫码枪配置失败', e);
return null;

View File

@ -3,10 +3,16 @@
import {
getScanConfig,
hasShownNoConfigHint,
isPhoneScanConfig,
markNoConfigHintShown,
normalizeConfig,
PHONE_SCAN_CONFIG,
} from '@/common/scanConfig.js';
const PDA_DEVICE_KEYWORDS = [
'newland', 'nls', 'sunmi', 'urovo', 'zebra', 'honeywell', 'idata', 'seuic',
];
export default {
data() {
return {
@ -14,6 +20,8 @@ export default {
isPageActive: false,
registeredBrands: [],
remoteScannerConfigs: [],
usePhoneScanFallback: false,
_scanCallback: null,
}
},
@ -40,9 +48,43 @@ export default {
dataKey: 'data',
needSetup: false
},
{ ...PHONE_SCAN_CONFIG },
];
},
getPhoneScanConfig() {
return { ...PHONE_SCAN_CONFIG };
},
isPdaDevice() {
// #ifdef APP-PLUS
try {
const Build = plus.android.importClass('android.os.Build');
const manufacturer = String(Build.MANUFACTURER || '').toLowerCase();
const model = String(Build.MODEL || '').toLowerCase();
return PDA_DEVICE_KEYWORDS.some((keyword) => {
return manufacturer.includes(keyword) || model.includes(keyword);
});
} catch (error) {
console.log('检测设备类型失败', error);
return false;
}
// #endif
return false;
},
shouldUsePhoneScan() {
const builtinConfigs = this.getScannerConfigs();
const activeConfig = getScanConfig(builtinConfigs);
if (activeConfig && isPhoneScanConfig(activeConfig)) {
return true;
}
if (!activeConfig && !this.isPdaDevice()) {
return true;
}
return false;
},
getAllScannerConfigs() {
const builtinConfigs = this.getScannerConfigs();
if (!this.remoteScannerConfigs.length) {
@ -52,23 +94,77 @@ export default {
const configMap = new Map();
builtinConfigs.forEach((item) => configMap.set(item.brand, item));
this.remoteScannerConfigs.forEach((item) => configMap.set(item.brand, item));
if (!configMap.has(PHONE_SCAN_CONFIG.brand)) {
configMap.set(PHONE_SCAN_CONFIG.brand, { ...PHONE_SCAN_CONFIG });
}
return Array.from(configMap.values());
},
getActiveScanConfig() {
return getScanConfig();
return getScanConfig(this.getScannerConfigs());
},
getDefaultScanConfig() {
const configs = this.getAllScannerConfigs();
const configs = this.getAllScannerConfigs().filter((item) => !isPhoneScanConfig(item));
return configs.length ? configs[0] : null;
},
openPhoneScan(options = {}) {
const callback = options.callback || this._scanCallback;
// #ifdef APP-PLUS || MP-WEIXIN
uni.scanCode({
scanType: options.scanType || ['qrCode', 'barCode'],
success: (res) => {
this.handleScanResult(res.result, callback);
},
fail: (err) => {
console.error('手机扫码失败:', err);
if (options.fail) {
options.fail(err);
return;
}
uni.showToast({
title: '扫码失败',
icon: 'none',
});
},
});
return;
// #endif
// #ifdef H5
uni.showToast({
title: 'H5环境不支持扫码功能',
icon: 'none',
});
// #endif
},
triggerScan() {
if (this.usePhoneScanFallback) {
this.openPhoneScan();
}
},
registerScanBroadcast(scanCallback) {
this._scanCallback = scanCallback;
// #ifndef APP-PLUS
this.usePhoneScanFallback = true;
return true;
// #endif
// #ifdef APP-PLUS
if (this.shouldUsePhoneScan()) {
this.usePhoneScanFallback = true;
console.log('使用手机扫码模式');
return true;
}
this.usePhoneScanFallback = false;
const activeConfig = this.getActiveScanConfig();
if (activeConfig) {
if (activeConfig && !isPhoneScanConfig(activeConfig)) {
return this.registerScanBroadcastByConfig(activeConfig, scanCallback);
}
@ -101,21 +197,34 @@ export default {
return false;
}
if (isPhoneScanConfig(config)) {
this._scanCallback = scanCallback;
this.usePhoneScanFallback = true;
return true;
}
return this.registerScanBroadcastByConfig(config, scanCallback);
// #endif
},
registerScanBroadcastByConfig(config, scanCallback) {
// #ifdef APP-PLUS
const normalizedConfig = normalizeConfig(config);
const normalizedConfig = normalizeConfig(config, this.getScannerConfigs());
if (!normalizedConfig) {
console.error('扫码枪配置无效');
return false;
}
if (isPhoneScanConfig(normalizedConfig)) {
this._scanCallback = scanCallback;
this.usePhoneScanFallback = true;
return true;
}
try {
this.registerSingleBroadcast(normalizedConfig, scanCallback);
this.registeredBrands.push(normalizedConfig.brand);
this.usePhoneScanFallback = false;
console.log(`${normalizedConfig.name}扫码广播注册成功`);
return true;
} catch (error) {

View File

@ -10,6 +10,14 @@
@input="$emit('input', $event.detail.value)"
@confirm="$emit('scan')"
/>
<u-icon
v-if="showPhoneScan"
class="fabric-out-phoneScanIcon"
name="scan"
size="44"
color="#2979ff"
@click="$emit('phone-scan')"
/>
<checkbox-group @change="$emit('delete-mode-change', $event)">
<label class="fabric-out-delCheck">
<checkbox :checked="deleteMode" />删除
@ -29,10 +37,16 @@ export default {
deleteMode: { type: Boolean, default: false },
message: { type: String, default: '' },
disabledTip: { type: String, default: '当前状态不可扫码' },
showPhoneScan: { type: Boolean, default: false },
},
};
</script>
<style scoped lang="scss">
@import '@/pages/storefabric/storeFabricBusinessOutPage.scss';
.fabric-out-phoneScanIcon {
margin-left: 16rpx;
flex-shrink: 0;
}
</style>

View File

@ -17,6 +17,14 @@
</u-form-item>
<u-form-item label-width="130" label="条码资料:">
<input type="text" v-model="QRBarCode" maxlength="-1" style="width: 170px" @confirm="SalePickBillDetailScan" />
<u-icon
v-if="usePhoneScanFallback"
name="scan"
size="44"
color="#2979ff"
style="margin-left: 8px;"
@click="handlePhoneScan"
/>
<checkbox-group @change="handleAllCrockNoChange">
<checkbox :checked="AllCrockNoScanStatus">整缸</checkbox>
</checkbox-group>
@ -182,21 +190,16 @@ export default {
onUnload() {
this.isPageActive = false;
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
},
onHide() {
this.isPageActive = false;
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
},
onShow() {
this.isPageActive = true;
// #ifdef APP-PLUS
this.registerScanBroadcast((scanResult) => {
console.log("配布单详情-扫码结果:", scanResult);
this.QRBarCode = scanResult;
@ -204,10 +207,13 @@ export default {
this.handleScans();
});
});
// #endif
},
methods: {
handlePhoneScan() {
this.openPhoneScan();
},
playSuccess() {
util.playSuccessAudio();
},

View File

@ -73,25 +73,17 @@
>
<FabricOutScanBar
slot="before-list"
:can-scan="canScan"
:value="QRBarCode"
:delete-mode="BarCodeDelStatus"
:message="BillDataMessage"
disabled-tip="请先提交保存后再扫码"
:show-phone-scan="usePhoneScanFallback"
@input="QRBarCode = $event"
@scan="handleScan"
@phone-scan="handlePhoneScan"
@delete-mode-change="BarCodeDelChange"
/>
<FabricOutScanDetail

View File

@ -34,8 +34,10 @@
:value="QRBarCode"
:delete-mode="BarCodeDelStatus"
:message="BillDataMessage"
:show-phone-scan="usePhoneScanFallback"
@input="QRBarCode = $event"
@scan="handleScan"
@phone-scan="handlePhoneScan"
@delete-mode-change="BarCodeDelChange"
/>
<FabricOutScanDetail

View File

@ -91,42 +91,35 @@ export default {
},
onShow() {
this.isPageActive = true;
// #ifdef APP-PLUS
if (this.canScan && this.isEditable) {
this.registerScanBroadcast((scanResult) => {
this.QRBarCode = scanResult.trim().replace(/[\r\n]/g, '');
this.$nextTick(() => this.handleScan());
});
}
// #endif
},
onHide() {
this.isPageActive = false;
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
},
onUnload() {
this.isPageActive = false;
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
},
methods: {
handlePhoneScan() {
this.openPhoneScan();
},
registerOrderScanBroadcast() {
// #ifdef APP-PLUS
this.registerScanBroadcast((scanResult) => {
this.QRBarCode = scanResult.trim().replace(/[\r\n]/g, '');
this.$nextTick(() => this.handleScan());
});
// #endif
},
syncCanScanFromStatus(status) {
const nextCanScan = this.isEditable && canScanAtStatus(status);
if (this.canScan && !nextCanScan) {
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
}
this.canScan = nextCanScan;
},

View File

@ -31,6 +31,14 @@
</view>
<u-form-item label-width="150" label="条码资料:">
<input type="text" maxlength="-1" v-model="QRBarCode" style="width:200px;" @confirm="GoodsCheckBillDetailScan" />
<u-icon
v-if="usePhoneScanFallback"
name="scan"
size="44"
color="#2979ff"
style="margin-left: 8px;"
@click="handlePhoneScan"
/>
<checkbox-group @change="BarCodeDelChange">
<checkbox ref="checkBoxRef" :checked="BarCodeDelStatus">删除</checkbox>
</checkbox-group>
@ -193,28 +201,20 @@
},
onUnload() {
// #ifdef APP-PLUS
this.isPageActive = false;
this.unregisterScanBroadcast();
// #endif
},
onHide() {
//
this.isPageActive = false;
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
},
onShow() {
//
this.isPageActive = true;
// #ifdef APP-PLUS
this.registerScanBroadcast((scanResult) => {
console.log("扫码结果:", scanResult);
//
if(!this.StoreName){
this.showError('请先选择仓库');
return;
@ -240,9 +240,11 @@
return;
}
});
// #endif
},
methods: {
handlePhoneScan() {
this.openPhoneScan();
},
//
showError(message) {

View File

@ -12,6 +12,14 @@
<u-form-item label-width="150" label="条码资料:">
<input type="text" v-model="QRBarCode" maxlength="-1" style="width:200px;"
@confirm="GoodsCheckBillDetailScan" />
<u-icon
v-if="usePhoneScanFallback"
name="scan"
size="44"
color="#2979ff"
style="margin-left: 8px;"
@click="handlePhoneScan"
/>
<checkbox-group @change="BarCodeDelChange">
<checkbox ref="checkBoxRef" :checked="BarCodeDelStatus">删除</checkbox>
</checkbox-group>
@ -147,36 +155,30 @@ export default {
},
onShow() {
//
this.isPageActive = true;
// #ifdef APP-PLUS
this.registerScanBroadcast((scanResult) => {
console.log("扫码结果:", scanResult);
//
this.QRBarCode = scanResult;
this.$nextTick(() => {
this.GoodsCheckBillDetailScan();
});
});
// #endif
},
onHide() {
this.isPageActive = false;
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
},
onUnload() {
this.isPageActive = false;
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
// #endif
},
methods: {
handlePhoneScan() {
this.openPhoneScan();
},
//
showError(message) {
this.playError();

View File

@ -26,6 +26,14 @@
<u-form-item label-width="150" label="条码资料:" :class="{ 'input-highlight': shouldHighlightQR }">
<input type="text" v-model="QRBarCode" maxlength="-1" style="width:150px;"
@confirm="GoodsStoreStationMoveScan" />
<u-icon
v-if="usePhoneScanFallback"
name="scan"
size="44"
color="#2979ff"
style="margin-left: 8px;"
@click="handlePhoneScan"
/>
</u-form-item>
<u-form-item>
<text class="title">{{ BillDataMessage }}</text>
@ -150,10 +158,8 @@ export default {
this.StoreNameID = getApp().globalData.StoreNameID;
this.StoreName = getApp().globalData.StoreName;
};
// #ifdef APP-PLUS
// 使 scanMixin 广
this.registerScanBroadcast(this.handleScanCode);
// #endif
},
// onUnload, onHide, onShow scanMixin
/* onBackPress() {
@ -174,6 +180,10 @@ export default {
}
},
methods: {
handlePhoneScan() {
this.openPhoneScan();
},
playSuccess() {
util.playSuccessAudio();
},

View File

@ -3,25 +3,27 @@
<common-navbar title="扫描设置"></common-navbar>
<u-gap height="20" bg-color="#f5f5f5"></u-gap>
<u-cell-group :border="false">
<u-cell-item title="PDA品牌" :value="form.name || '请选择'" @click="brandSelectShow = true"></u-cell-item>
<u-cell-item title="广播动作" :arrow="false">
<u-input
slot="right-icon"
v-model="form.action"
placeholder="请输入广播动作"
input-align="right"
:clearable="false"
></u-input>
</u-cell-item>
<u-cell-item title="数据标签" :arrow="false">
<u-input
slot="right-icon"
v-model="form.dataKey"
placeholder="请输入数据标签"
input-align="right"
:clearable="false"
></u-input>
</u-cell-item>
<u-cell-item title="扫码方式" :value="form.name || '请选择'" @click="brandSelectShow = true"></u-cell-item>
<template v-if="!form.isPhoneScan">
<u-cell-item title="广播动作" :arrow="false">
<u-input
slot="right-icon"
v-model="form.action"
placeholder="请输入广播动作"
input-align="right"
:clearable="false"
></u-input>
</u-cell-item>
<u-cell-item title="数据标签" :arrow="false">
<u-input
slot="right-icon"
v-model="form.dataKey"
placeholder="请输入数据标签"
input-align="right"
:clearable="false"
></u-input>
</u-cell-item>
</template>
</u-cell-group>
<view class="form-footer">
<u-button type="primary" :loading="submitting" @click="submit">确定</u-button>
@ -33,7 +35,14 @@
<script>
import CommonNavbar from '@/components/common-navbar/index';
import scanMixin from '@/common/scanMixin.js';
import { getScanConfig, DICTIONARY_TYPE_SCANNER, mapDictionaryToScannerConfig, saveScanConfig } from '@/common/scanConfig.js';
import {
getScanConfig,
DICTIONARY_TYPE_SCANNER,
isPhoneScanConfig,
mapDictionaryToScannerConfig,
PHONE_SCAN_BRAND,
saveScanConfig,
} from '@/common/scanConfig.js';
export default {
components: {
@ -50,6 +59,7 @@ export default {
name: '',
action: '',
dataKey: '',
isPhoneScan: false,
needSetup: false,
setupAction: '',
setupParams: null,
@ -79,7 +89,7 @@ export default {
this.configList = list
.map((item) => mapDictionaryToScannerConfig(item, builtinConfigs))
.filter(Boolean);
this.remoteScannerConfigs = this.configList;
this.remoteScannerConfigs = this.configList.filter((item) => !isPhoneScanConfig(item));
} else {
this.useFallbackConfigList();
}
@ -87,28 +97,42 @@ export default {
console.log('获取扫码枪字典配置失败,使用内置配置', error);
this.useFallbackConfigList();
}
this.ensurePhoneScanOption();
this.initFormFromSaved();
},
useFallbackConfigList() {
this.configList = this.getScannerConfigs();
this.remoteScannerConfigs = [];
},
ensurePhoneScanOption() {
const phoneConfig = this.getPhoneScanConfig();
if (!this.configList.find((item) => item.brand === PHONE_SCAN_BRAND)) {
this.configList.push(phoneConfig);
}
},
initFormFromSaved() {
const savedConfig = getScanConfig();
const savedConfig = getScanConfig(this.getScannerConfigs());
if (savedConfig) {
this.applyConfig(savedConfig);
return;
}
if (this.configList.length) {
this.applyConfig(this.configList[0]);
if (!this.isPdaDevice()) {
this.applyConfig(this.getPhoneScanConfig());
return;
}
const pdaConfigs = this.configList.filter((item) => !isPhoneScanConfig(item));
if (pdaConfigs.length) {
this.applyConfig(pdaConfigs[0]);
}
},
applyConfig(config) {
const isPhoneScan = isPhoneScanConfig(config);
this.form = {
brand: config.brand || '',
name: config.name || '',
action: config.action || '',
dataKey: config.dataKey || '',
isPhoneScan,
needSetup: !!config.needSetup,
setupAction: config.setupAction || '',
setupParams: config.setupParams || null,
@ -122,27 +146,31 @@ export default {
}
},
submit() {
const action = (this.form.action || '').trim();
const dataKey = (this.form.dataKey || '').trim();
if (!this.form.brand) {
this.$u.toast('请选择PDA品牌');
return;
}
if (!action) {
this.$u.toast('请输入广播动作');
return;
}
if (!dataKey) {
this.$u.toast('请输入数据标签');
this.$u.toast('请选择扫码方式');
return;
}
const payload = {
...this.form,
action,
dataKey,
};
let payload;
if (this.form.isPhoneScan) {
payload = this.getPhoneScanConfig();
} else {
const action = (this.form.action || '').trim();
const dataKey = (this.form.dataKey || '').trim();
if (!action) {
this.$u.toast('请输入广播动作');
return;
}
if (!dataKey) {
this.$u.toast('请输入数据标签');
return;
}
payload = {
...this.form,
action,
dataKey,
};
}
this.submitting = true;
if (!saveScanConfig(payload)) {
@ -153,7 +181,7 @@ export default {
// #ifdef APP-PLUS
this.unregisterScanBroadcast();
const registered = this.registerScanBroadcastByConfig(payload);
const registered = this.registerScanBroadcastByConfig(payload, null);
if (!registered) {
this.submitting = false;
this.$u.toast('保存成功,但广播注册失败');

View File

@ -140,10 +140,7 @@ export default {
},
onLoad() {
// #ifdef APP-PLUS
// 使 scanMixin 广
this.registerScanBroadcast(this.handleScanCode);
// #endif
},
// onUnload, onHide, onShow scanMixin
@ -178,10 +175,10 @@ export default {
},
//
handleScan() {
// uni.navigateTo({
// url: '/pages/saleship/salepickscandetail?order_no=FPD-PB-202412270196'
// })
// return
if (this.usePhoneScanFallback) {
this.openPhoneScan();
return;
}
// #ifdef APP-PLUS || MP-WEIXIN
uni.scanCode({
scanType: ['qrCode', 'barCode'], //