<template>
|
<view class="inspection-upload-page">
|
<!-- 页面头部 -->
|
<PageHeader title="设备巡检"
|
@back="goBack" />
|
<!-- 数据列表 -->
|
<view class="table-section">
|
<!-- 生产巡检列表 -->
|
<view class="task-list">
|
<view v-for="(item, index) in taskTableData"
|
:key="index"
|
class="task-item">
|
<view class="task-header">
|
<view class="task-info">
|
<text class="task-name">{{ item.taskName }}</text>
|
<text class="task-location">{{ item.inspectionLocation }}</text>
|
</view>
|
<view class="task-actions">
|
<!-- <u-button type="primary"
|
size="small"
|
@click.stop="startScanForTask(item)"
|
:customStyle="{
|
borderRadius: '15px',
|
height: '30px',
|
fontSize: '12px',
|
marginRight: '8px'
|
}">
|
扫码上传
|
</u-button> -->
|
<u-button type="primary"
|
size="small"
|
@click.stop="startUploadForTask(item)"
|
:customStyle="{
|
borderRadius: '15px',
|
height: '30px',
|
fontSize: '12px',
|
marginRight: '8px'
|
}">
|
图片上传
|
</u-button>
|
<u-button type="success"
|
size="small"
|
@click.stop="viewAttachments(item)"
|
:customStyle="{
|
borderRadius: '15px',
|
height: '30px',
|
fontSize: '12px'
|
}">
|
查看附件
|
</u-button>
|
</view>
|
</view>
|
<view class="task-details">
|
<view class="detail-item">
|
<text class="detail-label">任务ID</text>
|
<text class="detail-value">{{ item.taskId || item.id }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">巡检项目</text>
|
<text class="detail-value">{{ item.inspectionProject || '无' }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">备注</text>
|
<text class="detail-value">{{ item.remarks || '无' }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">执行人</text>
|
<text class="detail-value">{{ item.inspector }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">任务下发日期</text>
|
<text class="detail-value">{{ item.dateStr }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">巡检状态</text>
|
<view class="detail-value">
|
<uni-tag v-if="item.fileStatus==2"
|
text="已完成"
|
size="small"
|
type="success"
|
inverted></uni-tag>
|
<uni-tag v-else-if="item.fileStatus==1"
|
text="巡检中"
|
size="small"
|
type="primary"
|
inverted></uni-tag>
|
<uni-tag v-else
|
text="未巡检"
|
size="small"
|
type="warning"
|
inverted></uni-tag>
|
</view>
|
</view>
|
</view>
|
</view>
|
<uni-load-more :status="loadMoreStatus"></uni-load-more>
|
</view>
|
<!-- 空状态 -->
|
<view v-if="taskTableData?.length === 0"
|
class="no-data">
|
<text>暂无数据</text>
|
</view>
|
</view>
|
<!-- 视频预览弹窗 -->
|
<view v-if="showVideoDialog"
|
class="video-modal-overlay"
|
@click="closeVideoPreview">
|
<view class="video-modal-container"
|
@click.stop>
|
<view class="video-modal-header">
|
<text class="video-modal-title">{{ currentVideoFile?.originalFilename || '视频预览' }}</text>
|
<view class="close-btn-video"
|
@click="closeVideoPreview">
|
<u-icon name="close"
|
size="16"
|
color="#fff"></u-icon>
|
</view>
|
</view>
|
<view class="video-modal-body">
|
<video v-if="currentVideoFile"
|
:src="currentVideoFile.url || currentVideoFile.downloadUrl"
|
class="video-player"
|
controls
|
autoplay
|
@error="handleVideoError"></video>
|
</view>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { onMounted, onUnmounted, ref, nextTick, computed, reactive } from "vue";
|
import { onShow, onReachBottom, onPullDownRefresh } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { getLedgerById } from "@/api/equipmentManagement/ledger.js";
|
import {
|
inspectionTaskList,
|
uploadInspectionTask,
|
} from "@/api/inspectionManagement";
|
import { getToken } from "@/utils/auth";
|
import config from "@/config";
|
|
// 组件引用已移除
|
|
// 加载提示方法
|
const showLoadingToast = message => {
|
uni.showLoading({
|
title: message,
|
mask: true,
|
});
|
};
|
const closeToast = () => {
|
uni.hideLoading();
|
};
|
|
// 表格数据
|
const taskTableData = ref([]); // 生产巡检数据
|
|
// 当前扫描的任务
|
const currentScanningTask = ref(null);
|
const infoData = ref(null);
|
|
// 视频预览相关状态
|
const showVideoDialog = ref(false);
|
const currentVideoFile = ref(null);
|
|
// 请求取消标志,用于取消正在进行的请求
|
let isRequestCancelled = false;
|
|
const pagesPames = reactive({
|
size: 10,
|
current: 1,
|
});
|
|
const loadMoreStatus = computed(() => {
|
if (loading.value) {
|
return "loading";
|
}
|
if (noMore.value) {
|
return "noMore";
|
}
|
return "more";
|
});
|
const totalSize = ref(0);
|
const noMore = computed(() => {
|
return taskTableData.value.length >= totalSize.value;
|
});
|
const loading = ref(false);
|
|
const reloadPage = () => {
|
pagesPames.current = 1;
|
taskTableData.value = [];
|
getList();
|
};
|
const loadPage = () => {
|
if (noMore.value || loading.value) return;
|
pagesPames.current += 1;
|
getList();
|
};
|
|
// 生命周期
|
onMounted(() => {
|
// 延迟初始化,确保DOM已渲染
|
// nextTick(() => {
|
// getList()
|
// })
|
});
|
|
onReachBottom(() => {
|
loadPage();
|
});
|
onPullDownRefresh(() => {
|
reloadPage();
|
uni.stopPullDownRefresh();
|
});
|
|
onShow(() => {
|
// 页面显示时刷新数据
|
reloadPage();
|
});
|
|
// 组件销毁时的清理
|
onUnmounted(() => {
|
// 设置取消标志,阻止后续的异步操作
|
isRequestCancelled = true;
|
});
|
|
// 返回上一页
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
// 获取列表数据
|
const getList = () => {
|
// 显示加载提示
|
// showLoadingToast('加载中...')
|
|
// 设置取消标志
|
isRequestCancelled = false;
|
loading.value = true;
|
inspectionTaskList({ ...pagesPames })
|
.then(res => {
|
// 检查组件是否还存在且请求未被取消
|
if (!isRequestCancelled) {
|
// 处理不同的数据结构
|
let records = [];
|
if (res && res.data) {
|
// 尝试多种可能的数据结构
|
totalSize.value = res.data.total;
|
if (Array.isArray(res.data.records)) {
|
records = res.data.records;
|
} else if (Array.isArray(res.data.rows)) {
|
records = res.data.rows;
|
} else if (Array.isArray(res.data)) {
|
records = res.data;
|
} else if (Array.isArray(res.data.list)) {
|
records = res.data.list;
|
}
|
}
|
|
if (records.length > 0) {
|
taskTableData.value = [
|
...taskTableData.value,
|
...records.map(record => {
|
record.fileStatus = getFileStatus(record);
|
return record;
|
}),
|
];
|
} else {
|
taskTableData.value = [];
|
uni.showToast({
|
title: "暂无巡检任务数据",
|
icon: "none",
|
});
|
}
|
}
|
loading.value = false;
|
// 关闭加载提示
|
// closeToast()
|
})
|
.catch(err => {
|
// 检查组件是否还存在且请求未被取消
|
if (!isRequestCancelled) {
|
taskTableData.value = [];
|
// 添加错误提示
|
uni.showToast({
|
title: "获取数据失败",
|
icon: "error",
|
});
|
}
|
loading.value = false;
|
// 关闭加载提示
|
// closeToast()
|
});
|
};
|
|
const getFileStatus = record => {
|
let _beforeProduction =
|
record.beforeProduction && record.beforeProduction.length;
|
let _afterProduction =
|
record.afterProduction && record.afterProduction.length;
|
let _productionIssues =
|
record.productionIssues && record.productionIssues.length;
|
if (_beforeProduction && _afterProduction && _productionIssues) {
|
return 2;
|
} else if (_beforeProduction || _afterProduction || _productionIssues) {
|
return 1;
|
} else {
|
return 0;
|
}
|
};
|
|
// 为指定任务开始扫码(真机)
|
const startScanForTask = async task => {
|
try {
|
currentScanningTask.value = task;
|
uni.scanCode({
|
success: res => {
|
handleScanSuccess(res);
|
},
|
fail: err => {
|
console.error("扫码失败:", err);
|
uni.showToast({
|
title: "扫码失败",
|
icon: "error",
|
});
|
},
|
});
|
} catch (e) {
|
console.error("启动扫码失败:", e);
|
uni.showToast({
|
title: "启动扫码失败",
|
icon: "error",
|
});
|
}
|
};
|
|
// 扫码成功处理:校验后打开上传弹窗
|
const handleScanSuccess = result => {
|
try {
|
// 解析二维码数据,提取deviceId
|
let deviceId = "";
|
|
if (result?.result && typeof result.result === "string") {
|
if (result.result.includes("deviceId=")) {
|
const match = result.result.match(/deviceId=(\d+)/);
|
if (match && match[1]) deviceId = match[1];
|
} else {
|
try {
|
const qrData = JSON.parse(result.result);
|
deviceId = qrData.deviceId || qrData.qrCodeId || "";
|
} catch (e) {
|
deviceId = result.result;
|
}
|
}
|
}
|
|
if (!deviceId) {
|
uni.showToast({ title: "未识别到设备ID", icon: "error" });
|
return;
|
}
|
|
const currentTaskId =
|
currentScanningTask.value?.taskId || currentScanningTask.value?.id;
|
if (!currentTaskId) {
|
uni.showToast({ title: "任务信息缺失", icon: "error" });
|
return;
|
}
|
|
if (deviceId === currentTaskId.toString()) {
|
uni.showToast({ title: "识别成功", icon: "success" });
|
openUploadDialog(currentScanningTask.value);
|
} else {
|
uni.showToast({ title: "请扫描正确的设备", icon: "error" });
|
}
|
} catch (error) {
|
console.error("扫码结果处理失败:", error);
|
uni.showToast({
|
title: error?.message || "数据解析失败",
|
icon: "error",
|
});
|
}
|
};
|
|
// 打开上传页面
|
const openUploadDialog = task => {
|
// 将任务信息传递到上传页面
|
const taskData = encodeURIComponent(JSON.stringify(task));
|
uni.navigateTo({
|
url: `/pages/inspectionUpload/upload?taskInfo=${taskData}`,
|
});
|
};
|
|
// 图片上传(可选择图片上传或者是相机拍照)
|
const startUploadForTask = async (task, type) => {
|
// 打开上传页面
|
openUploadDialog(task);
|
};
|
|
// 查看附件 - 跳转到附件页面
|
const viewAttachments = async task => {
|
const taskData = encodeURIComponent(JSON.stringify(task));
|
uni.navigateTo({
|
url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`,
|
});
|
};
|
|
// 判断是否为图片文件
|
const isImageFile = file => {
|
// 检查contentType字段
|
if (file.contentType && file.contentType.startsWith("image/")) {
|
return true;
|
}
|
|
// 检查原有的type字段
|
if (file.type === "image") return true;
|
|
// 检查文件扩展名
|
const name = file.bucketFilename || file.originalFilename || file.name || "";
|
const ext = name.split(".").pop()?.toLowerCase();
|
return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
|
};
|
|
// 文件访问基础域(后端要求前缀)
|
const filePreviewBase = config.fileUrl;
|
|
// 将后端返回的文件地址规范成可访问URL
|
// 兼容场景:
|
// - 已经是 http/https:直接返回
|
// - 以 / 开头:拼接 filePreviewBase
|
// - Windows 本地路径(如 D:\ruoyi\prod\uploads...\xx.jpg):尝试截取 prod 之后的相对路径并拼接 filePreviewBase
|
const normalizeFileUrl = rawUrl => {
|
try {
|
if (!rawUrl || typeof rawUrl !== "string") return "";
|
const url = rawUrl.trim();
|
if (!url) return "";
|
if (/^https?:\/\//i.test(url)) return url;
|
if (url.startsWith("/")) return `${filePreviewBase}${url}`;
|
|
// Windows path -> web path
|
if (/^[a-zA-Z]:\\/.test(url)) {
|
const normalized = url.replace(/\\/g, "/");
|
const idx = normalized.indexOf("/prod/");
|
if (idx >= 0) {
|
const relative = normalized.slice(idx + "/prod/".length);
|
return `${filePreviewBase}/${relative}`;
|
}
|
// 兜底:无法推断映射规则时,至少把反斜杠变成正斜杠
|
return normalized;
|
}
|
|
// 其他相对路径:直接用 baseUrl 拼一下
|
return `${filePreviewBase}/${url.replace(/^\//, "")}`;
|
} catch (e) {
|
return rawUrl || "";
|
}
|
};
|
|
// 预览附件
|
const previewAttachment = file => {
|
if (isImageFile(file)) {
|
// 预览图片
|
const imageUrls = getCurrentViewAttachments()
|
.filter(f => isImageFile(f))
|
.map(f => f.url || f.downloadUrl);
|
|
uni.previewImage({
|
urls: imageUrls,
|
current: file.url || file.downloadUrl,
|
});
|
} else {
|
// 预览视频 - 显示视频播放弹窗
|
showVideoPreview(file);
|
}
|
};
|
|
// 显示视频预览
|
const showVideoPreview = file => {
|
currentVideoFile.value = file;
|
showVideoDialog.value = true;
|
};
|
|
// 关闭视频预览
|
const closeVideoPreview = () => {
|
showVideoDialog.value = false;
|
currentVideoFile.value = null;
|
};
|
|
// 视频播放错误处理
|
const handleVideoError = error => {
|
uni.showToast({
|
title: "视频播放失败",
|
icon: "error",
|
});
|
};
|
</script>
|
|
<style scoped>
|
.inspection-upload-page {
|
min-height: 100vh;
|
background-color: #f5f5f5;
|
}
|
|
.table-section {
|
padding: 15px;
|
}
|
|
.task-list {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
|
.task-item {
|
background: #fff;
|
border-radius: 12px;
|
padding: 15px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
transition: all 0.3s ease;
|
}
|
|
.task-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: flex-start;
|
margin-bottom: 12px;
|
}
|
|
.task-info {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.task-name {
|
font-size: 16px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
.task-location {
|
font-size: 14px;
|
color: #666;
|
}
|
|
.task-actions {
|
display: flex;
|
gap: 8px;
|
}
|
|
.task-details {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
}
|
|
.detail-item {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.detail-label {
|
font-size: 12px;
|
color: #999;
|
}
|
|
.detail-value {
|
font-size: 12px;
|
color: #666;
|
font-weight: 500;
|
}
|
|
.no-data {
|
text-align: center;
|
padding: 40px 20px;
|
color: #999;
|
font-size: 14px;
|
}
|
|
/* 扫码弹窗样式 */
|
.qr-scan-overlay {
|
position: fixed;
|
top: 0;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: rgba(0, 0, 0, 0.8);
|
z-index: 9999;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.qr-scan-container {
|
width: 90vw;
|
max-width: 400px;
|
background: #fff;
|
border-radius: 12px;
|
padding: 20px;
|
position: relative;
|
}
|
|
.scan-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
}
|
|
.scan-title {
|
font-size: 18px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
.qr-camera {
|
width: 100%;
|
height: 300px;
|
border-radius: 8px;
|
overflow: hidden;
|
margin-bottom: 15px;
|
}
|
|
.scan-frame-wrapper {
|
position: relative;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
.scan-frame {
|
width: 200px;
|
height: 200px;
|
border: 2px solid #409eff;
|
border-radius: 8px;
|
position: relative;
|
}
|
|
.scan-frame::before,
|
.scan-frame::after {
|
content: "";
|
position: absolute;
|
width: 20px;
|
height: 20px;
|
border: 2px solid #409eff;
|
}
|
|
.scan-frame::before {
|
top: -2px;
|
left: -2px;
|
border-right: none;
|
border-bottom: none;
|
}
|
|
.scan-frame::after {
|
bottom: -2px;
|
right: -2px;
|
border-left: none;
|
border-top: none;
|
}
|
|
.scan-tip {
|
margin-top: 10px;
|
font-size: 14px;
|
color: #666;
|
text-align: center;
|
}
|
|
/* 自定义模态框样式 */
|
.custom-modal-overlay {
|
position: fixed;
|
top: 0;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: rgba(0, 0, 0, 0.5);
|
z-index: 10000;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 20px;
|
}
|
|
.custom-modal-container {
|
width: 100%;
|
max-width: 500px;
|
max-height: 80vh;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
/* 视频预览弹窗样式 */
|
.video-modal-overlay {
|
position: fixed;
|
top: 0;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: rgba(0, 0, 0, 0.8);
|
z-index: 10001;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 20px;
|
}
|
|
.video-modal-container {
|
width: 90%;
|
max-width: 800px;
|
max-height: 80vh;
|
background: #000;
|
border-radius: 12px;
|
overflow: hidden;
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
}
|
|
.video-modal-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 15px 20px;
|
background: rgba(0, 0, 0, 0.7);
|
color: #fff;
|
}
|
|
.video-modal-title {
|
font-size: 16px;
|
font-weight: 500;
|
color: #fff;
|
}
|
|
.close-btn-video {
|
width: 28px;
|
height: 28px;
|
border-radius: 50%;
|
background: rgba(255, 255, 255, 0.2);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
}
|
|
.close-btn-video:hover {
|
background: rgba(255, 255, 255, 0.3);
|
transform: scale(1.1);
|
}
|
|
.video-modal-body {
|
position: relative;
|
background: #000;
|
}
|
|
.video-player {
|
width: 100%;
|
height: auto;
|
max-height: 60vh;
|
display: block;
|
}
|
</style>
|