|
|
<template> <div class="mod-config"> <el-form :inline="true" label-position="top" class="query-form"> <el-form-item label="物料号"> <el-input v-model="searchData.modelNo" clearable placeholder="请输入物料号" style="width: 160px"></el-input> </el-form-item> <el-form-item label="物料名称"> <el-input v-model="searchData.materialName" clearable placeholder="请输入物料名称" style="width: 160px"></el-input> </el-form-item> <el-form-item label="规格"> <el-input v-model="searchData.materialSpec" clearable placeholder="请输入规格" style="width: 160px"></el-input> </el-form-item> <el-form-item label="状态"> <el-select v-model="searchData.status" clearable placeholder="全部" style="width: 120px"> <el-option label="已排产" value="已排产"></el-option> <el-option label="进行中" value="进行中"></el-option> <el-option label="已完成" value="已完成"></el-option> </el-select> </el-form-item> <el-form-item label="计划完工日期"> <el-date-picker v-model="searchData.planStartDate" type="date" value-format="yyyy-MM-dd" placeholder="开始" style="width: 130px"></el-date-picker> </el-form-item> <el-form-item label="至"> <el-date-picker v-model="searchData.planEndDate" type="date" value-format="yyyy-MM-dd" placeholder="结束" style="width: 130px"></el-date-picker> </el-form-item> <el-form-item label=" " style="margin-top: -11px"> <el-button @click="searchTable('Y')" plain class="search-btn">查询</el-button> <el-button @click="resetQuery()" plain class="reset-btn">重置</el-button> <el-button @click="openEditDialog()" plain class="add-btn">新增任务单</el-button> </el-form-item> <el-form-item label=" " class="query-finish-item"> <el-button @click="finishTask()" plain class="search-btn" :disabled="!selectedOrder.orderNo || selectedOrder.status === '已完成'"> 完工 </el-button> </el-form-item> </el-form>
<el-table ref="orderTable" class="data-table" :data="dataList" :height="tableHeight" border highlight-current-row v-loading="dataListLoading" style="width: 100%" @current-change="onCurrentRowChange"> <el-table-column label="操作" width="200" align="center"> <template slot-scope="scope"> <a type="text" @click="openEditDialog(scope.row)">修改</a> <a type="text" @click="openAssignDialog(scope.row)" v-if="scope.row.status !== '已完成' && scope.row.currentNode!=='全部完成'">分配人员</a> <a type="text" style="color:#F56C6C" @click="deleteTask(scope.row)" v-if="scope.row.status !== '已完成'">删除</a> </template> </el-table-column> <el-table-column type="index" label="#" width="50" align="center"></el-table-column><!-- <el-table-column prop="orderNo" label="任务单号" width="180" align="center"></el-table-column>--> <el-table-column prop="modelNo" label="物料号" width="160" align="center"></el-table-column> <el-table-column prop="materialName" label="物料名称" width="160" align="center" show-overflow-tooltip></el-table-column> <el-table-column prop="materialSpec" label="规格" width="160" align="center" show-overflow-tooltip></el-table-column> <el-table-column prop="taskQty" label="计划数量" width="100" align="center"></el-table-column> <el-table-column prop="reportQty" label="累计报工数量" width="120" align="center"></el-table-column> <el-table-column prop="currentNode" label="当前节点" width="120" align="center"></el-table-column> <el-table-column prop="assigneeSummary" label="节点负责人" min-width="160" show-overflow-tooltip></el-table-column> <el-table-column label="节点进度" width="100" align="center"> <template slot-scope="scope"> <el-tag size="small" type="info">{{ scope.row.nodeDoneCount }}/{{ scope.row.nodeTotalCount }}</el-tag> </template> </el-table-column> <el-table-column prop="status" label="状态" width="90" align="center"> <template slot-scope="scope"> <el-tag :type="getStatusType(scope.row.status)" size="small">{{ scope.row.status }}</el-tag> </template> </el-table-column> <el-table-column prop="planFinishDate" label="计划完工日期" width="120" align="center"></el-table-column> <el-table-column prop="finishDate" label="完工日期" width="120" align="center"> <template slot-scope="scope">{{ scope.row.finishDate || '-' }}</template> </el-table-column>
</el-table>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage" layout="total, sizes, prev, pager, next, jumper" style="margin-top: 20px; text-align: right"> </el-pagination>
<div class="detail-tabs-wrap"> <el-tabs v-model="detailTabName" type="border-card"> <el-tab-pane label="节点状态和日志" name="statusLogs"> <div v-if="selectedOrder.orderNo" class="two-column-layout"> <div class="stages-column"> <div class="column-header"> <i class="el-icon-s-order"></i> <span>节点流程</span> <span class="progress-badge">{{ selectedOrderProgressPercent }}%</span> </div> <div class="stages-list"> <div v-for="(stage, index) in selectedOrderNodeList" :key="stage.nodeCode || index" class="stage-item" :class="'stage-' + getStageClass(stage.status)"> <div class="stage-icon"> <i :class="getStageIcon(stage.status)"></i> </div> <div class="stage-content"> <div class="stage-name">{{ stage.nodeName }}</div> <div class="stage-meta"> <el-tag :type="getStageTagType(stage.status)" size="mini" effect="plain">{{ stage.status || '未开始' }}</el-tag> <span class="stage-owner">负责人:{{ stage.assigneeUserName || '-' }}</span> </div> </div> </div> </div> </div> <div class="logs-column"> <div class="column-header"> <i class="el-icon-tickets"></i> <span>操作日志</span> <span class="logs-count">{{ selectedOrderLogList.length }}条</span> </div> <div class="logs-table-wrapper"> <el-table :data="selectedOrderLogList" border size="small" class="detail-table" v-loading="detailLogLoading" height="295px"> <el-table-column prop="logTime" label="时间" width="160" align="center"></el-table-column> <el-table-column prop="action" label="操作" width="95" align="center"></el-table-column> <el-table-column prop="nodeName" label="节点" width="130" align="center"> <template slot-scope="scope">{{ scope.row.nodeName || scope.row.nodeCode || '-' }}</template> </el-table-column> <el-table-column prop="operatorName" label="操作人" width="100" align="center"></el-table-column> <el-table-column prop="comment" label="备注" min-width="220" show-overflow-tooltip> <template slot-scope="scope">{{ scope.row.comment || '-' }}</template> </el-table-column> <el-table-column label="影像" width="90" align="center"> <template slot-scope="scope"> <a v-if="isMediaNodeLog(scope.row)" type="text" size="mini" @click="openMediaFileDialog(scope.row)"> 查看文件 </a> <span v-else>-</span> </template> </el-table-column> </el-table> </div> </div> </div> <el-empty v-else description="请先选择一条任务记录"></el-empty> </el-tab-pane> </el-tabs> </div>
<el-dialog :title="saveHeaderData.orderNo ? '修改机加工生产任务单' : '新增机加工生产任务单'" :visible.sync="setUp.reviewFlag" width="550px" :close-on-click-modal="false" v-drag> <el-form ref="editForm" :model="saveHeaderData" label-position="top" class="edit-form"> <el-row :gutter="20"> <el-col :span="12"><el-form-item label="物料号" required><el-input v-model="saveHeaderData.modelNo" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col> <el-col :span="12"><el-form-item label="物料名称" required><el-input v-model="saveHeaderData.materialName" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"><el-form-item label="规格" required><el-input v-model="saveHeaderData.materialSpec" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col> <el-col :span="12"><el-form-item label="计划数量" required><el-input v-model="saveHeaderData.taskQty" :min="1" :max="999999" style="width: 100%" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"><el-form-item label="计划完工日期" required><el-date-picker v-model="saveHeaderData.planFinishDate" type="date" value-format="yyyy-MM-dd" style="width: 100%"></el-date-picker></el-form-item></el-col> <el-col :span="12"><el-form-item label="状态"><el-select disabled v-model="saveHeaderData.status" style="width: 100%"><el-option label="已排产" value="已排产"></el-option><el-option label="进行中" value="进行中"></el-option><el-option label="已完成" value="已完成"></el-option></el-select></el-form-item></el-col> </el-row><!-- <el-row :gutter="20"> <el-col :span="12"> <el-form-item> <template slot="label"> 人员分配策略 <el-tooltip effect="dark" placement="top"> <div slot="content"> 默认分配:创建订单后,系统会按节点角色自动分配该角色下全部人员。<br> 手动分配:创建订单后不自动分配,需要在“分配人员”里手工选择负责人。 </div> <i class="el-icon-question" style="margin-left:6px;color:#909399;cursor:pointer;"></i> </el-tooltip> </template> <el-radio-group v-model="saveHeaderData.autoAssignAllUsers"> <el-radio :label="true">默认分配</el-radio> <el-radio :label="false">手动分配</el-radio> </el-radio-group> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="节点报工模式"> <el-radio-group v-model="saveHeaderData.nodeReportMode"> <el-radio label="PARALLEL">并行</el-radio> <el-radio label="SEQUENTIAL">串行</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row>--> </el-form> <el-footer style="height: 40px; margin-top: 50px; text-align: center"> <el-button plain class="reset-btn" @click="setUp.reviewFlag = false">取消</el-button> <el-button plain class="add-btn" :loading="setUp.saveButton" @click="saveTask">保存</el-button> </el-footer> </el-dialog>
<el-dialog title="机加工节点报工" :visible.sync="setUp.reportFlag" width="460px" :close-on-click-modal="false" @close="handleReportDialogClose" v-drag> <el-form :model="reportData" label-width="110px" label-position="top"> <el-form-item label="任务单号"><el-input v-model="reportData.taskNo" disabled></el-input></el-form-item> <el-form-item label="报工节点"> <el-select v-model="reportData.nodeCode" placeholder="请选择节点" style="width: 100%" @change="handleReportNodeChange"> <el-option v-for="item in reportNodeOptions" :key="item.nodeCode" :label="item.nodeName" :value="item.nodeCode"></el-option> </el-select> </el-form-item> <el-form-item v-if="isMachiningProductionNode(reportData.nodeCode)" label="本次报工数量"> <el-input v-model="reportData.reportQty" :min="1" :max="999999" style="width: 100%"></el-input> </el-form-item> <el-form-item v-if="isMachiningInspectionNode(reportData.nodeCode)" label="检验影像上传"> <div class="inspection-upload-wrap"> <el-button plain type="primary" size="mini" @click="triggerInspectionImagePicker">选择图片</el-button> <span class="inspection-upload-tip">支持图片格式,可多选上传</span> <input ref="inspectionImageInput" class="inspection-file-input" type="file" accept="image/*" multiple @change="handleInspectionImageSelect"> <div v-if="inspectionImageList.length > 0" class="inspection-file-list"> <div v-for="(item, index) in inspectionImageList" :key="item.uid" class="inspection-file-item"> <span class="inspection-file-name">{{ item.name }}</span> <el-button type="text" style="color:#F56C6C" @click="removeInspectionImage(index)">删除</el-button> </div> </div> <div v-else class="inspection-upload-empty">请至少上传1张检验图片</div> </div> </el-form-item> <el-form-item label="报工备注"><el-input v-model="reportData.remark" type="textarea" :rows="2"></el-input></el-form-item> </el-form> <el-footer style="height: 40px; margin-top: 50px; text-align: center"> <el-button plain class="reset-btn" @click="setUp.reportFlag = false">取消</el-button> <el-button plain class="search-btn" :loading="setUp.reportButton" @click="submitNodeReport">提交报工</el-button> </el-footer> </el-dialog>
<el-dialog title="节点负责人分配" :visible.sync="setUp.assignFlag" width="550px" :close-on-click-modal="false" v-drag> <el-table :data="assignNodeList" border class="data-table"> <el-table-column prop="nodeName" label="节点" width="150"></el-table-column> <el-table-column label="负责人" width="370"> <template slot-scope="scope"> <el-select v-model="scope.row.assigneeUserIdList" filterable clearable multiple placeholder="请选择负责人" style="width: 100%"> <el-option v-for="user in scope.row.userOptions" :key="user.userId" :label="user.displayName || user.username" :value="user.userId"></el-option> </el-select> </template> </el-table-column> </el-table> <el-footer style="height: 40px; margin-top: 20px; text-align: center"> <el-button plain class="reset-btn" @click="setUp.assignFlag = false">取消</el-button> <el-button plain class="add-btn" :loading="setUp.assignButton" @click="saveNodeAssigneeAction">保存分配</el-button> </el-footer> </el-dialog>
<el-dialog title="报工影像文件" :visible.sync="mediaDialogVisible" width="680px" :close-on-click-modal="false" @close="handleMediaDialogClose" v-drag> <div v-loading="mediaDialogLoading" class="media-dialog-body"> <div class="media-dialog-meta"> <span>节点:{{ mediaDialogLog.nodeName || mediaDialogLog.nodeCode || '-' }}</span> <span>报工时间:{{ mediaDialogLog.logTime || '-' }}</span> </div> <el-table v-if="mediaFileList.length > 0" class="file-table" :data="mediaFileList" border size="small" height="360px"> <el-table-column type="index" label="#" width="55" align="center"></el-table-column> <el-table-column label="缩略图" width="150" align="center"> <template slot-scope="scope"> <div class="media-thumb-cell"> <el-button v-if="resolveMediaKind(scope.row) === 'other'" type="text" size="mini" disabled>不支持</el-button> <span v-else-if="scope.row.previewLoading" class="media-thumb-loading">加载中...</span> <img v-else-if="resolveMediaKind(scope.row) === 'image' && scope.row.previewUrl" :src="scope.row.previewUrl" class="media-thumb media-thumb-image" alt="media-thumb" @click="previewMediaFile(scope.row)"> <video v-else-if="resolveMediaKind(scope.row) === 'video'" :src="getVideoStreamUrl(scope.row)" class="media-thumb media-thumb-video" muted playsinline preload="metadata" @click="previewMediaFile(scope.row)"> </video> <el-button v-else type="text" size="mini" @click="previewMediaFile(scope.row)">加载预览</el-button> </div> </template> </el-table-column> <el-table-column label="类型" width="90" align="center"> <template slot-scope="scope"> <el-tag size="mini" :type="resolveMediaKind(scope.row) === 'video' ? 'warning' : (resolveMediaKind(scope.row) === 'image' ? 'success' : 'info')"> {{ getMediaKindLabel(scope.row) }} </el-tag> </template> </el-table-column> <el-table-column label="步骤" width="120" align="center"> <template slot-scope="scope">{{ getMediaStepLabel(scope.row) }}</template> </el-table-column> <el-table-column label="上传时间" min-width="170" align="center"> <template slot-scope="scope">{{ scope.row.createDate || '-' }}</template> </el-table-column> <el-table-column label="操作" width="60" align="center"> <template slot-scope="scope"> <el-button type="text" style="color:#F56C6C" :loading="scope.row.deleting" @click="deleteMediaFile(scope.row)"> 删除 </el-button> </template> </el-table-column> </el-table> <el-empty v-else description="当前节点暂无影像文件"></el-empty> </div> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="mediaDialogVisible = false">关闭</el-button> </div> </el-dialog>
<div v-if="mediaPreviewVisible" class="media-preview-overlay" @click="closeMediaPreview"> <i class="el-icon-close media-preview-close" @click.stop="closeMediaPreview"></i> <img v-if="mediaPreviewType === 'image'" :src="mediaPreviewUrl" :alt="mediaPreviewName || 'media-preview'" class="media-preview-image" @click.stop="closeMediaPreview"> <video v-else-if="mediaPreviewType === 'video'" ref="mediaPreviewVideo" :key="mediaPreviewUrl" :src="mediaPreviewUrl" class="media-preview-video" controls autoplay playsinline preload="auto" @error="handleMediaPreviewError" @click.stop> </video> </div> </div></template>
<script>import { deleteMachiningTask, finishMachiningTask, getMachiningTaskList, getNodeAssigneeList, getNodeAssigneeUsers, getReportLogList, reportMachiningTaskNode, reportWorkNodeWithMedia, saveMachiningTask, saveNodeAssignee } from '@/api/longchuang/productionPlan'import { getOssVideoStreamUrl, previewOssFileById2, queryOssFilePlus, removeOss } from '@/api/oss/oss'
export default { name: 'ProductionPlanMachiningTask', data() { return { searchData: { modelNo: '', materialName: '', materialSpec: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: 20 }, saveHeaderData: {}, reportData: { orderNo: '', taskNo: '', nodeCode: 'machiningProduction', reportQty: '', remark: '' }, reportNodeOptions: [], inspectionImageList: [], setUp: { reviewFlag: false, reportFlag: false, assignFlag: false, saveButton: false, reportButton: false, assignButton: false }, dataList: [], currentAssignOrder: { orderNo: '', orderType: 'MACHINING' }, assignNodeList: [], selectedOrder: {}, detailTabName: 'statusLogs', selectedOrderLogList: [], detailLogLoading: false, mediaDialogVisible: false, mediaDialogLoading: false, mediaDialogLog: {}, mediaFileList: [], mediaPreviewVisible: false, mediaPreviewUrl: '', mediaPreviewType: '', mediaPreviewName: '', pageIndex: 1, pageSize: 20, totalPage: 0, dataListLoading: false, tableHeight: (window.innerHeight - 320) / 2 } }, activated() { this.searchTable() }, beforeDestroy() { this.releaseMediaFileUrls() }, methods: { isDateOnlyEditMode() { const status = String(this.saveHeaderData.status || '').trim() return !!this.saveHeaderData.orderNo && !!status && status !== '已排产' }, searchTable(flag) { if (flag === 'Y') this.pageIndex = 1 this.searchData.page = this.pageIndex this.searchData.limit = this.pageSize this.dataListLoading = true getMachiningTaskList(this.searchData).then(({ data }) => { this.dataListLoading = false if (data && data.code === 0) { this.dataList = (data.page.list || []).map(this.normalizeRow) this.totalPage = data.page.totalCount || 0 this.syncSelectedOrder() } else this.loadMockData() }).catch(() => { this.dataListLoading = false this.loadMockData() }) }, normalizeRow(row) { const list = row.nodeList || [] const done = list.filter(item => item.status === '已完成').length const currentNode = (list.find(item => item.status !== '已完成') || {}).nodeName || '全部完成' const assigneeSummary = list.filter(item => item.assigneeUserName).map(item => `${item.nodeName}:${item.assigneeUserName}`).join(';') return { ...row, autoAssignAllUsers: !!row.autoAssignAllUsers, nodeReportMode: row.nodeReportMode || 'SEQUENTIAL', nodeList: list, nodeDoneCount: done, nodeTotalCount: list.length, currentNode: row.currentNode || currentNode, reportQty: row.reportQty || 0, assigneeSummary: assigneeSummary || '-' } }, getMachiningDefaultNodeList() { return [ { nodeCode: 'machiningProduction', nodeName: '机加工生产', status: '未开始' }, { nodeCode: 'machiningInspection', nodeName: '机加工检验', status: '未开始' } ] }, isMachiningProductionNode(nodeCode) { return nodeCode === 'machiningProduction' }, isMachiningInspectionNode(nodeCode) { return nodeCode === 'machiningInspection' }, loadMockData() { this.dataList = [ { orderNo: 'MOCK-MACH-001', projectNo: 'MAT-120001', modelNo: 'MAT-120001', materialName: '导轨压板', materialSpec: 'DG-80-12', taskQty: 120, reportQty: 30, autoAssignAllUsers: true, nodeReportMode: 'SEQUENTIAL', planFinishDate: '2026-05-25', finishDate: '', status: '进行中', nodeList: [ { nodeCode: 'machiningProduction', nodeName: '机加工生产', status: '已完成' }, { nodeCode: 'machiningInspection', nodeName: '机加工检验', status: '进行中' } ] }, { orderNo: 'MOCK-MACH-002', projectNo: 'MAT-120188', modelNo: 'MAT-120188', materialName: '轿底连接件', materialSpec: 'JD-LJ-06', taskQty: 240, reportQty: 0, autoAssignAllUsers: true, nodeReportMode: 'SEQUENTIAL', planFinishDate: '2026-05-27', finishDate: '', status: '已排产', nodeList: this.getMachiningDefaultNodeList() } ].map(this.normalizeRow) this.totalPage = this.dataList.length this.syncSelectedOrder() }, syncSelectedOrder() { if (!this.dataList.length) { this.selectedOrder = {} this.selectedOrderLogList = [] return } const selectedOrderNo = this.selectedOrder && this.selectedOrder.orderNo const matched = selectedOrderNo ? this.dataList.find(item => item.orderNo === selectedOrderNo) : null const current = matched || this.dataList[0] this.selectedOrder = current this.$nextTick(() => { if (this.$refs.orderTable && current) { this.$refs.orderTable.setCurrentRow(current) } }) this.loadSelectedOrderLogList(current) }, onCurrentRowChange(row) { if (!row || !row.orderNo) { this.selectedOrder = {} this.selectedOrderLogList = [] return } this.selectedOrder = row this.loadSelectedOrderLogList(row) }, loadSelectedOrderLogList(row) { if (!row || !row.orderNo) { this.selectedOrderLogList = [] return } this.detailLogLoading = true getReportLogList({ orderNo: row.orderNo, orderType: 'MACHINING' }).then(({ data }) => { this.detailLogLoading = false this.selectedOrderLogList = data && data.code === 0 ? (data.rows || []) : [] }).catch(() => { this.detailLogLoading = false this.selectedOrderLogList = [] }) }, isMediaNodeLog(row) { if (!row || !row.logNo) { return false } if (row.action && row.action !== '报工完成') { return false } const codeSet = ['machiningInspection'] const nameSet = ['机加工检验', '检验'] const nodeCode = String(row.nodeCode || '').trim() const nodeName = String(row.nodeName || '').replace(/\s+/g, '') return codeSet.includes(nodeCode) || nameSet.some(item => item.replace(/\s+/g, '') === nodeName) }, async openMediaFileDialog(row) { if (!row || !row.logNo || !row.nodeCode) { this.$message.warning('当前日志未关联影像文件') return } this.releaseMediaFileUrls() this.closeMediaPreview() this.mediaFileList = [] this.mediaDialogLog = { ...row } this.mediaDialogVisible = true this.mediaDialogLoading = true try { const { data } = await queryOssFilePlus({ orderRef1: this.selectedOrder.orderNo || row.orderNo, orderRef2: row.nodeCode }) this.mediaFileList = (data && data.code === 0 ? (data.rows || []) : []).map(item => ({ ...item, previewUrl: '', previewLoading: false, deleting: false })) this.mediaFileList.forEach(item => { const kind = this.resolveMediaKind(item) if (kind === 'image') { this.loadMediaPreviewUrl(item, kind, false) } }) } catch (e) { this.mediaFileList = [] this.$message.error('影像文件加载失败') } finally { this.mediaDialogLoading = false } }, handleMediaDialogClose() { this.mediaDialogLoading = false this.mediaDialogLog = {} this.releaseMediaFileUrls() this.mediaFileList = [] this.closeMediaPreview() }, releaseMediaFileUrls() { this.mediaFileList.forEach(item => { if (item && item.previewUrl) { URL.revokeObjectURL(item.previewUrl) } }) }, getMediaExt(fileRow) { if (!fileRow) { return '' } const ext = fileRow.fileType || fileRow.fileSuffix || this.getExtFromFileName(fileRow.fileName || fileRow.newFileName || '') return String(ext).replace(/^\./, '').toLowerCase() }, getExtFromFileName(fileName) { const name = String(fileName || '') const dotIndex = name.lastIndexOf('.') if (dotIndex < 0 || dotIndex >= name.length - 1) { return '' } return name.slice(dotIndex + 1) }, resolveMediaKind(fileRow) { const ext = this.getMediaExt(fileRow) const imageExtList = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'] const videoExtList = ['mp4', 'webm', 'mov', 'avi', 'm4v', '3gp'] if (imageExtList.includes(ext)) { return 'image' } if (videoExtList.includes(ext)) { return 'video' } return 'other' }, getMediaKindLabel(fileRow) { const kind = this.resolveMediaKind(fileRow) if (kind === 'image') { return '照片' } if (kind === 'video') { return '视频' } return '其他' }, getMediaStepLabel(fileRow) { const step = String((fileRow && fileRow.orderRef6) || '').replace(/\s+/g, '') const stepList = ['1平台箱', '2门箱', '3背景墙箱', '4框架箱', '5钣金箱', '6铝型材箱'] return stepList.includes(step) ? step : '-' }, buildPreviewMimeType(fileRow, kind) { const ext = this.getMediaExt(fileRow) if (kind === 'image') { const imageMimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', bmp: 'image/bmp', webp: 'image/webp' } return imageMimeMap[ext] || 'image/jpeg' } if (kind === 'video') { const videoMimeMap = { mp4: 'video/mp4', m4v: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', avi: 'video/x-msvideo', '3gp': 'video/3gpp' } return videoMimeMap[ext] || 'video/mp4' } return 'application/octet-stream' }, async loadMediaPreviewUrl(fileRow, fixedKind = '', showError = true) { if (!fileRow || !fileRow.id) { return false } const kind = fixedKind || this.resolveMediaKind(fileRow) if (kind === 'other') { return false } if (fileRow.previewUrl) { return true } if (fileRow.previewLoading) { return false } this.$set(fileRow, 'previewLoading', true) const mimeType = this.buildPreviewMimeType(fileRow, kind) try { const { data } = await previewOssFileById2({ id: fileRow.id, fileType: mimeType }) const sourceBlob = data instanceof Blob ? data : new Blob([data]) const blobType = mimeType || sourceBlob.type || 'application/octet-stream' const blob = new Blob([sourceBlob], { type: blobType }) this.$set(fileRow, 'previewUrl', URL.createObjectURL(blob)) return true } catch (e) { if (showError) { this.$message.error('文件预览加载失败') } return false } finally { this.$set(fileRow, 'previewLoading', false) } }, getVideoStreamUrl(fileRow) { if (!fileRow || !fileRow.id) { return '' } return getOssVideoStreamUrl(fileRow.id) }, async previewMediaFile(fileRow) { const kind = this.resolveMediaKind(fileRow) if (kind === 'other') { this.$message.warning('当前文件暂不支持预览') return } if (!fileRow || !fileRow.id) { this.$message.warning('文件信息不完整,无法预览') return } if (kind === 'video') { const videoUrl = this.getVideoStreamUrl(fileRow) if (!videoUrl) { this.$message.warning('视频地址无效,无法预览') return } this.mediaPreviewType = 'video' this.mediaPreviewName = fileRow.fileName || fileRow.newFileName || '' this.mediaPreviewUrl = videoUrl this.mediaPreviewVisible = true this.$nextTick(() => { const previewVideo = this.$refs.mediaPreviewVideo if (previewVideo && typeof previewVideo.play === 'function') { const playPromise = previewVideo.play() if (playPromise && typeof playPromise.catch === 'function') { playPromise.catch(() => {}) } } }) return } const loaded = await this.loadMediaPreviewUrl(fileRow, kind, true) if (!loaded) { return } this.mediaPreviewType = kind this.mediaPreviewName = fileRow.fileName || fileRow.newFileName || '' this.mediaPreviewUrl = fileRow.previewUrl this.mediaPreviewVisible = true }, closeMediaPreview() { const previewVideo = this.$refs.mediaPreviewVideo if (previewVideo && typeof previewVideo.pause === 'function') { previewVideo.pause() } this.mediaPreviewVisible = false this.mediaPreviewType = '' this.mediaPreviewName = '' this.mediaPreviewUrl = '' }, handleMediaPreviewError() { this.$message.warning('视频播放失败,请稍后重试') }, async deleteMediaFile(fileRow) { const fileId = Number(fileRow && fileRow.id) if (!Number.isFinite(fileId) || fileId <= 0) { this.$message.warning('文件ID无效,无法删除') return } try { await this.$confirm('确定删除该影像文件吗?删除后不可恢复。', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) } catch (e) { return } this.$set(fileRow, 'deleting', true) try { const { data } = await removeOss([fileId]) if (!data || data.code !== 0) { this.$message.error((data && data.msg) || '删除失败') return } const currentPreviewUrl = this.mediaPreviewUrl || '' const isCurrentImage = !!(fileRow.previewUrl && currentPreviewUrl && fileRow.previewUrl === currentPreviewUrl) const isCurrentVideo = !!(currentPreviewUrl && currentPreviewUrl.indexOf(`/video/${fileId}`) > -1) if (isCurrentImage || isCurrentVideo) { this.closeMediaPreview() } if (fileRow.previewUrl) { URL.revokeObjectURL(fileRow.previewUrl) } this.mediaFileList = this.mediaFileList.filter(item => Number(item.id) !== fileId) this.$message.success('删除成功') } catch (e) { this.$message.error('删除失败') } finally { this.$set(fileRow, 'deleting', false) } }, resetQuery() { this.searchData = { modelNo: '', materialName: '', materialSpec: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: 20 } this.searchTable('Y') }, openEditDialog(row) { this.saveHeaderData = row ? { ...row, autoAssignAllUsers: !!row.autoAssignAllUsers, nodeReportMode: row.nodeReportMode || 'SEQUENTIAL' } : { orderNo: '', modelNo: '', materialName: '', materialSpec: '', taskQty: 1, reportQty: 0, planFinishDate: '', status: '已排产', autoAssignAllUsers: true, nodeReportMode: 'SEQUENTIAL', nodeList: [] } this.setUp.reviewFlag = true }, saveTask() { if (!this.saveHeaderData.modelNo) return this.$message.warning('请先填写物料号') if (!this.saveHeaderData.materialName) return this.$message.warning('请先填写物料名称') if (!this.saveHeaderData.materialSpec) return this.$message.warning('请先填写规格') if (!this.saveHeaderData.taskQty || Number(this.saveHeaderData.taskQty) <= 0) return this.$message.warning('请输入有效的计划数量') if (!this.saveHeaderData.planFinishDate) return this.$message.warning('请选择计划完工日期') const payload = { ...this.saveHeaderData, projectNo: this.saveHeaderData.modelNo, nodeList: this.saveHeaderData.nodeList && this.saveHeaderData.nodeList.length ? this.saveHeaderData.nodeList : this.getMachiningDefaultNodeList() } this.setUp.saveButton = true saveMachiningTask(payload).then(({ data }) => { this.setUp.saveButton = false if (data && data.code === 0) { this.$message.success(data.msg || '保存成功') this.setUp.reviewFlag = false this.searchTable() } else this.$message.error(data.msg || '保存失败') }).catch(() => { this.setUp.saveButton = false const orderNo = this.saveHeaderData.orderNo || String(Date.now()) const index = this.dataList.findIndex(item => item.orderNo === orderNo) const saveData = this.normalizeRow({ ...payload, orderNo: orderNo }) if (index > -1) this.$set(this.dataList, index, saveData) else this.dataList.unshift(saveData) this.totalPage = this.dataList.length this.setUp.reviewFlag = false this.$message.success('后端未完成,已在前端演示保存') }) }, openReportDialog(row) { this.reportNodeOptions = (row.nodeList || []).filter(item => item.status !== '已完成') const defaultNode = this.reportNodeOptions[0] || {} const nodeCode = defaultNode.nodeCode || 'machiningProduction' this.reportData = { orderNo: row.orderNo, taskNo: row.orderNo, nodeCode: nodeCode, reportQty: this.isMachiningProductionNode(nodeCode) ? '' : 1, remark: '' } this.clearInspectionImageList() this.setUp.reportFlag = true }, handleReportNodeChange(nodeCode) { if (this.isMachiningProductionNode(nodeCode)) { this.reportData.reportQty = '' } else { this.reportData.reportQty = 1 } if (!this.isMachiningInspectionNode(nodeCode)) { this.clearInspectionImageList() } }, handleReportDialogClose() { this.clearInspectionImageList() }, triggerInspectionImagePicker() { if (this.$refs.inspectionImageInput) { this.$refs.inspectionImageInput.click() } }, handleInspectionImageSelect(event) { const fileList = Array.from((event && event.target && event.target.files) || []) if (!fileList.length) { return } let invalidCount = 0 fileList.forEach(file => { const fileType = String((file && file.type) || '').toLowerCase() const fileName = String((file && file.name) || '').toLowerCase() const isImage = fileType.indexOf('image/') === 0 || /\.(jpg|jpeg|png|gif|bmp|webp)$/.test(fileName) if (!file || !isImage) { invalidCount++ return } this.inspectionImageList.push({ uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, name: file.name || 'inspection-image', raw: file }) }) if (invalidCount > 0) { this.$message.warning('仅支持上传图片文件') } if (event && event.target) { event.target.value = '' } }, removeInspectionImage(index) { this.inspectionImageList.splice(index, 1) }, clearInspectionImageList() { this.inspectionImageList = [] if (this.$refs.inspectionImageInput) { this.$refs.inspectionImageInput.value = '' } }, openAssignDialog(row) { if (!row.orderNo) return this.$message.warning('请先保存任务单后再分配人员') this.currentAssignOrder = { orderNo: row.orderNo, orderType: 'MACHINING' } getNodeAssigneeList({ orderNo: row.orderNo, orderType: 'MACHINING' }).then(({ data }) => { const assignRows = data && data.code === 0 ? (data.rows || []) : [] this.assignNodeList = assignRows.map(item => ({ ...item, assigneeUserIdList: item.assigneeUserIdList || [], userOptions: [] })) const requests = this.assignNodeList.map(item => getNodeAssigneeUsers({ orderType: 'MACHINING', nodeCode: item.nodeCode }).then(({ data: userData }) => { item.userOptions = userData && userData.code === 0 ? (userData.rows || []) : [] }).catch(() => { item.userOptions = [] }) ) Promise.all(requests).finally(() => { this.setUp.assignFlag = true }) }).catch(() => { this.$message.error('加载节点分配信息失败') }) }, saveNodeAssigneeAction() { this.setUp.assignButton = true const assigneeList = this.assignNodeList.map(item => ({ nodeCode: item.nodeCode, nodeName: item.nodeName, assigneeUserIdList: item.assigneeUserIdList || [] })) saveNodeAssignee({ orderNo: this.currentAssignOrder.orderNo, orderType: this.currentAssignOrder.orderType, assigneeList: assigneeList }).then(({ data }) => { this.setUp.assignButton = false if (data && data.code === 0) { this.$message.success(data.msg || '分配成功') this.setUp.assignFlag = false this.searchTable() } else this.$message.error(data.msg || '分配失败') }).catch(() => { this.setUp.assignButton = false this.$message.error('分配失败') }) }, submitNodeReport() { if (!this.reportData.nodeCode) this.reportData.nodeCode = 'machiningProduction' if (this.isMachiningProductionNode(this.reportData.nodeCode)) { const qty = Number(this.reportData.reportQty) if (!Number.isFinite(qty) || qty <= 0) return this.$message.warning('请输入有效报工数量') this.submitMachiningProductionReport(qty) return } if (this.isMachiningInspectionNode(this.reportData.nodeCode)) { this.submitMachiningInspectionReport() return } this.$message.warning('当前节点暂不支持报工') }, submitMachiningProductionReport(reportQty) { const payload = { ...this.reportData, reportQty: reportQty } this.setUp.reportButton = true reportMachiningTaskNode(payload).then(({ data }) => { this.setUp.reportButton = false if (data && data.code === 0) { this.$message.success(data.msg || '报工成功') this.setUp.reportFlag = false this.searchTable() } else this.$message.error(data.msg || '报工失败') }).catch(() => { this.setUp.reportButton = false this.simulateNodeReport(this.reportData.orderNo, this.reportData.nodeCode, reportQty) this.setUp.reportFlag = false this.$message.success('后端未完成,已在前端演示节点报工') }) }, submitMachiningInspectionReport() { const uploadFileList = this.inspectionImageList.map(item => item.raw).filter(item => !!item) if (!uploadFileList.length) { return this.$message.warning('请先上传检验图片') } const formData = new FormData() formData.append('orderNo', this.reportData.orderNo) formData.append('orderType', 'MACHINING') formData.append('nodeCode', this.reportData.nodeCode) formData.append('reportQty', '1') formData.append('remark', this.reportData.remark || '') formData.append('reportNode', 'true') uploadFileList.forEach(file => { formData.append('file', file, file.name || 'inspection-image') }) this.setUp.reportButton = true reportWorkNodeWithMedia(formData).then(({ data }) => { this.setUp.reportButton = false if (data && data.code === 0) { this.$message.success(data.msg || '检验报工成功') this.setUp.reportFlag = false this.clearInspectionImageList() this.searchTable() } else { this.$message.error(data.msg || '检验报工失败') } }).catch(() => { this.setUp.reportButton = false this.simulateNodeReport(this.reportData.orderNo, this.reportData.nodeCode, 0) this.setUp.reportFlag = false this.clearInspectionImageList() this.$message.success('后端未完成,已在前端演示检验报工') }) }, simulateNodeReport(orderNo, nodeCode, qty) { const row = this.dataList.find(item => item.orderNo === orderNo) if (!row) return const node = row.nodeList.find(item => item.nodeCode === nodeCode) if (!node) return node.status = '已完成' if (this.isMachiningProductionNode(nodeCode)) { row.reportQty = Number(row.reportQty || 0) + Number(qty || 0) } row.status = '进行中' const nextNode = row.nodeList.find(item => item.status !== '已完成') row.currentNode = nextNode ? nextNode.nodeName : '全部完成' row.nodeDoneCount = row.nodeList.filter(item => item.status === '已完成').length row.finishDate = '' }, finishTask(row) { const targetRow = row || this.selectedOrder if (!targetRow || !targetRow.orderNo) { this.$message.warning('请先选择一条任务记录') return } if (targetRow.status === '已完成') { this.$message.warning('该任务单已完工') return } this.$confirm('确定将该任务单标记为完工吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { finishMachiningTask({ orderNo: targetRow.orderNo }).then(({ data }) => { if (data && data.code === 0) { this.$message.success(data.msg || '完工成功') this.searchTable() } else this.$message.error(data.msg || '完工失败') }).catch(() => { targetRow.status = '已完成' targetRow.finishDate = this.dayjs().format('YYYY-MM-DD') targetRow.nodeList = targetRow.nodeList.map(item => ({ ...item, status: '已完成' })) targetRow.currentNode = '全部完成' targetRow.nodeDoneCount = targetRow.nodeTotalCount this.$message.success('后端未完成,已在前端演示完工') }) }).catch(() => {}) }, deleteTask(row) { this.$confirm('确定删除该任务单吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { deleteMachiningTask({ orderNo: row.orderNo }).then(({ data }) => { if (data && data.code === 0) { this.$message.success(data.msg || '删除成功') this.searchTable() } else this.$message.error(data.msg || '删除失败') }).catch(() => { this.dataList = this.dataList.filter(item => item.orderNo !== row.orderNo) this.totalPage = this.dataList.length this.$message.success('后端未完成,已在前端演示删除') }) }).catch(() => {}) }, getStatusType(status) { const map = { 已排产: 'info', 进行中: 'warning', 已完成: 'success' } return map[status] || 'info' }, getStageClass(status) { if (status === '已完成') return 'done' if (status === '进行中') return 'processing' return 'pending' }, getStageIcon(status) { if (status === '已完成') return 'el-icon-check' if (status === '进行中') return 'el-icon-loading' return 'el-icon-time' }, getStageTagType(status) { if (status === '已完成') return 'success' if (status === '进行中') return 'warning' return 'info' }, sizeChangeHandle(val) { this.pageSize = val this.pageIndex = 1 this.searchTable() }, currentChangeHandle(val) { this.pageIndex = val this.searchTable() } }, computed: { selectedOrderNodeList() { return (this.selectedOrder && this.selectedOrder.nodeList) ? this.selectedOrder.nodeList : [] }, selectedOrderProgressPercent() { const total = this.selectedOrderNodeList.length if (!total) { return 0 } const done = this.selectedOrderNodeList.filter(item => item.status === '已完成').length return Math.round((done * 100) / total) } }}</script>
<style scoped>.data-table { background-color: #fff; border-radius: 4px;}.data-table >>> .cell { line-height: 20px; height: 20px;}.data-table >>> .el-table__header-wrapper th,.data-table >>> .el-table__fixed-header-wrapper th { background-color: #f5f7fa !important; color: #333; font-weight: 600; border-color: #ebeef5; padding: 8px 0;}
.data-table >>> .el-table__header-wrapper .cell,.data-table >>> .el-table__fixed-header-wrapper .cell,.data-table >>> .el-table__body-wrapper .cell,.data-table >>> .el-table__fixed-body-wrapper .cell { padding: 0 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px !important;}
.data-table >>> .el-table__body tr:hover > td { background-color: #f5f7fa !important;}
.data-table >>> .el-table__body tr.current-row > td { background-color: #ecf5ff !important;}
.query-form { background-color: #fff; padding: 15px 15px 5px 15px; border-radius: 4px; margin-bottom: 12px;}
.query-finish-item { margin-top: -11px; float: right; margin-right: 0 !important;}
.query-form >>> .el-form-item__label { color: #333; font-size: 13px; padding-bottom: 5px;}
.query-form >>> .el-input__inner { height: 32px; line-height: 32px; border-radius: 4px; font-size: 13px;}
.query-form >>> .el-button { height: 32px; padding: 0 15px; font-size: 13px; border-radius: 4px;}
.search-btn { background-color: #ecf5ff; border-color: #b3d8ff; color: #409eff;}
.search-btn:hover { background-color: #409eff; border-color: #409eff; color: #fff;}
.reset-btn { background-color: #f5f7fa; border-color: #d3d4d6; color: #606266;}
.reset-btn:hover { background-color: #909399; border-color: #909399; color: #fff;}
.add-btn { background-color: #f0f9eb; border-color: #c2e7b0; color: #67c23a;}
.add-btn:hover { background-color: #67c23a; border-color: #67c23a; color: #fff;}
.dialog-footer { text-align: right;}
.edit-form { margin-left: 5px; margin-top: -5px;}
.detail-tabs-wrap { margin-top: 12px; background-color: #fff; border-radius: 4px;}
.detail-table { width: 100%;}
.detail-table >>> .el-table__header-wrapper th,.detail-table >>> .el-table__fixed-header-wrapper th { background-color: #f5f7fa !important; color: #333; font-weight: 600; border-color: #ebeef5;}
.two-column-layout { display: flex; gap: 12px;}
.stages-column { width: 38%; min-width: 320px; border: 1px solid #ebeef5; border-radius: 4px; background: #fff;}
.logs-column { flex: 1; border: 1px solid #ebeef5; border-radius: 4px; background: #fff;}
.column-header { height: 24px; display: flex; align-items: center; gap: 6px; padding: 0 12px; border-bottom: 1px solid #ebeef5; font-weight: 600; color: #303133;}
.progress-badge,.logs-count { margin-left: auto; color: #409eff; font-size: 12px; font-weight: 500;}
.stages-list { max-height: 295px; overflow-y: auto; padding: 10px;}
.stage-item { display: flex; align-items: flex-start; gap: 10px; padding: 3px 8px; border-radius: 4px;}
.stage-item + .stage-item { margin-top: 2px;}
.stage-item.stage-done { background: #f0f9eb;}
.stage-item.stage-processing { background: #fdf6ec;}
.stage-item.stage-pending { background: #f5f7fa;}
.stage-icon { width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; background: #fff; border: 1px solid #dcdfe6; color: #606266;}
.stage-content { flex: 1;}
.stage-name { font-size: 13px; color: #303133; line-height: 20px;}
.stage-meta { margin-top: 6px; display: flex; align-items: center; gap: 8px;}
.stage-owner { color: #606266; font-size: 12px;}
.logs-table-wrapper { padding: 8px;}
.inspection-upload-wrap { border: 1px dashed #dcdfe6; border-radius: 4px; padding: 10px;}
.inspection-upload-tip { margin-left: 8px; color: #909399; font-size: 12px;}
.inspection-file-input { display: none;}
.inspection-file-list { margin-top: 10px; max-height: 150px; overflow-y: auto;}
.inspection-file-item { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #f0f2f5; padding: 4px 0;}
.inspection-file-name { flex: 1; padding-right: 10px; font-size: 12px; color: #606266; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.inspection-upload-empty { margin-top: 8px; font-size: 12px; color: #909399;}
.media-dialog-body { min-height: 360px;}
.media-dialog-meta { display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 13px; color: #606266;}
.media-thumb-cell { min-height: 70px; display: flex; align-items: center; justify-content: center;}
.media-thumb-loading { color: #909399; font-size: 12px;}
.media-thumb { width: 110px; height: 68px; border-radius: 4px; object-fit: cover; cursor: pointer; border: 1px solid #ebeef5; background: #f5f7fa;}
.media-thumb-video { background: #000;}
.media-preview-overlay { position: fixed; inset: 0; z-index: 2100; background: rgba(0, 0, 0, 0.82); display: flex; justify-content: center; align-items: center; padding: 30px;}
.media-preview-close { position: absolute; top: 18px; right: 22px; color: #fff; font-size: 30px; cursor: pointer;}
.media-preview-image,.media-preview-video { max-width: 92vw; max-height: 88vh; object-fit: contain; border-radius: 6px; background: #000;}
.file-table { background-color: #fff; border-radius: 4px;}.file-table >>> .cell { line-height: 55px; height: 55px;}.file-table >>> .el-table__header-wrapper th,.file-table >>> .el-table__fixed-header-wrapper th { background-color: #f5f7fa !important; color: #333; font-weight: 600; border-color: #ebeef5; padding: 8px 0;}
.file-table >>> .el-table__header-wrapper .cell,.file-table >>> .el-table__fixed-header-wrapper .cell{ padding: 0 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px !important; line-height: 20px; height: 20px;}
.file-table >>> .el-table__body tr:hover > td { background-color: #f5f7fa !important;}
.file-table >>> .el-table__body tr.current-row > td { background-color: #ecf5ff !important;}
.el-icon-check { color: #67c23a; font-weight: 1000;}</style>
|