|
|
|
@ -170,12 +170,14 @@ |
|
|
|
</div> |
|
|
|
|
|
|
|
<el-dialog |
|
|
|
title="节点报工" |
|
|
|
:title="reportDialogTitle" |
|
|
|
:visible.sync="reportDialogVisible" |
|
|
|
width="400px" |
|
|
|
:width="requiresMediaUpload(reportData.orderType, reportData.nodeCode, reportData.nodeName) ? '80%' : '400px'" |
|
|
|
:close-on-click-modal="false" |
|
|
|
:before-close="handleReportDialogClose" |
|
|
|
:custom-class="reportDialogClass" |
|
|
|
v-drag> |
|
|
|
<el-form :model="reportData" label-position="top"> |
|
|
|
<el-form :model="reportData" label-position="top" class="report-dialog-form"> |
|
|
|
<el-row :gutter="12"> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="项目号"> |
|
|
|
@ -188,21 +190,98 @@ |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
</el-row> |
|
|
|
<el-row :gutter="24"> |
|
|
|
<el-row :gutter="24" v-if="isQtyRequired(reportData.orderType)"> |
|
|
|
<el-col :span="24"> |
|
|
|
<el-form-item label="报工数量" required> |
|
|
|
<el-input v-model="reportData.reportQty" style="width: 100%"></el-input> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
</el-row> |
|
|
|
<el-row :gutter="24" v-if="requiresMediaUpload(reportData.orderType, reportData.nodeCode, reportData.nodeName)"> |
|
|
|
<el-col :span="24"> |
|
|
|
<el-form-item label="报工影像(支持拍照和录像,可多次采集)"> |
|
|
|
<div class="media-layout"> |
|
|
|
<div class="media-layout-left"> |
|
|
|
<div class="camera-preview-wrap"> |
|
|
|
<video ref="cameraPreview" class="camera-preview" autoplay playsinline muted></video> |
|
|
|
<div v-if="!cameraActive" class="camera-placeholder">摄像头未开启</div> |
|
|
|
</div> |
|
|
|
<div class="camera-action-bar"> |
|
|
|
<!-- <el-button size="mini" type="primary" plain icon="el-icon-video-camera" @click="startCamera" :disabled="reportLoading"> |
|
|
|
开启摄像头 |
|
|
|
</el-button> |
|
|
|
<el-button size="mini" plain icon="el-icon-camera" @click="capturePhoto" :disabled="!cameraActive || reportLoading || recording"> |
|
|
|
拍照 |
|
|
|
</el-button> |
|
|
|
<el-button |
|
|
|
size="mini" |
|
|
|
:type="recording ? 'danger' : 'warning'" |
|
|
|
plain |
|
|
|
icon="el-icon-video-play" |
|
|
|
@click="toggleVideoRecording" |
|
|
|
:disabled="!cameraActive || reportLoading"> |
|
|
|
{{ recording ? '停止录像' : '开始录像' }} |
|
|
|
</el-button>--> |
|
|
|
<el-button class="camera-btn" plain icon="el-icon-camera" @click="triggerSystemCamera('image')" :disabled="reportLoading"> |
|
|
|
拍照 |
|
|
|
</el-button> |
|
|
|
<el-button class="video-btn" plain icon="el-icon-video-camera" @click="triggerSystemCamera('video')" :disabled="reportLoading"> |
|
|
|
录像 |
|
|
|
</el-button> |
|
|
|
<!-- <el-button size="mini" plain icon="el-icon-switch-button" @click="stopCamera" :disabled="!cameraActive || reportLoading"> |
|
|
|
关闭摄像头 |
|
|
|
</el-button>--> |
|
|
|
</div> |
|
|
|
<input |
|
|
|
ref="cameraImageInput" |
|
|
|
class="camera-file-input" |
|
|
|
type="file" |
|
|
|
accept="image/*" |
|
|
|
capture="environment" |
|
|
|
multiple |
|
|
|
@change="handleSystemCameraChange($event, 'image')"> |
|
|
|
<input |
|
|
|
ref="cameraVideoInput" |
|
|
|
class="camera-file-input" |
|
|
|
type="file" |
|
|
|
accept="video/*" |
|
|
|
capture="environment" |
|
|
|
multiple |
|
|
|
@change="handleSystemCameraChange($event, 'video')"> |
|
|
|
</div> |
|
|
|
<div class="media-layout-right"> |
|
|
|
<div class="capture-list-title">已采集 {{ capturedMediaList.length }} 个文件</div> |
|
|
|
<div class="capture-list-panel"> |
|
|
|
<div v-if="capturedMediaList.length > 0" class="capture-list"> |
|
|
|
<div v-for="(item, index) in capturedMediaList" :key="item.uid" class="capture-item"> |
|
|
|
<img v-if="isImageFile(item)" :src="item.previewUrl" class="capture-thumb" alt="capture-image"> |
|
|
|
<video v-else class="capture-thumb" controls preload="metadata"> |
|
|
|
<source :src="item.previewUrl" :type="item.mimeType || 'video/webm'"> |
|
|
|
</video> |
|
|
|
<div class="capture-meta"> |
|
|
|
<span class="capture-name">{{ item.name }}</span> |
|
|
|
<el-tag size="mini" :type="item.kind === 'video' ? 'warning' : 'success'"> |
|
|
|
{{ item.kind === 'video' ? '视频' : '照片' }} |
|
|
|
</el-tag> |
|
|
|
</div> |
|
|
|
<el-button type="danger" @click="removeCapturedMedia(index)">删除</el-button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div v-else class="empty-capture-list"> |
|
|
|
<i class="el-icon-camera"></i> |
|
|
|
<p>暂无采集文件,请先拍照或录像</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
</el-row> |
|
|
|
</el-form> |
|
|
|
<div slot="footer" class="dialog-footer"> |
|
|
|
<el-button size="mini" class="reset-btn" |
|
|
|
plain @click="reportDialogVisible = false">取消</el-button> |
|
|
|
<el-button size="mini" |
|
|
|
type="primary" |
|
|
|
plain :loading="reportLoading" @click="submitNodeReport"> |
|
|
|
{{ reportLoading ? '提交中...' : '确认报工' }} |
|
|
|
<el-button size="mini" class="reset-btn" plain @click="closeReportDialog">取消</el-button> |
|
|
|
<el-button size="mini" type="primary" plain :loading="reportLoading" @click="submitNodeReport"> |
|
|
|
{{ reportLoading ? (requiresMediaUpload(reportData.orderType, reportData.nodeCode, reportData.nodeName) ? '上传中...' : '提交中...') : (requiresMediaUpload(reportData.orderType, reportData.nodeCode, reportData.nodeName) ? '上传并报工' : '确认报工') }} |
|
|
|
</el-button> |
|
|
|
</div> |
|
|
|
</el-dialog> |
|
|
|
@ -239,7 +318,7 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import { getWorkReportOrderList, reportCableCopTaskNode, reportHomeLiftOrderNode, reportMachiningTaskNode, reportRenovationOrderNode } from '@/api/longchuang/productionPlan' |
|
|
|
import { getWorkReportOrderList, reportCableCopTaskNode, reportHomeLiftOrderNode, reportMachiningTaskNode, reportRenovationOrderNode, reportWorkNodeWithMedia } from '@/api/longchuang/productionPlan' |
|
|
|
|
|
|
|
export default { |
|
|
|
name: 'ProductionWorkReport', |
|
|
|
@ -272,7 +351,14 @@ export default { |
|
|
|
reportQty: 1, |
|
|
|
reportBy: '', |
|
|
|
remark: '' |
|
|
|
} |
|
|
|
}, |
|
|
|
cameraActive: false, |
|
|
|
cameraStream: null, |
|
|
|
mediaRecorder: null, |
|
|
|
recording: false, |
|
|
|
recordedChunks: [], |
|
|
|
captureIndex: 1, |
|
|
|
capturedMediaList: [] |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
@ -281,11 +367,27 @@ export default { |
|
|
|
}, |
|
|
|
pendingNodeCount () { |
|
|
|
return this.dataList.reduce((sum, item) => sum + (item.visibleNodeList || []).filter(node => node.status !== '已完成').length, 0) |
|
|
|
}, |
|
|
|
reportDialogTitle () { |
|
|
|
return this.requiresMediaUpload(this.reportData.orderType, this.reportData.nodeCode, this.reportData.nodeName) ? '节点报工与影像上传' : '节点报工' |
|
|
|
}, |
|
|
|
reportDialogClass () { |
|
|
|
const classList = ['report-node-dialog'] |
|
|
|
if (this.requiresMediaUpload(this.reportData.orderType, this.reportData.nodeCode, this.reportData.nodeName)) { |
|
|
|
classList.push('report-node-dialog--media') |
|
|
|
} |
|
|
|
return classList.join(' ') |
|
|
|
} |
|
|
|
}, |
|
|
|
activated () { |
|
|
|
this.getDataList('Y') |
|
|
|
}, |
|
|
|
deactivated () { |
|
|
|
this.releaseMediaResources() |
|
|
|
}, |
|
|
|
beforeDestroy () { |
|
|
|
this.releaseMediaResources() |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
getDataList (flag) { |
|
|
|
if (flag === 'Y') { |
|
|
|
@ -400,12 +502,49 @@ export default { |
|
|
|
return !!order.currentNodeCode && order.currentNodeCode === node.nodeCode |
|
|
|
}, |
|
|
|
handleReportNode (order, node) { |
|
|
|
if (order && order.orderType === 'MACHINING') { |
|
|
|
if (!order || !node) { |
|
|
|
return |
|
|
|
} |
|
|
|
if (order.orderType === 'MACHINING') { |
|
|
|
this.openReportDialog(order, node, true) |
|
|
|
return |
|
|
|
} |
|
|
|
if (this.requiresMediaUpload(order.orderType, node.nodeCode, node.nodeName)) { |
|
|
|
this.openReportDialog(order, node, false) |
|
|
|
return |
|
|
|
} |
|
|
|
this.directReportNode(order, node) |
|
|
|
}, |
|
|
|
isQtyRequired (orderType) { |
|
|
|
return ['CABLE_COP', 'MACHINING'].includes(orderType) |
|
|
|
}, |
|
|
|
requiresMediaUpload (orderType, nodeCode, nodeName) { |
|
|
|
if (!orderType) { |
|
|
|
return false |
|
|
|
} |
|
|
|
const requiredMap = { |
|
|
|
HOME_LIFT: { |
|
|
|
codeList: ['platformDebug', 'bgCeiling', 'doorAssy', 'pack'], |
|
|
|
nameList: ['平台组装/调试', '背景墙/吊顶组装', '门组装', '打包'] |
|
|
|
}, |
|
|
|
RENOVATION: { |
|
|
|
codeList: ['inspect', 'pack'], |
|
|
|
nameList: ['检验', '打包'] |
|
|
|
} |
|
|
|
} |
|
|
|
const config = requiredMap[orderType] |
|
|
|
if (!config) { |
|
|
|
return false |
|
|
|
} |
|
|
|
if (nodeCode && config.codeList.includes(nodeCode)) { |
|
|
|
return true |
|
|
|
} |
|
|
|
if (!nodeName) { |
|
|
|
return false |
|
|
|
} |
|
|
|
const normalizedName = String(nodeName).replace(/\s+/g, '') |
|
|
|
return config.nameList.some(name => name.replace(/\s+/g, '') === normalizedName) |
|
|
|
}, |
|
|
|
directReportNode (order, node) { |
|
|
|
this.reportData = { |
|
|
|
orderNo: order.orderNo, |
|
|
|
@ -431,19 +570,35 @@ export default { |
|
|
|
remark: '' |
|
|
|
} |
|
|
|
this.reportDialogVisible = true |
|
|
|
this.releaseMediaResources() |
|
|
|
this.captureIndex = 1 |
|
|
|
}, |
|
|
|
closeReportDialog () { |
|
|
|
this.reportDialogVisible = false |
|
|
|
this.releaseMediaResources() |
|
|
|
}, |
|
|
|
handleReportDialogClose (done) { |
|
|
|
this.closeReportDialog() |
|
|
|
if (typeof done === 'function') { |
|
|
|
done() |
|
|
|
} |
|
|
|
}, |
|
|
|
submitNodeReport () { |
|
|
|
if (!this.reportData.nodeCode) { |
|
|
|
this.$message.warning('请选择报工节点') |
|
|
|
return |
|
|
|
} |
|
|
|
if (['CABLE_COP', 'MACHINING'].includes(this.reportData.orderType)) { |
|
|
|
if (this.isQtyRequired(this.reportData.orderType)) { |
|
|
|
const qty = Number(this.reportData.reportQty) |
|
|
|
if (!Number.isFinite(qty) || qty <= 0) { |
|
|
|
this.$message.warning('请填写大于0的实际生产数量') |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
if (this.requiresMediaUpload(this.reportData.orderType, this.reportData.nodeCode, this.reportData.nodeName)) { |
|
|
|
this.submitNodeReportWithMedia() |
|
|
|
return |
|
|
|
} |
|
|
|
const apiMap = { |
|
|
|
HOME_LIFT: reportHomeLiftOrderNode, |
|
|
|
CABLE_COP: reportCableCopTaskNode, |
|
|
|
@ -456,16 +611,11 @@ export default { |
|
|
|
return |
|
|
|
} |
|
|
|
this.reportLoading = true |
|
|
|
apiFn({ |
|
|
|
orderNo: this.reportData.orderNo, |
|
|
|
nodeCode: this.reportData.nodeCode, |
|
|
|
reportQty: this.reportData.reportQty, |
|
|
|
remark: this.reportData.remark |
|
|
|
}).then(({ data }) => { |
|
|
|
apiFn(this.buildNodeReportPayload()).then(({ data }) => { |
|
|
|
this.reportLoading = false |
|
|
|
if (data && data.code === 0) { |
|
|
|
this.$message.success(data.msg || '报工成功') |
|
|
|
this.reportDialogVisible = false |
|
|
|
this.closeReportDialog() |
|
|
|
this.getDataList() |
|
|
|
} else { |
|
|
|
this.$message.error((data && data.msg) || '报工失败') |
|
|
|
@ -475,6 +625,291 @@ export default { |
|
|
|
this.$message.error('报工失败') |
|
|
|
}) |
|
|
|
}, |
|
|
|
submitNodeReportWithMedia () { |
|
|
|
if (!this.capturedMediaList.length) { |
|
|
|
this.$message.warning('请先上传检验照片或视频') |
|
|
|
return |
|
|
|
} |
|
|
|
const formData = new FormData() |
|
|
|
const payload = this.buildNodeReportPayload() |
|
|
|
formData.append('orderNo', payload.orderNo) |
|
|
|
formData.append('orderType', this.reportData.orderType || '') |
|
|
|
formData.append('nodeCode', payload.nodeCode) |
|
|
|
formData.append('reportQty', String(payload.reportQty || 1)) |
|
|
|
formData.append('remark', payload.remark || '') |
|
|
|
this.capturedMediaList.forEach(item => { |
|
|
|
formData.append('file', item.raw, item.name) |
|
|
|
}) |
|
|
|
this.reportLoading = true |
|
|
|
reportWorkNodeWithMedia(formData).then(({ data }) => { |
|
|
|
this.reportLoading = false |
|
|
|
if (data && data.code === 0) { |
|
|
|
this.$message.success(data.msg || '上传并报工成功') |
|
|
|
this.closeReportDialog() |
|
|
|
this.getDataList() |
|
|
|
} else { |
|
|
|
this.$message.error((data && data.msg) || '上传失败') |
|
|
|
} |
|
|
|
}).catch(() => { |
|
|
|
this.reportLoading = false |
|
|
|
this.$message.error('上传失败') |
|
|
|
}) |
|
|
|
}, |
|
|
|
buildNodeReportPayload () { |
|
|
|
const reportQty = this.isQtyRequired(this.reportData.orderType) ? this.reportData.reportQty : 1 |
|
|
|
return { |
|
|
|
orderNo: this.reportData.orderNo, |
|
|
|
nodeCode: this.reportData.nodeCode, |
|
|
|
reportQty: reportQty, |
|
|
|
remark: this.reportData.remark || '' |
|
|
|
} |
|
|
|
}, |
|
|
|
async startCamera () { |
|
|
|
if (!this.requiresMediaUpload(this.reportData.orderType, this.reportData.nodeCode, this.reportData.nodeName)) { |
|
|
|
return |
|
|
|
} |
|
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { |
|
|
|
this.$message.error('当前设备不支持摄像头调用,请使用“系统相机”') |
|
|
|
return |
|
|
|
} |
|
|
|
this.stopCamera(true) |
|
|
|
try { |
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
|
|
video: { |
|
|
|
facingMode: { ideal: 'environment' }, |
|
|
|
width: { ideal: 1280 }, |
|
|
|
height: { ideal: 720 } |
|
|
|
}, |
|
|
|
audio: false |
|
|
|
}) |
|
|
|
this.cameraStream = stream |
|
|
|
this.cameraActive = true |
|
|
|
const videoEl = this.$refs.cameraPreview |
|
|
|
if (videoEl) { |
|
|
|
videoEl.srcObject = stream |
|
|
|
const playPromise = videoEl.play() |
|
|
|
if (playPromise && typeof playPromise.then === 'function') { |
|
|
|
playPromise.catch(() => {}) |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
this.cameraActive = false |
|
|
|
this.$message.error('摄像头打开失败,请检查权限或改用“系统相机”') |
|
|
|
} |
|
|
|
}, |
|
|
|
stopCamera (discardRecording = false) { |
|
|
|
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') { |
|
|
|
if (discardRecording) { |
|
|
|
this.mediaRecorder.ondataavailable = null |
|
|
|
this.mediaRecorder.onstop = null |
|
|
|
this.mediaRecorder.stop() |
|
|
|
this.resetRecordingState() |
|
|
|
} else { |
|
|
|
this.stopVideoRecording(false) |
|
|
|
} |
|
|
|
} |
|
|
|
if (this.cameraStream && this.cameraStream.getTracks) { |
|
|
|
this.cameraStream.getTracks().forEach(track => track.stop()) |
|
|
|
} |
|
|
|
this.cameraStream = null |
|
|
|
this.cameraActive = false |
|
|
|
const videoEl = this.$refs.cameraPreview |
|
|
|
if (videoEl) { |
|
|
|
videoEl.srcObject = null |
|
|
|
} |
|
|
|
}, |
|
|
|
capturePhoto () { |
|
|
|
if (!this.cameraActive || !this.cameraStream) { |
|
|
|
this.$message.warning('请先开启摄像头') |
|
|
|
return |
|
|
|
} |
|
|
|
const videoEl = this.$refs.cameraPreview |
|
|
|
if (!videoEl || !videoEl.videoWidth || !videoEl.videoHeight) { |
|
|
|
this.$message.warning('摄像头画面尚未准备完成') |
|
|
|
return |
|
|
|
} |
|
|
|
const canvas = document.createElement('canvas') |
|
|
|
canvas.width = videoEl.videoWidth |
|
|
|
canvas.height = videoEl.videoHeight |
|
|
|
const ctx = canvas.getContext('2d') |
|
|
|
if (!ctx) { |
|
|
|
this.$message.error('无法采集照片,请稍后重试') |
|
|
|
return |
|
|
|
} |
|
|
|
ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height) |
|
|
|
canvas.toBlob(blob => { |
|
|
|
if (!blob) { |
|
|
|
this.$message.error('照片生成失败,请重试') |
|
|
|
return |
|
|
|
} |
|
|
|
this.appendCapturedFile(blob, 'image', 'image/jpeg') |
|
|
|
}, 'image/jpeg', 0.92) |
|
|
|
}, |
|
|
|
toggleVideoRecording () { |
|
|
|
if (this.recording) { |
|
|
|
this.stopVideoRecording(true) |
|
|
|
} else { |
|
|
|
this.startVideoRecording() |
|
|
|
} |
|
|
|
}, |
|
|
|
startVideoRecording () { |
|
|
|
if (!this.cameraActive || !this.cameraStream) { |
|
|
|
this.$message.warning('请先开启摄像头') |
|
|
|
return |
|
|
|
} |
|
|
|
if (typeof MediaRecorder === 'undefined') { |
|
|
|
this.$message.warning('当前浏览器不支持录像,请使用“系统相机”') |
|
|
|
return |
|
|
|
} |
|
|
|
const recorderOptions = this.resolveMediaRecorderOptions() |
|
|
|
this.recordedChunks = [] |
|
|
|
try { |
|
|
|
this.mediaRecorder = new MediaRecorder(this.cameraStream, recorderOptions) |
|
|
|
} catch (e) { |
|
|
|
this.$message.warning('当前设备录像能力受限,请使用“系统相机”') |
|
|
|
return |
|
|
|
} |
|
|
|
this.mediaRecorder.ondataavailable = event => { |
|
|
|
if (event.data && event.data.size > 0) { |
|
|
|
this.recordedChunks.push(event.data) |
|
|
|
} |
|
|
|
} |
|
|
|
this.mediaRecorder.onstop = () => { |
|
|
|
if (!this.recordedChunks.length) { |
|
|
|
this.resetRecordingState() |
|
|
|
return |
|
|
|
} |
|
|
|
const mimeType = this.mediaRecorder && this.mediaRecorder.mimeType ? this.mediaRecorder.mimeType : 'video/webm' |
|
|
|
const blob = new Blob(this.recordedChunks, { type: mimeType }) |
|
|
|
this.appendCapturedFile(blob, 'video', mimeType) |
|
|
|
this.resetRecordingState() |
|
|
|
} |
|
|
|
this.mediaRecorder.start() |
|
|
|
this.recording = true |
|
|
|
this.$message.success('开始录像') |
|
|
|
}, |
|
|
|
stopVideoRecording (showMessage) { |
|
|
|
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') { |
|
|
|
return |
|
|
|
} |
|
|
|
this.mediaRecorder.stop() |
|
|
|
this.recording = false |
|
|
|
if (showMessage) { |
|
|
|
this.$message.success('录像已结束') |
|
|
|
} |
|
|
|
}, |
|
|
|
resolveMediaRecorderOptions () { |
|
|
|
if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function') { |
|
|
|
return {} |
|
|
|
} |
|
|
|
const mimeTypeList = [ |
|
|
|
'video/webm;codecs=vp9', |
|
|
|
'video/webm;codecs=vp8', |
|
|
|
'video/webm', |
|
|
|
'video/mp4' |
|
|
|
] |
|
|
|
for (let i = 0; i < mimeTypeList.length; i++) { |
|
|
|
const mimeType = mimeTypeList[i] |
|
|
|
if (MediaRecorder.isTypeSupported(mimeType)) { |
|
|
|
return { mimeType: mimeType } |
|
|
|
} |
|
|
|
} |
|
|
|
return {} |
|
|
|
}, |
|
|
|
appendCapturedFile (blob, kind, mimeType) { |
|
|
|
const name = this.createCaptureName(kind, mimeType) |
|
|
|
const file = new File([blob], name, { type: mimeType || blob.type || '' }) |
|
|
|
const previewUrl = URL.createObjectURL(file) |
|
|
|
this.capturedMediaList.push({ |
|
|
|
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, |
|
|
|
name: name, |
|
|
|
kind: kind, |
|
|
|
mimeType: file.type, |
|
|
|
raw: file, |
|
|
|
previewUrl: previewUrl |
|
|
|
}) |
|
|
|
}, |
|
|
|
createCaptureName (kind, mimeType) { |
|
|
|
const extMap = { |
|
|
|
'image/jpeg': 'jpg', |
|
|
|
'image/png': 'png', |
|
|
|
'video/webm': 'webm', |
|
|
|
'video/mp4': 'mp4' |
|
|
|
} |
|
|
|
const fallbackExt = kind === 'video' ? 'webm' : 'jpg' |
|
|
|
const extension = extMap[mimeType] || fallbackExt |
|
|
|
const now = new Date() |
|
|
|
const dateText = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}` |
|
|
|
const nodeCode = this.reportData.nodeCode || 'node' |
|
|
|
const seq = String(this.captureIndex++).padStart(3, '0') |
|
|
|
return `${nodeCode}_${kind}_${dateText}_${seq}.${extension}` |
|
|
|
}, |
|
|
|
removeCapturedMedia (index) { |
|
|
|
const target = this.capturedMediaList[index] |
|
|
|
if (target && target.previewUrl) { |
|
|
|
URL.revokeObjectURL(target.previewUrl) |
|
|
|
} |
|
|
|
this.capturedMediaList.splice(index, 1) |
|
|
|
}, |
|
|
|
resetCapturedMedia () { |
|
|
|
this.capturedMediaList.forEach(item => { |
|
|
|
if (item && item.previewUrl) { |
|
|
|
URL.revokeObjectURL(item.previewUrl) |
|
|
|
} |
|
|
|
}) |
|
|
|
this.capturedMediaList = [] |
|
|
|
}, |
|
|
|
resetRecordingState () { |
|
|
|
this.mediaRecorder = null |
|
|
|
this.recording = false |
|
|
|
this.recordedChunks = [] |
|
|
|
}, |
|
|
|
triggerSystemCamera (mode) { |
|
|
|
const refName = mode === 'video' ? 'cameraVideoInput' : 'cameraImageInput' |
|
|
|
const inputEl = this.$refs[refName] |
|
|
|
if (!inputEl) { |
|
|
|
return |
|
|
|
} |
|
|
|
inputEl.value = '' |
|
|
|
inputEl.click() |
|
|
|
}, |
|
|
|
handleSystemCameraChange (event, expectedKind) { |
|
|
|
const files = (event && event.target && event.target.files) ? Array.from(event.target.files) : [] |
|
|
|
if (!files.length) { |
|
|
|
return |
|
|
|
} |
|
|
|
files.forEach(file => { |
|
|
|
const previewUrl = URL.createObjectURL(file) |
|
|
|
const fileKind = expectedKind || (file.type && file.type.indexOf('video/') === 0 ? 'video' : 'image') |
|
|
|
const fileName = file.name || this.createCaptureName(fileKind, file.type || '') |
|
|
|
this.capturedMediaList.push({ |
|
|
|
uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, |
|
|
|
name: fileName, |
|
|
|
kind: fileKind, |
|
|
|
mimeType: file.type || '', |
|
|
|
raw: file, |
|
|
|
previewUrl: previewUrl |
|
|
|
}) |
|
|
|
}) |
|
|
|
if (event && event.target) { |
|
|
|
event.target.value = '' |
|
|
|
} |
|
|
|
}, |
|
|
|
isImageFile (item) { |
|
|
|
return !!(item && item.mimeType && item.mimeType.indexOf('image/') === 0) |
|
|
|
}, |
|
|
|
releaseMediaResources () { |
|
|
|
this.stopCamera(true) |
|
|
|
this.resetCapturedMedia() |
|
|
|
this.resetRecordingState() |
|
|
|
const inputRefList = ['cameraImageInput', 'cameraVideoInput'] |
|
|
|
inputRefList.forEach(refName => { |
|
|
|
const inputEl = this.$refs[refName] |
|
|
|
if (inputEl) { |
|
|
|
inputEl.value = '' |
|
|
|
} |
|
|
|
}) |
|
|
|
}, |
|
|
|
openHistoryDialog (item) { |
|
|
|
this.historyOrder = item |
|
|
|
this.historyDialogVisible = true |
|
|
|
@ -622,8 +1057,9 @@ export default { |
|
|
|
|
|
|
|
.cards-grid { |
|
|
|
display: grid; |
|
|
|
grid-template-columns: repeat(auto-fill, minmax(390px, 1fr)); |
|
|
|
grid-template-columns: repeat(auto-fill, 320px); |
|
|
|
gap: 15px; |
|
|
|
justify-content: start; |
|
|
|
} |
|
|
|
|
|
|
|
.report-card { |
|
|
|
@ -831,6 +1267,210 @@ export default { |
|
|
|
font-size: 42px; |
|
|
|
} |
|
|
|
|
|
|
|
.media-capture-tip { |
|
|
|
font-size: 12px; |
|
|
|
color: #909399; |
|
|
|
margin-bottom: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media { |
|
|
|
margin-top: 4vh !important; |
|
|
|
width: min(1080px, 96vw) !important; |
|
|
|
max-width: calc(100vw - 12px); |
|
|
|
height: 85vh; |
|
|
|
max-height: 85vh; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .el-dialog__header, |
|
|
|
.report-container >>> .report-node-dialog--media .el-dialog__footer { |
|
|
|
flex: 0 0 auto; |
|
|
|
padding-left: 16px; |
|
|
|
padding-right: 16px; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .el-dialog__body { |
|
|
|
flex: 1 1 auto; |
|
|
|
min-height: 0; |
|
|
|
overflow: hidden; |
|
|
|
padding: 8px 16px 10px; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
.report-dialog-form { |
|
|
|
width: 100%; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .report-dialog-form { |
|
|
|
flex: 1 1 auto; |
|
|
|
min-height: 0; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .report-dialog-form > .el-row:last-child { |
|
|
|
flex: 1 1 auto; |
|
|
|
min-height: 0; |
|
|
|
margin-bottom: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .report-dialog-form .el-form-item { |
|
|
|
margin-bottom: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.media-layout { |
|
|
|
display: grid; |
|
|
|
grid-template-columns: minmax(0, 2.1fr) minmax(300px, 1fr); |
|
|
|
gap: 12px; |
|
|
|
align-items: stretch; |
|
|
|
height: 100%; |
|
|
|
min-height: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.media-layout-left { |
|
|
|
min-width: 0; |
|
|
|
min-height: 0; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
.media-layout-right { |
|
|
|
min-width: 0; |
|
|
|
min-height: 0; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-preview-wrap { |
|
|
|
position: relative; |
|
|
|
width: 100%; |
|
|
|
flex: 1 1 auto; |
|
|
|
min-height: 480px; |
|
|
|
height: auto; |
|
|
|
border: 1px solid #dcdfe6; |
|
|
|
border-radius: 4px; |
|
|
|
overflow: hidden; |
|
|
|
background: #000; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-preview { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
object-fit: cover; |
|
|
|
display: block; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-placeholder { |
|
|
|
position: absolute; |
|
|
|
inset: 0; |
|
|
|
color: #e5e9f2; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
font-size: 14px; |
|
|
|
letter-spacing: 0.5px; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-action-bar { |
|
|
|
display: flex; |
|
|
|
flex-wrap: wrap; |
|
|
|
gap: 6px; |
|
|
|
margin-top: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-action-bar .el-button { |
|
|
|
margin-left: 0 !important; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-file-input { |
|
|
|
display: none; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-list-title { |
|
|
|
margin: 0 0 8px 0; |
|
|
|
font-size: 12px; |
|
|
|
color: #606266; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-list-panel { |
|
|
|
flex: 1 1 auto; |
|
|
|
min-height: 480px; |
|
|
|
border: 1px solid #ebeef5; |
|
|
|
border-radius: 4px; |
|
|
|
padding: 8px; |
|
|
|
overflow: hidden; |
|
|
|
background: #fafbfc; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-list { |
|
|
|
display: grid; |
|
|
|
grid-template-columns: 1fr; |
|
|
|
grid-auto-rows: 168px; |
|
|
|
gap: 8px; |
|
|
|
height: 100%; |
|
|
|
align-content: start; |
|
|
|
overflow-y: auto; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-item { |
|
|
|
height: 168px; |
|
|
|
min-height: 168px; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
border: 1px solid #ebeef5; |
|
|
|
border-radius: 4px; |
|
|
|
overflow: hidden; |
|
|
|
background: #fff; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-thumb { |
|
|
|
width: 100%; |
|
|
|
height: 100px; |
|
|
|
display: block; |
|
|
|
object-fit: cover; |
|
|
|
background: #000; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-meta { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
gap: 6px; |
|
|
|
padding: 6px 8px 2px; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-name { |
|
|
|
font-size: 12px; |
|
|
|
color: #606266; |
|
|
|
white-space: nowrap; |
|
|
|
overflow: hidden; |
|
|
|
text-overflow: ellipsis; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-remove { |
|
|
|
margin-top: 2px; |
|
|
|
padding: 4px 8px 2px 4px; |
|
|
|
line-height: 1; |
|
|
|
text-align: left; |
|
|
|
} |
|
|
|
|
|
|
|
.empty-capture-list { |
|
|
|
text-align: center; |
|
|
|
color: #909399; |
|
|
|
padding: 36px 12px; |
|
|
|
} |
|
|
|
|
|
|
|
.empty-capture-list i { |
|
|
|
font-size: 34px; |
|
|
|
} |
|
|
|
|
|
|
|
.empty-capture-list p { |
|
|
|
margin: 8px 0 0 0; |
|
|
|
font-size: 12px; |
|
|
|
} |
|
|
|
|
|
|
|
.history-table >>> .el-table__header-wrapper th, |
|
|
|
.history-table >>> .el-table__header-wrapper .el-table__cell { |
|
|
|
background-color: #F5F7FA !important; |
|
|
|
@ -868,6 +1508,46 @@ export default { |
|
|
|
.cards-grid { |
|
|
|
grid-template-columns: 1fr; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media { |
|
|
|
width: 98% !important; |
|
|
|
margin-top: 1vh !important; |
|
|
|
height: 95vh; |
|
|
|
max-height: 95vh; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .el-dialog__header, |
|
|
|
.report-container >>> .report-node-dialog--media .el-dialog__footer { |
|
|
|
padding-left: 10px; |
|
|
|
padding-right: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .el-dialog__body { |
|
|
|
padding: 8px 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.report-container >>> .report-node-dialog--media .report-dialog-form > .el-row:first-child .el-col { |
|
|
|
width: 100%; |
|
|
|
} |
|
|
|
|
|
|
|
.media-layout { |
|
|
|
grid-template-columns: 1fr; |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.media-layout-right { |
|
|
|
margin-top: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.capture-list-panel { |
|
|
|
min-height: 340px; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-preview-wrap { |
|
|
|
flex: 0 0 auto; |
|
|
|
height: 400px; |
|
|
|
min-height: 340px; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.status-info { |
|
|
|
@ -892,4 +1572,45 @@ export default { |
|
|
|
border-color: #909399; |
|
|
|
color: #fff; |
|
|
|
} |
|
|
|
|
|
|
|
/* 拍照按钮 */ |
|
|
|
.camera-btn { |
|
|
|
font-size: 16px !important; |
|
|
|
padding: 10px 16px !important; |
|
|
|
border-radius: 6px; |
|
|
|
color: #409EFF !important; |
|
|
|
border-color: #409EFF !important; |
|
|
|
background-color: #ecf5ff !important; |
|
|
|
} |
|
|
|
|
|
|
|
.camera-btn i { |
|
|
|
font-size: 18px !important; |
|
|
|
} |
|
|
|
|
|
|
|
/* 悬停效果 */ |
|
|
|
.camera-btn:hover { |
|
|
|
background-color: #409EFF !important; |
|
|
|
color: #fff !important; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 录像按钮 */ |
|
|
|
.video-btn { |
|
|
|
font-size: 16px !important; |
|
|
|
padding: 10px 16px !important; |
|
|
|
border-radius: 6px; |
|
|
|
color: #F56C6C !important; |
|
|
|
border-color: #F56C6C !important; |
|
|
|
background-color: #fef0f0 !important; |
|
|
|
} |
|
|
|
|
|
|
|
.video-btn i { |
|
|
|
font-size: 18px !important; |
|
|
|
} |
|
|
|
|
|
|
|
/* 悬停效果 */ |
|
|
|
.video-btn:hover { |
|
|
|
background-color: #F56C6C !important; |
|
|
|
color: #fff !important; |
|
|
|
} |
|
|
|
</style> |