ry_app/package_a/translation/translation.vue

637 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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()
},
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>