ry_app/package_a/translation/translation.vue

675 lines
18 KiB
Vue
Raw Normal View History

2026-03-27 00:18:36 +08:00
<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根据平台差异选择实现
- AppAPP-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()
},
2026-04-02 22:45:01 +08:00
onShow() {
// 从其他页面返回时,如果未处于暂停状态,重新启动扫码
this.lastScanValue = ''
this.lastScanAt = 0
// #ifdef APP-PLUS
if (this._isFirstShow === undefined) {
// 首次进入时由 onReady 初始化,这里跳过
this._isFirstShow = false
} else {
// 从其他页面返回时
if (!this.barcodeView) {
// 延迟重建,确保 DOM 已经就绪
setTimeout(() => {
this.initAppScanner()
}, 200)
} else if (!this.paused) {
try {
this.barcodeView.start && this.barcodeView.start()
} catch (e) {}
}
}
// #endif
},
onHide() {
// 页面隐藏时暂停扫码,节省性能
// #ifdef APP-PLUS
// 为避免与其他页面的系统扫码(如 addProduct 的 uni.scanCode)产生布局冲突
// 页面隐藏时彻底销毁原生组件onShow 时再重新创建
this.destroyAppScanner()
// #endif
},
2026-03-27 00:18:36 +08:00
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({
2026-03-28 21:37:27 +08:00
url: `/package_a/addProduct/addProduct?barcode=${this.lastResult}&fromData=${encodeURIComponent(JSON.stringify(data))}`
2026-03-27 00:18:36 +08:00
})
}
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({
2026-03-28 21:37:27 +08:00
url: `/package_a/addProduct/addProduct?barcode=6901028064835&fromData=${encodeURIComponent(JSON.stringify(data))}`
2026-03-27 00:18:36 +08:00
})
}
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'
}
2026-04-02 22:45:01 +08:00
const barcodeId = 'scan-barcode-' + Date.now()
const barcode = plus.barcode.create(barcodeId, types, {
2026-03-27 00:18:36 +08:00
frameColor: '#FFFFFF',
scanbarColor: '#FFFFFF'
})
barcode.setStyle(styles)
// 将原生视图添加到页面
webview.append(barcode)
// 识别结果事件
barcode.onmarked = (type, result) => {
this.handleScanResult(type, result)
}
// 启动识别
2026-04-02 22:45:01 +08:00
if (!this.paused) {
barcode.start()
}
// 如果手电筒原本是开启的,恢复状态
if (this.flashOn && typeof barcode.setFlash === 'function') {
barcode.setFlash(true)
}
2026-03-27 00:18:36 +08:00
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>