app端新增原材料送检功能

This commit is contained in:
geht
2026-07-20 17:31:54 +08:00
parent 531cac7341
commit f24a21a41c
24 changed files with 1806 additions and 52 deletions

View File

@@ -0,0 +1,16 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-android"
},
{
"app-plus" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

View File

@@ -0,0 +1,136 @@
# DT50 扫描识别 —— 广播参数与接入说明
> 面向 APP「协作」页「扫描识别」功能。
> 设备:优博讯 UROVO **DT50**Android 11
> 参数来源:**设备实测 adb logcat 抓真实扫码** + **优博讯官方 ScanManager 文档** + **DCloud UniApp 官方示例** 三方交叉验证。
> 记录日期2026-07-20。
---
## 一、核心参数(最重要)
| 项目 | 值 | 说明 |
|---|---|---|
| 输出方式 | **Intent / 广播** | 设备当前**已是此模式**,无需在设置页切换 |
| 广播 Action | `android.intent.ACTION_DECODE_DATA` | 实测 + 官方一致 |
| 条码文本String | `barcode_string` | 官方常量 `BARCODE_STRING_TAG`**首选** |
| 原始字节byte[] | `barcode` | 官方常量 `DECODE_DATA_TAG`,兜底用 |
| 长度int | `length` | 官方常量 `BARCODE_LENGTH_TAG` |
| 码制int/String | `barcodeType` | 官方常量 `BARCODE_TYPE_TAG`,实测读出 `Code128` |
| 扫描键 keycode | `523` | KEYCODE_SCAN机身侧键 |
## 二、设备侧验证结论
- **硬件**:成像头正常,成功解出 Code128 条码 ✅
- **触发**:侧键 keycode = 523按下发 `ACTION_KEYCODE_SCAN_PRESSED`
- **广播**:成功解码时实测发出 `android.intent.ACTION_DECODE_DATA (has extras)` 并被接收 ✅
- **模式**:当前即广播输出(非键盘输出),**直接 registerReceiver 即可**
- **无需**:原生 `.aar` 插件 / 自定义基座 / USB 驱动 / 完整 AndroidStudio 套壳;标准基座即可
相关系统组件(备查):
- 扫描设置 App`com.ubx.barcodescanner.tool`
- 扫描服务:`com.ubx.uscanner`
- DataWedge 式配置:`com.ubx.datawedge`
## 三、UniApp 监听代码(标准基座,无需插件)
```js
let scanReceiver = null;
/** 开始监听 DT50 扫描广播 */
function startScanListener(onBarcode) {
const main = plus.android.runtimeMainActivity();
const IntentFilter = plus.android.importClass('android.content.IntentFilter');
const filter = new IntentFilter();
filter.addAction('android.intent.ACTION_DECODE_DATA'); // DT50 广播 Action
scanReceiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
onReceive: function (context, intent) {
plus.android.importClass(intent);
// 首选直接取字符串
let code = intent.getStringExtra('barcode_string');
// 兜底:部分场景用字节数组
if (!code) {
const bytes = intent.getByteArrayExtra('barcode');
if (bytes) code = plus.android.newObject('java.lang.String', bytes);
}
const type = intent.getStringExtra('barcodeType'); // 码制,可选
if (code) onBarcode(code, type);
}
});
main.registerReceiver(scanReceiver, filter);
}
/** 停止监听(务必在页面卸载时调用,避免重复注册 / 内存泄漏) */
function stopScanListener() {
if (scanReceiver) {
plus.android.runtimeMainActivity().unregisterReceiver(scanReceiver);
scanReceiver = null;
}
}
```
页面接入:
```js
onLoad() {
startScanListener((code, type) => {
this.barcode = code;
// TODO: 调 MES 后端接口 / 查料
});
},
onUnload() {
stopScanListener();
}
```
> 注意:仅 Appplus 环境下可用H5/小程序无 `plus`,接入时需做平台判断(`#ifdef APP-PLUS`)。
## 四、故障排查
| 现象 | 原因 / 处理 |
|---|---|
| 收不到广播 | 确认扫描设置里输出为「Intent/广播」而非「键盘/输入法」;确认 Action 拼写 |
| 扫描后文本框被输入内容 | 键盘输出模式也开了,去扫描设置关掉键盘输出 |
| `barcode_string` 为空 | 用兜底的 `barcode`(byte[]) 转 String |
| 一直解码失败(红光有但不出码) | 距离 15~20cm红光完整压住条码拿稳 1~2 秒;避免手机屏幕反光码 |
## 五、参考
- 优博讯官方 ScanManager 文档https://en.urovo.com/developer/android/device/ScanManager.html
- DCloud UniApp 监听优博讯扫码广播含示例https://ask.dcloud.net.cn/article/36468
## 六、本仓库接入位置2026-07-20
| 项 | 位置 |
|---|---|
| 菜单入口 | `src/common/work.ts` → 其他服务「合同」后「扫描识别」 |
| 页面 | `src/pages/scanIdentify/scanIdentify.vue` |
| 广播工具 | `src/utils/dt50Scan.ts` |
| 行为 | 仅广播展示条码/码制/时间,不调后端、不弹输入法 |
| 生命周期 | `onShow` 注册 / `onHide`+`onUnload` 注销;同码防抖 |
## 七、键盘输出 vs 广播输出
| | 键盘输出Keyboard / 楔入) | 广播输出Intent / Broadcast |
|---|---|---|
| 原理 | 把条码当成「敲键盘」打进当前焦点输入框 | 系统发广播App 用 `registerReceiver` 收 |
| App 是否要抢焦点 | 要,通常还会弹出软键盘 | **不需要**输入框焦点 |
| 适合场景 | 记事本、任意输入框快速验证 | 业务页稳定取码、拿码制等扩展字段 |
| 本页策略 | 已关闭楔入,避免弹输入法 | **本页只走广播** |
设备设置建议:扫描设置 → 关闭「键盘输出」开启「Intent/广播输出」。
本页进入时会主动 `hideKeyboard`,不自动聚焦任何输入框。
## 附:参数是如何抓到的(可复现)
```bash
# adb 路径(本机 Android SDK
ADB="C:/Users/hp/AppData/Local/Android/Sdk/platform-tools/adb.exe"
$ADB devices -l # 确认 DT50 已连USB 调试)
$ADB logcat -c # 清空日志缓冲
$ADB shell "timeout 40 logcat -v threadtime '*:V'" > scan.log # 抓 40s
# 抓取期间用 DT50 侧键扫几次条码,然后:
grep -E "ACTION_DECODE_DATA|barcodeType|DECODER" scan.log
```

View File

@@ -2,7 +2,7 @@
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true
# 是否开启sourcemapApp 真机编译易 OOM开发期先关闭
VITE_SHOW_SOURCEMAP = false
VITE_SERVER_BASEURL = 'http://localhost:8080/jeecg-boot'
VITE_SERVER_BASEURL = 'http://192.168.1.3:8888/jeecg-boot'

View File

@@ -36,7 +36,7 @@ export default defineUniPages({
iconPath: 'static/tabbar/tabbar-home-2.png',
selectedIconPath: 'static/tabbar/tabbar-home.png',
pagePath: 'pages/index/index',
text: '协作',
text: '生产',
},
// {
// iconPath: 'static/tabbar/tabbar-workHome-2.png',

View File

@@ -1,5 +1,5 @@
/**
* 常用服务
* 基础服务(原常用服务
* useful server
*/
@@ -108,36 +108,28 @@ export const us = {
}
/**
* other server 其他服务
* 生产服务(原其他服务
* 首页「更多」按钮仍按 type=other 追加,勿删空数组
*/
export const os = {
data: [
//update-begin---author:xsl ---date:20260720 for【APP首页】生产服务菜单调整-----------
{
title: '新闻中心',
icon: icon_prefix + 'xinwen.png',
description: '新闻中心',
routeIndex: 'news',
title: '原材料送检',
icon: icon_prefix + 'renwu.png',
description: '扫码送检与录入检验结果',
useCount: 10000,
},
{
title: '会议',
icon: icon_prefix + 'huiyi.png',
description: '会议',
useCount: 10000,
routeIndex: 'meeting',
routeIndex: 'rawMaterialInspect',
enabled: true,
},
{
title: '任务中心',
icon: icon_prefix + 'renwu.png',
description: '任务中心',
useCount: 10000,
},
{
title: '合同',
icon: icon_prefix + 'hetong.png',
description: '合同',
title: '扫描识别',
icon: icon_prefix + 'wendang.png',
description: '手持机扫描头识别条码',
useCount: 10000,
routeIndex: 'scanIdentify',
enabled: true,
},
//update-end---author:xsl ---date:20260720 for【APP首页】生产服务菜单调整-----------
],
}

View File

@@ -0,0 +1,91 @@
<template>
<view class="help-wrap">
<view class="help-icon" @click.stop="open">
<text class="q">?</text>
</view>
<view v-if="visible" class="mask" @click="close" @touchmove.stop.prevent="">
<view class="bubble" :style="bubbleStyle" @click.stop="">
<text class="bubble-text">{{ text }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
defineOptions({ name: 'HelpTipBubble' })
const props = defineProps({
text: {
type: String,
default: '',
},
top: {
type: String,
default: '180upx',
},
})
const visible = ref(false)
const bubbleStyle = computed(() => ({
top: props.top,
}))
function open() {
visible.value = true
}
function close() {
visible.value = false
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped>
.help-wrap {
display: inline-flex;
align-items: center;
}
.help-icon {
width: 36upx;
height: 36upx;
border-radius: 50%;
border: 2upx solid #888;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8upx;
}
.q {
font-size: 22upx;
color: #666;
font-weight: 600;
line-height: 1;
}
.mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1000;
background: transparent;
}
.bubble {
position: absolute;
left: 32upx;
right: 32upx;
padding: 24upx 28upx;
border-radius: 16upx;
background: rgb(180, 180, 180);
box-sizing: border-box;
}
.bubble-text {
font-size: 26upx;
color: #fff;
line-height: 1.5;
}
</style>

View File

@@ -5,6 +5,7 @@ Breadcrumb: typeof import('./Breadcrumb/Breadcrumb.vue')['default']
CategorySelect: typeof import('./CategorySelect/CategorySelect.vue')['default']
DateTime: typeof import('./DateTime/DateTime.vue')['default']
Grid: typeof import('./Grid/Grid.vue')['default']
HelpTipBubble: typeof import('./HelpTipBubble/HelpTipBubble.vue')['default']
ImgPreview: typeof import('./ImgPreview/ImgPreview.vue')['default']
LFile: typeof import('./LFile/LFile.vue')['default']
PageLayout: typeof import('./PageLayout/PageLayout.vue')['default']

View File

@@ -130,8 +130,8 @@
"sdkConfigs": {
"maps": {
"amap": {
"key": "???",
"securityJsCode": "???",
"key": "20854e7d231ee339bfa3b277c840070c",
"securityJsCode": "7a542edee4a82e56ed88fef8ef42b5a5",
"serviceHost": ""
}
}

View File

@@ -33,7 +33,7 @@
"iconPath": "static/tabbar/tabbar-home-2.png",
"selectedIconPath": "static/tabbar/tabbar-home.png",
"pagePath": "pages/index/index",
"text": "协作"
"text": "生产"
},
{
"iconPath": "static/tabbar/tabbar-user-2.png",
@@ -197,6 +197,45 @@
}
}
},
{
"path": "pages/rawMaterialInspect/inspectResult",
"type": "page",
"layout": "default",
"style": {
"navigationBarTitleText": "录入检验结果",
"navigationStyle": "custom",
"disableScroll": true,
"app-plus": {
"bounce": "none"
}
}
},
{
"path": "pages/rawMaterialInspect/rawMaterialInspect",
"type": "page",
"layout": "default",
"style": {
"navigationBarTitleText": "原材料送检",
"navigationStyle": "custom",
"disableScroll": true,
"app-plus": {
"bounce": "none"
}
}
},
{
"path": "pages/scanIdentify/scanIdentify",
"type": "page",
"layout": "default",
"style": {
"navigationBarTitleText": "扫描识别",
"navigationStyle": "custom",
"disableScroll": true,
"app-plus": {
"bounce": "none"
}
}
},
{
"path": "pages/user/people",
"type": "page",

View File

@@ -28,22 +28,22 @@
</swiper-item>
</swiper>
<scroll-view class="scrollView" :scroll-y="true" scroll-with-animation>
<!--常用服务-->
<!--生产服务原其他服务置顶-->
<view class="serveBox">
<view class="title">
<view class="dot"></view>
<wd-text text="常用服务"></wd-text>
</view>
<Grid :column="4" v-model="usList" @itemClik="goPage"></Grid>
</view>
<!--其他服务-->
<view class="serveBox">
<view class="title">
<view class="dot"></view>
<wd-text text="其他服务"></wd-text>
<wd-text text="生产服务"></wd-text>
</view>
<Grid :column="4" v-model="osList" @itemClik="goPage"></Grid>
</view>
<!--基础服务原常用服务-->
<view class="serveBox">
<view class="title">
<view class="dot"></view>
<wd-text text="基础服务"></wd-text>
</view>
<Grid :column="4" v-model="usList" @itemClik="goPage"></Grid>
</view>
</scroll-view>
</PageLayout>
</template>

View File

@@ -14,9 +14,12 @@
<template>
<PageLayout :navbarShow="false">
<view class="page-container">
<view class="text-center">
<image :src="compLogo" mode="aspectFit" class="logo"></image>
<view class="title text-shadow">{{ compTitle || 'JEECG BOOT' }}</view>
<view class="brand text-center">
<view class="brand-title">启航密炼</view>
<view class="brand-subtitle">智能制造MES手持端</view>
</view>
<view class="form-wrap">
<view class="enter-area">
<view v-if="loginWay == 1" class="account-login-area">
<view class="box account">
@@ -80,6 +83,11 @@
</wd-button>
</view>
</view>
<view class="footer">
<image src="/static/logo/xslmes.png" mode="aspectFit" class="footer-logo"></image>
<text class="footer-text">XSL-MES APP</text>
</view>
<wd-notify />
</view>
</PageLayout>
@@ -361,20 +369,64 @@ if (isLocalConfig === false) {
.page-container {
padding: 0 20upx;
padding-top: 100upx;
padding-bottom: calc(24upx + env(safe-area-inset-bottom));
position: relative;
font-size: 15px;
color: var(--UI-FG-0);
.logo {
width: 200upx;
height: 150px;
min-height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
.brand {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.title {
font-size: 58upx;
.brand-title {
font-size: 52upx;
font-weight: 700;
color: #1a1a1a;
letter-spacing: 6upx;
line-height: 1.3;
}
.brand-subtitle {
margin-top: 12upx;
font-size: 28upx;
color: #666;
letter-spacing: 2upx;
line-height: 1.4;
}
.form-wrap {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 0;
}
.footer {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 12upx;
padding-top: 24upx;
padding-bottom: 12upx;
}
.footer-logo {
width: 48upx;
height: 48upx;
border-radius: 8upx;
}
.footer-text {
font-size: 22upx;
color: #888;
letter-spacing: 1upx;
}
.enter-area {
padding-top: 75px;
padding-top: 24px;
width: 87%;
margin: 0 auto 60upx;
margin: 0 auto 40upx;
.box {
display: flex;
align-items: center;

View File

@@ -0,0 +1,245 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '录入检验结果',
navigationStyle: 'custom',
disableScroll: true,
'app-plus': {
bounce: 'none',
},
},
}
</route>
<template>
<PageLayout
:backRouteName="backRouteName"
navTitle="录入检验结果"
:routeMethod="backRouteName === 'index' ? 'pushTab' : 'replace'"
>
<template #navRight>
<HelpTipBubble :text="helpText" top="176upx" />
</template>
<view class="page">
<view class="info-card">
<view class="row"><text class="label">条码</text><text class="val">{{ record.barcode || '-' }}</text></view>
<view class="row"><text class="label">批次</text><text class="val">{{ record.batchNo || '-' }}</text></view>
<view class="row"><text class="label">物料</text><text class="val">{{ record.materialName || '-' }}</text></view>
<view class="row"><text class="label">送检时间</text><text class="val">{{ record.inspectTime || '-' }}</text></view>
</view>
<scroll-view class="list" scroll-y>
<view v-if="loading" class="empty">加载中</view>
<view v-else-if="!lines.length" class="empty">暂无检验明细</view>
<view v-for="(line, idx) in lines" :key="line.id" class="line-card">
<view class="line-title">{{ idx + 1 }}. {{ line.inspectItemName || '检验项' }}</view>
<view class="range">容差{{ formatRange(line) }}</view>
<view class="status">状态{{ passText(line.passFlag) }}</view>
<view v-if="String(line.passFlag || '0') === '0'" class="input-row">
<text class="label">检验值</text>
<input
class="input"
type="digit"
:value="line.inspectValue == null ? '' : String(line.inspectValue)"
placeholder="请输入"
@input="(e) => onValueInput(line, e)"
/>
</view>
<view v-else class="input-row">
<text class="label">检验值</text>
<text class="val">{{ line.inspectValue ?? '-' }}</text>
</view>
</view>
</scroll-view>
<view class="footer">
<wd-button block type="primary" :loading="saving" @click="handleSave">保存</wd-button>
</view>
</view>
</PageLayout>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useToast } from 'wot-design-uni'
import HelpTipBubble from '@/components/HelpTipBubble/HelpTipBubble.vue'
import {
prepareResultEntry,
queryRecordById,
saveInspectResult,
type InspectRecord,
type InspectRecordLine,
} from '@/service/xslmes/rawMaterialInspect'
defineOptions({ name: 'inspectResult' })
const helpText = '请填写待检项目的检验值后点击保存。已录入项目不可重复修改;可分次录入,全部完成后再判定合格/不合格。'
const toast = useToast()
const backRouteName = ref('rawMaterialInspect')
const recordId = ref('')
const record = ref<InspectRecord>({})
const lines = ref<InspectRecordLine[]>([])
const loading = ref(false)
const saving = ref(false)
function passText(flag?: string) {
const f = String(flag ?? '0')
if (f === '1') return '合格'
if (f === '2') return '不合格'
return '待检'
}
function formatRange(line: InspectRecordLine) {
const min = line.allowMin
const max = line.allowMax
const left = min == null ? '-∞' : `${line.includeMinFlag === 1 ? '[' : '('}${min}`
const right = max == null ? '+∞' : `${max}${line.includeMaxFlag === 1 ? ']' : ')'}`
return `${left}, ${right}`
}
function onValueInput(line: InspectRecordLine, e: any) {
const v = e?.detail?.value
if (v === '' || v == null) {
line.inspectValue = null
return
}
line.inspectValue = v
}
//update-begin---author:xsl ---date:20260720 for【APP原材料送检】录入检验结果页-----------
async function loadData() {
if (!recordId.value) return
loading.value = true
try {
const mainRes: any = await queryRecordById(recordId.value)
if (mainRes?.success) {
record.value = mainRes.result || {}
}
const lineRes: any = await prepareResultEntry(recordId.value)
if (!lineRes?.success) {
toast.warning(lineRes?.message || '加载明细失败')
return
}
lines.value = Array.isArray(lineRes.result) ? lineRes.result : []
} catch (e: any) {
toast.warning(e?.data?.message || e?.message || '加载失败')
} finally {
loading.value = false
}
}
async function handleSave() {
const pending = lines.value.filter((x) => String(x.passFlag || '0') === '0')
const filled = pending.filter(
(x) => x.inspectValue !== null && x.inspectValue !== undefined && x.inspectValue !== '',
)
if (!filled.length) {
toast.warning('请至少录入一条检验值')
return
}
saving.value = true
try {
const lineList = filled.map((x) => ({
id: x.id,
inspectValue: x.inspectValue as string | number,
}))
const res: any = await saveInspectResult(recordId.value, lineList)
if (!res?.success) {
toast.warning(res?.message || '保存失败')
return
}
toast.success('保存成功')
uni.navigateBack({
fail: () => {
uni.redirectTo({ url: '/pages/rawMaterialInspect/rawMaterialInspect' })
},
})
} catch (e: any) {
toast.warning(e?.data?.message || e?.message || '保存失败')
} finally {
saving.value = false
}
}
onLoad((opts: any) => {
recordId.value = opts?.recordId || ''
if (opts?.backRouteName) {
backRouteName.value = opts.backRouteName
}
loadData()
})
//update-end---author:xsl ---date:20260720 for【APP原材料送检】录入检验结果页-----------
</script>
<style lang="scss" scoped>
.page {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f6f8;
}
.info-card,
.line-card {
background: #fff;
border-radius: 16upx;
padding: 24upx;
margin: 20upx 24upx 0;
}
.row {
display: flex;
padding: 6upx 0;
font-size: 24upx;
}
.label {
width: 140upx;
color: #999;
flex-shrink: 0;
}
.val {
flex: 1;
color: #333;
word-break: break-all;
}
.list {
flex: 1;
min-height: 0;
padding-bottom: 20upx;
}
.empty {
padding: 60upx;
text-align: center;
color: #bbb;
}
.line-title {
font-size: 28upx;
font-weight: 600;
color: #222;
}
.range,
.status {
margin-top: 8upx;
font-size: 22upx;
color: #888;
}
.input-row {
margin-top: 16upx;
display: flex;
align-items: center;
}
.input {
flex: 1;
height: 64upx;
padding: 0 20upx;
background: #f5f6f8;
border-radius: 8upx;
font-size: 28upx;
}
.footer {
padding: 20upx 24upx calc(20upx + env(safe-area-inset-bottom));
background: #fff;
}
</style>

View File

@@ -0,0 +1,289 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '原材料送检',
navigationStyle: 'custom',
disableScroll: true,
'app-plus': {
bounce: 'none',
},
},
}
</route>
<template>
<PageLayout backRouteName="index" navTitle="原材料送检" routeMethod="pushTab">
<template #navRight>
<HelpTipBubble :text="helpText" top="176upx" />
</template>
<view class="page">
<view class="search-bar">
<input
class="search-input"
type="text"
v-model="barcodeInput"
placeholder="扫码或输入条码"
confirm-type="search"
@confirm="handleQuery"
/>
<wd-button size="small" type="primary" :loading="querying" @click="handleQuery">查询</wd-button>
</view>
<scroll-view class="list" scroll-y>
<view v-if="!list.length" class="empty">暂无待送检数据请扫码或查询</view>
<view v-for="(item, index) in list" :key="item.id" class="card">
<view class="card-head">
<text class="barcode">{{ item.barcode }}</text>
<text class="remove" @click="removeItem(index)">移除</text>
</view>
<view class="row"><text class="label">批次</text><text class="val">{{ item.batchNo || '-' }}</text></view>
<view class="row"><text class="label">入场日期</text><text class="val">{{ formatDate(item.entryDate) }}</text></view>
<view class="row"><text class="label">物料名称</text><text class="val">{{ item.materialName || '-' }}</text></view>
<view class="row"><text class="label">供应商</text><text class="val">{{ item.supplierName || '-' }}</text></view>
<view class="row"><text class="label">厂家物料</text><text class="val">{{ item.manufacturerMaterialName || '-' }}</text></view>
<view class="row"><text class="label">保质期</text><text class="val">{{ item.shelfLife || '-' }}</text></view>
<view class="row"><text class="label">库区</text><text class="val">{{ item.warehouseArea || '-' }}</text></view>
<view class="row"><text class="label">卸货人</text><text class="val">{{ item.unloadOperator || '-' }}</text></view>
</view>
</scroll-view>
<view class="footer">
<wd-button block type="primary" :loading="sending" :disabled="!list.length" @click="handleSendInspect">
送检{{ list.length }}
</wd-button>
</view>
</view>
</PageLayout>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
import { useToast } from 'wot-design-uni'
import HelpTipBubble from '@/components/HelpTipBubble/HelpTipBubble.vue'
import { startDt50ScanListener, stopDt50ScanListener } from '@/utils/dt50Scan'
import { createByBarcode, queryByBarcode, type RawMaterialCard } from '@/service/xslmes/rawMaterialInspect'
defineOptions({ name: 'rawMaterialInspect' })
const helpText =
'请按下侧键扫描原材料条码,也可手动输入后点查询。未送检的加入下方列表;已送检待录入的将跳转检验结果页。列表中已有相同条码不会重复添加。'
const toast = useToast()
const list = ref<RawMaterialCard[]>([])
const barcodeInput = ref('')
const querying = ref(false)
const sending = ref(false)
let listening = false
function formatDate(v?: string) {
if (!v) return '-'
return String(v).substring(0, 10)
}
function hideSoftKeyboard() {
try {
uni.hideKeyboard()
} catch (_e) {
// ignore
}
}
//update-begin---author:xsl ---date:20260720 for【APP原材料送检】条码输入查询与帮助气泡-----------
async function queryCard(barcode: string, fromScan = false) {
const code = String(barcode || '').trim()
if (!code) {
toast.warning('请输入或扫描条码')
return
}
barcodeInput.value = code
hideSoftKeyboard()
querying.value = true
try {
uni.showLoading({ title: '查询中', mask: true })
const res: any = await queryByBarcode(code)
if (!res?.success) {
toast.warning(res?.message || '查询失败')
return
}
const data = res.result
const action = data?.action
const card = data?.card
if (!card?.id) {
toast.warning('未返回卡片数据')
return
}
if (action === 'enterResult') {
const recordId = data.pendingRecordId
if (!recordId) {
toast.warning('缺少送检记录ID')
return
}
uni.navigateTo({
url: `/pages/rawMaterialInspect/inspectResult?recordId=${encodeURIComponent(recordId)}&backRouteName=rawMaterialInspect`,
})
return
}
if (action === 'finished') {
toast.info('该条码已完成检验,无需再送检')
return
}
// 列表已有则不重复添加
if (list.value.some((x) => x.id === card.id || x.barcode === card.barcode)) {
if (!fromScan) toast.info('列表中已存在该条码')
return
}
list.value.unshift(card)
toast.success('已加入待送检列表')
} catch (e: any) {
toast.warning(e?.data?.message || e?.message || '查询失败')
} finally {
uni.hideLoading()
querying.value = false
}
}
function handleQuery() {
queryCard(barcodeInput.value, false)
}
function onBarcode(code: string) {
queryCard(code, true)
}
function removeItem(index: number) {
list.value.splice(index, 1)
}
async function handleSendInspect() {
if (!list.value.length || sending.value) return
sending.value = true
const okCodes: string[] = []
const failMsgs: string[] = []
try {
for (const item of [...list.value]) {
try {
const res: any = await createByBarcode(item.barcode as string)
if (res?.success) {
okCodes.push(item.barcode as string)
} else {
failMsgs.push(`${item.barcode}: ${res?.message || '失败'}`)
}
} catch (e: any) {
failMsgs.push(`${item.barcode}: ${e?.data?.message || e?.message || '失败'}`)
}
}
list.value = list.value.filter((x) => !okCodes.includes(x.barcode as string))
if (okCodes.length) {
toast.success(`成功送检 ${okCodes.length}`)
}
if (failMsgs.length) {
toast.warning(failMsgs[0])
}
} finally {
sending.value = false
}
}
function ensureListen() {
if (typeof plus === 'undefined') return
startDt50ScanListener((code) => onBarcode(code))
listening = true
}
onShow(() => ensureListen())
onHide(() => {
if (!listening) return
stopDt50ScanListener()
listening = false
})
onUnload(() => {
stopDt50ScanListener()
listening = false
})
//update-end---author:xsl ---date:20260720 for【APP原材料送检】条码输入查询与帮助气泡-----------
</script>
<style lang="scss" scoped>
.page {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f6f8;
box-sizing: border-box;
}
.search-bar {
display: flex;
align-items: center;
gap: 16upx;
margin: 20upx 24upx;
padding: 16upx 20upx;
background: #fff;
border-radius: 16upx;
}
.search-input {
flex: 1;
height: 64upx;
padding: 0 16upx;
background: #f5f6f8;
border-radius: 8upx;
font-size: 28upx;
}
.list {
flex: 1;
min-height: 0;
padding: 0 24upx;
box-sizing: border-box;
}
.empty {
padding: 80upx 24upx;
text-align: center;
color: #bbb;
font-size: 26upx;
}
.card {
background: #fff;
border-radius: 16upx;
padding: 24upx;
margin-bottom: 20upx;
}
.card-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12upx;
}
.barcode {
font-size: 30upx;
font-weight: 600;
color: #111;
word-break: break-all;
}
.remove {
color: #e54d42;
font-size: 24upx;
padding-left: 16upx;
}
.row {
display: flex;
padding: 6upx 0;
font-size: 24upx;
}
.label {
width: 160upx;
color: #999;
flex-shrink: 0;
}
.val {
flex: 1;
color: #333;
word-break: break-all;
}
.footer {
padding: 20upx 24upx calc(20upx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
}
</style>

View File

@@ -0,0 +1,318 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '扫描识别',
navigationStyle: 'custom',
disableScroll: true,
'app-plus': {
bounce: 'none',
},
},
}
</route>
<template>
<PageLayout backRouteName="index" navTitle="扫描识别" routeMethod="pushTab">
<view class="page">
<view class="tip-card">
<view class="tip-title">{{ isAppEnv ? '请按下侧键扫描条码' : '请在 DT50 手持机 App 中使用' }}</view>
<view class="tip-desc">
{{
isAppEnv
? '本页使用广播接收扫码结果,不会自动弹出输入法。请在设备扫描设置中开启 Intent/广播输出。'
: 'H5 无法调用扫描头'
}}
</view>
</view>
<view class="result-card">
<view class="label">最近一次识别</view>
<view class="code" :class="{ empty: !latestCode }">{{ latestCode || '等待扫描…' }}</view>
<view v-if="latestType || latestTime || latestSource" class="meta">
<text v-if="latestSource">来源{{ latestSource }}</text>
<text v-if="latestType">码制{{ latestType }}</text>
<text v-if="latestTime">时间{{ latestTime }}</text>
</view>
</view>
<view class="history-head">
<text>扫描记录</text>
<text v-if="history.length" class="clear" @click="clearHistory">清空</text>
</view>
<scroll-view class="history-list" scroll-y :show-scrollbar="true">
<view v-if="!history.length" class="empty">暂无记录</view>
<view v-for="(item, index) in history" :key="item.id" class="history-item">
<view class="idx">{{ history.length - index }}</view>
<view class="body">
<view class="h-code">{{ item.code }}</view>
<view class="h-meta">
<text v-if="item.source">{{ item.source }}</text>
<text v-if="item.type">{{ item.type }}</text>
<text>{{ item.time }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="debug-head">
<text>调试日志</text>
<text class="clear" @click="debugLogs = []">清空</text>
</view>
<scroll-view class="debug-list" scroll-y>
<view v-for="(line, i) in debugLogs" :key="i" class="debug-line">{{ line }}</view>
</scroll-view>
</view>
</PageLayout>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { useToast } from 'wot-design-uni'
import {
normalizeScanCode,
setDt50ScanLogger,
startDt50ScanListener,
stopDt50ScanListener,
} from '@/utils/dt50Scan'
defineOptions({
name: 'scanIdentify',
})
interface ScanRecord {
id: number
code: string
type?: string
source?: string
time: string
}
const toast = useToast()
const isAppEnv = ref(typeof plus !== 'undefined')
const latestCode = ref('')
const latestType = ref('')
const latestTime = ref('')
const latestSource = ref('')
const history = ref<ScanRecord[]>([])
const debugLogs = ref<string[]>([])
let seq = 0
let started = false
function formatNow() {
const d = new Date()
const p = (n: number) => String(n).padStart(2, '0')
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}
function pushDebug(msg: string) {
debugLogs.value.unshift(`${formatNow()} ${msg}`)
if (debugLogs.value.length > 80) {
debugLogs.value = debugLogs.value.slice(0, 80)
}
}
/** 强制收起软键盘,避免页面抢焦点弹出输入法 */
function hideSoftKeyboard() {
try {
uni.hideKeyboard()
} catch (_e) {
// ignore
}
// #ifdef APP-PLUS
try {
const Context = plus.android.importClass('android.content.Context') as any
const InputMethodManager = plus.android.importClass('android.view.inputmethod.InputMethodManager') as any
const main = plus.android.runtimeMainActivity()
const imm = main.getSystemService(Context.INPUT_METHOD_SERVICE)
plus.android.importClass(imm)
const decor = main.getWindow().getDecorView()
imm.hideSoftInputFromWindow(decor.getWindowToken(), 0)
} catch (_e) {
// ignore
}
// #endif
}
//update-begin---author:xsl ---date:20260720 for【需求】DT50扫描识别页展示条码-----------
function onBarcode(code: string, type?: string, source = 'broadcast') {
const normalized = normalizeScanCode(code)
if (!normalized) return
hideSoftKeyboard()
latestCode.value = normalized
latestType.value = type || ''
latestSource.value = source
latestTime.value = formatNow()
history.value.unshift({
id: ++seq,
code: normalized,
type,
source,
time: latestTime.value,
})
if (history.value.length > 50) {
history.value = history.value.slice(0, 50)
}
toast.success('识别成功')
}
function clearHistory() {
history.value = []
}
function ensureListen(from: string) {
setDt50ScanLogger((msg) => pushDebug(msg))
isAppEnv.value = typeof plus !== 'undefined'
hideSoftKeyboard()
if (!isAppEnv.value) {
pushDebug(`${from}:非 App 环境`)
return
}
// 仅广播监听,不抢输入框焦点,避免弹出输入法
startDt50ScanListener((code, type, source) => onBarcode(code, type, source || 'broadcast'))
started = true
pushDebug(`${from}:已启动广播监听(无输入法)`)
}
onLoad(() => ensureListen('onLoad'))
onShow(() => {
ensureListen('onShow')
hideSoftKeyboard()
})
onHide(() => {
if (!started) return
stopDt50ScanListener()
setDt50ScanLogger(null)
started = false
pushDebug('onHide停止监听')
})
onUnload(() => {
stopDt50ScanListener()
setDt50ScanLogger(null)
started = false
})
//update-end---author:xsl ---date:20260720 for【需求】DT50扫描识别页展示条码-----------
</script>
<style lang="scss" scoped>
.page {
display: flex;
flex-direction: column;
height: 100%;
padding: 24upx;
background: #f5f6f8;
box-sizing: border-box;
}
.tip-card,
.result-card {
background: #fff;
border-radius: 16upx;
padding: 28upx;
margin-bottom: 24upx;
}
.tip-title {
font-size: 30upx;
color: #333;
font-weight: 600;
}
.tip-desc {
margin-top: 12upx;
font-size: 24upx;
color: #999;
}
.label {
font-size: 24upx;
color: #999;
}
.code {
margin-top: 16upx;
font-size: 40upx;
color: #111;
word-break: break-all;
font-weight: 600;
&.empty {
color: #bbb;
font-weight: 400;
}
}
.meta {
margin-top: 16upx;
display: flex;
flex-wrap: wrap;
gap: 24upx;
font-size: 24upx;
color: #666;
}
.history-head,
.debug-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8upx 8upx 16upx;
font-size: 28upx;
color: #666;
.clear {
color: #39b54a;
}
}
.history-list {
flex: 1;
min-height: 160upx;
max-height: 360upx;
background: #fff;
border-radius: 16upx;
margin-bottom: 16upx;
}
.debug-list {
height: 240upx;
background: #1e1e1e;
border-radius: 16upx;
padding: 12upx;
box-sizing: border-box;
}
.debug-line {
color: #9cdcfe;
font-size: 20upx;
line-height: 1.5;
margin-bottom: 6upx;
word-break: break-all;
}
.empty {
padding: 48upx;
text-align: center;
color: #bbb;
font-size: 26upx;
}
.history-item {
display: flex;
padding: 24upx;
border-bottom: 1px solid #f0f0f0;
.idx {
width: 48upx;
color: #999;
font-size: 24upx;
padding-top: 6upx;
}
.body {
flex: 1;
min-width: 0;
}
.h-code {
font-size: 28upx;
color: #333;
word-break: break-all;
}
.h-meta {
margin-top: 8upx;
display: flex;
gap: 24upx;
font-size: 22upx;
color: #999;
}
}
</style>

View File

@@ -0,0 +1,68 @@
import { http } from '@/utils/http'
const BASE = '/xslmes/app/rawMaterialInspect'
/** 原材料卡片(展示字段) */
export interface RawMaterialCard {
id: string
barcode?: string
batchNo?: string
entryDate?: string
materialName?: string
supplierName?: string
manufacturerMaterialName?: string
shelfLife?: string
warehouseArea?: string
unloadOperator?: string
testResult?: string
}
export interface InspectScanResult {
card: RawMaterialCard
/** sendInspect | enterResult | finished */
action: string
hasPendingInspect?: boolean
pendingRecordId?: string
}
export interface InspectRecordLine {
id: string
recordId?: string
inspectItemName?: string
allowMin?: number | null
includeMinFlag?: number
allowMax?: number | null
includeMaxFlag?: number
inspectValue?: number | string | null
passFlag?: string
sortNo?: number
}
export interface InspectRecord {
id: string
barcode?: string
batchNo?: string
materialName?: string
inspectStatus?: string
inspectTime?: string
}
/** 按条码查询卡片及送检状态 */
export const queryByBarcode = (barcode: string) =>
http.get<InspectScanResult>(`${BASE}/queryByBarcode`, { barcode })
/** 按条码送检 */
export const createByBarcode = (barcode: string) =>
http.post<any>(`${BASE}/createByBarcode`, { barcode })
/** 加载检验明细 */
export const prepareResultEntry = (id: string) =>
http.post<InspectRecordLine[]>(`${BASE}/prepareResultEntry`, { id })
/** 保存检验结果 */
export const saveInspectResult = (id: string, lineList: Array<{ id: string; inspectValue: number | string }>) =>
http.post<string>(`${BASE}/saveInspectResult`, { id, lineList })
/** 查询送检记录主表 */
export const queryRecordById = (id: string) =>
http.get<InspectRecord>(`${BASE}/queryRecordById`, { id })

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -17,6 +17,9 @@ interface NavigateToOptions {
"/pages/login/loginOauth2" |
"/pages/message/message" |
"/pages/more/more" |
"/pages/rawMaterialInspect/inspectResult" |
"/pages/rawMaterialInspect/rawMaterialInspect" |
"/pages/scanIdentify/scanIdentify" |
"/pages/user/people" |
"/pages/workHome/workHome" |
"/pages-home/home/home" |

View File

@@ -0,0 +1,291 @@
/**
* 优博讯 DT50 扫描头广播监听
* 参数见 docs/DT50扫描识别-广播参数与接入说明.md
*
* 说明:设备常同时开启「广播 + 键盘」双通道,需优先广播并合并去重,
* 否则键盘缓冲会把同一条码拼成两遍(如 6942…28286942…2828
*/
export type Dt50ScanCallback = (code: string, type?: string, source?: string) => void
export type Dt50LogCallback = (msg: string) => void
const SCAN_ACTIONS = ['android.intent.ACTION_DECODE_DATA']
const CODE_KEYS = ['barcode_string', 'scannerdata', 'data', 'barcode']
const TYPE_KEYS = ['codetype', 'barcodeType', 'codeType', 'type']
/** 同码防抖窗口(广播+键盘几乎同时到达) */
const DEBOUNCE_MS = 1200
/** 广播成功后,忽略键盘楔入的时间窗 */
const IGNORE_KEYBOARD_AFTER_BROADCAST_MS = 1500
let scanReceiver: any = null
let listening = false
let registering = false
let lastCode = ''
let lastTime = 0
let lastBroadcastTime = 0
let broadcastConfirmed = false
let activeCallback: Dt50ScanCallback | null = null
let logCallback: Dt50LogCallback | null = null
function log(msg: string) {
const line = `[dt50Scan] ${msg}`
console.log(line)
try {
logCallback && logCallback(line)
} catch (_e) {
// ignore
}
}
function hasPlusAndroid() {
return typeof plus !== 'undefined' && !!(plus as any).android
}
function toJsString(val: any): string {
if (val == null || val === undefined) return ''
if (typeof val === 'string') return val.trim()
try {
if (typeof val === 'object' && typeof val.toString === 'function') {
const s = val.toString()
if (s && s !== '[object Object]') return String(s).trim()
}
} catch (_e) {
// ignore
}
try {
return String(plus.android.invoke(val, 'toString') || '').trim()
} catch (_e) {
return String(val).trim()
}
}
function bytesToString(bytes: any): string {
if (!bytes) return ''
try {
const JString = plus.android.importClass('java.lang.String') as any
return toJsString(new JString(bytes, 'UTF-8'))
} catch (_e) {
try {
return toJsString(plus.android.newObject('java.lang.String', bytes))
} catch (_e2) {
return ''
}
}
}
/** 拆开「同一条码粘成两遍」的情况 */
export function normalizeScanCode(raw: string): string {
const s = String(raw || '')
.replace(/[\r\n\t]+/g, '')
.trim()
if (!s) return ''
// 整串 = A+A
if (s.length >= 6 && s.length % 2 === 0) {
const half = s.length / 2
const a = s.slice(0, half)
const b = s.slice(half)
if (a && a === b) {
log(`检测到粘连双码,已拆半: ${s} -> ${a}`)
return a
}
}
return s
}
function dumpAllExtras(intent: any): string {
const found: string[] = []
const allKeys = Array.from(new Set([...CODE_KEYS, ...TYPE_KEYS, 'length']))
for (const key of allKeys) {
try {
const s = intent.getStringExtra(key)
if (s != null) found.push(`${key}(str)=${toJsString(s)}`)
} catch (_e) {
// ignore
}
try {
const bytes = intent.getByteArrayExtra(key)
if (bytes) found.push(`${key}(bytes)=${bytesToString(bytes)}`)
} catch (_e) {
// ignore
}
try {
const n = intent.getIntExtra(key, -999999)
if (n !== -999999) found.push(`${key}(int)=${n}`)
} catch (_e) {
// ignore
}
}
return found.length ? found.join(' | ') : '(无可用 extra)'
}
function parseBarcode(intent: any): string {
for (const key of CODE_KEYS) {
try {
const s = toJsString(intent.getStringExtra(key))
if (s) return normalizeScanCode(s)
} catch (_e) {
// ignore
}
}
try {
const bytes = intent.getByteArrayExtra('barcode')
const s = bytesToString(bytes)
if (s) return normalizeScanCode(s)
} catch (_e) {
// ignore
}
return ''
}
function parseType(intent: any): string {
for (const key of TYPE_KEYS) {
try {
const s = toJsString(intent.getStringExtra(key))
if (s) return s
} catch (_e) {
// ignore
}
}
return ''
}
function emitBarcode(code: string, type: string, source: string) {
const normalized = normalizeScanCode(code)
if (!normalized) return
const now = Date.now()
// 广播已出结果时,短时间内的键盘结果一律丢弃(避免粘连/重复)
if (source === 'keyboard') {
if (broadcastConfirmed && now - lastBroadcastTime < IGNORE_KEYBOARD_AFTER_BROADCAST_MS) {
log(`忽略键盘重复(广播优先) code=${normalized}`)
return
}
if (broadcastConfirmed) {
log(`忽略键盘(本页已确认广播可用) code=${normalized}`)
return
}
}
if (normalized === lastCode && now - lastTime < DEBOUNCE_MS) {
log(`防抖忽略重复码 source=${source} code=${normalized}`)
return
}
lastCode = normalized
lastTime = now
if (source === 'broadcast') {
lastBroadcastTime = now
broadcastConfirmed = true
}
log(`识别成功 source=${source} code=${normalized} type=${type || '-'}`)
const cb = activeCallback
if (!cb) {
log('警告:无页面回调,条码未展示')
return
}
setTimeout(() => {
try {
cb(normalized, type || undefined, source)
} catch (e) {
log(`回调异常: ${e}`)
}
}, 0)
}
function doReceive(_context: any, intent: any) {
log('>>> onReceive 触发(说明广播已进入 App')
try {
plus.android.importClass(intent)
const action = toJsString(intent.getAction())
log(`action=${action}`)
log(`extras=${dumpAllExtras(intent)}`)
const code = parseBarcode(intent)
if (!code) {
log('广播已收到,但未能解析出条码文本')
return
}
emitBarcode(code, parseType(intent), 'broadcast')
} catch (e) {
log(`onReceive 异常: ${e}`)
}
}
function registerNow(onBarcode: Dt50ScanCallback) {
activeCallback = onBarcode
if (listening && scanReceiver) {
log('已在监听中,仅更新回调')
return
}
if (registering) {
log('正在注册中,跳过重复注册')
return
}
registering = true
const main = plus.android.runtimeMainActivity()
const IntentFilter = plus.android.importClass('android.content.IntentFilter') as any
const filter = new IntentFilter()
SCAN_ACTIONS.forEach((action) => filter.addAction(action))
scanReceiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
onReceive: doReceive,
})
try {
main.registerReceiver(scanReceiver, filter)
listening = true
log(`已注册广播监听 actions=${SCAN_ACTIONS.join(',')}`)
} catch (e) {
scanReceiver = null
listening = false
log(`registerReceiver 失败: ${e}`)
} finally {
registering = false
}
}
export function setDt50ScanLogger(cb: Dt50LogCallback | null) {
logCallback = cb
}
/** 本页是否已确认广播可用(用于关闭键盘楔入) */
export function isDt50BroadcastConfirmed() {
return broadcastConfirmed
}
export function reportDt50ScanCode(code: string, source = 'keyboard') {
emitBarcode(String(code || ''), '', source)
}
export function startDt50ScanListener(onBarcode: Dt50ScanCallback) {
if (!hasPlusAndroid()) {
log('当前环境无 plus.android无法监听扫描头广播')
return
}
registerNow(onBarcode)
}
export function stopDt50ScanListener() {
activeCallback = null
broadcastConfirmed = false
lastBroadcastTime = 0
if (!hasPlusAndroid() || !scanReceiver) {
listening = false
registering = false
scanReceiver = null
return
}
try {
plus.android.runtimeMainActivity().unregisterReceiver(scanReceiver)
log('已取消广播监听')
} catch (e) {
log(`unregisterReceiver 失败: ${e}`)
}
scanReceiver = null
listening = false
registering = false
}

View File

@@ -1247,3 +1247,13 @@ jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/
jeecgboot-vue3/src/views/xslmes/mesXslDryingProcessSwitch/MesXslDryingProcessSwitchPage.vue
jeecgboot-vue3/src/views/xslmes/mesXslDryingProcessSwitch/MesXslDryingProcessSwitch.api.ts
-- author:jiangxh---date:20260708--for: 【MES】烘胶流程开启总开关+二次确认,不校验烘胶房日期) ---
-- author:xsl---date:20260720--for: <20><>APPԭ<50><D4AD><EFBFBD><EFBFBD><EFBFBD>ͼ졿<CDBC>ֳֶ˰<D6B6><CBB0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѯ/<2F>ͼ<EFBFBD><><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӿ<EFBFBD><D3BF><EFBFBD>ҳ<EFBFBD><D2B3> ---
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslRawMaterialInspectAppScanVo.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialInspectAppController.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialInspectRecordService.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialInspectRecordServiceImpl.java
JeecgUniapp-master/src/service/xslmes/rawMaterialInspect.ts
JeecgUniapp-master/src/pages/rawMaterialInspect/rawMaterialInspect.vue
JeecgUniapp-master/src/pages/rawMaterialInspect/inspectResult.vue
JeecgUniapp-master/src/common/work.ts

View File

@@ -0,0 +1,95 @@
package org.jeecg.modules.xslmes.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialInspectRecord;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialInspectRecordLine;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialInspectRecordService;
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialInspectAppScanVo;
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialInspectRecordPage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* APP 原材料送检接口(手持端)
* 判重规则与 PC 一致:存在 inspect_status=0 的送检记录视为已送检。
*/
@Tag(name = "APP-原材料送检")
@RestController
@RequestMapping("/xslmes/app/rawMaterialInspect")
public class MesXslRawMaterialInspectAppController {
@Autowired private IMesXslRawMaterialInspectRecordService inspectRecordService;
//update-begin---author:xsl ---date:20260720 for【APP原材料送检】扫码查询-----------
@AutoLog(value = "APP原材料送检-按条码查询")
@Operation(summary = "按条码查询卡片及送检状态")
@GetMapping("/queryByBarcode")
public Result<MesXslRawMaterialInspectAppScanVo> queryByBarcode(
@RequestParam(name = "barcode") String barcode) {
if (StringUtils.isBlank(barcode)) {
return Result.error("条码不能为空");
}
return Result.OK(inspectRecordService.queryByBarcodeForApp(barcode.trim()));
}
//update-end---author:xsl ---date:20260720 for【APP原材料送检】扫码查询-----------
//update-begin---author:xsl ---date:20260720 for【APP原材料送检】按条码送检-----------
@AutoLog(value = "APP原材料送检-按条码送检")
@Operation(summary = "按条码送检")
@PostMapping("/createByBarcode")
public Result<MesXslRawMaterialInspectRecord> createByBarcode(@RequestBody Map<String, Object> body) {
String barcode = body.get("barcode") == null ? "" : body.get("barcode").toString();
if (StringUtils.isBlank(barcode)) {
return Result.error("条码不能为空");
}
MesXslRawMaterialInspectRecord record = inspectRecordService.createByBarcode(barcode.trim());
return Result.OK("送检成功", record);
}
//update-end---author:xsl ---date:20260720 for【APP原材料送检】按条码送检-----------
//update-begin---author:xsl ---date:20260720 for【APP原材料送检】录入检验结果-----------
@AutoLog(value = "APP原材料送检-加载检验明细")
@Operation(summary = "录入检验结果前加载明细")
@PostMapping("/prepareResultEntry")
public Result<List<MesXslRawMaterialInspectRecordLine>> prepareResultEntry(
@RequestBody Map<String, Object> body) {
String id = body.get("id") == null ? "" : body.get("id").toString();
if (StringUtils.isBlank(id)) {
return Result.error("id不能为空");
}
return Result.OK(inspectRecordService.prepareResultEntryLines(id.trim()));
}
@AutoLog(value = "APP原材料送检-保存检验结果")
@Operation(summary = "保存检验结果")
@PostMapping("/saveInspectResult")
public Result<String> saveInspectResult(@RequestBody MesXslRawMaterialInspectRecordPage page) {
if (StringUtils.isBlank(page.getId())) {
return Result.error("id不能为空");
}
inspectRecordService.saveInspectResult(page.getId(), page.getLineList());
return Result.OK("保存成功");
}
@Operation(summary = "按送检记录ID查询主表")
@GetMapping("/queryRecordById")
public Result<MesXslRawMaterialInspectRecord> queryRecordById(@RequestParam(name = "id") String id) {
MesXslRawMaterialInspectRecord db = inspectRecordService.getById(id);
if (db == null) {
return Result.error("未找到对应数据");
}
return Result.OK(db);
}
//update-end---author:xsl ---date:20260720 for【APP原材料送检】录入检验结果-----------
}

View File

@@ -23,4 +23,16 @@ public interface IMesXslRawMaterialInspectRecordService
Integer includeMinFlag,
BigDecimal allowMax,
Integer includeMaxFlag);
//update-begin---author:xsl ---date:20260720 for【APP原材料送检】按条码查询/送检-----------
/**
* APP按条码查询卡片及送检状态是否已有待检送检记录
*/
org.jeecg.modules.xslmes.vo.MesXslRawMaterialInspectAppScanVo queryByBarcodeForApp(String barcode);
/**
* APP按条码发起送检
*/
MesXslRawMaterialInspectRecord createByBarcode(String barcode);
//update-end---author:xsl ---date:20260720 for【APP原材料送检】按条码查询/送检-----------
}

View File

@@ -324,4 +324,69 @@ public class MesXslRawMaterialInspectRecordServiceImpl
private String normalize(String value) {
return StringUtils.trimToEmpty(value);
}
//update-begin---author:xsl ---date:20260720 for【APP原材料送检】按条码查询/送检-----------
@Override
public org.jeecg.modules.xslmes.vo.MesXslRawMaterialInspectAppScanVo queryByBarcodeForApp(String barcode) {
String code = normalize(barcode);
if (StringUtils.isBlank(code)) {
throw new JeecgBootException("条码不能为空");
}
MesXslRawMaterialCard card =
rawMaterialCardService
.lambdaQuery()
.eq(MesXslRawMaterialCard::getBarcode, code)
.last("LIMIT 1")
.one();
if (card == null) {
throw new JeecgBootException("未找到条码对应的原材料卡片");
}
MesXslRawMaterialInspectRecord pending =
this.lambdaQuery()
.eq(MesXslRawMaterialInspectRecord::getRawMaterialCardId, card.getId())
.eq(MesXslRawMaterialInspectRecord::getInspectStatus, STATUS_PENDING)
.orderByDesc(MesXslRawMaterialInspectRecord::getInspectTime)
.last("LIMIT 1")
.one();
org.jeecg.modules.xslmes.vo.MesXslRawMaterialInspectAppScanVo vo =
new org.jeecg.modules.xslmes.vo.MesXslRawMaterialInspectAppScanVo();
vo.setCard(card);
if (pending != null) {
// 已有待检送检记录 → 视为已送检,进入录入检验结果
vo.setHasPendingInspect(true);
vo.setPendingRecordId(pending.getId());
vo.setAction("enterResult");
return vo;
}
vo.setHasPendingInspect(false);
String cardTestResult = normalize(card.getTestResult());
if (StringUtils.isBlank(cardTestResult) || CARD_TEST_RESULT_UNTESTED.equals(cardTestResult)) {
vo.setAction("sendInspect");
} else {
// 已合格/不合格且无待检记录
vo.setAction("finished");
}
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public MesXslRawMaterialInspectRecord createByBarcode(String barcode) {
String code = normalize(barcode);
if (StringUtils.isBlank(code)) {
throw new JeecgBootException("条码不能为空");
}
MesXslRawMaterialCard card =
rawMaterialCardService
.lambdaQuery()
.eq(MesXslRawMaterialCard::getBarcode, code)
.last("LIMIT 1")
.one();
if (card == null) {
throw new JeecgBootException("未找到条码对应的原材料卡片");
}
return createByRawMaterialCardId(card.getId());
}
//update-end---author:xsl ---date:20260720 for【APP原材料送检】按条码查询/送检-----------
}

View File

@@ -0,0 +1,30 @@
package org.jeecg.modules.xslmes.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import lombok.Data;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
/**
* APP 原材料送检扫码查询结果
*/
@Data
@Schema(description = "APP原材料送检扫码结果")
public class MesXslRawMaterialInspectAppScanVo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "原材料卡片")
private MesXslRawMaterialCard card;
/**
* sendInspect=可送检enterResult=已送检待录入finished=已完成检验
*/
@Schema(description = "下一步动作sendInspect/enterResult/finished")
private String action;
@Schema(description = "是否存在待检送检记录")
private Boolean hasPendingInspect;
@Schema(description = "待检送检记录IDaction=enterResult 时有值)")
private String pendingRecordId;
}

View File

@@ -17,7 +17,8 @@
"name": "打印调节器"
},
{
"path": "JeecgUniapp-master"
"path": "JeecgUniapp-master",
"name": "APP端"
}
],
"settings": {