|
|
@@ -1,99 +1,961 @@
|
|
|
<template>
|
|
|
- <div>
|
|
|
- <button @click="_openCamera">打开相机</button>
|
|
|
- <button @click="_openGallery">打开相册选择图片并返回路径</button>
|
|
|
- <button @click="_openImageEditor">打开图片编辑器</button>
|
|
|
-
|
|
|
- <!-- 显示图片的容器 -->
|
|
|
- <div v-if="imageUrl" class="image-preview">
|
|
|
- <img :src="imageUrl" alt="Selected Image" />
|
|
|
+ <van-overlay :show="!isOnline">
|
|
|
+ <div class="wrapper">
|
|
|
+ <!-- 断网提示(固定在顶部) -->
|
|
|
+ <div v-if="!isOnline" class="network-alert offline">
|
|
|
+ ⚠️ 网络已断开,请检查您的网络连接!
|
|
|
+ </div>
|
|
|
+ <!-- 网络恢复提示 -->
|
|
|
+ <Transition name="fade">
|
|
|
+ <div v-if="showReconnected" class="network-alert online">
|
|
|
+ ✅ 网络已恢复
|
|
|
+ </div>
|
|
|
+ </Transition>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </van-overlay>
|
|
|
+ <van-row class="container">
|
|
|
+ <van-col span="6" class="user-panel">
|
|
|
+ <div class="user-header">
|
|
|
+ <h3>用户信息</h3>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="info-grid">
|
|
|
+<!-- <div class="info-item">-->
|
|
|
+<!-- <div class="info-label">用户ID</div>-->
|
|
|
+<!-- <div class="info-value">{{ userInfo.userId }}</div>-->
|
|
|
+<!-- </div>-->
|
|
|
+
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">用户姓名</div>
|
|
|
+ <div class="info-value" :style="{ color: !userInfo.name ? 'red' : '#1e1f1f' }">{{ userInfo.name || '请登录' }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">MAC地址</div>
|
|
|
+ <div class="info-value">{{ mac }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">仓库</div>
|
|
|
+ <div class="info-value">{{ frogPosition.warehouse }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">位置编号</div>
|
|
|
+ <div class="info-value">{{ frogPosition.code }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">用途类型</div>
|
|
|
+ <div class="info-value">{{ frogPosition.type }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="error-section">
|
|
|
+ <div class="error-display">
|
|
|
+ <div class="error-label" @click="logout">退出登录</div>
|
|
|
+ <!-- <div class="error-count">{{ errNum }}</div>-->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </van-col>
|
|
|
+ <van-col span="18" class="table-container">
|
|
|
+ <van-cell-group class="custom-cell-group">
|
|
|
+ <van-cell title="系统提示:" :value="message" :value-class="{'success-message': message && message.includes('扫描成功')}"/>
|
|
|
+ </van-cell-group>
|
|
|
+
|
|
|
+ <div class="waterfall-table">
|
|
|
+ <!-- 固定表头 -->
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="col index">序号</div>
|
|
|
+ <div class="col time">时间</div>
|
|
|
+ <div class="col operator">操作人</div>
|
|
|
+ <div class="col content">操作内容</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <van-pull-refresh
|
|
|
+ ref="pullRefreshRef"
|
|
|
+ v-model="refreshing"
|
|
|
+ success-text="刷新成功"
|
|
|
+ @refresh="onRefresh"
|
|
|
+ class="table-body"
|
|
|
+ >
|
|
|
+ <!-- 表格主体 -->
|
|
|
+ <van-list
|
|
|
+ v-model:loading="loading"
|
|
|
+ :finished="finished"
|
|
|
+ finished-text="没有更多了"
|
|
|
+ @load="onLoad">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in list"
|
|
|
+ :key="item.id"
|
|
|
+ class="table-row"
|
|
|
+ >
|
|
|
+ <div class="col index">{{ index + 1 }}</div>
|
|
|
+ <div class="col time">{{ item.operationTime.replace(/^\d{4}-/, '') }}</div>
|
|
|
+ <div class="col operator">{{ item.operatorName }}</div>
|
|
|
+ <div class="col content">{{ item.deliveryNo }}</div>
|
|
|
+ </div>
|
|
|
+ <van-back-top @click="scrollTop"/>
|
|
|
+ </van-list>
|
|
|
+ </van-pull-refresh>
|
|
|
+ </div>
|
|
|
+ </van-col>
|
|
|
+ </van-row>
|
|
|
+ <van-button class="add" icon="setting-o" type="primary" @click="_openSetting" />
|
|
|
+ <van-sticky>
|
|
|
+ <van-dialog
|
|
|
+ v-model:show="setting.show"
|
|
|
+ title="设置"
|
|
|
+ class="setting-dialog"
|
|
|
+ show-cancel-button
|
|
|
+ :before-close="_verifyMac"
|
|
|
+ @confirm="_saveMac"
|
|
|
+ @closed="_closeSetting"
|
|
|
+ width="380px">
|
|
|
+
|
|
|
+ <van-notice-bar
|
|
|
+ wrapable
|
|
|
+ left-icon="volume-o"
|
|
|
+ :scrollable="false"
|
|
|
+ :text="hintMessage"
|
|
|
+ />
|
|
|
+
|
|
|
+ <van-cell-group inset>
|
|
|
+ <van-field
|
|
|
+ v-model="setting.mac"
|
|
|
+ label="MAC地址"
|
|
|
+ placeholder="请输入MAC地址"
|
|
|
+ />
|
|
|
+ <van-field
|
|
|
+ v-model="setting.password"
|
|
|
+ type="password"
|
|
|
+ label="密码"
|
|
|
+ placeholder="设置密码"
|
|
|
+ />
|
|
|
+ </van-cell-group>
|
|
|
+
|
|
|
+ <van-notice-bar color="#1989fa" background="#ecf9ff" @click="checkUpdate"
|
|
|
+ style="font-size: 16px; padding-left: 10px" left-icon="info-o">
|
|
|
+ 版本号:{{ versionName }} (点击检查更新)
|
|
|
+ </van-notice-bar>
|
|
|
+ </van-dialog>
|
|
|
+ </van-sticky>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref } from 'vue'
|
|
|
+import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
+import { closeListener, openListener, scanInit } from '@/utils/keydownListener'
|
|
|
+import { scanSuccess, scanError } from '@/utils/android'
|
|
|
+import { getVersionName, checkUpdate, saveUserId, getUserId, scanRepeat, saveMacAddress, listErrRecords, reLoginTip,
|
|
|
+ readMacAddress, pageDelivery, addDelivery, isDeliveryNoExists, markAsPushed, getErrRecordsCount, removeUserId, scanErr } from '@/utils/androidPiece'
|
|
|
+import { showLoadingToast, showNotify } from 'vant'
|
|
|
+import { getUserIdByCert, getUserNameById } from '@/api/login/index'
|
|
|
+import { receive, getScanDriverInfo } from '@/api/scan/index'
|
|
|
+import { formatDateTime } from '@/utils/date'
|
|
|
+
|
|
|
+// 在输入对话框或界面中添加提示文本
|
|
|
+const hintMessage = ref(`请前往:设置 → 关于平板电脑 → 设备WLAN MAC 地址 长按复制并粘贴到此处。`);
|
|
|
+const mac = ref('')
|
|
|
+const userInfo = ref({
|
|
|
+ userId: '',
|
|
|
+ name: ''
|
|
|
+})
|
|
|
+const setting = ref({
|
|
|
+ show: false,
|
|
|
+ mac: '',
|
|
|
+ password: ''
|
|
|
+})
|
|
|
+const message = ref('')
|
|
|
+const list = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+const finished = ref(false)
|
|
|
+const errNum = ref(0)
|
|
|
+const preDeliveryNo = ref('')
|
|
|
|
|
|
-const imagePath = ref('')
|
|
|
-const imageUrl = ref('')
|
|
|
+// 小青蛙位置属性
|
|
|
+const frogPosition = ref({
|
|
|
+ warehouse: '',
|
|
|
+ type:'',
|
|
|
+ code:''
|
|
|
+})
|
|
|
|
|
|
-const _openCamera = () => {
|
|
|
- if (!window.android) {
|
|
|
- alert('Android 接口不可用')
|
|
|
+
|
|
|
+const isOnline = ref(navigator.onLine)
|
|
|
+// 更新网络状态
|
|
|
+const updateNetworkStatus = () => {
|
|
|
+ isOnline.value = navigator.onLine
|
|
|
+}
|
|
|
+
|
|
|
+// 退出登录
|
|
|
+const logout = () => {
|
|
|
+ if (!userInfo.value.userId) {
|
|
|
+ scanError()
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '请先登录!' });
|
|
|
return
|
|
|
}
|
|
|
- window.android.openCamera()
|
|
|
+ userInfo.value = {}
|
|
|
+ removeUserId()
|
|
|
+ scanSuccess()
|
|
|
+ showNotify({ type: 'success', style: 'font-size: 30px !important;height:50px', message: '已退出登录!' });
|
|
|
}
|
|
|
|
|
|
-const _openGallery = () => {
|
|
|
- if (!window.android) {
|
|
|
- alert('Android 接口不可用')
|
|
|
- return
|
|
|
+// 网络提示
|
|
|
+const showReconnected = ref(false)
|
|
|
+let reconnectTimer = null
|
|
|
+watch(isOnline, (newVal, oldVal) => {
|
|
|
+ // 当网络从离线变为在线时
|
|
|
+ if (newVal && !oldVal) {
|
|
|
+ scanSuccess()
|
|
|
+ showReconnected.value = true
|
|
|
+
|
|
|
+ // 3秒后自动隐藏恢复提示
|
|
|
+ reconnectTimer = setTimeout(() => {
|
|
|
+ showReconnected.value = false
|
|
|
+ }, 3000)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 当网络从在线变为离线时,隐藏恢复提示
|
|
|
+ if (!newVal && oldVal) {
|
|
|
+ scanError()
|
|
|
+ showReconnected.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('online', updateNetworkStatus)
|
|
|
+ window.removeEventListener('offline', updateNetworkStatus)
|
|
|
+ closeListener()
|
|
|
+})
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ window.addEventListener('online', updateNetworkStatus)
|
|
|
+ window.addEventListener('offline', updateNetworkStatus)
|
|
|
+
|
|
|
+ versionName.value = getVersionName()
|
|
|
+ mac.value = readMacAddress()
|
|
|
+ // 获取用户信息
|
|
|
+ if (!userInfo.value.userId) {
|
|
|
+ const userId = getUserId()
|
|
|
+ if (userId) {
|
|
|
+ userInfo.value.userId = userId
|
|
|
+ getUserName(userId)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 获取机器位置信息
|
|
|
+ if (!frogPosition.value.warehouse && mac.value) {
|
|
|
+ getFrogPosition()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mac.value) {
|
|
|
+ _openScan()
|
|
|
+ } else {
|
|
|
+ _openSetting()
|
|
|
+ }
|
|
|
+ // ip
|
|
|
+ // fetchIP()
|
|
|
+ // err数量
|
|
|
+ errNum.value = getErrNum()
|
|
|
+})
|
|
|
+
|
|
|
+const getFrogPosition = () => {
|
|
|
+ getScanDriverInfo(mac.value).then(res => {
|
|
|
+ if (res && res.data) {
|
|
|
+ frogPosition.value.warehouse = res.data.warehouseCode
|
|
|
+ frogPosition.value.type = res.data.type
|
|
|
+ frogPosition.value.code = res.data.code
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const _openScan = () => {
|
|
|
+ openListener()
|
|
|
+ setTimeout(() => {
|
|
|
+ scanInit(debounceScan)
|
|
|
+ },300)
|
|
|
+}
|
|
|
+
|
|
|
+const pushing = ref(false)
|
|
|
+const pullRefreshRef = ref(null)
|
|
|
+const scanDebounceTimeout = ref(null)
|
|
|
+
|
|
|
+const debounceScan = (code) => {
|
|
|
+ // 清除之前的防抖计时器
|
|
|
+ if (scanDebounceTimeout.value) {
|
|
|
+ clearTimeout(scanDebounceTimeout.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置新的防抖计时器
|
|
|
+ scanDebounceTimeout.value = setTimeout(() => {
|
|
|
+ _handlerScan(code)
|
|
|
+ }, 300) // 300ms 防抖时间
|
|
|
+}
|
|
|
+const _handlerScan = (code) => {
|
|
|
+ code = fixDuplicateText(code)
|
|
|
+ // 校验扫描的是否为登录二维码
|
|
|
+ const regex = /\{"sign":"([^"]*)",\s*"identity":(\d+)\}/;
|
|
|
+ if (code !== null && regex.test(code)) {
|
|
|
+ const match = regex.exec(code)
|
|
|
+ const sign = match[1]
|
|
|
+ const identity = match[2]
|
|
|
+ getUserIdByCert({sign: sign, identity: identity}).then(res => {
|
|
|
+ if (res && res.data) {
|
|
|
+ userInfo.value.userId = res.data
|
|
|
+ getUserName(res.data)
|
|
|
+ } else {
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '登录失败!' });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 检查长度是否在11到25个字符之间
|
|
|
+ if (typeof code !== 'string' || code.length < 11 || code.length > 25) {
|
|
|
+ console.log(code)
|
|
|
+ scanErr()
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '请扫描正确的快递单号!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 数字0-9,大写字母A-Z,部分特殊字符(空格、! " % & ' ( ) * + , - . / : ; < = > ? _)
|
|
|
+ const code128BPattern = /^[\x20-\x7F]+$/;
|
|
|
+ // 新增:过滤以430300开头的正则
|
|
|
+ const startsWithPattern = /^430300.*/;
|
|
|
+ // 检查是否只包含有效字符
|
|
|
+ if (!code128BPattern.test(code) || startsWithPattern.test(code)) {
|
|
|
+ console.log(code)
|
|
|
+ scanErr()
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '请扫描正确的快递单号!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ scrollTop()
|
|
|
+ // 校验是否已登录
|
|
|
+ if (!userInfo.value.userId) {
|
|
|
+ scanError()
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '请先登录!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 校验是否为空
|
|
|
+ if (!code) {
|
|
|
+ scanErr()
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '请先扫描快递单号!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pushing.value) {
|
|
|
+ scanErr()
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '正在提交中,请稍后扫描!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ pushing.value = true // 请求状态锁
|
|
|
+
|
|
|
+ // 校验是否已扫描
|
|
|
+ if (list.value.some(item => item.code === code)) {
|
|
|
+ scanRepeat()
|
|
|
+ pushing.value = false
|
|
|
+ message.value = '该快递单已扫描!'
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '该快递单已扫描!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const exists = isDeliveryNoExists(code)
|
|
|
+ if (!exists) {
|
|
|
+ scanRepeat()
|
|
|
+ pushing.value = false
|
|
|
+ message.value = '该快递单已扫描!'
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '该快递单已扫描!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const toast = showLoadingToast({
|
|
|
+ message: '上传中...',
|
|
|
+ forbidClick: true,
|
|
|
+ duration: 0 // 禁止自动关闭
|
|
|
+ });
|
|
|
+ // 校验是否已存在
|
|
|
+ const currentTime = formatDateTime(new Date())
|
|
|
+ message.value = ''
|
|
|
+ const dto = {
|
|
|
+ deliveryNo: code,
|
|
|
+ machine: mac.value,
|
|
|
+ operator: userInfo.value.userId,
|
|
|
+ operationTime: currentTime,
|
|
|
+ operatorName: userInfo.value.name,
|
|
|
+ isPush: 1,
|
|
|
+ preDeliveryNo: preDeliveryNo.value,
|
|
|
+ version: versionName.value }
|
|
|
+
|
|
|
+ receive(dto).then(res => {
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ const result = addDelivery(mac.value, code, userInfo.value.userId, userInfo.value.name)
|
|
|
+ if (result === -1) {
|
|
|
+ scanRepeat()
|
|
|
+ message.value = '该快递单已扫描!'
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '该快递单已扫描!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (result === -2) {
|
|
|
+ scanErr()
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '添加失败,请联系开发人员!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ preDeliveryNo.value = code
|
|
|
+ scanSuccess()
|
|
|
+ message.value = code + '扫描成功!'
|
|
|
+ list.value.unshift(dto)
|
|
|
+ showNotify({ type: 'success', style: 'font-size: 30px !important;height:50px', message: '扫描成功!' });
|
|
|
+ }
|
|
|
+ }).catch(err => {
|
|
|
+ if (err.code === 700) {
|
|
|
+ userInfo.value = {}
|
|
|
+ reLoginTip()
|
|
|
+ } else {
|
|
|
+ scanErr()
|
|
|
+ }
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: `扫描失败!${err.message}` });
|
|
|
+ }).finally(() => {
|
|
|
+ toast.close();
|
|
|
+ pushing.value = false
|
|
|
+ })
|
|
|
}
|
|
|
- window.android.openGallery()
|
|
|
}
|
|
|
|
|
|
-const _openImageEditor = () => {
|
|
|
- if (!window.android) {
|
|
|
- alert('Android 接口不可用')
|
|
|
+const scrollTop = () => {
|
|
|
+ // 添加滚动到顶部的逻辑
|
|
|
+ nextTick(() => {
|
|
|
+ const el = pullRefreshRef.value?.$el;
|
|
|
+ if (el && el.scrollTo) {
|
|
|
+ el.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
+ } else if (el) {
|
|
|
+ // 回退方案
|
|
|
+ el.scrollTop = 0;
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+function fixDuplicateText(input) {
|
|
|
+ // 改进版正则表达式(支持任意字符且精确匹配完全重复)
|
|
|
+ const regex = /^(.+?)\1$/; // 非贪婪模式防止误匹配
|
|
|
+ const match = input.match(regex);
|
|
|
+ // 返回处理逻辑
|
|
|
+ return match ? match[1] : input;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// 在外部定义一个防抖函数映射表,用于跟踪每个单据的防抖状态
|
|
|
+const debounceMap = new Map();
|
|
|
+
|
|
|
+// 防抖函数封装
|
|
|
+const debounce = (func, wait = 500) => {
|
|
|
+ let timer;
|
|
|
+ return (...args) => {
|
|
|
+ clearTimeout(timer);
|
|
|
+ timer = setTimeout(() => {
|
|
|
+ func.apply(this, args);
|
|
|
+ }, wait);
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+// 修改后的推送函数
|
|
|
+const _rePush = (item) => {
|
|
|
+ if (!item || item.isPush === 1) return;
|
|
|
+ let status = false
|
|
|
+ showLoadingToast({
|
|
|
+ message: '上传中...',
|
|
|
+ forbidClick: true,
|
|
|
+ closeToast: status
|
|
|
+ });
|
|
|
+ // 初始化当前单据的防抖状态
|
|
|
+ if (!debounceMap.has(item.deliveryNo)) {
|
|
|
+ debounceMap.set(item.deliveryNo, {
|
|
|
+ isPushing: false, // 请求状态锁
|
|
|
+ debouncedFn: debounce(() => {
|
|
|
+ const entry = debounceMap.get(item.deliveryNo);
|
|
|
+ if (!entry || entry.isPushing) return;
|
|
|
+
|
|
|
+ entry.isPushing = true; // 加锁
|
|
|
+
|
|
|
+ const dto = {
|
|
|
+ deliveryNo: item.deliveryNo,
|
|
|
+ machine: item.machine,
|
|
|
+ operator: item.operator,
|
|
|
+ operationTime: item.operationTime
|
|
|
+ };
|
|
|
+
|
|
|
+ receive(dto).then(res => {
|
|
|
+ const result = markAsPushed(item.deliveryNo);
|
|
|
+ entry.isPushing = false; // 请求完成解锁
|
|
|
+ item.isPush = 1;
|
|
|
+ scanSuccess();
|
|
|
+ status = true
|
|
|
+ errNum.value -= 1
|
|
|
+ showNotify({
|
|
|
+ type: 'success',
|
|
|
+ style: 'font-size: 30px !important;height:50px',
|
|
|
+ message: '推送成功!'
|
|
|
+ });
|
|
|
+ }).catch(err => {
|
|
|
+ entry.isPushing = false; // 请求失败解锁
|
|
|
+ scanError();
|
|
|
+ status = true
|
|
|
+ showNotify({
|
|
|
+ type: 'danger',
|
|
|
+ style: 'font-size: 30px !important;height:50px',
|
|
|
+ message: `推单失败!${err.message}`
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }, 500) // 500ms防抖时间
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行防抖函数
|
|
|
+ const { debouncedFn } = debounceMap.get(item.deliveryNo);
|
|
|
+ debouncedFn();
|
|
|
+};
|
|
|
+
|
|
|
+const getUserName = (userId) => {
|
|
|
+ getUserNameById(userId, mac.value).then(res => {
|
|
|
+ if (res && res.data) {
|
|
|
+ saveUserId(userInfo.value.userId)
|
|
|
+ userInfo.value.name = res.data
|
|
|
+ scanSuccess()
|
|
|
+ showNotify({ type: 'success', style: 'font-size: 30px !important;height:50px', message: '登录成功!' });
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const versionName = ref('')
|
|
|
+const _openSetting = () => {
|
|
|
+ closeListener()
|
|
|
+ setting.value.show = true
|
|
|
+ versionName.value = getVersionName()
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// 校验mac地址是否已配置
|
|
|
+const _verifyMac = (done) => {
|
|
|
+ if (!mac.value) {
|
|
|
+ setting.value.show = true
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// 关闭设置
|
|
|
+const _closeSetting = () => {
|
|
|
+ _openScan()
|
|
|
+}
|
|
|
+
|
|
|
+const _saveMac = () => {
|
|
|
+ if(!isValidMacAddress(setting.value.mac)) {
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '请输入正确的MAC地址!' });
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!isValidPassword(setting.value.password)) {
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '请输入正确的密码!' });
|
|
|
return
|
|
|
}
|
|
|
- if (!imagePath.value) {
|
|
|
- alert('请先选择图片')
|
|
|
+ if(!saveMacAddress(setting.value.mac)) {
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: 'MAC保存失败!' });
|
|
|
return
|
|
|
}
|
|
|
- window.android.openImageEditor(imagePath.value)
|
|
|
+ showNotify({ type: 'success', style: 'font-size: 30px !important;height:50px', message: 'MAC保存成功!' });
|
|
|
+ mac.value = setting.value.mac
|
|
|
+ getFrogPosition()
|
|
|
}
|
|
|
|
|
|
-// 这个函数应该由 Android 调用
|
|
|
-window.handleImageResult = (path) => {
|
|
|
- imagePath.value = path
|
|
|
- // 如果是文件路径,直接使用
|
|
|
- if (path.startsWith('file://') || path.startsWith('/storage')) {
|
|
|
- imageUrl.value = path
|
|
|
- }
|
|
|
- // 如果是 base64,直接使用
|
|
|
- else if (path.startsWith('data:image')) {
|
|
|
- imageUrl.value = path
|
|
|
+function isValidMacAddress(mac) {
|
|
|
+ const regex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
|
|
+ return mac !== null && regex.test(mac);
|
|
|
+}
|
|
|
+function isValidPassword(password) {
|
|
|
+ return password === '123456';
|
|
|
+}
|
|
|
+
|
|
|
+// 网络状态和ip
|
|
|
+const ipAddress = ref(null)
|
|
|
+
|
|
|
+// 获取 IP 地址
|
|
|
+const fetchIP = async () => {
|
|
|
+ const RTCPeerConnection = window.RTCPeerConnection;
|
|
|
+ if (RTCPeerConnection) {
|
|
|
+ const pc = new RTCPeerConnection({ iceServers: [] });
|
|
|
+ pc.createDataChannel('');
|
|
|
+ pc.createOffer()
|
|
|
+ .then(sdp => pc.setLocalDescription(sdp))
|
|
|
+ .catch(console.error);
|
|
|
+ pc.onicecandidate = e => {
|
|
|
+ if (!e.candidate) return;
|
|
|
+ const ip = /([0-9]{1,3}(\.[0-9]{1,3}){3})/.exec(e.candidate.candidate)?.[1];
|
|
|
+ if (ip) ipAddress.value = ip;
|
|
|
+ };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 处理 Blob 数据
|
|
|
-window.handleImageBlob = (base64) => {
|
|
|
- const blob = base64ToBlob(base64, 'image/jpeg')
|
|
|
- imageUrl.value = URL.createObjectURL(blob)
|
|
|
+const getErrNum = () => {
|
|
|
+ return getErrRecordsCount()
|
|
|
}
|
|
|
|
|
|
-// 将 base64 转换为 Blob
|
|
|
-const base64ToBlob = (base64, mimeType) => {
|
|
|
- const byteCharacters = atob(base64.split(',')[1])
|
|
|
- const byteNumbers = new Array(byteCharacters.length)
|
|
|
- for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
- byteNumbers[i] = byteCharacters.charCodeAt(i)
|
|
|
+const page = ref(1)
|
|
|
+const size = ref(10)
|
|
|
+// 模拟数据加载
|
|
|
+const onLoad = () => {
|
|
|
+ if (refreshing.value) {
|
|
|
+ list.value = [];
|
|
|
+ refreshing.value = false;
|
|
|
+ }
|
|
|
+ const result = pageDelivery(page.value, size.value)
|
|
|
+ if (result.size < size.value) {
|
|
|
+ finished.value = true;
|
|
|
+ } else {
|
|
|
+ page.value += 1
|
|
|
+ }
|
|
|
+ list.value.push(...result)
|
|
|
+ loading.value = false;
|
|
|
+};
|
|
|
+const refreshing = ref(false);
|
|
|
+const onRefresh = () => {
|
|
|
+ // 清空列表数据
|
|
|
+ finished.value = false;
|
|
|
+
|
|
|
+ // 重新加载数据
|
|
|
+ // 将 loading 设置为 true,表示处于加载状态
|
|
|
+ loading.value = true;
|
|
|
+ page.value = 1
|
|
|
+ onLoad();
|
|
|
+};
|
|
|
+const isOnePush = ref(false)
|
|
|
+const _one_click_push = () => {
|
|
|
+ isOnePush.value = true
|
|
|
+ const result = listErrRecords()
|
|
|
+ if (result && result != '[]') {
|
|
|
+ const records = JSON.parse(result);
|
|
|
+ const promises = []; // 收集所有请求的Promise
|
|
|
+
|
|
|
+ records.forEach(item => {
|
|
|
+ // 初始化当前单据的防抖状态
|
|
|
+ if (!debounceMap.has(item.deliveryNo)) {
|
|
|
+ const debounceEntry = {
|
|
|
+ isPushing: false,
|
|
|
+ debouncedFn: debounce(() => {
|
|
|
+ const entry = debounceMap.get(item.deliveryNo);
|
|
|
+ if (!entry || entry.isPushing) return;
|
|
|
+
|
|
|
+ entry.isPushing = true; // 加锁
|
|
|
+
|
|
|
+ const dto = {
|
|
|
+ deliveryNo: item.deliveryNo,
|
|
|
+ machine: item.machine,
|
|
|
+ operator: item.operator,
|
|
|
+ operationTime: item.operationTime
|
|
|
+ };
|
|
|
+
|
|
|
+ // 将请求包装成Promise并收集
|
|
|
+ const promise = receive(dto)
|
|
|
+ .then(res => {
|
|
|
+ const result = markAsPushed(item.deliveryNo);
|
|
|
+ list.value.filter(entry => entry.deliveryNo === item.deliveryNo).forEach(entry => {
|
|
|
+ entry.isPush = 1;
|
|
|
+ })
|
|
|
+ errNum.value -= 1
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ entry.isPushing = false; // 无论成功失败都解锁
|
|
|
+ });
|
|
|
+
|
|
|
+ promises.push(promise); // 添加到Promise数组
|
|
|
+ return promise;
|
|
|
+ }, 500)
|
|
|
+ };
|
|
|
+ debounceMap.set(item.deliveryNo, debounceEntry);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 立即执行防抖函数并收集Promise
|
|
|
+ const promise = debounceMap.get(item.deliveryNo).debouncedFn();
|
|
|
+ if (promise) promises.push(promise);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 所有请求完成后重置状态
|
|
|
+ Promise.allSettled(promises)
|
|
|
+ .then(_ => {
|
|
|
+ scanSuccess();
|
|
|
+ showNotify({ type: 'success', style: 'font-size: 30px !important;height:50px', message: '数据已推送!' });
|
|
|
+ })
|
|
|
+ .catch(_ => {
|
|
|
+ scanError();
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '存在数据推送失败!' });
|
|
|
+ }).finally(() => {
|
|
|
+ isOnePush.value = false;
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: '暂无异常数据!' });
|
|
|
+ isOnePush.value = false; // 没有数据时立即重置
|
|
|
}
|
|
|
- const byteArray = new Uint8Array(byteNumbers)
|
|
|
- return new Blob([byteArray], { type: mimeType })
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="sass">
|
|
|
-button
|
|
|
- margin: 10px
|
|
|
- padding: 8px 16px
|
|
|
- background: #42b983
|
|
|
+*
|
|
|
+ margin: 0
|
|
|
+ padding: 0
|
|
|
+ box-sizing: border-box
|
|
|
+
|
|
|
+
|
|
|
+body
|
|
|
+ font-family: "PingFang SC", "Microsoft YaHei", sans-serif
|
|
|
+ background: #f5f7fa
|
|
|
+ color: #333
|
|
|
+ overflow: hidden
|
|
|
+ height: 100vh
|
|
|
+
|
|
|
+.container
|
|
|
+ height: 100vh
|
|
|
+ display: flex
|
|
|
+ overflow: hidden
|
|
|
+ font-size: large
|
|
|
+ background: #fff
|
|
|
+
|
|
|
+span
|
|
|
+ font-size: 7px
|
|
|
+
|
|
|
+.table-container
|
|
|
+ display: flex
|
|
|
+ flex-direction: column
|
|
|
+ height: 100%
|
|
|
+ overflow: hidden
|
|
|
+ background: #fff
|
|
|
+
|
|
|
+.waterfall-table
|
|
|
+ position: relative
|
|
|
+ flex: 1
|
|
|
+ display: flex
|
|
|
+ flex-direction: column
|
|
|
+ overflow: hidden
|
|
|
+ height: 100%
|
|
|
+
|
|
|
+.table-header
|
|
|
+ font-size: 8px
|
|
|
+ display: flex
|
|
|
+ background: #f2f3f5
|
|
|
+ font-weight: bold
|
|
|
+ z-index: 0
|
|
|
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05)
|
|
|
+ flex-shrink: 0 // 防止表头被压缩
|
|
|
+
|
|
|
+.table-body
|
|
|
+ flex: 1
|
|
|
+ overflow-y: auto
|
|
|
+ position: relative
|
|
|
+ padding-top: 0 // 移除之前的padding-top
|
|
|
+
|
|
|
+.table-row
|
|
|
+ font-size: 10px
|
|
|
+ height: 21px
|
|
|
+ display: flex
|
|
|
+ box-sizing: border-box
|
|
|
+
|
|
|
+.col
|
|
|
+ padding: 8px 4px
|
|
|
+ overflow: hidden
|
|
|
+ text-overflow: ellipsis
|
|
|
+ box-sizing: border-box
|
|
|
+
|
|
|
+/* 列宽设置 - 确保表头和表体使用相同的宽度 */
|
|
|
+.index
|
|
|
+ flex: 0 0 30px
|
|
|
+ width: 30px
|
|
|
+ text-align: center
|
|
|
+
|
|
|
+.time
|
|
|
+ flex: 0 0 75px
|
|
|
+ width: 75px
|
|
|
+
|
|
|
+.operator
|
|
|
+ flex: 0 0 55px
|
|
|
+ width: 55px
|
|
|
+
|
|
|
+.content
|
|
|
+ flex: 1
|
|
|
+ min-width: 0 /* 允许内容换行 */
|
|
|
+ white-space: normal /* 允许换行 */
|
|
|
+
|
|
|
+.status
|
|
|
+ flex: 0 0 40px
|
|
|
+ width: 40px
|
|
|
+
|
|
|
+/* 确保表体中的单元格与表头对齐 */
|
|
|
+:deep(.van-list)
|
|
|
+ .van-cell
|
|
|
+ padding: 0
|
|
|
+ width: 100%
|
|
|
+ box-sizing: border-box
|
|
|
+ .van-cell__title, .van-cell__value
|
|
|
+ flex: none
|
|
|
+
|
|
|
+.add
|
|
|
+ position: fixed
|
|
|
+ bottom: 10px
|
|
|
+ left: 10px
|
|
|
+ width: 20px
|
|
|
+ height: 20px
|
|
|
+ border-radius: 50%
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15)
|
|
|
+
|
|
|
+:deep(.custom-cell-group)
|
|
|
+ height: auto // 移除固定高度
|
|
|
+ display: flex
|
|
|
+ align-items: center
|
|
|
+ min-height: 24px // 确保有足够高度
|
|
|
+ .van-cell
|
|
|
+ display: flex
|
|
|
+ align-items: center
|
|
|
+ height: 100%
|
|
|
+ .van-cell__title
|
|
|
+ flex: 0 0 30px !important
|
|
|
+ font-size: 10px
|
|
|
+ line-height: 24px
|
|
|
+ max-width: 70px
|
|
|
+ min-width: 70px
|
|
|
+ .van-cell__value
|
|
|
+ flex: 1
|
|
|
+ line-height: 24px
|
|
|
+ color: red
|
|
|
+ font-size: 13px
|
|
|
+ text-align: left
|
|
|
+ .success-message
|
|
|
+ font-size: 12px
|
|
|
+ color: #2ed573 !important
|
|
|
+
|
|
|
+.van-notice-bar
|
|
|
+ background-color: #f8f9fa
|
|
|
+ margin: 5px 0 5px 0
|
|
|
+ border: 1px solid #eaeaea
|
|
|
+
|
|
|
+.van-field
|
|
|
+ padding: 5px 16px
|
|
|
+ transition: border-color 0.3s
|
|
|
+
|
|
|
+:deep(.custom-notify)
|
|
|
+ font-size: 100px
|
|
|
+
|
|
|
+.network-alert
|
|
|
+ position: fixed
|
|
|
+ top: 0
|
|
|
+ left: 0
|
|
|
+ right: 0
|
|
|
+ padding: 15px
|
|
|
+ text-align: center
|
|
|
+ font-weight: bold
|
|
|
+ z-index: 9999
|
|
|
+ animation: slideIn 0.3s ease-out
|
|
|
+
|
|
|
+
|
|
|
+@keyframes slideIn
|
|
|
+ from
|
|
|
+ opacity: 0
|
|
|
+ transform: translateY(-100%)
|
|
|
+ to
|
|
|
+ opacity: 1
|
|
|
+ transform: translateY(0)
|
|
|
+
|
|
|
+.network-alert.offline
|
|
|
+ background-color: #ff4757
|
|
|
color: white
|
|
|
- border: none
|
|
|
- border-radius: 4px
|
|
|
- cursor: pointer
|
|
|
|
|
|
+.network-alert.online
|
|
|
+ background-color: #2ed573
|
|
|
+ color: white
|
|
|
+ top: 0px
|
|
|
+
|
|
|
+.fade-leave-active
|
|
|
+ transition: opacity 0.5s ease
|
|
|
+
|
|
|
+.fade-leave-to
|
|
|
+ opacity: 0
|
|
|
+
|
|
|
+/* 左侧用户信息面板样式优化 */
|
|
|
+.user-panel
|
|
|
+ background-color: #f5f7fa
|
|
|
+ padding: 10px 10px
|
|
|
+ height: 100%
|
|
|
+ display: flex
|
|
|
+ flex-direction: column
|
|
|
+
|
|
|
+.user-header
|
|
|
+ text-align: center
|
|
|
+ margin-bottom: 6px
|
|
|
+ position: relative
|
|
|
+ padding-bottom: 6px
|
|
|
+
|
|
|
+.user-header h3
|
|
|
+ color: #3498db
|
|
|
+ font-weight: 500
|
|
|
+ font-size: 10px
|
|
|
+
|
|
|
+.user-header::after
|
|
|
+ content: ''
|
|
|
+ position: absolute
|
|
|
+ bottom: 0
|
|
|
+ left: 0
|
|
|
+ right: 0
|
|
|
+ height: 2px
|
|
|
+ background: linear-gradient(to right, transparent, #3498db, transparent)
|
|
|
+
|
|
|
+.info-grid
|
|
|
+ display: grid
|
|
|
+ grid-template-columns: 1fr
|
|
|
+ padding-bottom: 5px
|
|
|
+ gap: 0px
|
|
|
+ flex: 1
|
|
|
|
|
|
-.image-preview
|
|
|
- margin-top: 20px
|
|
|
- img
|
|
|
- max-width: 100%
|
|
|
- max-height: 400px
|
|
|
- border: 1px solid #ddd
|
|
|
- border-radius: 4px
|
|
|
+.info-item
|
|
|
+ display: flex
|
|
|
+ flex-direction: column
|
|
|
|
|
|
+.info-label
|
|
|
+ font-size: 7px
|
|
|
+ color: #7f8c8d
|
|
|
+ margin-bottom: 1px
|
|
|
+ display: flex
|
|
|
+ align-items: center
|
|
|
|
|
|
+.info-label::before
|
|
|
+ content: ''
|
|
|
+ display: inline-block
|
|
|
+ width: 3px
|
|
|
+ height: 10px
|
|
|
+ background-color: #3498db
|
|
|
+ margin-right: 3px
|
|
|
+ border-radius: 2px
|
|
|
+
|
|
|
+.info-value
|
|
|
+ font-size: 8px
|
|
|
+ font-weight: 400
|
|
|
+ color: #1e1f1f
|
|
|
+ padding-left: 8px
|
|
|
+
|
|
|
+.error-section
|
|
|
+ padding-bottom: 25px
|
|
|
+
|
|
|
+.error-display
|
|
|
+ background: #fff5f5
|
|
|
+ padding: 3px 4px
|
|
|
+ border-radius: 3px
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1)
|
|
|
+
|
|
|
+.error-label
|
|
|
+ font-size: 8px
|
|
|
+ color: #778181
|
|
|
+
|
|
|
+.error-count
|
|
|
+ font-size: 10px
|
|
|
+ font-weight: bold
|
|
|
+ color: #ff4757
|
|
|
+
|
|
|
+.retry-btn
|
|
|
+ width: 45px
|
|
|
+ height: 18px
|
|
|
+ font-size: 7px
|
|
|
+ font-weight: bold
|
|
|
+ border-radius: 4px
|
|
|
+ box-shadow: 0 4px 10px rgba(231, 76, 60, 0.3)
|
|
|
+ transition: all 0.3s
|
|
|
+ margin-top: 5px
|
|
|
+ margin-left: 28px
|
|
|
</style>
|