|
|
@@ -1,959 +1,99 @@
|
|
|
<template>
|
|
|
- <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>
|
|
|
+ <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" />
|
|
|
</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>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-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('')
|
|
|
+import { ref } from 'vue'
|
|
|
|
|
|
-// 小青蛙位置属性
|
|
|
-const frogPosition = ref({
|
|
|
- warehouse: '',
|
|
|
- type:'',
|
|
|
- code:''
|
|
|
-})
|
|
|
+const imagePath = ref('')
|
|
|
+const imageUrl = ref('')
|
|
|
|
|
|
-
|
|
|
-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: '请先登录!' });
|
|
|
+const _openCamera = () => {
|
|
|
+ if (!window.android) {
|
|
|
+ alert('Android 接口不可用')
|
|
|
return
|
|
|
}
|
|
|
- userInfo.value = {}
|
|
|
- removeUserId()
|
|
|
- scanSuccess()
|
|
|
- showNotify({ type: 'success', style: 'font-size: 30px !important;height:50px', message: '已退出登录!' });
|
|
|
-}
|
|
|
-
|
|
|
-// 网络提示
|
|
|
-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]+$/;
|
|
|
- // 检查是否只包含有效字符
|
|
|
- if (!code128BPattern.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
|
|
|
- })
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const scrollTop = () => {
|
|
|
- // 添加滚动到顶部的逻辑
|
|
|
- nextTick(() => {
|
|
|
- const el = pullRefreshRef.value?.$el;
|
|
|
- if (el && el.scrollTo) {
|
|
|
- el.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
- } else if (el) {
|
|
|
- // 回退方案
|
|
|
- el.scrollTop = 0;
|
|
|
- }
|
|
|
- })
|
|
|
+ window.android.openCamera()
|
|
|
}
|
|
|
-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
|
|
|
+const _openGallery = () => {
|
|
|
+ if (!window.android) {
|
|
|
+ alert('Android 接口不可用')
|
|
|
+ return
|
|
|
}
|
|
|
- return true
|
|
|
-}
|
|
|
-
|
|
|
-// 关闭设置
|
|
|
-const _closeSetting = () => {
|
|
|
- _openScan()
|
|
|
+ window.android.openGallery()
|
|
|
}
|
|
|
|
|
|
-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: '请输入正确的密码!' });
|
|
|
+const _openImageEditor = () => {
|
|
|
+ if (!window.android) {
|
|
|
+ alert('Android 接口不可用')
|
|
|
return
|
|
|
}
|
|
|
- if(!saveMacAddress(setting.value.mac)) {
|
|
|
- showNotify({ type: 'danger', style: 'font-size: 30px !important;height:50px', message: 'MAC保存失败!' });
|
|
|
+ if (!imagePath.value) {
|
|
|
+ alert('请先选择图片')
|
|
|
return
|
|
|
}
|
|
|
- showNotify({ type: 'success', style: 'font-size: 30px !important;height:50px', message: 'MAC保存成功!' });
|
|
|
- mac.value = setting.value.mac
|
|
|
- getFrogPosition()
|
|
|
-}
|
|
|
-
|
|
|
-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';
|
|
|
+ window.android.openImageEditor(imagePath.value)
|
|
|
}
|
|
|
|
|
|
-// 网络状态和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;
|
|
|
- };
|
|
|
+// 这个函数应该由 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
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const getErrNum = () => {
|
|
|
- return getErrRecordsCount()
|
|
|
+// 处理 Blob 数据
|
|
|
+window.handleImageBlob = (base64) => {
|
|
|
+ const blob = base64ToBlob(base64, 'image/jpeg')
|
|
|
+ imageUrl.value = URL.createObjectURL(blob)
|
|
|
}
|
|
|
|
|
|
-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; // 没有数据时立即重置
|
|
|
+// 将 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 byteArray = new Uint8Array(byteNumbers)
|
|
|
+ return new Blob([byteArray], { type: mimeType })
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="sass">
|
|
|
-*
|
|
|
- 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
|
|
|
-
|
|
|
-.network-alert.online
|
|
|
- background-color: #2ed573
|
|
|
+button
|
|
|
+ margin: 10px
|
|
|
+ padding: 8px 16px
|
|
|
+ background: #42b983
|
|
|
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
|
|
|
-
|
|
|
-.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
|
|
|
+ border: none
|
|
|
+ border-radius: 4px
|
|
|
+ cursor: pointer
|
|
|
|
|
|
-.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
|
|
|
+.image-preview
|
|
|
+ margin-top: 20px
|
|
|
+ img
|
|
|
+ max-width: 100%
|
|
|
+ max-height: 400px
|
|
|
+ border: 1px solid #ddd
|
|
|
+ border-radius: 4px
|
|
|
|
|
|
-.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>
|