| | |
| | | /> |
| | | </el-tabs> |
| | | <div> |
| | | <!-- 扫码模块 --> |
| | | <div v-if="activeTab === 'qrCode'" class="scan-section"> |
| | | <div class="scan-controls"> |
| | | <el-button |
| | | type="primary" |
| | | :loading="scanLoading" |
| | | @click="toggleScan" |
| | | > |
| | | {{ scanButtonText }} |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 扫码视频容器 --> |
| | | <div v-show="isScanning" class="qr-video-container"> |
| | | <video |
| | | ref="qrVideo" |
| | | class="qr-video" |
| | | playsinline |
| | | webkit-playsinline |
| | | ></video> |
| | | <div class="scan-overlay"></div> |
| | | </div> |
| | | |
| | | <!-- 状态提示 --> |
| | | <div class="status-info"> |
| | | <el-alert |
| | | v-if="cameraError" |
| | | :title="cameraError" |
| | | type="error" |
| | | show-icon |
| | | closable |
| | | /> |
| | | <div v-if="isScanning" class="scanning-text"> |
| | | <el-icon :color="statusColor"><Loading /></el-icon> |
| | | 正在扫描二维码... |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <el-table ref="table" :data="tableData" height="480" v-loading="tableLoading"> |
| | | <el-table ref="table" :data="tableData" height="480" v-loading="tableLoading" v-if="activeTab !== 'qrCode'" border style="width: 100%;height: calc(100vh - 20em)"> |
| | | <el-table-column label="序号" type="index" width="60" align="center" /> |
| | | <el-table-column prop="taskName" label="巡检任务名称" :show-overflow-tooltip="true"></el-table-column> |
| | | <el-table-column prop="port" label="地点" :show-overflow-tooltip="true"></el-table-column> |
| | |
| | | <el-table-column fixed="right" label="操作"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="handleAdd(scope.row)">上传</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-table ref="table" :data="tableData" height="480" v-loading="tableLoading" v-if="activeTab === 'qrCode'" border style="width: 100%;height: calc(100vh - 23em)"> |
| | | <el-table-column label="序号" type="index" width="60" align="center" /> |
| | | <el-table-column prop="deviceName" label="设备名称" :show-overflow-tooltip="true"> |
| | | <template #default="scope"> |
| | | {{scope.row.qrCode.deviceName}} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="location" label="所在位置描述" :show-overflow-tooltip="true"> |
| | | <template #default="scope"> |
| | | {{scope.row.qrCode.location}} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="scanner" label="巡检人"></el-table-column> |
| | | <el-table-column prop="scanTime" label="巡检时间"></el-table-column> |
| | | <el-table-column fixed="right" label="操作"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="viewFile(scope.row)">查看附件</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </div> |
| | | </el-card> |
| | | <form-dia ref="formDia" @closeDia="handleQuery"></form-dia> |
| | | <qr-code-form-dia ref="qrCodeFormDia" @closeDia="handleQuery"></qr-code-form-dia> |
| | | <view-qr-code-files ref="viewQrCodeFiles"></view-qr-code-files> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import Pagination from "@/components/Pagination/index.vue"; |
| | | import {inspectionTaskList} from "@/api/inspectionManagement/index.js"; |
| | | import {onMounted, ref} from "vue"; |
| | | import FormDia from "@/views/inspectionUpload/components/formDia.vue"; |
| | | import {ElMessage} from "element-plus"; |
| | | import QrScanner from 'qr-scanner' |
| | | import QrCodeFormDia from "@/views/inspectionUpload/components/qrCodeFormDia.vue"; |
| | | import {qrCodeList, qrCodeScanRecordList} from "@/api/inspectionUpload/index.js"; |
| | | import {inspectionTaskList} from "@/api/inspectionManagement/index.js"; |
| | | import ViewQrCodeFiles from "@/views/inspectionManagement/components/viewQrCodeFiles.vue"; |
| | | const formDia = ref() |
| | | const qrCodeFormDia = ref() |
| | | const viewQrCodeFiles = ref() |
| | | // 当前标签 |
| | | const activeTab = ref("task"); |
| | | const tabName = ref("task"); |
| | | // 标签页数据 |
| | | const tabs = reactive([ |
| | | { name: "task", label: "任务下发" }, |
| | | { name: "qrCode", label: "二维码管理" }, |
| | | { name: "task", label: "生产巡检" }, |
| | | { name: "qrCode", label: "现场巡检" }, |
| | | ]); |
| | | // 表格 |
| | | const tableData = ref([]); |
| | |
| | | const total = ref(0); |
| | | const pageNum = ref(1); |
| | | const pageSize = ref(10); |
| | | // 扫码相关状态 |
| | | const qrVideo = ref(null) |
| | | const isScanning = ref(false) |
| | | const scanLoading = ref(false) |
| | | const cameraError = ref(null) |
| | | const scanner = ref(null) |
| | | const hasInit = ref(false) |
| | | |
| | | onMounted(() => { |
| | | const statusColor = computed(() => { |
| | | return isScanning.value ? '#67C23A' : '#F56C6C' |
| | | }) |
| | | // 生命周期管理优化 |
| | | onMounted(async () => { |
| | | handleTabClick({ props: { name: "task" } }); |
| | | }); |
| | | if (!import.meta.env.SSR && QrScanner) { // [!code focus] |
| | | await initScanner() |
| | | } |
| | | }) |
| | | |
| | | onBeforeUnmount(async () => { |
| | | if (scanner.value) { |
| | | await scanner.value.destroy() |
| | | scanner.value = null |
| | | } |
| | | hasInit.value = false |
| | | }) |
| | | // 标签页点击 |
| | | const handleTabClick = (tab) => { |
| | | tabName.value = tab.props.name; |
| | |
| | | } |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | inspectionTaskList({size: pageSize.value, current: pageNum.value}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | total.value = res.data.total; |
| | | }) |
| | | if (tabName.value === "task") { |
| | | inspectionTaskList({size: pageSize.value, current: pageNum.value}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | total.value = res.data.total; |
| | | }) |
| | | } else { |
| | | qrCodeScanRecordList({size: pageSize.value, current: pageNum.value}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | total.value = res.data.total; |
| | | }) |
| | | } |
| | | }; |
| | | // 上传 |
| | | const handleAdd = (row) => { |
| | |
| | | formDia.value?.openDialog(row) |
| | | }) |
| | | } |
| | | // 查看附件 |
| | | const viewFile = (row) => { |
| | | nextTick(() => { |
| | | viewQrCodeFiles.value?.openDialog(row) |
| | | }) |
| | | } |
| | | // 扫码按钮文本 |
| | | const scanButtonText = computed(() => { |
| | | if (scanLoading.value) return '正在初始化...' |
| | | return isScanning.value ? '停止扫码' : '开始扫码' |
| | | }) |
| | | |
| | | // 增强型初始化 |
| | | const initScanner = async () => { |
| | | try { |
| | | await nextTick() // 确保DOM更新 |
| | | // 新增多重空值校验 |
| | | if (!qrVideo.value || !QrScanner) { |
| | | throw new Error('依赖未正确初始化') |
| | | } |
| | | // 增加摄像头权限预检查 |
| | | const hasCamera = await QrScanner.hasCamera() |
| | | if (!hasCamera) { |
| | | throw new Error('未检测到可用摄像头') |
| | | } |
| | | // 显式销毁旧实例 |
| | | if (scanner.value) { |
| | | await scanner.value.destroy() |
| | | } |
| | | // 创建新实例 |
| | | scanner.value = new QrScanner( |
| | | qrVideo.value, |
| | | result => { |
| | | handleScanSuccess(result) |
| | | // stopScan() |
| | | }, |
| | | { |
| | | preferredCamera: 'environment', |
| | | maxScansPerSecond: 5, |
| | | returnDetailedScanResult: true |
| | | } |
| | | ) |
| | | // 新增硬件加速检测 |
| | | if (!scanner.value._qrWorker) { |
| | | throw new Error('硬件加速不可用') |
| | | } |
| | | hasInit.value = true |
| | | } catch (e) { |
| | | // handleInitError(e) |
| | | } |
| | | } |
| | | // 扫描成功处理 |
| | | const handleScanSuccess = async (result) => { |
| | | try { |
| | | // 添加数据校验 |
| | | ElMessage.success('识别成功') |
| | | callBackendAPI(JSON.parse(result.data)) |
| | | await stopScan() |
| | | } catch (error) { |
| | | ElMessage.warning(error.message) |
| | | await startScan() // 数据无效时继续扫描 |
| | | } |
| | | } |
| | | const callBackendAPI = (result) => { |
| | | nextTick(() => { |
| | | qrCodeFormDia.value?.openDialog(result) |
| | | }) |
| | | } |
| | | // 切换扫码状态 |
| | | const toggleScan = async () => { |
| | | if (isScanning.value) { |
| | | await stopScan() |
| | | } else { |
| | | await startScan() |
| | | } |
| | | } |
| | | |
| | | // 增强启动方法 |
| | | const startScan = async () => { |
| | | if (!scanner.value || !hasInit.value) { // 新增状态检查 |
| | | await initScanner() |
| | | } |
| | | |
| | | try { |
| | | await scanner.value.start() |
| | | isScanning.value = true |
| | | } catch (e) { |
| | | ElMessage.error(`启动失败: ${e.message}`) |
| | | hasInit.value = false |
| | | } |
| | | } |
| | | |
| | | // 停止扫码 |
| | | const stopScan = async () => { |
| | | try { |
| | | await scanner.value.stop() |
| | | isScanning.value = false |
| | | } catch (err) { |
| | | console.error('停止摄像头失败:', err) |
| | | } |
| | | } |
| | | |
| | | |
| | | // 错误处理增强 |
| | | const handleInitError = (error) => { |
| | | console.error('初始化失败:', error) |
| | | const msg = { |
| | | 'NotAllowedError': '请允许摄像头权限', |
| | | 'NotFoundError': '未找到摄像头设备', |
| | | 'NotSupportedError': '浏览器不支持扫码功能' |
| | | }[error.name] || error.message |
| | | |
| | | ElMessage.error(`初始化失败: ${msg}`) |
| | | } |
| | | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .qr-video-container { |
| | | position: relative; |
| | | width: 100%; |
| | | max-width: 500px; |
| | | margin: 0 auto; |
| | | background: #000; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .qr-video { |
| | | width: 100%; |
| | | height: auto; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .scan-overlay { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 70%; |
| | | height: 70%; |
| | | border: 3px solid #409eff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 0 20px rgba(64, 158, 255, 0.3); |
| | | animation: pulse 2s infinite; |
| | | } |
| | | |
| | | @keyframes pulse { |
| | | 0% { opacity: 0.8; } |
| | | 50% { opacity: 0.4; } |
| | | 100% { opacity: 0.8; } |
| | | } |
| | | |
| | | .status-info { |
| | | margin-top: 16px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .scanning-text { |
| | | color: #409eff; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .table-section { |
| | | margin-top: 24px; |
| | | } |
| | | |
| | | /* 移动端优化 */ |
| | | @media (max-width: 768px) { |
| | | .qr-video-container { |
| | | height: 60vh; |
| | | } |
| | | |
| | | .el-table { |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | </style> |