2026-02-28 16:54:47 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="gi_page">
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<!-- 步骤导航 -->
|
|
|
|
|
|
<div class="step-nav">
|
|
|
|
|
|
<div class="step-item" :class="{ active: activeStep >= 1, completed: activeStep > 1 }">
|
|
|
|
|
|
<div class="step-circle">1</div>
|
|
|
|
|
|
<div class="step-title">扫码核验</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-line"></div>
|
|
|
|
|
|
<div class="step-item" :class="{ active: activeStep >= 2, completed: activeStep > 2 }">
|
|
|
|
|
|
<div class="step-circle">2</div>
|
|
|
|
|
|
<div class="step-title">称重登记</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-line"></div>
|
|
|
|
|
|
<div class="step-item" :class="{ active: activeStep >= 3, completed: activeStep > 3 }">
|
|
|
|
|
|
<div class="step-circle">3</div>
|
|
|
|
|
|
<div class="step-title">完成创建</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 步骤内容 -->
|
|
|
|
|
|
<!-- 扫码核验页面 -->
|
|
|
|
|
|
<div v-if="activeStep === 1" class="step-content">
|
|
|
|
|
|
<div class="main-content">
|
|
|
|
|
|
<!-- 左侧表单 -->
|
|
|
|
|
|
<div class="left-section">
|
2026-03-06 18:03:28 +08:00
|
|
|
|
<a-image v-if="!formData.photoUrl" :src="formData.photoUrl" />
|
2026-03-03 17:59:37 +08:00
|
|
|
|
<img v-else :src="formData.photoUrl" class="sample-image square-image" alt="样图">
|
2026-03-06 18:03:28 +08:00
|
|
|
|
|
2026-02-28 16:54:47 +08:00
|
|
|
|
<a-form :model="formData" layout="vertical">
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
|
<a-form-item label="物料名称">
|
2026-03-06 18:03:28 +08:00
|
|
|
|
<a-input v-model="formData.materialName" placeholder="物料名称" disabled />
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
|
<a-form-item label="物料规格">
|
2026-03-06 18:03:28 +08:00
|
|
|
|
<a-input v-model="formData.materialSpec" placeholder="物料规格" disabled />
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
|
<a-form-item label="物料编码">
|
2026-03-03 17:59:37 +08:00
|
|
|
|
<a-input v-model="formData.encoding" placeholder="物料编码" disabled />
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-item">
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<a-form-item label="重量(g)">
|
2026-03-03 17:59:37 +08:00
|
|
|
|
<a-input v-model="formData.unitWeight" placeholder="Kg" disabled />
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
|
<a-form-item label="物料编码">
|
|
|
|
|
|
<a-input
|
2026-03-11 10:56:33 +08:00
|
|
|
|
v-model="formData.inputMaterialCode"
|
|
|
|
|
|
placeholder="请点击此处确保光标闪烁,并使用扫码枪扫描物料编码"
|
|
|
|
|
|
@keydown="handleKeyDown"
|
|
|
|
|
|
@input="handleMaterialCodeChange"
|
2026-02-28 16:54:47 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧信息 -->
|
|
|
|
|
|
<div class="right-section">
|
|
|
|
|
|
<div class="camera-section">
|
2026-03-12 15:41:52 +08:00
|
|
|
|
<!-- 图片展示 -->
|
|
|
|
|
|
<div class="image-placeholder square-image">
|
|
|
|
|
|
<img
|
|
|
|
|
|
src="@/assets/images/001.bmp"
|
|
|
|
|
|
alt="图片展示"
|
|
|
|
|
|
style="width: 100%; height: 100%; object-fit: cover; border-radius: 4px;"
|
|
|
|
|
|
/>
|
2026-03-11 10:56:33 +08:00
|
|
|
|
</div>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
2026-03-09 16:22:17 +08:00
|
|
|
|
|
2026-02-28 16:54:47 +08:00
|
|
|
|
<div class="info-section">
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<h4>识别信息</h4>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="label">物料名称:</span>
|
|
|
|
|
|
<span class="value">{{ formData.materialName || '-' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="label">物料编码:</span>
|
2026-03-03 17:59:37 +08:00
|
|
|
|
<span class="value">{{ formData.encoding || '-' }}</span>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<h4>比对结果</h4>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="label">比对结果:</span>
|
2026-03-09 16:22:17 +08:00
|
|
|
|
<span
|
2026-03-11 10:56:33 +08:00
|
|
|
|
class="value match-result" :class="{
|
2026-03-11 18:38:45 +08:00
|
|
|
|
'match-success': compareMatchResult === 'success',
|
|
|
|
|
|
'match-failed': compareMatchResult === 'failed',
|
|
|
|
|
|
'match-pending': !compareMatchResult,
|
2026-03-09 16:22:17 +08:00
|
|
|
|
}"
|
|
|
|
|
|
>
|
2026-03-11 18:38:45 +08:00
|
|
|
|
<icon-check-circle-fill v-if="compareMatchResult === 'success'" style="color: #52c41a; margin-right: 8px; font-size: 25px;" />
|
|
|
|
|
|
<icon-close-circle-fill v-else-if="compareMatchResult === 'failed'" style="color: #ff4d4f; margin-right: 8px; font-size: 25px;" />
|
|
|
|
|
|
{{ compareMatchResult === 'success' ? '成功' : compareMatchResult === 'failed' ? '失败' : '请放置需比对的物料到扫描区域!' }}
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-06 18:03:28 +08:00
|
|
|
|
<!-- 称重登记页面 - 这里放FLV播放器 -->
|
2026-02-28 16:54:47 +08:00
|
|
|
|
<div v-else-if="activeStep === 2" class="step-content">
|
|
|
|
|
|
<div class="weighing-content">
|
|
|
|
|
|
<!-- 左侧输入区域 -->
|
|
|
|
|
|
<div class="left-weighing-section">
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
|
<label>物料名称:</label>
|
|
|
|
|
|
<a-input v-model="formData.materialName" placeholder="物料名称" disabled />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
|
<label>输入数量:</label>
|
2026-03-03 17:59:37 +08:00
|
|
|
|
<a-input v-model="inputQuantity" placeholder="请输入数量" @change="calculateWeight" />
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-item">
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<label>计算重量(g):</label>
|
2026-03-03 17:59:37 +08:00
|
|
|
|
<a-input v-model="calculatedWeight" placeholder="-" disabled />
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-item">
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<label>对应重量(g):</label>
|
2026-03-09 16:22:17 +08:00
|
|
|
|
<a-input v-model="ahDeviceWeight" placeholder="-" disabled />
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-06 18:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 这里是摄像头画面 - 替换成FLV播放器 -->
|
|
|
|
|
|
<div class="video-container large-image">
|
|
|
|
|
|
<video
|
2026-03-11 10:56:33 +08:00
|
|
|
|
ref="cameraVideo"
|
|
|
|
|
|
autoplay
|
|
|
|
|
|
muted
|
|
|
|
|
|
playsinline
|
|
|
|
|
|
style="width: 100%; height: 100%; object-fit: cover;"
|
2026-03-06 18:03:28 +08:00
|
|
|
|
></video>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 加载状态 -->
|
|
|
|
|
|
<div v-if="cameraStatus === 'connecting'" class="video-overlay">
|
|
|
|
|
|
<a-spin />
|
|
|
|
|
|
<span style="margin-left: 8px;">加载中...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 错误状态 -->
|
|
|
|
|
|
<div v-if="cameraStatus === 'error'" class="video-overlay error">
|
|
|
|
|
|
<icon-close-circle-fill style="color: #ff4d4f; font-size: 24px;" />
|
|
|
|
|
|
<span>连接失败</span>
|
|
|
|
|
|
<a-button size="small" type="primary" @click="reconnectCamera">重试</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 未连接状态 -->
|
|
|
|
|
|
<div v-if="cameraStatus === 'disconnected'" class="video-overlay">
|
|
|
|
|
|
<icon-pause-circle-fill style="color: #999; font-size: 24px;" />
|
|
|
|
|
|
<span>未连接</span>
|
|
|
|
|
|
<a-button size="small" type="primary" @click="initCamera">连接</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 摄像头状态标识 -->
|
|
|
|
|
|
<div v-if="cameraStatus === 'connected'" class="camera-badge">
|
|
|
|
|
|
<span class="live-badge">LIVE</span>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-06 18:03:28 +08:00
|
|
|
|
|
2026-02-28 16:54:47 +08:00
|
|
|
|
<div class="weighing-actions">
|
|
|
|
|
|
<a-button type="primary" @click="handleConfirm">确定</a-button>
|
|
|
|
|
|
<a-button style="margin-left: 12px;" @click="handleReset">重置</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧表格区域 -->
|
|
|
|
|
|
<div class="right-weighing-section">
|
|
|
|
|
|
<div class="weighing-section">
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<h4>称重列表</h4>
|
|
|
|
|
|
<div class="table-actions">
|
|
|
|
|
|
<a-button>打印</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-table :columns="columns" :data="weighingList" bordered>
|
|
|
|
|
|
<template #action="{ record }">
|
|
|
|
|
|
<a-button type="text" status="danger" @click="handleDelete(record.key)">
|
|
|
|
|
|
删除
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 完成创建页面 -->
|
|
|
|
|
|
<div v-else-if="activeStep === 3" class="step-content">
|
|
|
|
|
|
<div class="completion-content">
|
|
|
|
|
|
<div class="completion-icon">
|
|
|
|
|
|
<a-icon type="check-circle" :size="64" style="color: #52c41a;" />
|
|
|
|
|
|
</div>
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<h2>{{ workOrderResp.matchResult === 'success' ? '创建成功' : '创建失败' }}</h2>
|
|
|
|
|
|
<p>{{ workOrderResp.matchResult === 'success' ? '任务创建成功' : '任务创建失败' }},以下是任务详情:</p>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
<div class="completion-info">
|
|
|
|
|
|
<div class="info-item">
|
2026-03-05 18:14:25 +08:00
|
|
|
|
<span class="label">任务工单号:</span>
|
|
|
|
|
|
<span class="value">{{ workOrderResp.orderNo }}</span>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="label">物料名称:</span>
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<span class="value">{{ formData.materialName }}</span>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="label">物料编码:</span>
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<span class="value">{{ formData.encoding }}</span>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="label">物料规格:</span>
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<span class="value">{{ formData.materialSpec }}</span>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
2026-03-06 15:54:36 +08:00
|
|
|
|
<span class="label">物料总个数:</span>
|
|
|
|
|
|
<span class="value">{{ workOrderResp.totalCount }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="label">物料总重量(g):</span>
|
|
|
|
|
|
<span class="value">{{ workOrderResp.totalWeight }}</span>
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="completion-actions">
|
|
|
|
|
|
<a-button type="primary" @click="handleBackToFirst">返回首页</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
|
<a-button
|
2026-03-11 10:56:33 +08:00
|
|
|
|
v-if="activeStep > 1 && activeStep < 3"
|
|
|
|
|
|
class="previous-button"
|
|
|
|
|
|
@click="handlePrevious"
|
2026-02-28 16:54:47 +08:00
|
|
|
|
>
|
|
|
|
|
|
上一步
|
|
|
|
|
|
</a-button>
|
2026-03-09 16:22:17 +08:00
|
|
|
|
<a-button
|
2026-03-11 10:56:33 +08:00
|
|
|
|
v-if="activeStep < 3"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
class="next-button"
|
|
|
|
|
|
@click="handleNext"
|
2026-02-28 16:54:47 +08:00
|
|
|
|
>
|
2026-03-11 18:38:45 +08:00
|
|
|
|
{{ activeStep === 2 ? '完成' : (compareMatchResult === 'success' ? '下一步' : '开始比对') }}
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-09 16:22:17 +08:00
|
|
|
|
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import { Message, Modal, Notification } from '@arco-design/web-vue'
|
2026-03-06 18:25:24 +08:00
|
|
|
|
|
2026-03-11 18:38:45 +08:00
|
|
|
|
import {getMaterialDetail, validateWeighing, vmSend} from '@/apis/weightManage/weightManage'
|
2026-03-09 20:34:20 +08:00
|
|
|
|
import {type WorkOrderResp, addWorkOrder, getWorkOrder} from '@/apis/workOrder/workOrder'
|
2026-03-09 16:22:17 +08:00
|
|
|
|
import { catchPhoto } from '@/apis/material/materialInfo'
|
2026-03-06 18:03:28 +08:00
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'WeightManage' })
|
|
|
|
|
|
|
|
|
|
|
|
// 动态导入flv.js
|
|
|
|
|
|
const flvjs = ref<any>(null)
|
2026-02-28 16:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 当前步骤
|
|
|
|
|
|
const activeStep = ref(1)
|
|
|
|
|
|
|
|
|
|
|
|
// 表单数据
|
|
|
|
|
|
const formData = reactive({
|
|
|
|
|
|
inputMaterialCode: '', // 输入的物料编码
|
2026-03-05 18:14:25 +08:00
|
|
|
|
id: '', // 物料ID
|
2026-03-03 17:59:37 +08:00
|
|
|
|
encoding: '', // 物料编码
|
2026-02-28 16:54:47 +08:00
|
|
|
|
materialName: '', // 物料名称
|
|
|
|
|
|
materialSpec: '', // 物料规格
|
2026-03-03 17:59:37 +08:00
|
|
|
|
unitWeight: 0, // 重量
|
|
|
|
|
|
photoUrl: '', // 样图URL
|
2026-03-06 18:03:28 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-11 18:38:45 +08:00
|
|
|
|
//比对结果
|
|
|
|
|
|
const compareMatchResult = ref('')
|
|
|
|
|
|
|
2026-03-06 18:03:28 +08:00
|
|
|
|
// 摄像头状态
|
|
|
|
|
|
const cameraStatus = ref<'connected' | 'connecting' | 'disconnected' | 'error'>('disconnected')
|
|
|
|
|
|
|
|
|
|
|
|
// 视频元素引用
|
|
|
|
|
|
const cameraVideo = ref<HTMLVideoElement | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// FLV播放器实例
|
|
|
|
|
|
let player: any = null
|
|
|
|
|
|
|
2026-03-09 20:34:20 +08:00
|
|
|
|
const reconnectCount = ref(1)
|
|
|
|
|
|
const maxReconnectAttempts = 3
|
2026-03-06 18:03:28 +08:00
|
|
|
|
const reconnectTimer = ref<any>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// 直接写死FLV流地址(根据你的实际地址修改)
|
|
|
|
|
|
const FLV_URL = 'http://192.168.2.87:8866/live?url=rtsp://admin:admin%40123@192.168.2.59:554/media/video1' // 摄像头的FLV地址
|
|
|
|
|
|
|
|
|
|
|
|
// 加载flv.js
|
|
|
|
|
|
const loadFlvJs = async () => {
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
const module = await import('flv.js')
|
|
|
|
|
|
flvjs.value = module.default
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化摄像头
|
|
|
|
|
|
const initCamera = () => {
|
|
|
|
|
|
if (!cameraVideo.value || !flvjs.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查浏览器支持
|
|
|
|
|
|
if (!flvjs.value.isSupported()) {
|
|
|
|
|
|
Message.error('当前浏览器不支持FLV播放')
|
|
|
|
|
|
cameraStatus.value = 'error'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cameraStatus.value = 'connecting'
|
|
|
|
|
|
|
|
|
|
|
|
// 清除之前的重连定时器
|
|
|
|
|
|
if (reconnectTimer.value) {
|
|
|
|
|
|
clearTimeout(reconnectTimer.value)
|
|
|
|
|
|
reconnectTimer.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 销毁旧的播放器
|
|
|
|
|
|
if (player) {
|
|
|
|
|
|
player.destroy()
|
|
|
|
|
|
player = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建新的播放器
|
|
|
|
|
|
player = flvjs.value.createPlayer({
|
|
|
|
|
|
type: 'flv',
|
|
|
|
|
|
url: FLV_URL,
|
|
|
|
|
|
isLive: true,
|
|
|
|
|
|
hasAudio: false,
|
|
|
|
|
|
hasVideo: true,
|
|
|
|
|
|
enableStashBuffer: false,
|
|
|
|
|
|
stashInitialSize: 128,
|
|
|
|
|
|
enableWorker: true,
|
|
|
|
|
|
autoCleanupSourceBuffer: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定视频元素
|
|
|
|
|
|
player.attachMediaElement(cameraVideo.value)
|
|
|
|
|
|
|
|
|
|
|
|
// 加载并播放
|
|
|
|
|
|
player.load()
|
|
|
|
|
|
player.play().then(() => {
|
|
|
|
|
|
cameraStatus.value = 'connected'
|
2026-03-09 20:34:20 +08:00
|
|
|
|
reconnectCount.value = 1 // 连接成功,重置重连计数
|
2026-03-06 18:03:28 +08:00
|
|
|
|
}).catch((error: any) => {
|
|
|
|
|
|
cameraStatus.value = 'error'
|
|
|
|
|
|
Message.error('摄像头播放失败,正在重连')
|
|
|
|
|
|
// 触发自动重连
|
|
|
|
|
|
handleReconnect()
|
2026-03-09 20:34:20 +08:00
|
|
|
|
return
|
2026-03-06 18:03:28 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 监听各种事件
|
|
|
|
|
|
player.on(flvjs.value.Events.ERROR, (err: any) => {
|
|
|
|
|
|
cameraStatus.value = 'error'
|
|
|
|
|
|
// 触发自动重连
|
|
|
|
|
|
handleReconnect()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
player.on(flvjs.value.Events.STALLED, () => {
|
|
|
|
|
|
// 可以在这里处理卡顿,但不立即重连
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
player.on(flvjs.value.Events.RECOVERED_EARLY, () => {
|
|
|
|
|
|
if (cameraStatus.value !== 'connected') {
|
|
|
|
|
|
cameraStatus.value = 'connected'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
cameraStatus.value = 'error'
|
|
|
|
|
|
handleReconnect()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理自动重连
|
|
|
|
|
|
const handleReconnect = () => {
|
|
|
|
|
|
// 只有当前在称重页面才重连
|
|
|
|
|
|
if (activeStep.value !== 2) return
|
|
|
|
|
|
|
|
|
|
|
|
// 检查重连次数
|
|
|
|
|
|
if (reconnectCount.value >= maxReconnectAttempts) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 增加重连计数
|
|
|
|
|
|
reconnectCount.value++
|
|
|
|
|
|
|
|
|
|
|
|
// 计算重连延迟(指数退避)
|
|
|
|
|
|
const delay = Math.min(1000 * 2 ** (reconnectCount.value - 1), 30000)
|
|
|
|
|
|
|
|
|
|
|
|
// 清除旧的定时器
|
|
|
|
|
|
if (reconnectTimer.value) {
|
|
|
|
|
|
clearTimeout(reconnectTimer.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置新的重连定时器
|
|
|
|
|
|
reconnectTimer.value = setTimeout(() => {
|
|
|
|
|
|
initCamera()
|
|
|
|
|
|
}, delay)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重新连接摄像头(手动)
|
|
|
|
|
|
const reconnectCamera = () => {
|
2026-03-09 20:34:20 +08:00
|
|
|
|
reconnectCount.value = 1 // 手动重连时重置计数
|
2026-03-06 18:03:28 +08:00
|
|
|
|
initCamera()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加一个定时检查状态的函数
|
|
|
|
|
|
const checkCameraStatus = () => {
|
|
|
|
|
|
if (!player || !cameraVideo.value) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 检查播放器状态
|
|
|
|
|
|
if (player.isPlaying && !player.isPlaying()) {
|
|
|
|
|
|
if (cameraStatus.value === 'connected') {
|
|
|
|
|
|
cameraStatus.value = 'error'
|
|
|
|
|
|
handleReconnect()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 启动状态检查定时器
|
|
|
|
|
|
let statusCheckTimer: any = null
|
|
|
|
|
|
|
|
|
|
|
|
// 监听步骤变化
|
|
|
|
|
|
watch(activeStep, (newVal) => {
|
|
|
|
|
|
if (newVal === 2) {
|
|
|
|
|
|
// 进入称重登记页面,启动摄像头
|
2026-03-09 20:34:20 +08:00
|
|
|
|
reconnectCount.value = 1 // 重置重连计数
|
2026-03-06 18:03:28 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
initCamera()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 启动状态检查(每30秒检查一次)
|
|
|
|
|
|
statusCheckTimer = setInterval(checkCameraStatus, 30000)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 离开称重登记页面,停止摄像头和定时器
|
|
|
|
|
|
stopCamera()
|
|
|
|
|
|
if (statusCheckTimer) {
|
|
|
|
|
|
clearInterval(statusCheckTimer)
|
|
|
|
|
|
statusCheckTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (reconnectTimer.value) {
|
|
|
|
|
|
clearTimeout(reconnectTimer.value)
|
|
|
|
|
|
reconnectTimer.value = null
|
|
|
|
|
|
}
|
2026-03-09 20:34:20 +08:00
|
|
|
|
reconnectCount.value = 1
|
2026-03-06 18:03:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清理所有定时器
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
stopCamera()
|
|
|
|
|
|
if (statusCheckTimer) {
|
|
|
|
|
|
clearInterval(statusCheckTimer)
|
|
|
|
|
|
statusCheckTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (reconnectTimer.value) {
|
|
|
|
|
|
clearTimeout(reconnectTimer.value)
|
|
|
|
|
|
reconnectTimer.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 重新连接摄像头
|
|
|
|
|
|
// const reconnectCamera = () => {
|
|
|
|
|
|
// if (player) {
|
|
|
|
|
|
// player.unload()
|
|
|
|
|
|
// player.detachMediaElement()
|
|
|
|
|
|
// player.destroy()
|
|
|
|
|
|
// player = null
|
|
|
|
|
|
// }
|
|
|
|
|
|
// setTimeout(() => initCamera(), 1000)
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// 停止摄像头
|
|
|
|
|
|
const stopCamera = () => {
|
|
|
|
|
|
if (player) {
|
|
|
|
|
|
player.pause()
|
|
|
|
|
|
player.unload()
|
|
|
|
|
|
player.detachMediaElement()
|
|
|
|
|
|
player.destroy()
|
|
|
|
|
|
player = null
|
|
|
|
|
|
cameraStatus.value = 'disconnected'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (reconnectTimer.value) {
|
|
|
|
|
|
clearTimeout(reconnectTimer.value)
|
|
|
|
|
|
reconnectTimer.value = null
|
|
|
|
|
|
}
|
2026-03-09 20:34:20 +08:00
|
|
|
|
reconnectCount.value = 1
|
2026-03-06 18:03:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听步骤变化
|
|
|
|
|
|
watch(activeStep, (newVal) => {
|
2026-03-12 15:41:52 +08:00
|
|
|
|
if (newVal === 2) {
|
2026-03-06 18:03:28 +08:00
|
|
|
|
// 进入称重登记页面,启动摄像头
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
initCamera()
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
2026-03-11 10:56:33 +08:00
|
|
|
|
// 离开相关页面,停止摄像头
|
2026-03-06 18:03:28 +08:00
|
|
|
|
stopCamera()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 组件挂载时
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadFlvJs()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
stopCamera()
|
2026-02-28 16:54:47 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
const workOrderResp = ref<WorkOrderResp>({
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
title: '',
|
|
|
|
|
|
orderNo: '',
|
|
|
|
|
|
materialName: '',
|
|
|
|
|
|
encoding: '',
|
|
|
|
|
|
unitWeight: '',
|
|
|
|
|
|
materialSpec: '',
|
|
|
|
|
|
photoUrl: '',
|
|
|
|
|
|
totalWeight: '',
|
|
|
|
|
|
totalCount: '',
|
|
|
|
|
|
createUserString: '',
|
|
|
|
|
|
updateUserString: '',
|
2026-03-09 16:06:00 +08:00
|
|
|
|
totalCalculatedWeight: '',
|
|
|
|
|
|
workOrderInfos: [],
|
2026-03-09 16:22:17 +08:00
|
|
|
|
matchResult: 'failed',
|
2026-03-09 20:34:20 +08:00
|
|
|
|
qrCodeData: '',
|
2026-03-05 18:14:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-28 16:54:47 +08:00
|
|
|
|
// 称重登记页面数据
|
|
|
|
|
|
const inputQuantity = ref('')
|
2026-03-09 20:34:20 +08:00
|
|
|
|
const ahDeviceWeight = ref('')
|
2026-03-03 17:59:37 +08:00
|
|
|
|
const calculatedWeight = ref('')
|
2026-02-28 16:54:47 +08:00
|
|
|
|
const weighingCount = ref(1)
|
|
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
// WebSocket连接
|
|
|
|
|
|
const ws = ref<WebSocket | null>(null)
|
|
|
|
|
|
const wsConnected = ref(false)
|
|
|
|
|
|
|
2026-02-28 16:54:47 +08:00
|
|
|
|
// 称重列表数据
|
|
|
|
|
|
const weighingList = ref([
|
|
|
|
|
|
// 初始数据可以在这里添加
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
// 称重表格列
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '称重次数',
|
2026-03-06 14:17:19 +08:00
|
|
|
|
dataIndex: 'weightTime',
|
2026-03-09 16:22:17 +08:00
|
|
|
|
key: 'weightTime',
|
2026-02-28 16:54:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '数量',
|
|
|
|
|
|
dataIndex: 'quantity',
|
|
|
|
|
|
key: 'quantity',
|
2026-03-09 16:22:17 +08:00
|
|
|
|
className: 'green-bg',
|
2026-02-28 16:54:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-06 15:54:36 +08:00
|
|
|
|
title: '重量(g)',
|
2026-02-28 16:54:47 +08:00
|
|
|
|
dataIndex: 'weight',
|
|
|
|
|
|
key: 'weight',
|
2026-03-09 16:22:17 +08:00
|
|
|
|
className: 'green-bg',
|
2026-02-28 16:54:47 +08:00
|
|
|
|
},
|
2026-03-03 17:59:37 +08:00
|
|
|
|
{
|
2026-03-06 15:54:36 +08:00
|
|
|
|
title: '计算重量(g)',
|
2026-03-03 17:59:37 +08:00
|
|
|
|
dataIndex: 'calculatedWeight',
|
|
|
|
|
|
key: 'calculatedWeight',
|
2026-03-09 16:22:17 +08:00
|
|
|
|
className: 'green-bg',
|
2026-03-03 17:59:37 +08:00
|
|
|
|
},
|
2026-02-28 16:54:47 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '抓拍图片',
|
|
|
|
|
|
dataIndex: 'image',
|
|
|
|
|
|
key: 'image',
|
2026-03-06 18:03:28 +08:00
|
|
|
|
className: 'green-bg',
|
|
|
|
|
|
// 使用自定义渲染
|
|
|
|
|
|
render: ({ record }) => {
|
|
|
|
|
|
if (record.image && record.image !== '未抓拍') {
|
|
|
|
|
|
return h('div', { class: 'image-preview' }, [
|
|
|
|
|
|
h('img', {
|
|
|
|
|
|
src: record.image,
|
|
|
|
|
|
alt: '抓拍图片',
|
|
|
|
|
|
style: 'width: 60px; height: 45px; object-fit: cover; border-radius: 4px; cursor: pointer;',
|
|
|
|
|
|
onClick: () => previewImage(record.image),
|
|
|
|
|
|
}),
|
|
|
|
|
|
])
|
|
|
|
|
|
}
|
|
|
|
|
|
return h('span', record.image || '-')
|
|
|
|
|
|
},
|
2026-02-28 16:54:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
dataIndex: 'action',
|
|
|
|
|
|
key: 'action',
|
2026-03-09 16:22:17 +08:00
|
|
|
|
slotName: 'action',
|
|
|
|
|
|
},
|
2026-02-28 16:54:47 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 防抖函数
|
|
|
|
|
|
const debounce = <T extends (...args: any[]) => any>(func: T, delay: number): ((...args: Parameters<T>) => void) => {
|
2026-03-09 16:22:17 +08:00
|
|
|
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
return function (...args: Parameters<T>) {
|
|
|
|
|
|
if (timer) clearTimeout(timer)
|
2026-03-06 15:54:36 +08:00
|
|
|
|
timer = setTimeout(() => {
|
2026-03-09 16:22:17 +08:00
|
|
|
|
func(...args)
|
|
|
|
|
|
}, delay)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-06 15:54:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 原始的物料编码变化处理函数
|
|
|
|
|
|
const originalHandleMaterialCodeChange = async () => {
|
|
|
|
|
|
// 确保输入框内容是完整的物料编码
|
2026-03-09 16:22:17 +08:00
|
|
|
|
const materialCode = formData.inputMaterialCode?.trim()
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 无论是否有输入,先重置所有物料相关字段,确保新数据能完全覆盖旧数据
|
2026-03-09 16:22:17 +08:00
|
|
|
|
formData.encoding = ''
|
|
|
|
|
|
formData.materialName = ''
|
|
|
|
|
|
formData.materialSpec = ''
|
|
|
|
|
|
formData.unitWeight = 0
|
|
|
|
|
|
formData.photoUrl = ''
|
2026-03-11 18:38:45 +08:00
|
|
|
|
compareMatchResult.value = '';
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 如果有物料编码输入,获取物料数据
|
|
|
|
|
|
if (materialCode) {
|
2026-02-28 16:54:47 +08:00
|
|
|
|
try {
|
2026-03-06 15:54:36 +08:00
|
|
|
|
await fetchMaterialData(materialCode)
|
2026-02-28 16:54:47 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取物料数据失败:', error)
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 即使获取失败,也保持字段为空,避免显示旧数据
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 16:22:17 +08:00
|
|
|
|
}
|
2026-02-28 16:54:47 +08:00
|
|
|
|
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 记录上次输入时间,用于判断是否是扫码枪输入
|
2026-03-09 16:22:17 +08:00
|
|
|
|
let lastInputTime = 0
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 记录输入内容,用于判断是否是新的扫码
|
2026-03-09 16:22:17 +08:00
|
|
|
|
const previousInput = ''
|
2026-03-06 15:54:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理键盘按下事件
|
|
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
|
// 获取当前时间
|
2026-03-09 16:22:17 +08:00
|
|
|
|
const currentTime = Date.now()
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 如果输入速度非常快(小于100ms),认为是扫码枪输入
|
|
|
|
|
|
if (currentTime - lastInputTime < 100 && formData.inputMaterialCode) {
|
|
|
|
|
|
// 扫码枪通常会以回车键结束,这里不做处理
|
|
|
|
|
|
} else if (event.key.length === 1 && !event.ctrlKey && !event.altKey) {
|
|
|
|
|
|
// 如果是新的输入(不是快速连续输入),清空输入框
|
|
|
|
|
|
// 这样可以确保每次扫码都从空输入框开始
|
2026-03-09 16:22:17 +08:00
|
|
|
|
formData.inputMaterialCode = ''
|
2026-03-06 15:54:36 +08:00
|
|
|
|
}
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 更新上次输入时间
|
2026-03-09 16:22:17 +08:00
|
|
|
|
lastInputTime = currentTime
|
|
|
|
|
|
}
|
2026-03-06 15:54:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 带防抖的物料编码变化处理函数
|
2026-03-09 16:22:17 +08:00
|
|
|
|
const handleMaterialCodeChange = debounce(originalHandleMaterialCodeChange, 500) // 500ms防抖延迟
|
2026-03-06 15:54:36 +08:00
|
|
|
|
|
2026-03-06 14:17:19 +08:00
|
|
|
|
// 扫码核验获取物料信息
|
2026-02-28 16:54:47 +08:00
|
|
|
|
const fetchMaterialData = async (code: string) => {
|
2026-03-09 16:22:17 +08:00
|
|
|
|
getMaterialDetail(code).then((res) => {
|
|
|
|
|
|
if (res.code === '0') {
|
2026-02-28 16:54:47 +08:00
|
|
|
|
// 更新表单数据
|
2026-03-09 16:22:17 +08:00
|
|
|
|
formData.id = res.data?.id || ''
|
|
|
|
|
|
formData.encoding = res.data?.encoding || ''
|
|
|
|
|
|
formData.materialName = res.data?.materialName || ''
|
|
|
|
|
|
formData.materialSpec = res.data?.materialSpec || ''
|
|
|
|
|
|
formData.unitWeight = res.data?.unitWeight || 0
|
|
|
|
|
|
formData.photoUrl = res.data?.photoUrl || ''
|
2026-02-28 16:54:47 +08:00
|
|
|
|
return true
|
|
|
|
|
|
}
|
2026-03-09 16:22:17 +08:00
|
|
|
|
})
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理下一步
|
2026-03-05 18:14:25 +08:00
|
|
|
|
const handleNext = async () => {
|
2026-02-28 16:54:47 +08:00
|
|
|
|
if (activeStep.value < 3) {
|
|
|
|
|
|
if (activeStep.value === 2) {
|
|
|
|
|
|
// 当在称重登记页面点击完成时,显示确认弹框
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认完成',
|
|
|
|
|
|
content: '确定要完成称重登记吗?',
|
2026-03-05 18:14:25 +08:00
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 准备工作订单数据
|
|
|
|
|
|
const workOrderData = {
|
|
|
|
|
|
materialId: formData.id,
|
2026-03-06 14:17:19 +08:00
|
|
|
|
materialName: formData.materialName,
|
2026-03-09 16:22:17 +08:00
|
|
|
|
workOrderInfos: weighingList.value,
|
2026-03-05 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 调用后端接口
|
2026-03-09 16:22:17 +08:00
|
|
|
|
addWorkOrder(workOrderData).then((res) => {
|
|
|
|
|
|
if (res.code === '0') {
|
2026-03-05 18:14:25 +08:00
|
|
|
|
Notification.success({
|
|
|
|
|
|
title: '操作成功',
|
2026-03-09 16:22:17 +08:00
|
|
|
|
content: `工单创建成功!`,
|
2026-03-05 18:14:25 +08:00
|
|
|
|
})
|
2026-03-09 16:22:17 +08:00
|
|
|
|
workOrderResp.value.matchResult = 'success'
|
|
|
|
|
|
workOrderResp.value.title = res.data?.title || ''
|
|
|
|
|
|
workOrderResp.value.orderNo = res.data?.orderNo || ''
|
|
|
|
|
|
workOrderResp.value.totalWeight = res.data?.totalWeight || ''
|
|
|
|
|
|
workOrderResp.value.totalCount = res.data?.totalCount || ''
|
2026-03-06 15:54:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 关闭WebSocket连接
|
|
|
|
|
|
closeWebSocket()
|
|
|
|
|
|
// 跳转到完成页面
|
|
|
|
|
|
activeStep.value++
|
2026-03-05 18:14:25 +08:00
|
|
|
|
return true
|
|
|
|
|
|
}
|
2026-03-09 16:22:17 +08:00
|
|
|
|
})
|
2026-03-05 18:14:25 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('创建工作订单失败:', error)
|
|
|
|
|
|
Message.error('创建工作订单失败')
|
|
|
|
|
|
}
|
2026-03-09 16:22:17 +08:00
|
|
|
|
},
|
2026-02-28 16:54:47 +08:00
|
|
|
|
})
|
2026-03-11 18:38:45 +08:00
|
|
|
|
} else if (activeStep.value === 1 && compareMatchResult.value !== 'success') {
|
|
|
|
|
|
// 当在扫码核验页面点击开始比对时,调用后端接口获取比对结果
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 这里应该调用后端的比对接口,暂时使用fetchMaterialData模拟
|
|
|
|
|
|
// 实际项目中应该替换为专门的比对接口
|
|
|
|
|
|
const materialCode = formData.inputMaterialCode?.trim()
|
|
|
|
|
|
if (!materialCode) {
|
|
|
|
|
|
Message.error('请先扫描物料编码')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-12 16:12:30 +08:00
|
|
|
|
|
2026-03-11 18:38:45 +08:00
|
|
|
|
// 调用后端接口获取比对结果
|
|
|
|
|
|
vmSend(materialCode).then((res) => {
|
|
|
|
|
|
if (res.code === '0') {
|
|
|
|
|
|
compareMatchResult.value = JSON.parse(res.data?.matchResult || '')
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (compareMatchResult.value === 'success') {
|
|
|
|
|
|
// 比对成功,按钮变为下一步
|
|
|
|
|
|
Message.success('比对成功')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 比对失败,提示错误
|
|
|
|
|
|
Message.error('比对失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('比对失败:', error)
|
|
|
|
|
|
Message.error('比对失败,请重试')
|
|
|
|
|
|
}
|
2026-02-28 16:54:47 +08:00
|
|
|
|
} else {
|
2026-03-12 16:12:30 +08:00
|
|
|
|
activeStep.value++
|
|
|
|
|
|
// 进入称重页面时建立WebSocket连接
|
|
|
|
|
|
if (activeStep.value === 2) {
|
|
|
|
|
|
establishWebSocket()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 10:56:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-28 16:54:47 +08:00
|
|
|
|
// 处理上一步
|
|
|
|
|
|
const handlePrevious = () => {
|
|
|
|
|
|
if (activeStep.value > 1) {
|
|
|
|
|
|
activeStep.value--
|
2026-03-05 18:14:25 +08:00
|
|
|
|
// 离开称重页面时关闭WebSocket连接
|
|
|
|
|
|
if (activeStep.value !== 2) {
|
|
|
|
|
|
closeWebSocket()
|
|
|
|
|
|
}
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回第一步
|
|
|
|
|
|
const handleBackToFirst = () => {
|
2026-03-09 16:06:00 +08:00
|
|
|
|
// 重置所有数据
|
2026-02-28 16:54:47 +08:00
|
|
|
|
activeStep.value = 1
|
2026-03-09 16:22:17 +08:00
|
|
|
|
|
2026-03-09 16:06:00 +08:00
|
|
|
|
// 重置表单数据
|
|
|
|
|
|
formData.inputMaterialCode = ''
|
|
|
|
|
|
formData.id = ''
|
|
|
|
|
|
formData.encoding = ''
|
|
|
|
|
|
formData.materialName = ''
|
|
|
|
|
|
formData.materialSpec = ''
|
|
|
|
|
|
formData.unitWeight = 0
|
|
|
|
|
|
formData.photoUrl = ''
|
2026-03-11 18:38:45 +08:00
|
|
|
|
|
|
|
|
|
|
compareMatchResult.value = ''
|
2026-03-09 16:22:17 +08:00
|
|
|
|
|
2026-03-09 16:06:00 +08:00
|
|
|
|
// 重置称重登记页面数据
|
|
|
|
|
|
inputQuantity.value = ''
|
2026-03-09 20:34:20 +08:00
|
|
|
|
ahDeviceWeight.value = ''
|
2026-03-09 16:06:00 +08:00
|
|
|
|
calculatedWeight.value = ''
|
|
|
|
|
|
weighingCount.value = 1
|
|
|
|
|
|
// 清空称重列表
|
|
|
|
|
|
weighingList.value = []
|
2026-03-09 16:22:17 +08:00
|
|
|
|
|
2026-03-09 16:06:00 +08:00
|
|
|
|
// 重置工作订单响应数据
|
|
|
|
|
|
workOrderResp.value = {}
|
2026-03-09 16:22:17 +08:00
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
// 离开称重页面时关闭WebSocket连接
|
|
|
|
|
|
closeWebSocket()
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 17:59:37 +08:00
|
|
|
|
// 计算重量
|
|
|
|
|
|
const calculateWeight = () => {
|
|
|
|
|
|
if (inputQuantity.value && formData.unitWeight) {
|
2026-03-09 16:22:17 +08:00
|
|
|
|
const singleWeight = Number.parseFloat(formData.unitWeight.toString())
|
|
|
|
|
|
const quantity = Number.parseFloat(inputQuantity.value)
|
2026-03-03 17:59:37 +08:00
|
|
|
|
if (!isNaN(singleWeight) && !isNaN(quantity)) {
|
2026-03-06 14:17:19 +08:00
|
|
|
|
calculatedWeight.value = (singleWeight * quantity).toFixed(2)
|
2026-03-03 17:59:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
calculatedWeight.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 20:34:20 +08:00
|
|
|
|
|
|
|
|
|
|
//处理确定
|
|
|
|
|
|
const handleConfirm = () => {
|
2026-03-06 15:54:36 +08:00
|
|
|
|
// 校验输入数量是否为空
|
|
|
|
|
|
if (!inputQuantity.value || inputQuantity.value.trim() === '') {
|
|
|
|
|
|
Message.error('请输入数量')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-06 16:29:31 +08:00
|
|
|
|
if (!ahDeviceWeight.value || ahDeviceWeight.value.trim() === '') {
|
|
|
|
|
|
Message.error('电子秤称重结果为空!')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-28 16:54:47 +08:00
|
|
|
|
|
2026-03-09 20:34:20 +08:00
|
|
|
|
const data = {
|
2026-03-06 14:17:19 +08:00
|
|
|
|
materialId: formData.id,
|
2026-03-09 20:34:20 +08:00
|
|
|
|
inputQuantity: inputQuantity.value,
|
|
|
|
|
|
ahDeviceWeight: ahDeviceWeight.value,
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
2026-03-09 16:22:17 +08:00
|
|
|
|
|
2026-03-09 20:34:20 +08:00
|
|
|
|
validateWeighing(data).then(res => {
|
|
|
|
|
|
if (res.code != '0') {
|
|
|
|
|
|
Message.error(res.msg || '系统异常!')
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-09 16:22:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 立即创建一个新项的基本数据
|
|
|
|
|
|
const newItem = {
|
|
|
|
|
|
key: (weighingList.value.length + 1).toString(),
|
|
|
|
|
|
weightTime: weighingCount.value,
|
|
|
|
|
|
materialId: formData.id,
|
|
|
|
|
|
quantity: inputQuantity.value,
|
|
|
|
|
|
weight: ahDeviceWeight.value,
|
|
|
|
|
|
calculatedWeight: calculatedWeight.value,
|
|
|
|
|
|
image: '加载中...', // 先显示加载状态
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 先添加到列表,让用户能立即看到记录
|
|
|
|
|
|
weighingList.value.push(newItem)
|
|
|
|
|
|
|
|
|
|
|
|
// 重置输入(让用户能继续输入)
|
|
|
|
|
|
inputQuantity.value = ''
|
2026-03-09 20:34:20 +08:00
|
|
|
|
ahDeviceWeight.value = ''
|
2026-03-09 16:22:17 +08:00
|
|
|
|
calculatedWeight.value = ''
|
|
|
|
|
|
weighingCount.value = weighingList.value.length + 1
|
|
|
|
|
|
|
|
|
|
|
|
// 异步执行抓拍,不阻塞主流程
|
|
|
|
|
|
if (cameraVideo.value && cameraStatus.value === 'connected') {
|
|
|
|
|
|
// 立即执行异步抓拍
|
|
|
|
|
|
capturePhoto().then((captureUrl) => {
|
|
|
|
|
|
// 找到刚刚添加的项,更新图片URL
|
|
|
|
|
|
const addedItem = weighingList.value.find((item) => item.key === newItem.key)
|
|
|
|
|
|
if (addedItem) {
|
|
|
|
|
|
addedItem.image = captureUrl || '抓拍失败'
|
|
|
|
|
|
// 触发视图更新
|
|
|
|
|
|
weighingList.value = [...weighingList.value]
|
|
|
|
|
|
}
|
|
|
|
|
|
Message.success('图片抓拍成功')
|
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 摄像头未连接,直接显示未抓拍
|
|
|
|
|
|
const addedItem = weighingList.value.find((item) => item.key === newItem.key)
|
|
|
|
|
|
if (addedItem) {
|
|
|
|
|
|
addedItem.image = '未抓拍'
|
|
|
|
|
|
weighingList.value = [...weighingList.value]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 18:03:28 +08:00
|
|
|
|
Message.success('添加成功')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 抓拍功能 - 内部方法,不暴露按钮
|
2026-03-09 15:24:02 +08:00
|
|
|
|
const capturePhoto = (): Promise<string> => {
|
2026-03-09 16:22:17 +08:00
|
|
|
|
// eslint-disable-next-line no-async-promise-executor
|
2026-03-06 18:03:28 +08:00
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const video = cameraVideo.value
|
|
|
|
|
|
if (!video) {
|
|
|
|
|
|
reject(new Error('视频未初始化'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建canvas抓取当前帧
|
|
|
|
|
|
const canvas = document.createElement('canvas')
|
|
|
|
|
|
canvas.width = video.videoWidth || 640
|
|
|
|
|
|
canvas.height = video.videoHeight || 480
|
|
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
|
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为Blob
|
|
|
|
|
|
const blob = await new Promise<Blob>((resolve) => {
|
|
|
|
|
|
canvas.toBlob((b) => resolve(b), 'image/jpeg', 0.9)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 创建FormData
|
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
|
formData.append('file', blob, `capture_${Date.now()}.jpg`)
|
|
|
|
|
|
|
|
|
|
|
|
// 调用后端接口
|
|
|
|
|
|
const response = await catchPhoto(formData)
|
2026-03-09 16:22:17 +08:00
|
|
|
|
if (response.msg !== 'ok') {
|
2026-03-06 18:03:28 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const imageUrl = await response.data
|
|
|
|
|
|
resolve(imageUrl)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
reject(error)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 图片预览函数
|
|
|
|
|
|
const previewImage = (url: string) => {
|
|
|
|
|
|
// 使用Arco Design的图片预览组件
|
|
|
|
|
|
Modal.info({
|
|
|
|
|
|
title: '图片预览',
|
|
|
|
|
|
content: () => h('div', { style: 'text-align: center;' }, [
|
|
|
|
|
|
h('img', {
|
|
|
|
|
|
src: url,
|
|
|
|
|
|
style: 'max-width: 100%; max-height: 500px; object-fit: contain;',
|
|
|
|
|
|
}),
|
|
|
|
|
|
]),
|
|
|
|
|
|
okText: '关闭',
|
|
|
|
|
|
hideCancel: true,
|
|
|
|
|
|
width: 'auto',
|
|
|
|
|
|
})
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理重置
|
|
|
|
|
|
const handleReset = () => {
|
|
|
|
|
|
inputQuantity.value = ''
|
2026-03-05 18:14:25 +08:00
|
|
|
|
ahDeviceWeight.value = ''
|
2026-03-03 17:59:37 +08:00
|
|
|
|
calculatedWeight.value = ''
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理删除
|
|
|
|
|
|
const handleDelete = (key) => {
|
|
|
|
|
|
// 过滤掉要删除的项
|
2026-03-09 16:22:17 +08:00
|
|
|
|
weighingList.value = weighingList.value.filter((item) => item.key !== key)
|
2026-02-28 16:54:47 +08:00
|
|
|
|
// 重新排序称重次数
|
|
|
|
|
|
weighingList.value.forEach((item, index) => {
|
2026-03-06 14:17:19 +08:00
|
|
|
|
item.weightTime = index + 1
|
2026-02-28 16:54:47 +08:00
|
|
|
|
})
|
|
|
|
|
|
// 更新当前称重次数
|
|
|
|
|
|
weighingCount.value = weighingList.value.length + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
// 建立WebSocket连接
|
|
|
|
|
|
const establishWebSocket = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 这里替换为实际的WebSocket服务器地址
|
|
|
|
|
|
const wsUrl = 'ws://localhost:6609/ws/scale'
|
|
|
|
|
|
ws.value = new WebSocket(wsUrl)
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
ws.value.onopen = () => {
|
|
|
|
|
|
Message.success('已和电子秤建立连接')
|
|
|
|
|
|
}
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
ws.value.onmessage = (event) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (event.data) {
|
|
|
|
|
|
ahDeviceWeight.value = event.data
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('WebSocket消息解析失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
ws.value.onclose = () => {
|
|
|
|
|
|
wsConnected.value = false
|
|
|
|
|
|
}
|
2026-03-06 18:15:58 +08:00
|
|
|
|
|
2026-03-05 18:14:25 +08:00
|
|
|
|
ws.value.onerror = (error) => {
|
|
|
|
|
|
console.error('WebSocket错误:', error)
|
|
|
|
|
|
wsConnected.value = false
|
|
|
|
|
|
Message.error('称重连接失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('建立WebSocket连接失败:', error)
|
|
|
|
|
|
Message.error('无法建立称重连接')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭WebSocket连接
|
|
|
|
|
|
const closeWebSocket = () => {
|
|
|
|
|
|
if (ws.value) {
|
|
|
|
|
|
ws.value.close()
|
|
|
|
|
|
ws.value = null
|
|
|
|
|
|
wsConnected.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时关闭WebSocket连接
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
closeWebSocket()
|
2026-03-12 15:41:52 +08:00
|
|
|
|
// 清除图片刷新定时器
|
2026-03-12 16:12:30 +08:00
|
|
|
|
// if (imageRefreshTimer) {
|
|
|
|
|
|
// clearInterval(imageRefreshTimer)
|
|
|
|
|
|
// imageRefreshTimer = null
|
|
|
|
|
|
// }
|
2026-03-05 18:14:25 +08:00
|
|
|
|
})
|
2026-02-28 16:54:47 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.container {
|
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 步骤导航 */
|
|
|
|
|
|
.step-nav {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background-color: var(--color-bg-2);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-circle {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background-color: #e8e8e8;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-line {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
background-color: #e8e8e8;
|
|
|
|
|
|
margin: 0 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item.active .step-circle {
|
|
|
|
|
|
background-color: #1677ff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item.active .step-title {
|
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item.completed .step-circle {
|
|
|
|
|
|
background-color: #52c41a;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item.completed .step-title {
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-item.completed ~ .step-line {
|
|
|
|
|
|
background-color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 步骤内容 */
|
|
|
|
|
|
.step-content {
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 主内容区域 */
|
|
|
|
|
|
.main-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.left-section {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-right: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.right-section {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 称重登记页面 */
|
|
|
|
|
|
.weighing-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.left-weighing-section {
|
|
|
|
|
|
width: 400px;
|
|
|
|
|
|
margin-right: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.right-weighing-section {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.weighing-section {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-header h4 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 18:03:28 +08:00
|
|
|
|
/* 视频容器 - 替换原来的image-placeholder */
|
|
|
|
|
|
.video-container {
|
|
|
|
|
|
position: relative;
|
2026-02-28 16:54:47 +08:00
|
|
|
|
width: 100%;
|
2026-03-06 18:03:28 +08:00
|
|
|
|
height: 240px;
|
|
|
|
|
|
background: #000;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
overflow: hidden;
|
2026-03-12 15:41:52 +08:00
|
|
|
|
margin: 20px 0;
|
2026-02-28 16:54:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 18:03:28 +08:00
|
|
|
|
.video-container video {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.video-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.video-overlay.error {
|
|
|
|
|
|
background: rgba(255, 77, 79, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.camera-badge {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
right: 10px;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.live-badge {
|
|
|
|
|
|
background: #ff4d4f;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
animation: pulse 1.5s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
|
0% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0.7; }
|
|
|
|
|
|
100% { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.large-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 240px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 16:54:47 +08:00
|
|
|
|
.input-placeholder {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
background-color: #e8e8e8;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.weighing-actions {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 绿色背景列 */
|
|
|
|
|
|
:deep(.green-bg) {
|
|
|
|
|
|
background-color: #f6ffed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 完成创建页面 */
|
|
|
|
|
|
.completion-content {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 60px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completion-icon {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completion-content h2 {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completion-content p {
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completion-info {
|
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
|
margin: 0 auto 40px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
background-color: var(--color-bg-2);
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item .label {
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item .value {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completion-actions {
|
|
|
|
|
|
margin-top: 40px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 图片占位符 */
|
|
|
|
|
|
.image-placeholder {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 150px;
|
|
|
|
|
|
background-color: #e8e8e8;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 正方形图片 */
|
|
|
|
|
|
.square-image {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
height: 200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 样图 */
|
|
|
|
|
|
.sample-image {
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 相机区域 */
|
|
|
|
|
|
.camera-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.camera-section .image-placeholder {
|
|
|
|
|
|
width: calc(50% - 10px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表单区域 */
|
|
|
|
|
|
.form-section {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-right: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-item.full-width {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 确保输入框宽度一致 */
|
|
|
|
|
|
:deep(.arco-input-wrapper) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 35px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.arco-input) {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-item label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-actions {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 信息区域 */
|
|
|
|
|
|
.info-section {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-card {
|
|
|
|
|
|
background-color: var(--color-bg-2);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-card h4 {
|
|
|
|
|
|
margin: 0 0 16px 0;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item .label {
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-item .value {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.match-result {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.match-success {
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.match-failed {
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.match-pending {
|
|
|
|
|
|
color: #d9d9d9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-text {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 操作按钮 */
|
|
|
|
|
|
.action-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.next-button {
|
|
|
|
|
|
width: 120px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.previous-button {
|
|
|
|
|
|
width: 120px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
|
}
|
2026-03-11 10:56:33 +08:00
|
|
|
|
</style>
|