|
|
<template> <div class="report-container"> <div class="page-header"> <div class="header-left"> <h2 class="page-title"> <i class="el-icon-s-order"></i> 生产报工 </h2> <p class="page-subtitle">家用电梯 / 线缆COP / 改造项目节点报工管理</p> </div> <div class="header-right"> <div class="stat-cards"> <div class="stat-card stat-total"> <div class="stat-icon"><i class="el-icon-document"></i></div> <div class="stat-content"> <div class="stat-value">{{ totalOrders }}</div> <div class="stat-label">订单总数</div> </div> </div> <div class="stat-card stat-pending"> <div class="stat-icon"><i class="el-icon-time"></i></div> <div class="stat-content"> <div class="stat-value">{{ pendingNodeCount }}</div> <div class="stat-label">待报工节点</div> </div> </div> </div> </div> </div>
<div class="search-section"> <el-collapse class="no-arrow" v-model="searchExpanded"> <el-collapse-item name="1"> <template slot="title"> <i class="el-icon-search"></i> <span class="search-title">筛选条件</span> </template> <el-form :inline="true" label-position="top" class="search-form"> <el-form-item label="项目号"> <el-input v-model="searchData.projectNo" placeholder="支持模糊查询" clearable style="width: 150px"></el-input> </el-form-item> <el-form-item label="订单类型"> <el-select v-model="searchData.orderType" placeholder="请选择" clearable style="width: 150px"> <el-option label="全部" value=""></el-option> <el-option label="家用电梯" value="HOME_LIFT"></el-option> <el-option label="线缆COP" value="CABLE_COP"></el-option> <el-option label="改造项目" value="RENOVATION"></el-option> </el-select> </el-form-item> <el-form-item label="状态"> <el-select v-model="searchData.status" placeholder="请选择" clearable 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: 140px"> </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: 140px"> </el-date-picker> </el-form-item> <el-form-item label=" " class="search-btn-group"> <el-button @click="getDataList('Y')" type="primary" icon="el-icon-search">查询</el-button> <el-button @click="resetQuery()" type="default" icon="el-icon-refresh-left">重置</el-button> </el-form-item> </el-form> </el-collapse-item> </el-collapse> </div>
<div class="cards-container"> <div class="cards-grid"> <div v-for="item in dataList" :key="item.orderNo" class="report-card" :class="getCardClass(item)"> <div class="card-header"> <div class="card-title-area"> <el-tag :type="getTypeTag(item.orderType)" effect="dark" class="type-tag">{{ item.orderTypeName }}</el-tag> <span class="project-no">{{ item.projectNo }}</span> </div> <el-tag :type="getStatusType(item.status)" size="small">{{ item.status }}</el-tag> </div>
<div class="card-body"> <h3 class="item-title"> <i class="el-icon-document"></i> {{ item.productName }} </h3>
<div class="card-details"> <div class="detail-row"> <span class="detail-label"><i class="el-icon-goods"></i> 型号</span> <span class="detail-value">{{ item.modelNo }}</span> </div> <div class="detail-row"> <span class="detail-label"><i class="el-icon-date"></i> 计划完工</span> <span class="detail-value">{{ item.planDate }}</span> </div> <div class="detail-row"> <span class="detail-label"><i class="el-icon-user"></i> 责任人</span> <span class="detail-value">{{ item.owner }}</span> </div> <div class="detail-row"> <span class="detail-label"><i class="el-icon-finished"></i> 节点进度</span> <span class="detail-value">{{ item.nodeDoneCount }}/{{ item.nodeTotalCount }}</span> </div> </div>
<div class="node-section"> <div class="node-title"> <i class="el-icon-position"></i> 节点报工 </div> <div v-for="node in item.visiblePendingNodeList" :key="node.nodeCode" class="node-row"> <span class="node-name">{{ node.nodeName }}</span> <div class="node-right"><!-- <el-tag :type="getNodeStatusType(node.status)" size="mini">{{ node.status }}</el-tag>--> <el-button size="mini" type="primary" plain :disabled="node.status === '已完成' || !canReportNode(item, node)" @click="directReportNode(item, node)"> 报工 </el-button> </div> </div> </div> </div>
<!-- <div class="card-footer"> <div class="current-step"><i class="el-icon-caret-right"></i> 当前工序:{{ item.currentNode }}</div> <el-button type="text" @click="openHistoryDialog(item)">报工记录</el-button> </div>--> </div> </div>
<div v-if="dataList.length === 0" class="empty-state"> <i class="el-icon-document-checked empty-icon"></i> <p class="empty-text">暂无报工任务</p> <p class="empty-subtext">请调整筛选条件后重试</p> </div> </div>
<div class="pagination-wrapper" v-if="totalPage > 0"> <el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[8, 12, 24]" :page-size="pageSize" :total="totalPage" layout="total, sizes, prev, pager, next, jumper" background> </el-pagination> </div>
<el-dialog title="节点报工" :visible.sync="reportDialogVisible" width="520px" :close-on-click-modal="false" v-drag> <el-form :model="reportData" label-position="top"> <el-row :gutter="12"> <el-col :span="12"> <el-form-item label="项目号"> <el-input v-model="reportData.projectNo" disabled></el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="报工节点"> <el-input v-model="reportData.nodeName" disabled></el-input> </el-form-item> </el-col> </el-row> <el-row :gutter="12"> <el-col :span="12"> <el-form-item label="报工数量" required> <el-input v-model="reportData.reportQty" style="width: 100%"></el-input> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="报工人员"> <el-input v-model="reportData.reportBy" disabled></el-input> </el-form-item> </el-col> </el-row> <el-form-item label="报工备注"> <el-input v-model="reportData.remark" type="textarea" :rows="3" placeholder="请输入报工说明"></el-input> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="reportDialogVisible = false">取消</el-button> <el-button type="primary" :loading="reportLoading" @click="submitNodeReport"> {{ reportLoading ? '提交中...' : '确认报工' }} </el-button> </div> </el-dialog>
<el-dialog :title="`报工记录 - ${historyOrder.projectNo || ''}`" :visible.sync="historyDialogVisible" width="760px" :close-on-click-modal="false" v-drag> <el-table class="history-table" :data="historyOrder.visibleReportHistory || []" border stripe max-height="420px" style="width: 100%"> <el-table-column type="index" label="序号" width="60" align="center"></el-table-column> <el-table-column prop="nodeName" label="报工节点" min-width="140" align="left" header-align="center"></el-table-column> <el-table-column prop="reportQty" label="数量" width="90" align="center" header-align="center"></el-table-column> <el-table-column prop="reportBy" label="报工人" width="110" align="center" header-align="center"></el-table-column> <el-table-column prop="reportTime" label="报工时间" width="170" align="center" header-align="center"></el-table-column> <el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip align="left" header-align="center"></el-table-column> </el-table> <div v-if="(historyOrder.visibleReportHistory || []).length === 0" class="empty-attachment"> <i class="el-icon-folder-opened"></i> <p>暂无报工记录</p> </div> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="historyDialogVisible = false">关闭</el-button> </div> </el-dialog> </div></template>
<script>import { getWorkReportOrderList, reportCableCopTaskNode, reportHomeLiftOrderNode, reportRenovationOrderNode } from '@/api/longchuang/productionPlan'
export default { name: 'ProductionWorkReport', data() { return { searchExpanded: ['0'], searchData: { projectNo: '', orderType: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: 8 }, dataList: [], pageIndex: 1, pageSize: 8, totalPage: 0, reportDialogVisible: false, historyDialogVisible: false, historyOrder: {}, reportLoading: false, reportData: { orderNo: '', orderType: '', nodeCode: '', projectNo: '', nodeName: '', reportQty: 1, reportBy: '', remark: '' } } }, computed: { totalOrders() { return this.totalPage || 0 }, pendingNodeCount() { return this.dataList.reduce((sum, item) => sum + (item.visibleNodeList || []).filter(node => node.status !== '已完成').length, 0) } }, activated() { this.getDataList('Y') }, methods: { getDataList(flag) { if (flag === 'Y') { this.pageIndex = 1 } this.searchData.page = this.pageIndex this.searchData.limit = this.pageSize getWorkReportOrderList(this.searchData).then(({ data }) => { if (data && data.code === 0) { const page = data.page || {} const list = page.list || [] this.dataList = list.map(this.normalizeOrderRow) this.totalPage = page.totalCount || 0 } else { this.dataList = [] this.totalPage = 0 this.$message.error((data && data.msg) || '查询失败') } }).catch(() => { this.dataList = [] this.totalPage = 0 this.$message.error('查询失败') }) }, normalizeOrderRow(row) { const nodeList = row.nodeList || [] const fallbackNodeDoneCount = nodeList.filter(item => item.status === '已完成').length const fallbackCurrentNodeObj = nodeList.find(item => item.status !== '已完成') || {} const nodeDoneCount = typeof row.nodeDoneCount === 'number' ? row.nodeDoneCount : fallbackNodeDoneCount const nodeTotalCount = typeof row.nodeTotalCount === 'number' ? row.nodeTotalCount : nodeList.length const currentNode = row.currentNode || fallbackCurrentNodeObj.nodeName || '全部完成' return { ...row, nodeReportMode: row.nodeReportMode || 'PARALLEL', currentNodeCode: row.currentNodeCode || fallbackCurrentNodeObj.nodeCode || '', orderTypeName: this.getOrderTypeName(row.orderType), productName: row.taskNo || row.projectNo || '-', planDate: row.planFinishDate || row.planDeliveryDate || '-', owner: this.$store.state.user.userDisplay || this.$store.state.user.name || '-', nodeDoneCount: nodeDoneCount, nodeTotalCount: nodeTotalCount, currentNode: currentNode, visibleNodeList: nodeList, visiblePendingNodeList: nodeList.filter(item => item.status !== '已完成') } }, getOrderTypeName(orderType) { const map = { HOME_LIFT: '家用电梯', CABLE_COP: '线缆COP', RENOVATION: 'VL2.5升级' } return map[orderType] || orderType || '-' }, resetQuery() { this.searchData = { projectNo: '', orderType: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: this.pageSize } this.getDataList('Y') }, getTypeTag(type) { const tagMap = { HOME_LIFT: 'primary', CABLE_COP: 'warning', RENOVATION: 'success' } return tagMap[type] || 'info' }, getStatusType(status) { const statusMap = { 已排产: 'info', 进行中: 'warning', 已完成: 'success' } return statusMap[status] || 'info' }, getNodeStatusType(status) { const statusMap = { 未开始: 'info', 进行中: 'warning', 已完成: 'success' } return statusMap[status] || 'info' }, getCardClass(item) { if (item.status === '已完成') { return 'card-done' } if (item.status === '进行中') { return 'card-progress' } return 'card-plan' }, canReportNode(order, node) { if (!order || !node) { return false } if ((order.nodeReportMode || 'PARALLEL') !== 'SEQUENTIAL') { return true } return !!order.currentNodeCode && order.currentNodeCode === node.nodeCode }, directReportNode(order, node) { this.reportData = { orderNo: order.orderNo, orderType: order.orderType, nodeCode: node.nodeCode, projectNo: order.projectNo, nodeName: node.nodeName, reportQty: 1, reportBy: this.$store.state.user.userDisplay || this.$store.state.user.name || '当前用户', remark: '' } this.submitNodeReport() }, openReportDialog(order, node) { this.reportData = { orderNo: order.orderNo, orderType: order.orderType, nodeCode: node.nodeCode, projectNo: order.projectNo, nodeName: node.nodeName, reportQty: 1, reportBy: this.$store.state.user.userDisplay || this.$store.state.user.name || '当前用户', remark: '' } this.reportDialogVisible = true }, submitNodeReport() { if (!this.reportData.nodeCode) { this.$message.warning('请选择报工节点') return } const apiMap = { HOME_LIFT: reportHomeLiftOrderNode, CABLE_COP: reportCableCopTaskNode, RENOVATION: reportRenovationOrderNode } const apiFn = apiMap[this.reportData.orderType] if (!apiFn) { this.$message.error('订单类型不支持报工') return } this.reportLoading = true apiFn({ orderNo: this.reportData.orderNo, nodeCode: this.reportData.nodeCode, reportQty: this.reportData.reportQty, remark: this.reportData.remark }).then(({ data }) => { this.reportLoading = false if (data && data.code === 0) { this.$message.success(data.msg || '报工成功') this.reportDialogVisible = false this.getDataList() } else { this.$message.error((data && data.msg) || '报工失败') } }).catch(() => { this.reportLoading = false this.$message.error('报工失败') }) }, openHistoryDialog(item) { this.historyOrder = item this.historyDialogVisible = true }, sizeChangeHandle(val) { this.pageSize = val this.pageIndex = 1 this.getDataList() }, currentChangeHandle(val) { this.pageIndex = val this.getDataList() } }}</script>
<style scoped>.report-container { padding: 15px; background: #f5f7fa; min-height: calc(100vh - 80px);}
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 5px 20px; background: #FFFFFF; border-radius: 4px; border: 1px solid #EBEEF5;}
.page-title { margin: 0; font-size: 20px; font-weight: 600; color: #303133; display: flex; align-items: center; gap: 10px;}
.page-title i { font-size: 22px; color: #409EFF;}
.page-subtitle { margin: 6px 0 0 0; font-size: 13px; color: #909399;}
.stat-cards { display: flex; gap: 12px;}
.stat-card { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: #FFFFFF; border-radius: 4px; border: 1px solid #EBEEF5; min-width: 140px;}
.stat-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 4px; font-size: 20px; color: white;}
.stat-total .stat-icon { background: #409EFF;}
.stat-pending .stat-icon { background: #E6A23C;}
.stat-done .stat-icon { background: #67C23A;}
.stat-value { font-size: 24px; font-weight: 600; color: #303133; line-height: 1;}
.stat-label { font-size: 12px; color: #909399; margin-top: 4px;}
.search-section { margin-bottom: 8px; background: #FFFFFF; border-radius: 4px; border: 1px solid #EBEEF5; overflow: hidden;}
.search-title { margin-left: 8px; font-weight: 500;}
.search-section >>> .el-collapse-item__header { padding: 0 15px; height: 35px; line-height: 35px; background: white; border-bottom: 1px solid #ebeef5; font-size: 13px; color: #303133; font-weight: 500;}
.search-section >>> .el-collapse-item__content { padding: 15px; background: #FFFFFF;}
.search-form >>> .el-form-item { margin-bottom: 10px;}
.cards-container { min-height: 360px;}
.cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(390px, 1fr)); gap: 15px;}
.report-card { background: white; border-radius: 4px; padding: 8px 16px; border: 1px solid #EBEEF5; transition: all 0.3s ease; position: relative;}
.report-card::before { content: ''; position: absolute; left: 0; top: 0; width: 2px; height: 100%;}
.report-card.card-plan::before { background: #909399;}
.report-card.card-progress::before { background: #E6A23C;}
.report-card.card-done::before { background: #67C23A;}
.report-card:hover { border-color: #409EFF; box-shadow: 0 4px 12px rgba(64, 158, 255, 0.12);}
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; padding-bottom: 6px; border-bottom: 1px solid #EBEEF5;}
.card-title-area { display: flex; align-items: center; gap: 10px;}
.type-tag { font-size: 12px;}
.project-no { font-size: 13px; color: #606266; font-family: 'Courier New', monospace; font-weight: 500;}
.item-title { font-size: 15px; font-weight: 600; color: #303133; margin: 0 0 6px 0; display: flex; align-items: center; gap: 6px;}
.item-title i { color: #409EFF;}
.card-details { display: flex; flex-direction: column; gap: 4px;}
.detail-row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; padding: 6px 10px; background: #F5F7FA; border-radius: 3px;}
.detail-label { color: #909399; display: flex; align-items: center; gap: 5px; font-weight: 500;}
.detail-label i { color: #409EFF; font-size: 14px;}
.detail-value { color: #606266; font-weight: 500;}
.node-section { margin-top: 4px; border-top: 1px dashed #EBEEF5; padding-top: 10px;}
.node-title { font-size: 12px; color: #909399; margin-bottom: 2px;}
.node-row { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; border-radius: 4px;}
.node-row:hover { background: #F5F7FA;}
.node-name { color: #303133; font-size: 13px;}
.node-right { display: flex; align-items: center; gap: 6px;}
.card-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 10px; margin-top: 8px; border-top: 1px solid #EBEEF5;}
.current-step { font-size: 12px; color: #909399;}
.pagination-wrapper { display: flex; justify-content: center; padding: 15px; margin-top: 15px; background: #FFFFFF; border-radius: 4px; border: 1px solid #EBEEF5;}
.empty-state { text-align: center; padding: 80px 20px; background: white; border-radius: 4px; border: 1px solid #EBEEF5;}
.empty-icon { font-size: 64px; color: #c0c4cc; margin-bottom: 12px;}
.empty-text { font-size: 15px; color: #606266; margin: 0 0 6px 0; font-weight: 500;}
.empty-subtext { font-size: 13px; color: #909399; margin: 0;}
.empty-attachment { text-align: center; padding: 40px 20px; color: #909399;}
.empty-attachment i { font-size: 42px;}
.history-table >>> .el-table__header-wrapper th,.history-table >>> .el-table__header-wrapper .el-table__cell { background-color: #F5F7FA !important; color: #606266 !important; font-size: 12px; font-weight: 600;}
/deep/ .no-arrow .el-collapse-item__header .el-collapse-item__arrow { display: none !important;}
@media screen and (max-width: 1200px) { .page-header { flex-direction: column; align-items: flex-start; gap: 12px; }
.cards-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }}
@media screen and (max-width: 768px) { .stat-cards { flex-direction: column; width: 100%; }
.stat-card { width: 100%; }
.cards-grid { grid-template-columns: 1fr; }}</style>
|