637 lines
17 KiB
Vue
637 lines
17 KiB
Vue
<template>
|
||
<!-- 扫码页面整体容器 -->
|
||
<view class="scan-page">
|
||
<!-- 顶部扫描卡片(摄像头预览 + 叠加提示 + 操作按钮) -->
|
||
<view class="scan-card" :class="{ zoomed: zoomed }">
|
||
<!-- App(真机打包):使用 HTML5+ 原生条码识别视图,嵌入在当前 card 区域 -->
|
||
<!-- #ifdef APP-PLUS -->
|
||
<view class="camera-host"><!-- 仅用于测量与承载原生扫码视图 --></view>
|
||
<!-- #endif -->
|
||
|
||
<!-- 微信小程序:使用 camera 组件的扫码模式进行连续识别 -->
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<camera
|
||
class="camera-view"
|
||
mode="scanCode"
|
||
device-position="back"
|
||
flash="off"
|
||
@scancode="onScanResult"
|
||
>
|
||
<!-- 双击捕获层:用于切换放大/缩小视角 -->
|
||
<cover-view class="wx-tap-capture" @tap="onTapArea"></cover-view>
|
||
<cover-view class="wx-overlay">
|
||
<cover-view class="wx-overlay-tips">对准商品条码,自动识别</cover-view>
|
||
<cover-view class="wx-corners">
|
||
<cover-view class="wx-corner tl"></cover-view>
|
||
<cover-view class="wx-corner tr"></cover-view>
|
||
<cover-view class="wx-corner bl"></cover-view>
|
||
<cover-view class="wx-corner br"></cover-view>
|
||
</cover-view>
|
||
<cover-view class="wx-controls">
|
||
<cover-view class="wx-btn" @tap="toggleTorch">
|
||
<cover-view>{{ flashOn ? '关闭手电' : '开启手电' }}</cover-view>
|
||
</cover-view>
|
||
<cover-view class="wx-btn" @tap="togglePause">
|
||
<cover-view>{{ paused ? '继续扫码' : '暂停扫码' }}</cover-view>
|
||
</cover-view>
|
||
</cover-view>
|
||
</cover-view>
|
||
</camera>
|
||
<!-- #endif -->
|
||
|
||
<!-- 其它平台:回退提示(下方按钮触发系统扫码) -->
|
||
<!-- #ifndef APP-PLUS || MP-WEIXIN -->
|
||
<view class="camera-fallback">
|
||
<text class="fallback-text">当前平台不支持内嵌连续扫码,请使用下方按钮进行扫码</text>
|
||
</view>
|
||
<!-- #endif -->
|
||
|
||
<!-- 叠加层(提示文案、四角描边、控制按钮)- 非 App/微信小程序平台 -->
|
||
<!-- #ifndef APP-PLUS || MP-WEIXIN -->
|
||
<view class="tap-capture" @tap="onTapArea"></view>
|
||
<view class="overlay">
|
||
<text class="overlay-tips">对准商品条码,自动识别</text>
|
||
<view class="corners">
|
||
<view class="corner tl"></view>
|
||
<view class="corner tr"></view>
|
||
<view class="corner bl"></view>
|
||
<view class="corner br"></view>
|
||
</view>
|
||
<view class="controls">
|
||
<view class="control-btn" @tap="toggleTorch">
|
||
<text>{{ flashOn ? '关闭手电' : '开启手电' }}</text>
|
||
</view>
|
||
<view class="control-btn" @tap="togglePause">
|
||
<text>{{ paused ? '继续扫码' : '暂停扫码' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
|
||
<!-- App 平台:将控制按钮放在扫描卡片下方,避免被原生视图覆盖 -->
|
||
<!-- #ifdef APP-PLUS -->
|
||
<view class="controls controls-bar">
|
||
<view class="control-btn" @tap="toggleTorch">
|
||
<text>{{ flashOn ? '关闭手电' : '开启手电' }}</text>
|
||
</view>
|
||
<view class="control-btn" @tap="togglePause">
|
||
<text>{{ paused ? '继续扫码' : '暂停扫码' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
<!-- 下方内容卡片(占位样式与截图一致) -->
|
||
<view class="empty-card">
|
||
<view class="empty-icon"></view>
|
||
<text class="empty-title">暂未录入商品</text>
|
||
<view class="empty-tip">
|
||
<text>请在上方选择商品录入方式</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 回退平台的操作按钮:调用系统扫码 -->
|
||
<!-- #ifndef APP-PLUS || MP-WEIXIN -->
|
||
<view class="h5-actions">
|
||
<button type="primary" class="scan-btn" @tap="triggerSystemScan">开始扫码</button>
|
||
</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { PostData } from '@/api/translation.js'
|
||
import { getStoreId } from '@/utils/auth'
|
||
/*
|
||
页面功能说明:
|
||
1)支持条形码与二维码识别
|
||
2)支持打开/关闭手电筒(闪光灯)
|
||
3)支持暂停/继续扫码
|
||
4)根据平台差异选择实现:
|
||
- App(APP-PLUS):使用 HTML5+ 的 plus.barcode 原生视图,连续识别并可控闪光灯
|
||
- 微信小程序(MP-WEIXIN):使用 camera 组件的 scanCode 模式,事件回调处理结果,并通过 CameraContext 控制闪光灯
|
||
- 其它平台(如 H5):使用 uni.scanCode 作为回退方案(不嵌入连续预览)
|
||
*/
|
||
export default {
|
||
data() {
|
||
return {
|
||
storeId:null,
|
||
// 是否暂停识别
|
||
paused: false,
|
||
// 手电筒(闪光灯)开关状态
|
||
flashOn: false,
|
||
// 视角放大/缩小状态(双击切换)
|
||
zoomed: false,
|
||
// 最近一次点击时间(用于双击判断)
|
||
lastTapTime: 0,
|
||
// 最近一次识别结果(仅示例展示,可用于业务处理)
|
||
lastResult: '',
|
||
// 去重防抖:上一条结果与时间
|
||
lastScanValue: '',
|
||
lastScanAt: 0,
|
||
// App 原生条码视图实例
|
||
barcodeView: null,
|
||
// 小程序摄像头上下文
|
||
cameraCtx: null,
|
||
// 自动重新扫码的定时器
|
||
rescanTimer: null
|
||
}
|
||
},
|
||
onReady() {
|
||
// 页面渲染完成后,按平台初始化扫码能力
|
||
// #ifdef APP-PLUS
|
||
this.initAppScanner()
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
this.initMpScanner()
|
||
// #endif
|
||
},
|
||
onUnload() {
|
||
// 页面卸载时,释放资源
|
||
// #ifdef APP-PLUS
|
||
this.destroyAppScanner()
|
||
// #endif
|
||
},
|
||
onLoad() {
|
||
this.storeId = getStoreId()
|
||
},
|
||
methods: {
|
||
/* ========== 通用:处理识别结果 ========== */
|
||
async handleScanResult(type, result) {
|
||
// 暂停时忽略结果
|
||
if (this.paused) return
|
||
this.lastResult = typeof result === 'string' ? result.trim() : String(result || '')
|
||
// 防抖与去重:相同结果在 1200ms 内忽略
|
||
const now = Date.now()
|
||
if (this.lastScanValue === this.lastResult && (now - this.lastScanAt) < 1200) {
|
||
return
|
||
}
|
||
this.lastScanValue = this.lastResult
|
||
this.lastScanAt = now
|
||
// 判断是否为二维码,是则提示请扫条形码;否则跳转到添加商品页面并传递条码
|
||
if (this.isQRType(type)) {
|
||
uni.showToast({ title: '请扫条形码', icon: 'none' })
|
||
this.rescanAfter(1200)
|
||
return
|
||
}
|
||
// 非二维码但格式不符合条形码要求时,提示并自动重新扫码
|
||
if (!this.isValidBarcode(this.lastResult)) {
|
||
uni.showToast({ title: '请扫条形码', icon: 'none' })
|
||
this.rescanAfter(1200)
|
||
return
|
||
}
|
||
// 扫到条形码后自动关闭手电筒
|
||
this.setTorch(false)
|
||
let query = {
|
||
"barcodes": [this.lastResult],
|
||
"storeId":this.storeId
|
||
}
|
||
let data = '';
|
||
PostData(query).then(res=>{
|
||
if(res.code == 200){
|
||
data = res.data;
|
||
uni.navigateTo({
|
||
url: `/package_a/addProduct/addProduct?barcode=${this.lastResult}&fromData=${encodeURIComponent(JSON.stringify(data))}`
|
||
})
|
||
}
|
||
else{
|
||
uni.showToast({ title: res.msg, icon: 'none' })
|
||
}
|
||
})
|
||
|
||
|
||
|
||
},
|
||
|
||
// 判断是否为二维码(适配不同平台的类型标识)
|
||
isQRType(t) {
|
||
// App 原生视图:使用 plus.barcode 常量
|
||
// #ifdef APP-PLUS
|
||
if (typeof t === 'number') {
|
||
return t === plus.barcode.QR
|
||
}
|
||
// #endif
|
||
// 小程序/H5:字符串类型,包含 QR 即认为是二维码
|
||
if (typeof t === 'string') {
|
||
return t.toUpperCase().indexOf('QR') !== -1
|
||
}
|
||
return false
|
||
},
|
||
// 简单条形码校验:纯数字且长度 8-20
|
||
isValidBarcode(code) {
|
||
return /^\d{8,20}$/.test(code || '')
|
||
},
|
||
// 双击切换视角(放大/缩小),单击不触发
|
||
onTapArea() {
|
||
// App 平台使用原生条码视图,页面缩放与原生视图不一致,禁用双击缩放
|
||
// #ifdef APP-PLUS
|
||
return
|
||
// #endif
|
||
const now = Date.now()
|
||
if (now - this.lastTapTime <= 300) {
|
||
this.zoomed = !this.zoomed
|
||
this.lastTapTime = 0
|
||
} else {
|
||
this.lastTapTime = now
|
||
}
|
||
},
|
||
// 自动重新扫码(根据平台差异处理)
|
||
rescanAfter(delay = 800) {
|
||
if (this.rescanTimer) {
|
||
clearTimeout(this.rescanTimer)
|
||
this.rescanTimer = null
|
||
}
|
||
this.rescanTimer = setTimeout(() => {
|
||
// #ifdef APP-PLUS
|
||
if (this.barcodeView) {
|
||
try {
|
||
this.barcodeView.cancel && this.barcodeView.cancel()
|
||
this.barcodeView.start && this.barcodeView.start()
|
||
} catch (e) {}
|
||
}
|
||
// #endif
|
||
// #ifdef MP-WEIXIN
|
||
// 小程序 camera 的 scanCode 为持续识别,无需主动重启
|
||
// #endif
|
||
// #ifndef APP-PLUS || MP-WEIXIN
|
||
this.triggerSystemScan()
|
||
// #endif
|
||
this.rescanTimer = null
|
||
}, delay)
|
||
},
|
||
|
||
/* ========== 控制:手电筒开关 ========== */
|
||
toggleTorch() {
|
||
this.setTorch(!this.flashOn)
|
||
},
|
||
// 显式设置手电筒开关(用于扫码成功后关闭)
|
||
setTorch(isOn) {
|
||
this.flashOn = !!isOn
|
||
// App
|
||
// #ifdef APP-PLUS
|
||
if (this.barcodeView && typeof this.barcodeView.setFlash === 'function') {
|
||
this.barcodeView.setFlash(this.flashOn)
|
||
}
|
||
// #endif
|
||
// 微信小程序
|
||
// #ifdef MP-WEIXIN
|
||
if (this.cameraCtx && typeof this.cameraCtx.setTorch === 'function') {
|
||
this.cameraCtx.setTorch({ isTorchOn: this.flashOn })
|
||
}
|
||
// #endif
|
||
// 其它平台
|
||
// #ifndef APP-PLUS || MP-WEIXIN
|
||
if (isOn) {
|
||
uni.showToast({ title: '当前平台不支持手电筒', icon: 'none' })
|
||
}
|
||
// #endif
|
||
},
|
||
|
||
/* ========== 控制:暂停/继续扫码 ========== */
|
||
togglePause() {
|
||
this.paused = !this.paused
|
||
// App:调用 cancel()/start() 控制识别流程
|
||
// #ifdef APP-PLUS
|
||
if (this.barcodeView) {
|
||
if (this.paused) {
|
||
this.barcodeView.cancel && this.barcodeView.cancel()
|
||
} else {
|
||
this.barcodeView.start && this.barcodeView.start()
|
||
}
|
||
}
|
||
// #endif
|
||
// 微信小程序:事件回调里根据 paused 忽略结果即可
|
||
// 其它平台:系统扫码为一次性,无需处理
|
||
},
|
||
|
||
/* ========== H5/其它:系统扫码回退 ========== */
|
||
async triggerSystemScan() {
|
||
let query = {
|
||
"barcodes": ['6901028064835'],
|
||
"storeId":'2'
|
||
}
|
||
PostData(query).then(res=>{
|
||
if(res.code == 200){
|
||
let data = res.data;
|
||
uni.navigateTo({
|
||
url: `/package_a/addProduct/addProduct?barcode=6901028064835&fromData=${encodeURIComponent(JSON.stringify(data))}`
|
||
})
|
||
}
|
||
else{
|
||
uni.showToast({ title: res.msg, icon: 'none' })
|
||
}
|
||
})
|
||
// 使用系统提供的扫码能力(一次性)
|
||
uni.scanCode({
|
||
onlyFromCamera: true,
|
||
success: (res) => {
|
||
// res.result 为识别出的文本内容
|
||
this.handleScanResult(res.scanType || 'UNKNOWN', res.result)
|
||
},
|
||
fail: () => {
|
||
uni.showToast({ title: '扫码失败', icon: 'none' })
|
||
}
|
||
})
|
||
},
|
||
|
||
/* ========== 微信小程序:初始化摄像头上下文 ========== */
|
||
// 小程序 camera 的识别结果事件
|
||
onScanResult(e) {
|
||
// e.detail 包含 { result, scanType, charSet, rawData } 等
|
||
const { result, scanType } = e.detail || {}
|
||
this.handleScanResult(scanType || 'WEAPP', result || '')
|
||
},
|
||
initMpScanner() {
|
||
// 创建摄像头上下文,用于控制手电筒
|
||
try {
|
||
this.cameraCtx = uni.createCameraContext()
|
||
} catch (err) {
|
||
this.cameraCtx = null
|
||
}
|
||
},
|
||
|
||
/* ========== App:创建原生条码识别视图 ========== */
|
||
initAppScanner() {
|
||
// 通过选择器获取扫描卡片的尺寸,用以放置原生 Barcode 视图
|
||
this.$nextTick(() => {
|
||
uni.createSelectorQuery()
|
||
.in(this)
|
||
.select('.scan-card')
|
||
.boundingClientRect((rect) => {
|
||
if (!rect) return
|
||
// 获取当前页面的原生 Webview
|
||
const webview = this.$scope && this.$scope.$getAppWebview
|
||
? this.$scope.$getAppWebview()
|
||
: (this.$mp && this.$mp.page && this.$mp.page.$getAppWebview
|
||
? this.$mp.page.$getAppWebview()
|
||
: null)
|
||
if (!webview) return
|
||
|
||
// 创建原生条码识别视图(支持二维码与常见条形码)
|
||
const types = [
|
||
plus.barcode.QR,
|
||
plus.barcode.EAN13,
|
||
plus.barcode.EAN8,
|
||
plus.barcode.UPCA,
|
||
plus.barcode.UPCE,
|
||
plus.barcode.CODE128,
|
||
plus.barcode.CODE39,
|
||
plus.barcode.ITF,
|
||
plus.barcode.PDF417,
|
||
plus.barcode.DATAMATRIX
|
||
]
|
||
const styles = {
|
||
top: rect.top + 'px',
|
||
left: rect.left + 'px',
|
||
width: rect.width + 'px',
|
||
height: rect.height + 'px'
|
||
}
|
||
const barcode = plus.barcode.create('scan-barcode', types, {
|
||
frameColor: '#FFFFFF',
|
||
scanbarColor: '#FFFFFF'
|
||
})
|
||
barcode.setStyle(styles)
|
||
// 将原生视图添加到页面
|
||
webview.append(barcode)
|
||
// 识别结果事件
|
||
barcode.onmarked = (type, result) => {
|
||
this.handleScanResult(type, result)
|
||
}
|
||
// 启动识别
|
||
barcode.start()
|
||
this.barcodeView = barcode
|
||
})
|
||
.exec()
|
||
})
|
||
},
|
||
destroyAppScanner() {
|
||
// 关闭并释放原生条码视图
|
||
if (this.barcodeView && typeof this.barcodeView.close === 'function') {
|
||
try {
|
||
this.barcodeView.close()
|
||
} catch (e) {}
|
||
this.barcodeView = null
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
/* 页面背景与基本布局 */
|
||
.scan-page {
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
padding: 12px;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 顶部扫描卡片 */
|
||
.scan-card {
|
||
position: relative;
|
||
background: #000;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
height: 240px;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
.scan-card.zoomed {
|
||
transform: scale(1.2);
|
||
}
|
||
.camera-host {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.camera-view {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
/* 微信小程序覆盖层(使用 cover-view) */
|
||
/* 双击捕获层(非小程序使用 view,微信小程序使用 cover-view) */
|
||
.tap-capture {
|
||
position: absolute;
|
||
left: 0; top: 0; right: 0; bottom: 0;
|
||
z-index: 2;
|
||
background: transparent;
|
||
}
|
||
.wx-tap-capture {
|
||
position: absolute;
|
||
left: 0; top: 0; right: 0; bottom: 0;
|
||
}
|
||
|
||
.wx-overlay {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
right: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
}
|
||
.wx-overlay-tips {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 10px;
|
||
transform: translateX(-50%);
|
||
color: #fff;
|
||
background: rgba(0,0,0,0.35);
|
||
padding: 6px 12px;
|
||
border-radius: 18px;
|
||
font-size: 12px;
|
||
}
|
||
.wx-corners .wx-corner {
|
||
position: absolute;
|
||
width: 22px;
|
||
height: 22px;
|
||
border: 2px solid #fff;
|
||
}
|
||
.wx-corners .tl { left: 10px; top: 10px; border-right: none; border-bottom: none; }
|
||
.wx-corners .tr { right: 10px; top: 10px; border-left: none; border-bottom: none; }
|
||
.wx-corners .bl { left: 10px; bottom: 10px; border-right: none; border-top: none; }
|
||
.wx-corners .br { right: 10px; bottom: 10px; border-left: none; border-top: none; }
|
||
.wx-controls {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 10px;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 28px;
|
||
}
|
||
.wx-btn {
|
||
min-width: 96px;
|
||
height: 36px;
|
||
padding: 0 14px;
|
||
border-radius: 18px;
|
||
background: rgba(0,0,0,0.45);
|
||
border: 1px solid rgba(255,255,255,0.35);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
}
|
||
.camera-fallback {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: #222;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.fallback-text {
|
||
color: #bbb;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 叠加层(提示 + 四角 + 控制按钮) */
|
||
.overlay {
|
||
pointer-events: none; /* 使叠加层不阻挡相机事件,按钮区域再单独打开事件 */
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
}
|
||
.overlay-tips {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 10px;
|
||
transform: translateX(-50%);
|
||
color: #fff;
|
||
background: rgba(0,0,0,0.35);
|
||
padding: 6px 12px;
|
||
border-radius: 18px;
|
||
font-size: 12px;
|
||
}
|
||
.corners .corner {
|
||
position: absolute;
|
||
width: 22px;
|
||
height: 22px;
|
||
border: 2px solid #fff;
|
||
}
|
||
.corners .tl { left: 10px; top: 10px; border-right: none; border-bottom: none; }
|
||
.corners .tr { right: 10px; top: 10px; border-left: none; border-bottom: none; }
|
||
.corners .bl { left: 10px; bottom: 10px; border-right: none; border-top: none; }
|
||
.corners .br { right: 10px; bottom: 10px; border-left: none; border-top: none; }
|
||
|
||
.controls {
|
||
pointer-events: auto; /* 允许点击按钮 */
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 10px;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 28px;
|
||
}
|
||
.control-btn {
|
||
min-width: 96px;
|
||
height: 36px;
|
||
padding: 0 14px;
|
||
border-radius: 18px;
|
||
background: rgba(0,0,0,0.45);
|
||
border: 1px solid rgba(255,255,255,0.35);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.control-btn text {
|
||
color: #fff;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* App 平台下方控制条 */
|
||
.controls-bar {
|
||
margin-top: 8px;
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 28px;
|
||
}
|
||
|
||
/* 下方内容卡片 */
|
||
.empty-card {
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 24px 16px 28px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.empty-icon {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 30px;
|
||
background: #ffecec;
|
||
}
|
||
.empty-title {
|
||
color: #333;
|
||
font-size: 16px;
|
||
}
|
||
.empty-tip {
|
||
margin-top: 4px;
|
||
background: #f7f7f7;
|
||
padding: 8px 12px;
|
||
border-radius: 18px;
|
||
}
|
||
.empty-tip text {
|
||
color: #888;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* H5 回退按钮 */
|
||
.h5-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
.scan-btn {
|
||
width: 200px;
|
||
border-radius: 24px;
|
||
}
|
||
|
||
</style>
|