Pārlūkot izejas kodu

feat(photography): 集成电子秤称重功能

- 新增 useScale composable 管理电子秤连接、轮询及锁定逻辑
- 在拍摄页面添加电子秤实时重量显示与锁定/解锁操作栏
- 拍摄时将锁定重量附加到 run_mcu 请求数据中
- 在图片处理列表和拍摄记录中展示商品重量信息
- 电子秤高频轮询消息静默处理,避免控制台日志刷屏
- 在 socket 消息类型白名单中添加 anheng_scale
panqiuyao 1 nedēļu atpakaļ
vecāks
revīzija
ac09d88f7f

+ 13 - 6
electron/utils/socket.js

@@ -239,7 +239,7 @@ const pySocket = function () {
         try {
           let this_data = JSON.parse(data.toString());
 
-          if(!['blue_tooth','smart_shooter_enable_preview','smart_shooter_getinfo'].includes(this_data.msg_type)){
+          if(!['blue_tooth','smart_shooter_enable_preview','smart_shooter_getinfo','anheng_scale'].includes(this_data.msg_type)){
             console.log('message');
             console.log(this_data);
           //  Log.info(this_data);
@@ -329,16 +329,23 @@ const pySocket = function () {
     this.sendMessage(message);
   }
   this.sendMessage = async function (message) {
-    console.log('socket.=========================sendMessage');
-    console.log('socket.sendMessage');
-    console.log(message);
-    console.log(app.socket?.readyState);
+    let silent = false
+    try {
+      const parsed = typeof message === 'string' ? JSON.parse(message) : message
+      silent = parsed?.type === 'get_scale_data'
+    } catch (e) {}
+    if (!silent) {
+      console.log('socket.=========================sendMessage');
+      console.log('socket.sendMessage');
+      console.log(message);
+      console.log(app.socket?.readyState);
+    }
     if(!app.socket){
       await  this.init()
     }
     // 检查连接状态
     if (app.socket?.readyState === WebSocket.OPEN) {
-      console.log('send');
+      if (!silent) console.log('send');
       app.socket.send(message); // 使用 send() 发送
     }
   }

+ 141 - 0
frontend/src/composables/useScale.ts

@@ -0,0 +1,141 @@
+import { computed, ref } from 'vue'
+import client from '@/stores/modules/client'
+import socket from '@/stores/modules/socket'
+import icpList from '@/utils/ipc'
+
+const SCALE_CHANNEL = icpList.socket.message + '_anheng_scale'
+const STATUS_CHANNEL = icpList.socket.message + '_anheng_scale_status'
+const POLL_MS = 300
+
+const everConnected = ref(false)
+const status = ref<'unknown' | 'absent' | 'connected' | 'disconnected'>('unknown')
+const stable = ref<boolean | null>(null)
+const netGrams = ref<number | null>(null)
+const lockedGrams = ref<number | null>(null)
+
+let pollTimer: ReturnType<typeof setInterval> | null = null
+let listening = false
+
+export function formatScaleWeight(value: unknown): string {
+  if (value == null || value === '' || value === 'None' || value === 'null' || value === 'undefined') {
+    return ''
+  }
+  const grams = typeof value === 'number' ? value : Number(String(value).trim())
+  if (!Number.isFinite(grams)) return ''
+  if (Math.abs(grams) >= 1000) return `${(grams / 1000).toFixed(3)} kg`
+  return `${grams.toFixed(1)} g`
+}
+
+function requestScale() {
+  socket().sendMessage({ type: 'get_scale_data' })
+}
+
+function stopPoll() {
+  if (pollTimer != null) {
+    clearInterval(pollTimer)
+    pollTimer = null
+  }
+}
+
+function markConnected() {
+  if (!listening) return
+  everConnected.value = true
+  status.value = 'connected'
+  if (pollTimer != null) return
+  requestScale()
+  pollTimer = setInterval(requestScale, POLL_MS)
+}
+
+function markLost() {
+  if (!everConnected.value) {
+    status.value = 'absent'
+    stopPoll()
+    return
+  }
+  status.value = 'disconnected'
+  stable.value = null
+  netGrams.value = null
+  stopPoll()
+}
+
+function onScaleData(_event: unknown, result: any) {
+  if (!listening || !result || result.code !== 0 || !result.data) return
+  const grams = Number(result.data.net_grams)
+  stable.value = result.data.stable === true ? true : result.data.stable === false ? false : null
+  netGrams.value = Number.isFinite(grams) ? grams : null
+  markConnected()
+}
+
+function onScaleStatus(_event: unknown, result: any) {
+  if (!listening) return
+  const next = result?.data?.status
+  if (next === 'connected' || next === 'reconnected') {
+    markConnected()
+    return
+  }
+  if (next === 'disconnected' || next === 'reconnecting' || next === 'closed' || result?.code === 1) {
+    markLost()
+  }
+}
+
+export function peekLockedWeight(): number | null {
+  const grams = lockedGrams.value
+  if (grams == null || !Number.isFinite(grams)) return null
+  return grams
+}
+
+export function clearLockedWeight() {
+  lockedGrams.value = null
+}
+
+function lockScaleWeight() {
+  if (status.value !== 'connected' || stable.value !== true) return
+  if (netGrams.value == null || !Number.isFinite(netGrams.value)) return
+  lockedGrams.value = Math.round(netGrams.value * 10) / 10
+}
+
+function unlockScaleWeight() {
+  lockedGrams.value = null
+}
+
+async function startScale() {
+  const clientStore = client()
+  if (!clientStore.isClient || !clientStore.ipc || listening) return
+  listening = true
+  await socket().connectSocket()
+  if (!listening) return
+  clientStore.ipc.removeAllListeners(SCALE_CHANNEL)
+  clientStore.ipc.removeAllListeners(STATUS_CHANNEL)
+  clientStore.ipc.on(SCALE_CHANNEL, onScaleData)
+  clientStore.ipc.on(STATUS_CHANNEL, onScaleStatus)
+  requestScale()
+}
+
+function stopScale() {
+  const ipc = client().ipc
+  stopPoll()
+  if (ipc?.removeListener) {
+    ipc.removeListener(SCALE_CHANNEL, onScaleData)
+    ipc.removeListener(STATUS_CHANNEL, onScaleStatus)
+  }
+  listening = false
+  everConnected.value = false
+  status.value = 'unknown'
+  stable.value = null
+  netGrams.value = null
+  lockedGrams.value = null
+}
+
+export function useScale() {
+  return {
+    everConnected,
+    connected: computed(() => status.value === 'connected'),
+    stable,
+    liveText: computed(() => formatScaleWeight(netGrams.value)),
+    lockedText: computed(() => formatScaleWeight(lockedGrams.value)),
+    startScale,
+    stopScale,
+    lockScaleWeight,
+    unlockScaleWeight,
+  }
+}

+ 4 - 2
frontend/src/stores/modules/socket.ts

@@ -44,8 +44,10 @@ async function connectSocket(){
 
     async function sendMessage(data){
         if(!clientStore.isClient) return;
-        console.log('sendMessage');
-        console.log(data);
+        if (data?.type !== 'get_scale_data') {
+            console.log('sendMessage');
+            console.log(data);
+        }
         new  Promise((resolve,reject)=>{
             clientStore.ipc.removeAllListeners(icpList.socket.sendMessage);
             clientStore.ipc.send(icpList.socket.sendMessage,JSON.stringify(data));

+ 8 - 2
frontend/src/views/Photography/mixin/usePhotography.ts

@@ -14,6 +14,7 @@ import configInfo from '@/stores/modules/config';
 import tokenInfo from "@/stores/modules/token";
 import { getAllUserConfigs, getTopTabs } from '@/apis/setting';
 import { registerTakePhotoTrigger, type TakePhotoParams } from '@/utils/takePhotoApi';
+import { peekLockedWeight, clearLockedWeight } from '@/composables/useScale';
 
 export default function usePhotography() {
     const loading = ref(false)
@@ -219,9 +220,10 @@ export default function usePhotography() {
 
       await socketStore.connectSocket();
 
+      const weight = peekLockedWeight()
       socketStore.sendMessage({
         type: 'run_mcu',
-        data,
+        data: weight == null ? data : { ...data, weight },
       })
       runLoading.value = true;
       runAction.value.action = data.action
@@ -248,7 +250,9 @@ export default function usePhotography() {
           ElMessage.error(result.msg)
           runLoading.value = false
           return;
-        } else if (!isDemoMode.value) {
+        }
+        if (result.code === 0) clearLockedWeight()
+        if (!isDemoMode.value) {
           ElMessage.success('开始拍摄,请稍后')
         }
       }
@@ -834,6 +838,7 @@ const onRemoteControl = async (type) => {
       clientStore.ipc.on(icpList.socket.message + '_image_process', (event, result) => {
         console.log('_image_process')
         console.log(result)
+        if (result?.code === 0) clearLockedWeight()
         getPhotoRecords()
         // 延迟两秒再获取一遍数据
         setTimeout(() => {
@@ -893,6 +898,7 @@ const onRemoteControl = async (type) => {
           runLoading.value = false;
           runAction.value.goods_art_no = '';
           runAction.value.action = '';
+          clearLockedWeight()
 
           if (isDemoMode.value && !demoStopping.value) {
             continueDemoLoop(finishedGoodsNo || demoGoodsArtNo.value)

+ 2 - 0
frontend/src/views/Photography/processImage.vue

@@ -53,6 +53,7 @@
                     <span class="image-count mar-left-10 flex left">
                        <img src="@/assets/images/processImage.vue/tup.png" />
                       {{ item.items?.length || 0 }}张图片</span>
+                    <span v-if="formatScaleWeight(item.extras?.weight)" class="image-count mar-left-10">重量:{{ formatScaleWeight(item.extras?.weight).replace(' ', '') }}</span>
                   </div>
                 </div>
                 <div class="history-item-right">
@@ -167,6 +168,7 @@ import usePhotography from './mixin/usePhotography'
 import { useThumbnails } from './composables/useThumbnails'
 import ImagePreview, { PreviewItem } from '@/components/PreviewImage/index.vue'
 import { ElMessageBox } from 'element-plus'
+import { formatScaleWeight } from '@/composables/useScale'
 import client from "@/stores/modules/client";
 import icpList from '@/utils/ipc'
 // logging helpers not needed here

+ 69 - 0
frontend/src/views/Photography/shot.vue

@@ -57,6 +57,26 @@
               <div class="input-container flex-row">
                 <el-input class="input-item" ref="goodsArtNo" clearable v-model="goods_art_no" placeholder="请输入货号"> </el-input>
               </div>
+              <div v-if="scaleEverConnected" class="scale-bar">
+                <template v-if="scaleConnected">
+                  <span>电子秤</span>
+                  <span class="scale-value" :class="{ 'is-unstable': scaleStable !== true }">{{ scaleLiveText || '—' }}</span>
+                  <span class="scale-state">{{ scaleStable === true ? '稳定' : '抖动' }}</span>
+                </template>
+                <span v-else class="scale-off">电子秤已断开</span>
+                <template v-if="scaleLockedText">
+                  <span class="scale-locked">已锁定 {{ scaleLockedText }}</span>
+                  <el-button size="small" link type="primary" :disabled="runLoading || takePictureLoading" @click="unlockScaleWeight">取消</el-button>
+                </template>
+                <el-button
+                  v-else-if="scaleConnected"
+                  size="small"
+                  type="primary"
+                  plain
+                  :disabled="scaleStable !== true || runLoading || takePictureLoading"
+                  @click="lockScaleWeight"
+                >{{ scaleStable === true ? '记录此重量' : '等待稳定' }}</el-button>
+              </div>
               <div class="auto-method flex-row justify-between">
                 <img class="step-icon" referrerpolicy="no-referrer" src="@/assets/images/Photography/step1-icon.png" />
                 <div class="text-method-tag flex-col mar-left-10"><span class="text_4">自动获取</span></div>
@@ -140,6 +160,7 @@
                     <span class="image-count mar-left-10 flex left">
                        <img src="@/assets/images/processImage.vue/tup.png" />
                       {{ item.items?.length || 0 }}张图片</span>
+                    <span v-if="formatScaleWeight(item.extras?.weight)" class="image-count mar-left-10">重量:{{ formatScaleWeight(item.extras?.weight).replace(' ', '') }}</span>
                     <span v-if="!item.syncConfig" class="mar-left-10">无法读取到该商品拍摄配置,如需重新生成请删除该商品并重新拍摄</span>
                   </div>
                 </div>
@@ -325,8 +346,20 @@ import ImagePreview, { PreviewItem } from '@/components/PreviewImage/index.vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import useUserInfo from '@/stores/modules/user'
 import socket from '@/stores/modules/socket'
+import { formatScaleWeight, useScale } from '@/composables/useScale'
 
 const socketStore = socket()
+const {
+  everConnected: scaleEverConnected,
+  connected: scaleConnected,
+  stable: scaleStable,
+  liveText: scaleLiveText,
+  lockedText: scaleLockedText,
+  startScale,
+  stopScale,
+  lockScaleWeight,
+  unlockScaleWeight,
+} = useScale()
 
 const userInfoStore = useUserInfo()
 const isHLM = computed(() => userInfoStore.isHLM)
@@ -628,6 +661,7 @@ const openPreview = (item: any, index: number) => {
 onMounted(async () => {
   await getPhotoRecords()
   initEventListeners()
+  startScale()
   nextTick(() => {
     observe(containerRef, goodsList)
   })
@@ -635,6 +669,7 @@ onMounted(async () => {
 
 onBeforeUnmount(() => {
   cleanupEventListeners()
+  stopScale()
   stop()
 })
 
@@ -840,6 +875,40 @@ watch(goodsList, () => {
             }
           }
 
+          .scale-bar {
+            display: flex;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 8px;
+            width: calc(100% - 20px);
+            margin: 10px 10px 0;
+            padding: 6px 10px;
+            background: #f5f7fb;
+            border-radius: 6px;
+            font-size: 13px;
+            color: #333;
+            box-sizing: border-box;
+
+            .scale-value {
+              font-weight: 600;
+              color: #1677ff;
+
+              &.is-unstable {
+                color: #d48806;
+              }
+            }
+
+            .scale-state,
+            .scale-off {
+              font-size: 12px;
+              color: #999;
+            }
+
+            .scale-locked {
+              color: #1677ff;
+            }
+          }
+
           .auto-method {
             width: 253px;
             height: 24px;