<template>
|
<div class="app-container">
|
<el-card>
|
<!-- 标签页 -->
|
<el-tabs
|
v-model="activeTab"
|
class="info-tabs"
|
@tab-click="handleTabClick"
|
>
|
<el-tab-pane
|
v-for="tab in tabs"
|
:key="tab.name"
|
:label="tab.label"
|
:name="tab.name"
|
/>
|
</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" v-if="activeTab !== 'qrCode'">
|
<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 prop="remarks" label="备注"></el-table-column>
|
<el-table-column prop="inspector" label="执行巡检人"></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'">
|
<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>
|
<pagination
|
v-if="total>0"
|
:page-num="pageNum"
|
:page-size="pageSize"
|
:total="total"
|
@pagination="handleQuery"
|
:layout="'total, prev, pager, next, jumper'"
|
/>
|
</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 {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: "现场巡检" },
|
]);
|
// 表格
|
const tableData = ref([]);
|
const tableLoading = ref(false);
|
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)
|
|
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;
|
tableData.value = [];
|
getList();
|
};
|
// 点击查询
|
const handleQuery = () => {
|
pageNum.value = 1
|
pageSize.value = 10
|
getList()
|
}
|
const getList = () => {
|
tableLoading.value = true;
|
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) => {
|
nextTick(() => {
|
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>
|