343 lines
18 KiB
Vue
343 lines
18 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="app-container count-container">
|
|||
|
|
<DragResizePanel :initialSize="280" :minSize="280" :maxSize="600">
|
|||
|
|
<template #panelA>
|
|||
|
|
<div class="left-panel">
|
|||
|
|
<div class="panel-header">
|
|||
|
|
<div class="header-title">
|
|||
|
|
<i class="el-icon-s-promotion"></i>
|
|||
|
|
<span>维修执行</span>
|
|||
|
|
<el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表"></el-button>
|
|||
|
|
</div>
|
|||
|
|
<el-select v-model="queryParams.planStatus" placeholder="执行状态" clearable size="mini" @change="handleQuery" class="header-filter">
|
|||
|
|
<el-option label="待维修" :value="1" />
|
|||
|
|
<el-option label="维修中" :value="2" />
|
|||
|
|
<el-option label="已完成" :value="3" />
|
|||
|
|
</el-select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="search-row">
|
|||
|
|
<el-input v-model="queryParams.planNo" placeholder="搜索计划编号..." clearable prefix-icon="el-icon-search"
|
|||
|
|
size="small" @keyup.enter.native="handleQuery" @clear="handleQuery" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-loading="loading" class="list-body">
|
|||
|
|
<div v-for="item in dataList" :key="item.planId" class="list-item"
|
|||
|
|
:class="{ active: currentRow && currentRow.planId === item.planId }" @click="handleRowClick(item)">
|
|||
|
|
<div class="item-main">
|
|||
|
|
<span class="item-title">{{ item.planNo }}</span>
|
|||
|
|
<span class="item-sub">{{ item.planName }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-meta">
|
|||
|
|
<el-tag v-if="item.planStatus === 1" type="info" size="mini">待维修</el-tag>
|
|||
|
|
<el-tag v-else-if="item.planStatus === 2" type="warning" size="mini">维修中</el-tag>
|
|||
|
|
<el-tag v-else-if="item.planStatus === 3" type="success" size="mini">已完成</el-tag>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-actions">
|
|||
|
|
<el-button size="mini" type="text" icon="el-icon-view" @click.stop="handleRowClick(item)"></el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="dataList.length === 0 && !loading" class="list-empty">
|
|||
|
|
<i class="el-icon-folder-opened"></i>
|
|||
|
|
<span>暂无执行中的维修计划</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="list-footer">
|
|||
|
|
<pagination :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
|||
|
|
@pagination="getList" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<template #panelB>
|
|||
|
|
<div class="right-panel">
|
|||
|
|
<div v-if="!currentRow" class="empty-tip">
|
|||
|
|
<i class="el-icon-info"></i>
|
|||
|
|
<span>请在左侧列表中选择一条维修计划执行操作</span>
|
|||
|
|
</div>
|
|||
|
|
<div v-else v-loading="detailLoading" class="detail-content">
|
|||
|
|
<div class="doc-header">
|
|||
|
|
<div class="doc-header-top">
|
|||
|
|
<div class="doc-title-group">
|
|||
|
|
<div class="doc-title">{{ currentRow.planNo }}</div>
|
|||
|
|
<div class="doc-subtitle">Maintenance Plan · Execution</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="doc-header-right">
|
|||
|
|
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail" title="刷新详情">刷新</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="doc-status-row">
|
|||
|
|
<span class="doc-status-label">Status / 状态:</span>
|
|||
|
|
<el-tag v-if="currentRow.planStatus === 1" type="info" size="small">待维修</el-tag>
|
|||
|
|
<el-tag v-else-if="currentRow.planStatus === 2" type="warning" size="small">维修中</el-tag>
|
|||
|
|
<el-tag v-else-if="currentRow.planStatus === 3" type="success" size="small">已完成</el-tag>
|
|||
|
|
<el-button v-if="canComplete" size="mini" type="success" plain icon="el-icon-circle-check" style="margin-left:12px;" @click="handleCompletePlan">完成计划</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="detail-meta">
|
|||
|
|
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
|
|||
|
|
<span v-if="currentRow.repairType === 1">定期保养</span>
|
|||
|
|
<span v-else-if="currentRow.repairType === 2">安全整改</span>
|
|||
|
|
<span v-else-if="currentRow.repairType === 3">专项检修</span>
|
|||
|
|
<span v-else-if="currentRow.repairType === 4">故障维修</span>
|
|||
|
|
<span v-if="currentRow.dutyDept"><i class="el-icon-s-home"></i>{{ currentRow.dutyDept }}</span>
|
|||
|
|
<span v-if="currentRow.planOwner"><i class="el-icon-user-solid"></i>{{ currentRow.planOwner }}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-divider />
|
|||
|
|
|
|||
|
|
<div class="section-title">
|
|||
|
|
<span>维修明细进度 <span class="en-sub">· Detail Progress</span></span>
|
|||
|
|
</div>
|
|||
|
|
<el-table :data="detailList" border size="small" style="width:100%">
|
|||
|
|
<el-table-column label="设备部件" align="center" prop="componentName" width="120" />
|
|||
|
|
<el-table-column label="产线" align="center" prop="productionLine" width="100" />
|
|||
|
|
<el-table-column label="类型" align="center" width="60">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
<el-tag v-if="scope.row.maintenanceCategory === 0" size="mini">保养</el-tag>
|
|||
|
|
<el-tag v-else-if="scope.row.maintenanceCategory === 1" size="mini" type="warning">维修</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="维修内容" align="center" prop="repairContent" min-width="130" show-overflow-tooltip />
|
|||
|
|
<el-table-column label="负责人" align="center" prop="repairUser" width="80" />
|
|||
|
|
<el-table-column label="进度" align="center" width="160">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
<el-progress :percentage="scope.row.progressRate || 0" :status="scope.row.detailStatus === 2 ? 'success' : ''" :stroke-width="12" style="padding: 4px 0;" />
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="状态" align="center" width="80">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
<el-tag v-if="scope.row.detailStatus === 0" type="info" size="mini">未开始</el-tag>
|
|||
|
|
<el-tag v-else-if="scope.row.detailStatus === 1" type="warning" size="mini">进行中</el-tag>
|
|||
|
|
<el-tag v-else-if="scope.row.detailStatus === 2" type="success" size="mini">已完成</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="实际成本" align="center" width="100">
|
|||
|
|
<template slot-scope="scope">¥{{ scope.row.actualCost || 0 }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="操作" align="center" width="80" fixed="right" v-if="currentRow.planStatus !== 3">
|
|||
|
|
<template slot-scope="scope">
|
|||
|
|
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditProgress(scope.row)">进度</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
<div v-if="detailList.length === 0" class="empty-data" style="margin-top:8px;">暂无维修明细</div>
|
|||
|
|
|
|||
|
|
<div class="section-gap" />
|
|||
|
|
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
|
|||
|
|
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</DragResizePanel>
|
|||
|
|
|
|||
|
|
<!-- 进度编辑弹窗 -->
|
|||
|
|
<el-dialog title="更新维修进度" :visible.sync="progressDialogVisible" width="550px" append-to-body>
|
|||
|
|
<el-form ref="progressForm" :model="progressForm" label-width="100px">
|
|||
|
|
<el-form-item label="设备部件">
|
|||
|
|
<span>{{ progressForm.componentName }}</span>
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="维修内容">
|
|||
|
|
<span>{{ progressForm.repairContent }}</span>
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="完成进度" prop="progressRate">
|
|||
|
|
<el-slider v-model="progressForm.progressRate" :min="0" :max="100" :step="5" show-input style="padding:0 10px;" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="完成状态" prop="detailStatus">
|
|||
|
|
<el-select v-model="progressForm.detailStatus" placeholder="请选择" style="width:100%">
|
|||
|
|
<el-option label="未开始" :value="0" />
|
|||
|
|
<el-option label="进行中" :value="1" />
|
|||
|
|
<el-option label="已完成" :value="2" />
|
|||
|
|
</el-select>
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="实际成本(元)" prop="actualCost">
|
|||
|
|
<el-input-number v-model="progressForm.actualCost" :min="0" :precision="2" style="width:100%" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="负责人" prop="repairUser">
|
|||
|
|
<el-input v-model="progressForm.repairUser" placeholder="请输入维修负责人" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="完成时间" prop="completeTime" v-if="progressForm.detailStatus === 2">
|
|||
|
|
<el-date-picker clearable v-model="progressForm.completeTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择完成时间" style="width:100%" />
|
|||
|
|
</el-form-item>
|
|||
|
|
</el-form>
|
|||
|
|
<div slot="footer" class="dialog-footer">
|
|||
|
|
<el-button @click="progressDialogVisible = false">取 消</el-button>
|
|||
|
|
<el-button type="primary" :loading="progressButtonLoading" @click="submitProgressForm">保 存</el-button>
|
|||
|
|
</div>
|
|||
|
|
</el-dialog>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { listMaintenancePlan, getMaintenancePlan, updateMaintenancePlan } from "@/api/flow/maintenancePlan";
|
|||
|
|
import { listMaintenancePlanDetail, updateMaintenancePlanDetail } from "@/api/flow/maintenancePlanDetail";
|
|||
|
|
import DragResizePanel from "@/components/DragResizePanel/index.vue";
|
|||
|
|
import { parseTime } from '@/utils/klp'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: "EqpExecute",
|
|||
|
|
components: { DragResizePanel },
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
loading: false,
|
|||
|
|
detailLoading: false,
|
|||
|
|
progressButtonLoading: false,
|
|||
|
|
total: 0,
|
|||
|
|
queryParams: { pageNum: 1, pageSize: 10, planNo: undefined, approvalStatus: 2, planStatus: undefined },
|
|||
|
|
dataList: [],
|
|||
|
|
currentRow: null,
|
|||
|
|
detailList: [],
|
|||
|
|
progressDialogVisible: false,
|
|||
|
|
progressForm: {}
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
canComplete() {
|
|||
|
|
if (!this.currentRow || this.currentRow.planStatus === 3) return false;
|
|||
|
|
if (this.detailList.length === 0) return false;
|
|||
|
|
return this.detailList.every(function(d) { return d.detailStatus === 2; });
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
created() { this.getList(); },
|
|||
|
|
methods: {
|
|||
|
|
parseTime,
|
|||
|
|
getList() {
|
|||
|
|
this.loading = true;
|
|||
|
|
var self = this;
|
|||
|
|
listMaintenancePlan(this.queryParams).then(function(response) {
|
|||
|
|
self.dataList = response.rows;
|
|||
|
|
self.total = response.total;
|
|||
|
|
self.loading = false;
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
handleQuery() {
|
|||
|
|
this.queryParams.pageNum = 1;
|
|||
|
|
this.getList();
|
|||
|
|
},
|
|||
|
|
handleRowClick(row) {
|
|||
|
|
this.currentRow = row;
|
|||
|
|
this.loadDetail(row.planId);
|
|||
|
|
},
|
|||
|
|
loadDetail(planId) {
|
|||
|
|
this.detailLoading = true;
|
|||
|
|
var self = this;
|
|||
|
|
getMaintenancePlan(planId).then(function(response) {
|
|||
|
|
self.currentRow = response.data;
|
|||
|
|
self.loadDetailList(planId);
|
|||
|
|
}).finally(function() { self.detailLoading = false; });
|
|||
|
|
},
|
|||
|
|
loadDetailList(planId) {
|
|||
|
|
var self = this;
|
|||
|
|
listMaintenancePlanDetail({ planId: planId, pageNum: 1, pageSize: 999 }).then(function(r) {
|
|||
|
|
self.detailList = r.rows || [];
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
handleRefreshDetail() {
|
|||
|
|
if (this.currentRow && this.currentRow.planId) this.loadDetail(this.currentRow.planId);
|
|||
|
|
},
|
|||
|
|
// ---- Progress editing ----
|
|||
|
|
handleEditProgress(row) {
|
|||
|
|
this.progressForm = Object.assign({}, row, {
|
|||
|
|
completeTime: row.completeTime || undefined
|
|||
|
|
});
|
|||
|
|
this.progressDialogVisible = true;
|
|||
|
|
},
|
|||
|
|
submitProgressForm() {
|
|||
|
|
var self = this;
|
|||
|
|
var data = {
|
|||
|
|
detailId: this.progressForm.detailId,
|
|||
|
|
progressRate: this.progressForm.progressRate,
|
|||
|
|
detailStatus: this.progressForm.detailStatus,
|
|||
|
|
actualCost: this.progressForm.actualCost,
|
|||
|
|
repairUser: this.progressForm.repairUser
|
|||
|
|
};
|
|||
|
|
if (this.progressForm.detailStatus === 2) {
|
|||
|
|
data.completeTime = this.progressForm.completeTime || this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}');
|
|||
|
|
}
|
|||
|
|
this.progressButtonLoading = true;
|
|||
|
|
updateMaintenancePlanDetail(data).then(function() {
|
|||
|
|
self.$modal.msgSuccess("进度更新成功");
|
|||
|
|
self.progressDialogVisible = false;
|
|||
|
|
self.loadDetailList(self.currentRow.planId);
|
|||
|
|
}).finally(function() { self.progressButtonLoading = false; });
|
|||
|
|
},
|
|||
|
|
// ---- Complete plan ----
|
|||
|
|
handleCompletePlan() {
|
|||
|
|
var self = this;
|
|||
|
|
this.$modal.confirm('确认完成维修计划"' + this.currentRow.planNo + '"?').then(function() {
|
|||
|
|
return updateMaintenancePlan({
|
|||
|
|
planId: self.currentRow.planId,
|
|||
|
|
planStatus: 3,
|
|||
|
|
actualCompleteTime: self.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
|
|||
|
|
});
|
|||
|
|
}).then(function() {
|
|||
|
|
self.$modal.msgSuccess("维修计划已完成");
|
|||
|
|
self.loadDetail(self.currentRow.planId);
|
|||
|
|
self.getList();
|
|||
|
|
}).catch(function() { });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.count-container { height: calc(100vh - 84px); }
|
|||
|
|
.left-panel { display: flex; flex-direction: column; height: 100%; background: #f5f7fa; border-right: 1px solid #e4e7ed; }
|
|||
|
|
.panel-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px 8px; background: #f5f7fa; }
|
|||
|
|
.header-title { display: flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 600; color: #303133; }
|
|||
|
|
.header-title i { color: #409eff; font-size: 16px; }
|
|||
|
|
.header-filter { width: 120px; }
|
|||
|
|
.search-row { display: flex; align-items: center; gap: 6px; padding: 0 14px 10px; background: #f5f7fa; }
|
|||
|
|
.list-body { flex: 1; overflow-y: auto; padding: 0 6px; }
|
|||
|
|
.list-item { display: flex; align-items: center; padding: 10px 12px; margin-bottom: 2px; cursor: pointer; border-radius: 6px; transition: all 0.15s; }
|
|||
|
|
.list-item:hover { background: #ebeef5; }
|
|||
|
|
.list-item.active { background: #d9ecff; }
|
|||
|
|
.list-item.active .item-title { color: #409eff; font-weight: 600; }
|
|||
|
|
.item-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
|||
|
|
.item-title { font-size: 13px; font-weight: 500; color: #303133; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|||
|
|
.item-sub { font-size: 12px; color: #909399; }
|
|||
|
|
.item-meta { flex-shrink: 0; margin: 0 8px; }
|
|||
|
|
.item-actions { flex-shrink: 0; opacity: 0; transition: opacity 0.15s; }
|
|||
|
|
.list-item:hover .item-actions { opacity: 1; }
|
|||
|
|
.list-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 0; color: #c0c4cc; font-size: 13px; gap: 8px; }
|
|||
|
|
.list-empty i { font-size: 32px; }
|
|||
|
|
.list-footer { border-top: 1px solid #e4e7ed; padding: 2px 8px 0; background: #f5f7fa; }
|
|||
|
|
|
|||
|
|
.right-panel { height: 100%; overflow-y: auto; padding: 12px 16px; background: #faf8f5; }
|
|||
|
|
.right-panel .detail-content { margin: 0 auto; background: #ffffff; padding: 28px 32px 36px; box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 2px 12px rgba(0,0,0,0.04); min-height: 100%; }
|
|||
|
|
.empty-tip { display: flex; align-items: center; justify-content: center; height: 100%; color: #909399; font-size: 14px; gap: 8px; }
|
|||
|
|
|
|||
|
|
.doc-header { margin-bottom: 18px; padding-bottom: 14px; border-bottom: 2px solid #1a3c6e; }
|
|||
|
|
.doc-header-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
|||
|
|
.doc-title-group { flex: 1; min-width: 0; }
|
|||
|
|
.doc-title { font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif; font-size: 24px; font-weight: 700; color: #1a1a1a; line-height: 1.3; letter-spacing: 0.5px; }
|
|||
|
|
.doc-subtitle { font-family: 'Georgia', 'Times New Roman', serif; font-size: 12px; font-weight: 400; color: #8c8c8c; font-style: italic; letter-spacing: 0.8px; margin-top: 2px; }
|
|||
|
|
.doc-header-right { flex-shrink: 0; }
|
|||
|
|
.doc-status-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
|||
|
|
.doc-status-label { font-family: 'Georgia', 'Times New Roman', serif; font-size: 11px; color: #8c8c8c; letter-spacing: 0.3px; }
|
|||
|
|
|
|||
|
|
.detail-meta { display: flex; flex-wrap: wrap; gap: 16px; font-size: 12px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #e0dcd6; }
|
|||
|
|
.detail-meta span { display: inline-flex; align-items: center; gap: 4px; }
|
|||
|
|
.detail-meta i { font-size: 13px; }
|
|||
|
|
|
|||
|
|
.section-title { font-family: 'Georgia', 'Times New Roman', 'Noto Serif SC', 'SimSun', serif; width: 100%; font-size: 15px; font-weight: 700; color: #1a1a1a; margin: 22px 0 12px 0; padding: 0 0 10px 0; border-bottom: 1px solid #d4d0c8; display: flex; align-items: center; gap: 10px; letter-spacing: 0.3px; }
|
|||
|
|
.section-title:first-child { margin-top: 0; }
|
|||
|
|
.section-title .en-sub { font-size: 11px; font-weight: 400; color: #8c8c8c; letter-spacing: 0.5px; font-family: 'Georgia', 'Times New Roman', serif; font-style: italic; }
|
|||
|
|
|
|||
|
|
.remark-content { padding: 12px 16px; background: #faf8f5; border: 1px solid #e8e4de; border-radius: 2px; font-size: 13px; line-height: 1.8; color: #1a1a1a; }
|
|||
|
|
.section-gap { height: 16px; }
|
|||
|
|
.empty-data { color: #8c8c8c; font-size: 13px; font-style: italic; }
|
|||
|
|
|
|||
|
|
.right-panel .el-table { border: 1px solid #e8e4de !important; border-radius: 2px !important; font-size: 12px !important; }
|
|||
|
|
.right-panel .el-table thead th { background-color: #2c3e50 !important; color: #ffffff !important; font-weight: 600 !important; font-size: 11px !important; letter-spacing: 0.5px !important; border-bottom: none !important; font-family: 'Georgia', 'Times New Roman', serif; }
|
|||
|
|
.right-panel .el-table thead th .cell { color: #ffffff !important; }
|
|||
|
|
.right-panel .el-table__body tr:hover > td { background-color: #f7f5f0 !important; }
|
|||
|
|
.right-panel .el-table--border td { border-right: 1px solid #f0ece6 !important; }
|
|||
|
|
.right-panel .el-table--border th { border-right: 1px solid #3a5166 !important; }
|
|||
|
|
.right-panel .el-table td { padding: 6px 4px !important; color: #3a3a3a !important; }
|
|||
|
|
.right-panel .el-divider--horizontal { margin: 8px 0 4px; background-color: #e0dcd6; }
|
|||
|
|
.right-panel .el-tag { border-radius: 2px; font-family: 'Georgia', 'Times New Roman', serif; letter-spacing: 0.3px; }
|
|||
|
|
</style>
|