refactor(盘库流程): 重构盘库流程页面与组件,完善排产明细功能

1.  重构盘库流程的步骤与状态映射,调整流程节点顺序与名称
2.  拆分通用盘库详情组件PlanDetailPanel,复用各流程页面
3.  新增计划审批、盘库执行页面,完善差异审批页面
4.  为排产单明细添加增删改查API与前端操作功能
5.  为排产日期添加格式化注解,完善参数接收格式
This commit is contained in:
2026-06-27 11:15:13 +08:00
parent 097d45b420
commit b94b7823e5
13 changed files with 1167 additions and 558 deletions

View File

@@ -8,6 +8,7 @@ import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 排产单主加明细可合并业务对象 sch_prod_schedule_item
@@ -33,6 +34,8 @@ public class SchProdScheduleItemBo extends BaseEntity {
/**
* 生产日期(和合同号组成业务关联键)
*/
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date prodDate;
/**

View File

@@ -33,3 +33,40 @@ export function getCrmOrderItem(orderDetailId) {
method: 'get'
})
}
// ====== 排产单明细项SchProdScheduleItemCRUD ======
// 查询排产单明细项列表
export function listScheduleItem(query) {
return request({
url: '/flow/prodScheduleItem/list',
method: 'get',
params: query
})
}
// 新增排产单明细项
export function addScheduleItem(data) {
return request({
url: '/flow/prodScheduleItem',
method: 'post',
data
})
}
// 修改排产单明细项
export function updateScheduleItem(data) {
return request({
url: '/flow/prodScheduleItem',
method: 'put',
data
})
}
// 删除排产单明细项
export function delScheduleItem(ids) {
return request({
url: '/flow/prodScheduleItem/' + ids,
method: 'delete'
})
}

View File

@@ -7,14 +7,14 @@
<div class="header-title"><i class="el-icon-s-check"></i><span>盘库申请</span><el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" title="刷新列表" /></div>
<el-select v-model="queryParams.planStatus" size="mini" @change="handleQuery" class="header-filter">
<el-option label="草稿" :value="0" />
<el-option label="待审批" :value="1" />
<el-option label="计划待审批" :value="1" />
</el-select>
</div>
<div class="search-row"><el-input v-model="queryParams.planCode" placeholder="搜索计划编号..." clearable prefix-icon="el-icon-search" size="small" @keyup.enter.native="handleQuery" @clear="handleQuery" /><el-button type="primary" size="small" @click="handleAdd"><i class="el-icon-plus"></i></el-button></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.planCode }}</span><span class="item-sub">{{ item.planName }}</span></div>
<div class="item-meta"><el-tag v-if="item.planStatus === 0" type="info" size="mini">草稿</el-tag><el-tag v-else size="mini">待审批</el-tag></div>
<div class="item-meta"><el-tag v-if="item.planStatus === 0" type="info" size="mini">草稿</el-tag><el-tag v-else size="mini">计划待审批</el-tag></div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty"><i class="el-icon-folder-opened"></i><span>暂无盘库计划</span></div>
</div>
@@ -23,54 +23,28 @@
</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.planCode }}</div><div class="doc-subtitle">Application</div></div>
<div class="doc-header-right">
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail">刷新</el-button>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(currentRow)">编辑</el-button>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(currentRow)">删除</el-button>
</div>
</div>
<div class="doc-status-row"><span class="doc-status-label">Status</span><el-tag v-if="currentRow.planStatus === 0" type="info" size="small">草稿</el-tag><el-tag v-else size="small">待审批</el-tag></div>
</div>
<div class="detail-meta">
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
<span v-if="currentRow.countDate"><i class="el-icon-date"></i>{{ parseTime(currentRow.countDate, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.countUserName"><i class="el-icon-user-solid"></i>{{ currentRow.countUserName }}</span>
</div>
<CountFlowSection :planStatus="currentRow.planStatus" />
<el-divider />
<div class="section-title">库区盘点明细 <span class="en-sub">· Warehouses</span>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="primary" plain icon="el-icon-plus" @click="handleAddWarehouse">绑定库区</el-button>
</div>
<WarehouseDetailPanel ref="whPanel"
:planId="currentRow.planId"
:planStatus="currentRow.planStatus"
@submit-approval="handleSubmitApproval"
/>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
<PlanDetailPanel ref="detailPanel"
:planId="currentRow ? currentRow.planId : null"
subtitle="Application"
emptyText="请在左侧列表中选择一条盘库计划"
@submit-approval="handleSubmitApproval"
@edit-plan="handleUpdate"
@delete-plan="handleDelete"
@bind-warehouse="handleAddWarehouse"
/>
</div>
</template>
</DragResizePanel>
<el-dialog :title="planDialogTitle" :visible.sync="planDialogOpen" width="650px" append-to-body>
<el-form ref="planForm" :model="planForm" label-width="100px">
<el-form-item label="计划名称" prop="planName"><el-input v-model="planForm.planName" /></el-form-item>
<el-form-item label="盘库日期" prop="countDate"><el-date-picker v-model="planForm.countDate" type="date" value-format="yyyy-MM-dd" style="width:100%" /></el-form-item>
<el-form-item label="截止时间" prop="deadlineTime"><el-date-picker v-model="planForm.deadlineTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" style="width:100%" /></el-form-item>
<el-form-item label="盘点人"><el-input v-model="planForm.countUserName" /></el-form-item>
<el-form-item label="负责人"><el-input v-model="planForm.principalUserName" /></el-form-item>
<el-form-item label="参与人"><el-input v-model="planForm.participantNames" /></el-form-item>
<el-form-item label="备注"><el-input v-model="planForm.remark" type="textarea" :rows="3" /></el-form-item>
<el-form-item label="计划名称" prop="planName"><el-input v-model="planForm.planName" placeholder="请输入计划名称" /></el-form-item>
<el-form-item label="盘库日期" prop="countDate"><el-date-picker v-model="planForm.countDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择盘库日期" style="width:100%" /></el-form-item>
<el-form-item label="截止时间" prop="deadlineTime"><el-date-picker v-model="planForm.deadlineTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择截止时间" style="width:100%" /></el-form-item>
<el-form-item label="盘点人"><el-input v-model="planForm.countUserName" placeholder="请输入盘点人" /></el-form-item>
<el-form-item label="负责人"><el-input v-model="planForm.principalUserName" placeholder="请输入负责人" /></el-form-item>
<el-form-item label="参与人"><el-input v-model="planForm.participantNames" placeholder="请输入参与人员" /></el-form-item>
<el-form-item label="备注"><el-input v-model="planForm.remark" type="textarea" :rows="3" placeholder="请输入备注" /></el-form-item>
</el-form>
<div slot="footer"><el-button :loading="btnLoading" type="primary" @click="submitPlan">确定</el-button><el-button @click="planDialogOpen = false">取消</el-button></div>
</el-dialog>
@@ -88,21 +62,20 @@
</template>
<script>
import { listCountPlan, getCountPlan, addCountPlan, updateCountPlan, delCountPlan } from "@/api/flow/countPlan";
import { listCountPlan, addCountPlan, updateCountPlan, delCountPlan } from "@/api/flow/countPlan";
import { addCountPlanWarehouse } from "@/api/flow/countPlanWarehouse";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import CountFlowSection from "./components/CountFlowSection.vue";
import WarehouseDetailPanel from "./components/WarehouseDetailPanel.vue";
import PlanDetailPanel from "./components/PlanDetailPanel.vue";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect/index.vue";
import ActualWarehouseL1L2Select from "@/components/KLPService/ActualWarehouseL1L2Select/index.vue";
import { parseTime } from '@/utils/klp'
export default {
name: "InvCountApply",
components: { DragResizePanel, CountFlowSection, WarehouseDetailPanel, WarehouseSelect, ActualWarehouseL1L2Select },
components: { DragResizePanel, PlanDetailPanel, WarehouseSelect, ActualWarehouseL1L2Select },
data() {
return {
loading: false, detailLoading: false, btnLoading: false, whBtnLoading: false, submitLoading: false, total: 0, dataList: [], currentRow: null,
loading: false, btnLoading: false, whBtnLoading: false, submitLoading: false, total: 0, dataList: [], currentRow: null,
queryParams: { pageNum: 1, pageSize: 10, planCode: undefined, planStatus: 0 },
planDialogOpen: false, planDialogTitle: '', planForm: {},
whDialogOpen: false, whForm: { warehouseId: undefined, actualWarehouseId: undefined, warehouseName: '', actualWarehouseName: '', ioStartTime: undefined, ioEndTime: undefined }
@@ -113,18 +86,16 @@ export default {
parseTime,
getList() { var self = this; this.loading = true; listCountPlan(this.queryParams).then(function(r) { self.dataList = r.rows; self.total = r.total; }).finally(function() { self.loading = false; }); },
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
handleRowClick(row) { this.currentRow = row; this.loadDetail(row.planId); },
loadDetail(planId) { var self = this; this.detailLoading = true; getCountPlan(planId).then(function(r) { self.currentRow = r.data; }).finally(function() { self.detailLoading = false; }); },
handleRefreshDetail() { if (this.currentRow) this.loadDetail(this.currentRow.planId); },
handleAdd() { this.planForm = {}; this.planDialogTitle = '新增盘库计划'; this.planDialogOpen = true; },
handleUpdate(row) { var self = this; getCountPlan(row.planId).then(function(r) { self.planForm = r.data; self.planDialogTitle = '修改'; self.planDialogOpen = true; }); },
submitPlan() { var self = this; this.btnLoading = true; var api = this.planForm.planId ? updateCountPlan : addCountPlan; api(this.planForm).then(function() { self.$modal.msgSuccess('成功'); self.planDialogOpen = false; self.getList(); }).finally(function() { self.btnLoading = false; }); },
handleRowClick(row) { this.currentRow = row; },
handleAdd() { this.planForm = { planName: new Date().toLocaleDateString().replace(/\//g, '-') + '盘库计划' }; this.planDialogTitle = '新增盘库计划'; this.planDialogOpen = true; },
handleUpdate(row) { this.planForm = Object.assign({}, row); this.planDialogTitle = '修改'; this.planDialogOpen = true; },
submitPlan() { var self = this; this.btnLoading = true; this.planForm.planCode = this.planForm.planName; var api = this.planForm.planId ? updateCountPlan : addCountPlan; api(this.planForm).then(function() { self.$modal.msgSuccess('成功'); self.planDialogOpen = false; self.getList(); }).finally(function() { self.btnLoading = false; }); },
handleDelete(row) { var self = this; this.$modal.confirm('确认删除?').then(function() { return delCountPlan(row.planId); }).then(function() { self.$modal.msgSuccess('已删除'); self.currentRow = null; self.getList(); }).catch(function() {}); },
handleSubmitApproval() { var self = this; this.$modal.confirm('确认提交审批?').then(function() { self.submitLoading = true; return updateCountPlan({ planId: self.currentRow.planId, planStatus: 1 }); }).then(function() { self.$modal.msgSuccess('已提交'); self.loadDetail(self.currentRow.planId); self.getList(); }).finally(function() { self.submitLoading = false; }); },
handleSubmitApproval() { var self = this; this.$modal.confirm('确认提交审批?').then(function() { self.submitLoading = true; return updateCountPlan({ planId: self.currentRow.planId, planStatus: 1 }); }).then(function() { self.$modal.msgSuccess('已提交'); self.getList(); self.currentRow = null; }).finally(function() { self.submitLoading = false; }); },
handleAddWarehouse() { this.whForm = { warehouseId: undefined, actualWarehouseId: undefined, warehouseName: '', actualWarehouseName: '', ioStartTime: undefined, ioEndTime: undefined }; this.whDialogOpen = true; },
onWhChange(val) { if (val && this.$refs.wsRef) { var o = this.$refs.wsRef.warehouseOptions; var f = o.find(function(x) { return x.warehouseId === val; }); this.whForm.warehouseName = f ? f.warehouseName : ''; } else this.whForm.warehouseName = ''; },
onAwhChange(val) { if (val && this.$refs.awsRef) { var o = this.$refs.awsRef.options; var f = o.find(function(x) { return x.actualWarehouseId === val; }); this.whForm.actualWarehouseName = f ? f.actualWarehouseName : ''; } else this.whForm.actualWarehouseName = ''; },
submitWh() { var self = this; if (!this.whForm.warehouseId && !this.whForm.actualWarehouseId) { this.$modal.msgWarning('请至少选择一个库区'); return; } this.whBtnLoading = true; addCountPlanWarehouse({ planId: this.currentRow.planId, warehouseId: this.whForm.warehouseId || undefined, actualWarehouseId: this.whForm.actualWarehouseId || undefined, warehouseName: this.whForm.warehouseName || undefined, actualWarehouseName: this.whForm.actualWarehouseName || undefined, ioStartTime: this.whForm.ioStartTime, ioEndTime: this.whForm.ioEndTime }).then(function() { self.$modal.msgSuccess('绑定成功'); self.whDialogOpen = false; self.$refs.whPanel.refreshAll(); }).finally(function() { self.whBtnLoading = false; }); }
submitWh() { var self = this; if (!this.whForm.warehouseId && !this.whForm.actualWarehouseId) { this.$modal.msgWarning('请至少选择一个库区'); return; } this.whBtnLoading = true; addCountPlanWarehouse({ planId: this.currentRow.planId, warehouseId: this.whForm.warehouseId || undefined, actualWarehouseId: this.whForm.actualWarehouseId || undefined, warehouseName: this.whForm.warehouseName || undefined, actualWarehouseName: this.whForm.actualWarehouseName || undefined, ioStartTime: this.whForm.ioStartTime, ioEndTime: this.whForm.ioEndTime }).then(function() { self.$modal.msgSuccess('绑定成功'); self.whDialogOpen = false; self.$refs.detailPanel.refresh(); }).finally(function() { self.whBtnLoading = false; }); }
}
};
</script>

View File

@@ -4,98 +4,48 @@
<template #panelA>
<div class="left-panel">
<div class="panel-header">
<div class="header-title"><i class="el-icon-s-check"></i><span>盘库审批</span><el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表" /></div>
<div class="header-title"><i class="el-icon-s-check"></i><span>差异审批</span><el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表" /></div>
</div>
<div class="search-row"><el-input v-model="queryParams.planCode" 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.planCode }}</span><span class="item-sub">{{ item.planName }}</span></div>
<div class="item-meta"><el-tag size="mini">审批</el-tag></div>
<div class="item-meta"><el-tag size="mini">差异审批</el-tag></div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty"><i class="el-icon-folder-opened"></i><span>暂无待审批盘库计划</span></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.planCode }}</div><div class="doc-subtitle">Approval</div></div>
<div class="doc-header-right"><el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail">刷新</el-button></div>
</div>
<div class="doc-status-row"><span class="doc-status-label">Status</span><el-tag size="small">待审批</el-tag></div>
</div>
<div class="detail-meta">
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
<span v-if="currentRow.countDate"><i class="el-icon-date"></i>{{ parseTime(currentRow.countDate, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.countUserName"><i class="el-icon-user-solid"></i>{{ currentRow.countUserName }}</span>
</div>
<CountFlowSection :planStatus="1" />
<el-divider />
<WarehouseDetailPanel ref="whPanel"
:planId="currentRow.planId"
:planStatus="1"
@approve="handleApprove"
@reject="handleReject"
@mark-disc="handleMarkDisc"
@preview="handlePreview"
/>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
<PlanDetailPanel ref="detailPanel"
:planId="currentRow ? currentRow.planId : null"
subtitle="Discrepancy Approval"
emptyText="请在左侧列表中选择一条盘库计划进行差异审批"
@diff-approve="handleApprove"
@reject="handleReject"
/>
</div>
</template>
</DragResizePanel>
<el-dialog title="标记差异项" :visible.sync="markDialogVisible" width="500px" append-to-body>
<el-form :model="markForm" label-width="100px">
<el-form-item label="差异类型">
<el-tag v-if="markForm.discrepancyType === 1" type="success">盘盈</el-tag>
<el-tag v-else-if="markForm.discrepancyType === 2" type="danger">盘亏</el-tag>
<el-tag v-else-if="markForm.discrepancyType === 3" type="warning">不符</el-tag>
<el-tag v-else>偏差</el-tag>
</el-form-item>
<el-form-item label="钢卷号">{{ markForm.enterCoilNo }}</el-form-item>
<el-form-item label="差异详情">{{ markForm.discrepancyDetail }}</el-form-item>
<el-form-item label="须处理"><el-switch v-model="markForm.needResolve" active-text="是" inactive-text="否" /></el-form-item>
<el-form-item label="审批意见"><el-input v-model="markForm.approveRemark" type="textarea" :rows="3" placeholder="审批意见" /></el-form-item>
</el-form>
<div slot="footer"><el-button type="primary" @click="submitMark">确定</el-button><el-button @click="markDialogVisible = false">取消</el-button></div>
</el-dialog>
<el-dialog title="文件预览" :visible.sync="previewOpen" width="80%" top="5vh" append-to-body :before-close="function(){previewOpen=false;previewUrl='';}">
<div style="height:75vh;"><vue-office-excel v-if="previewUrl" :src="previewUrl" @render-error="function(){ $modal.msgError('预览失败'); }" /><span v-else>加载中...</span></div>
</el-dialog>
</div>
</template>
<script>
import { listCountPlan, getCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import { updateCountDiscrepancy } from "@/api/flow/countDiscrepancy";
import { listCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import CountFlowSection from "./components/CountFlowSection.vue";
import WarehouseDetailPanel from "./components/WarehouseDetailPanel.vue";
import VueOfficeExcel from '@vue-office/excel';
import '@vue-office/excel/lib/index.css';
import { listByIds } from '@/api/system/oss';
import PlanDetailPanel from "./components/PlanDetailPanel.vue";
import { parseTime } from '@/utils/klp'
export default {
name: "InvCountApproval",
components: { DragResizePanel, CountFlowSection, WarehouseDetailPanel, VueOfficeExcel },
name: "InvCountDiffApproval",
components: { DragResizePanel, PlanDetailPanel },
data() {
return {
loading: false, detailLoading: false, total: 0,
queryParams: { pageNum: 1, pageSize: 10, planCode: undefined, planStatus: 1 },
loading: false, total: 0,
dataList: [], currentRow: null,
markDialogVisible: false, markForm: { discrepancyId: null, discrepancyType: null, enterCoilNo: '', discrepancyDetail: '', needResolve: false, approveRemark: '' },
previewOpen: false, previewUrl: ''
queryParams: { pageNum: 1, pageSize: 10, planCode: undefined, planStatus: 3 }
};
},
created() { this.getList(); },
@@ -103,36 +53,18 @@ export default {
parseTime,
getList() { var self = this; this.loading = true; listCountPlan(this.queryParams).then(function(r) { self.dataList = r.rows; self.total = r.total; }).finally(function() { self.loading = false; }); },
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
handleRowClick(row) { this.currentRow = row; this.loadDetail(row.planId); },
loadDetail(planId) { var self = this; this.detailLoading = true; getCountPlan(planId).then(function(r) { self.currentRow = r.data; }).finally(function() { self.detailLoading = false; }); },
handleRefreshDetail() { if (this.currentRow) this.loadDetail(this.currentRow.planId); },
handleRowClick(row) { this.currentRow = row; },
handleApprove() {
var self = this;
this.$modal.confirm('确认通过"' + this.currentRow.planCode + '"的审批?').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 3 });
}).then(function() { self.$modal.msgSuccess("审批通过"); self.currentRow = null; self.getList(); });
this.$modal.confirm('确认通过"' + this.currentRow.planCode + '"的差异审批?通过后进入差异处理阶段。').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 4 });
}).then(function() { self.$modal.msgSuccess("审批通过,已进入差异处理"); self.currentRow = null; self.getList(); });
},
handleReject() {
var self = this;
this.$prompt('请输入驳回原因', '驳回审批', { inputType: 'textarea', inputValidator: function(v) { return v ? true : '驳回原因不能为空'; } }).then(function({ value }) {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 0, remark: value });
}).then(function() { self.$modal.msgSuccess("已驳回"); self.currentRow = null; self.getList(); });
},
handleMarkDisc(row) {
this.markForm = { discrepancyId: row.discrepancyId, discrepancyType: row.discrepancyType, enterCoilNo: row.enterCoilNo, discrepancyDetail: row.discrepancyDetail, needResolve: row.processStatus === 1, approveRemark: row.remark || '' };
this.markDialogVisible = true;
},
submitMark() {
var self = this;
updateCountDiscrepancy({ discrepancyId: this.markForm.discrepancyId, processStatus: this.markForm.needResolve ? 1 : 0, remark: this.markForm.approveRemark }).then(function() {
self.$modal.msgSuccess('标记已保存'); self.markDialogVisible = false;
var wh = self.$refs.whPanel.getActiveWarehouse();
if (wh) self.$refs.whPanel.refreshOneDisc(wh.relId);
}).catch(function() { self.$modal.msgError('保存失败'); });
},
handlePreview(ossId, label) {
var self = this; this.previewUrl = ''; this.previewOpen = true;
listByIds(ossId).then(function(r) { var list = r.data || r.rows || []; if (list.length > 0) self.previewUrl = list[0].url; else { self.$modal.msgError('未找到'); self.previewOpen = false; } }).catch(function() { self.$modal.msgError('失败'); self.previewOpen = false; });
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 2, remark: value });
}).then(function() { self.$modal.msgSuccess("已驳回至盘库执行"); self.currentRow = null; self.getList(); });
}
}
};
@@ -158,21 +90,5 @@ export default {
.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; }
.detail-content { margin: 0 auto; background: #fff; 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-size: 24px; font-weight: 700; color: #1a1a1a; line-height: 1.3; letter-spacing: 0.5px; }
.doc-subtitle { font-size: 12px; 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-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-size: 15px; font-weight: 700; color: #1a1a1a; margin: 22px 0 12px; padding: 0 0 10px; border-bottom: 1px solid #d4d0c8; display: flex; align-items: center; gap: 10px; }
.section-title .en-sub { font-size: 11px; font-weight: 400; color: #8c8c8c; letter-spacing: 0.5px; 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; }
</style>

View File

@@ -6,9 +6,10 @@
</div>
<el-steps :active="activeStep" align-center class="flow-steps">
<el-step title="创建计划" icon="el-icon-document" />
<el-step title="快照与对比" icon="el-icon-camera" />
<el-step title="提交审批" icon="el-icon-s-promotion" />
<el-step title="处理差异" icon="el-icon-warning" />
<el-step title="计划审批" icon="el-icon-s-promotion" />
<el-step title="盘库执行" icon="el-icon-camera" />
<el-step title="差异审批" icon="el-icon-s-check" />
<el-step title="差异处理" icon="el-icon-warning" />
<el-step title="完成归档" icon="el-icon-circle-check" />
</el-steps>
<div class="current-status">
@@ -34,28 +35,28 @@ export default {
computed: {
/**
* el-steps active 从 0 开始。
* 步骤0=创建计划, 1=快照与对比, 2=提交审批, 3=处理差异, 4=完成归档
* status 0=草稿 -> active=0 (创建计划)
* status 1=待审批 -> active=2 (提交审批)
* status 2=执行中 -> active=3 (处理差异)
* status 3=差异处理 -> active=3 (处理差异)
* status 4=已归档 -> active=5 (全部finish)
* 步骤0=创建计划, 1=计划审批, 2=盘库执行, 3=差异审批, 4=差异处理, 5=完成归档
* status 0=草稿 -> active=0
* status 1=计划待审批 -> active=1
* status 2=盘库执行中 -> active=2
* status 3=差异审批中 -> active=3
* status 4=差异处理中 -> active=4
* status 5=已归档 -> active=6 (全部finish)
*/
activeStep() {
if (this.planStatus == null) return -1;
const v = Number(this.planStatus);
if (v >= 4) return 5;
if (v === 1) return 2;
if (v === 2 || v === 3) return 3;
if (v >= 5) return 6;
return v;
},
flowStatusText() {
const map = {
0: '草稿',
1: '待审批',
2: '执行中',
3: '差异处理中',
4: '已归档'
1: '计划待审批',
2: '盘库执行中',
3: '差异审批中',
4: '差异处理中',
5: '已归档'
};
return map[this.planStatus] || '未知';
},
@@ -64,8 +65,9 @@ export default {
0: 'info',
1: '',
2: 'warning',
3: 'danger',
4: 'success'
3: '',
4: 'danger',
5: 'success'
};
return map[this.planStatus] || '';
}

View File

@@ -0,0 +1,127 @@
<template>
<div v-if="!currentRow" class="empty-tip"><i class="el-icon-info"></i><span>{{ emptyText }}</span></div>
<div v-else v-loading="loading" class="detail-content">
<div class="doc-header">
<div class="doc-header-top">
<div class="doc-title-group">
<div class="doc-title">{{ currentRow.planCode }}</div>
<div class="doc-subtitle">{{ subtitle }}</div>
</div>
<div class="doc-header-right">
<el-button size="mini" type="text" icon="el-icon-refresh" @click="refresh">刷新</el-button>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="text" icon="el-icon-edit" @click="$emit('edit-plan', currentRow)">编辑</el-button>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="text" icon="el-icon-delete" @click="$emit('delete-plan', currentRow)">删除</el-button>
<el-button v-if="currentRow.planStatus === 4" size="mini" type="success" icon="el-icon-circle-check" @click="$emit('archive')">归档封存</el-button>
</div>
</div>
<div class="doc-status-row">
<span class="doc-status-label">Status</span>
<el-tag v-if="currentRow.planStatus === 0" type="info" size="small">草稿</el-tag>
<el-tag v-else-if="currentRow.planStatus === 1" 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" size="small">差异审批中</el-tag>
<el-tag v-else-if="currentRow.planStatus === 4" type="danger" size="small">差异处理中</el-tag>
<el-tag v-else-if="currentRow.planStatus === 5" type="success" size="small">已归档</el-tag>
</div>
</div>
<div class="detail-meta">
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
<span v-if="currentRow.countDate"><i class="el-icon-date"></i>{{ formatTime(currentRow.countDate, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.countUserName"><i class="el-icon-user-solid"></i>{{ currentRow.countUserName }}</span>
<span v-if="currentRow.principalUserName"><i class="el-icon-s-custom"></i>{{ currentRow.principalUserName }}</span>
</div>
<CountFlowSection :planStatus="currentRow.planStatus" />
<el-divider />
<!-- 流程操作按钮 -->
<div v-if="currentRow.planStatus === 0" class="section-title">
<span>流程操作 <span class="en-sub">· Actions</span></span>
</div>
<div class="flow-actions" v-if="currentRow.planStatus === 0">
<el-button type="primary" size="small" icon="el-icon-s-promotion" @click="$emit('submit-approval')">提交审批</el-button>
</div>
<el-divider v-if="currentRow.planStatus === 0" />
<div class="section-title">库区盘点明细 <span class="en-sub">· Warehouses</span>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="primary" plain icon="el-icon-plus" style="margin-left:8px;" @click="$emit('bind-warehouse')">绑定库区</el-button>
</div>
<WarehouseDetailPanel ref="whPanel"
:planId="currentRow.planId"
:planStatus="currentRow.planStatus"
@approve="$emit('approve')"
@diff-approve="$emit('diff-approve')"
@reject="$emit('reject')"
@archive="$emit('archive')"
@process-disc="$emit('process-disc', $event)"
@submit-disc-approval="$emit('submit-disc-approval')"
/>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
</template>
<script>
import { getCountPlan } from "@/api/flow/countPlan";
import CountFlowSection from "./CountFlowSection.vue";
import WarehouseDetailPanel from "./WarehouseDetailPanel.vue";
import { parseTime } from '@/utils/klp';
export default {
name: 'PlanDetailPanel',
components: { CountFlowSection, WarehouseDetailPanel },
props: {
planId: { type: [Number, String], default: null },
emptyText: { type: String, default: '请在左侧列表中选择一条盘库计划' },
subtitle: { type: String, default: 'Inventory Count Plan' }
},
data() {
return { loading: false, currentRow: null };
},
watch: {
planId: {
immediate: true,
handler(val) {
if (!val) { this.currentRow = null; return; }
this.loadDetail(val);
}
}
},
methods: {
formatTime(val, fmt) { if (!val) return ''; return parseTime(val, fmt); },
loadDetail(planId) {
this.loading = true; var self = this;
getCountPlan(planId).then(function(r) { self.currentRow = r.data; self.$emit('loaded', r.data); }).finally(function() { self.loading = false; });
},
refresh() { if (this.currentRow) this.loadDetail(this.currentRow.planId); if (this.$refs.whPanel) this.$refs.whPanel.refreshAll(); }
}
};
</script>
<style scoped>
.empty-tip { display: flex; align-items: center; justify-content: center; height: 100%; color: #909399; font-size: 14px; gap: 8px; }
.detail-content { margin: 0 auto; background: #fff; 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%; }
.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-size: 24px; font-weight: 700; color: #1a1a1a; line-height: 1.3; letter-spacing: 0.5px; }
.doc-subtitle { font-size: 12px; 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-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-size: 15px; font-weight: 700; color: #1a1a1a; margin: 22px 0 12px; padding: 0 0 10px; 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-style: italic; }
.flow-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.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; }
</style>

View File

@@ -13,8 +13,8 @@
<div class="doc-tabs-content">
<div v-for="(wh, idx) in warehouseList" :key="wh.relId" v-show="activeIdx === idx">
<!-- ====== 草稿 (0)操作台 ====== -->
<template v-if="planStatus === 0">
<!-- ====== 盘库执行中 (2)操作台 ====== -->
<template v-if="planStatus === 2">
<div class="console-section">
<div class="console-steps">
<div class="console-step" :class="{ done: wh.snapshotCoilLogic }">
@@ -73,12 +73,34 @@
</div>
<div style="text-align:right;margin-bottom:8px;">
<el-button type="primary" size="small" icon="el-icon-s-promotion" @click="$emit('submit-approval')">提交审批</el-button>
<el-button type="primary" size="small" icon="el-icon-s-promotion" @click="$emit('submit-disc-approval')">提交差异审批</el-button>
</div>
</template>
<!-- ====== 概览卡片状态 0/1/4 ====== -->
<div v-if="planStatus !== 3" class="wh-overview">
<!-- ====== 草稿 (0) / 计划待审批 (1)库区清单 ====== -->
<template v-if="planStatus === 0 || planStatus === 1">
<div class="wh-detail-list">
<div class="wh-detail-row">
<span class="wh-detail-label">逻辑库区</span>
<span class="wh-detail-value">{{ wh.warehouseName || '-' }}</span>
</div>
<div class="wh-detail-row">
<span class="wh-detail-label">实际库区</span>
<span class="wh-detail-value">{{ wh.actualWarehouseName || '-' }}</span>
</div>
<div class="wh-detail-row">
<span class="wh-detail-label">出入库查询起始</span>
<span class="wh-detail-value">{{ wh.ioStartTime ? formatTime(wh.ioStartTime) : '-' }}</span>
</div>
<div class="wh-detail-row">
<span class="wh-detail-label">出入库查询截止</span>
<span class="wh-detail-value">{{ wh.ioEndTime ? formatTime(wh.ioEndTime) : '-' }}</span>
</div>
</div>
</template>
<!-- ====== 概览卡片状态 2/3/4/5 ====== -->
<div v-if="planStatus >= 2" class="wh-overview">
<div class="wh-box">
<div class="wh-box-title">盘点范围</div>
<div class="wh-box-body">
@@ -108,19 +130,28 @@
</div>
</div>
<!-- ===== 待审批操作 ===== -->
<!-- ===== 计划待审批 (1) 操作 ===== -->
<div v-if="planStatus === 1" style="text-align:right;margin-bottom:8px;">
<el-button size="small" type="danger" icon="el-icon-close" @click="$emit('reject')">驳回</el-button>
<el-button size="small" type="primary" icon="el-icon-check" @click="$emit('approve')">审批通过</el-button>
</div>
<!-- ===== 差异审批中 (3) 操作 ===== -->
<div v-if="planStatus === 3" style="text-align:right;margin-bottom:8px;">
<el-button size="small" type="danger" icon="el-icon-close" @click="$emit('reject')">驳回</el-button>
<el-button size="small" type="primary" icon="el-icon-check" @click="$emit('diff-approve')">审批通过</el-button>
</div>
<!-- ===== 差异处理中 (4) 操作 ===== -->
<div v-if="planStatus === 4" style="text-align:right;margin-bottom:8px;">
<el-button size="small" type="success" icon="el-icon-circle-check" @click="$emit('archive')">归档封存</el-button>
</div>
<!-- ====== 差异明细表格 ====== -->
<!-- ====== 差异明细表格状态 2+ ====== -->
<template v-if="planStatus >= 2">
<div class="section-title">差异明细</div>
<template v-if="planStatus === 0">
<template v-if="planStatus === 2">
<div style="display:flex;justify-content:flex-end;margin-bottom:4px;">
<el-button size="mini" type="danger" plain icon="el-icon-delete" :disabled="discSelected.length === 0" @click="batchDelDisc(wh)">批量删除</el-button>
</div>
@@ -167,21 +198,31 @@
<el-table-column label="处理建议" align="center" min-width="100">
<template slot-scope="ds">{{ ds.row.processSuggestion || '-' }}</template>
</el-table-column>
<el-table-column label="需处理" align="center" width="80">
<template slot-scope="ds">
<el-tag v-if="ds.row.processStatus === 1" type="warning" size="mini"></el-tag>
<el-tag v-else type="info" size="mini"></el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="80">
<template slot-scope="ds">
<el-button size="mini" type="text" icon="el-icon-edit" @click="$emit('mark-disc', ds.row)">标记</el-button>
</template>
</el-table-column>
</el-table>
</template>
<template v-else-if="planStatus === 3">
<el-table v-loading="discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300">
<el-table-column label="类型" align="center" width="80">
<template slot-scope="ds">
<el-tag v-if="ds.row.discrepancyType === 1" type="success" size="mini">盘盈</el-tag>
<el-tag v-else-if="ds.row.discrepancyType === 2" type="danger" size="mini">盘亏</el-tag>
<el-tag v-else-if="ds.row.discrepancyType === 3" type="warning" size="mini">不符</el-tag>
<el-tag v-else size="mini">偏差</el-tag>
</template>
</el-table-column>
<el-table-column label="钢卷号" align="center" prop="enterCoilNo" width="140" />
<el-table-column label="差异详情" align="center" prop="discrepancyDetail" min-width="120" show-overflow-tooltip />
<el-table-column label="原因分析" align="center" min-width="100">
<template slot-scope="ds">{{ ds.row.reasonAnalysis || '-' }}</template>
</el-table-column>
<el-table-column label="处理建议" align="center" min-width="100">
<template slot-scope="ds">{{ ds.row.processSuggestion || '-' }}</template>
</el-table-column>
</el-table>
</template>
<template v-else-if="planStatus === 4">
<el-table v-loading="discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300">
<el-table-column label="类型" align="center" width="80">
<template slot-scope="ds">
@@ -219,7 +260,7 @@
</el-table>
</template>
<template v-else-if="planStatus === 4">
<template v-else-if="planStatus === 5">
<el-table v-loading="discLoadingMap[wh.relId]" :data="discMap[wh.relId] || []" border size="small" style="width:100%" height="300">
<el-table-column label="类型" align="center" width="80">
<template slot-scope="ds">
@@ -257,6 +298,7 @@
<div v-else style="text-align:right;padding:4px 0;">
<el-pagination background layout="prev, pager, next, total" :total="discTotalMap[wh.relId] || 0" :page-size="discPageSize" :current-page="discPageMap[wh.relId] || 1" @current-change="function(p) { loadDisc(wh.relId, p); }" small />
</div>
</template>
</div>
</div>
</div>
@@ -402,8 +444,11 @@ export default {
listCountDiscrepancy({ relId, pageNum: pageNum, pageSize: this.discPageSize }).then(function(r) {
self.$set(self.discMap, relId, (r.rows || []).map(function(item) {
if (item._processResult === undefined) item._processResult = item.processResult || '';
if (item._needResolve === undefined) item._needResolve = item.processStatus === 1;
if (item._approveRemark === undefined) item._approveRemark = item.remark || '';
return item;
}));
self.$set(self.discTotalMap, relId, r.total || 0);
}).finally(function() { self.$set(self.discLoadingMap, relId, false); });
},
loadAllDisc() { var self = this; this.warehouseList.forEach(function(wh) { self.loadDisc(wh.relId); }); },
@@ -507,6 +552,10 @@ export default {
if (!row || !row.discrepancyId) return;
updateCountDiscrepancy({ discrepancyId: row.discrepancyId, reasonAnalysis: row.reasonAnalysis, processSuggestion: row.processSuggestion }).catch(function() {});
},
saveDiscMark(row) {
if (!row || !row.discrepancyId) return;
updateCountDiscrepancy({ discrepancyId: row.discrepancyId, processStatus: row._needResolve ? 1 : 0, remark: row._approveRemark }).catch(function() {});
},
// ===== 对比 =====
async doCompare(wh) {
@@ -568,6 +617,11 @@ export default {
<style scoped>
.wh-panel { }
.wh-empty { padding: 16px 0; color: #909399; font-size: 13px; }
.wh-detail-list { margin: 8px 0 16px; border: 1px solid #e8e4de; border-radius: 2px; overflow: hidden; }
.wh-detail-row { display: flex; padding: 8px 14px; font-size: 13px; border-bottom: 1px solid #f0ece6; }
.wh-detail-row:last-child { border-bottom: none; }
.wh-detail-label { width: 120px; flex-shrink: 0; color: #606266; font-weight: 500; }
.wh-detail-value { color: #303133; }
.doc-tabs-header { display: flex; border-bottom: 1px solid #d4d0c8; margin-bottom: 12px; }
.doc-tab-item { padding: 8px 16px; cursor: pointer; color: #8c8c8c; font-size: 13px; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.2s, border-color 0.2s; display: flex; align-items: center; gap: 6px; user-select: none; }
.doc-tab-item:hover { color: #1a3c6e; }

View File

@@ -0,0 +1,87 @@
<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-data"></i><span>盘库执行</span><el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表" /></div>
</div>
<div class="search-row"><el-input v-model="queryParams.planCode" 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.planCode }}</span><span class="item-sub">{{ item.planName }}</span></div>
<div class="item-meta"><el-tag type="warning" size="mini">盘库执行中</el-tag></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">
<PlanDetailPanel ref="detailPanel"
:planId="currentRow ? currentRow.planId : null"
subtitle="Count Execution"
emptyText="请在左侧列表中选择一条盘库计划进行执行操作"
@submit-disc-approval="handleSubmitDiscApproval"
/>
</div>
</template>
</DragResizePanel>
</div>
</template>
<script>
import { listCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import PlanDetailPanel from "./components/PlanDetailPanel.vue";
import { parseTime } from '@/utils/klp'
export default {
name: "InvCountCountExecute",
components: { DragResizePanel, PlanDetailPanel },
data() {
return {
loading: false, total: 0,
queryParams: { pageNum: 1, pageSize: 10, planCode: undefined, planStatus: 2 },
dataList: [], currentRow: null
};
},
created() { this.getList(); },
methods: {
parseTime,
getList() { var self = this; this.loading = true; listCountPlan(this.queryParams).then(function(r) { self.dataList = r.rows; self.total = r.total; }).finally(function() { self.loading = false; }); },
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
handleRowClick(row) { this.currentRow = row; },
handleSubmitDiscApproval() {
var self = this;
this.$modal.confirm('确认提交"' + this.currentRow.planCode + '"的差异审批?提交后将进入差异审批阶段。').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 3 });
}).then(function() { self.$modal.msgSuccess("已提交差异审批"); self.currentRow = null; self.getList(); });
}
}
};
</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; }
.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; }
.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; }
.empty-tip { display: flex; align-items: center; justify-content: center; height: 100%; color: #909399; font-size: 14px; gap: 8px; }
</style>

View File

@@ -26,32 +26,12 @@
</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.planCode }}</div><div class="doc-subtitle">Discrepancy Processing</div></div>
<div class="doc-header-right"><el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail">刷新</el-button></div>
</div>
<div class="doc-status-row"><span class="doc-status-label">Status / 状态</span><el-tag type="danger" size="small">差异处理中</el-tag></div>
</div>
<div class="detail-meta">
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
<span v-if="currentRow.countUserName"><i class="el-icon-user-solid"></i>{{ currentRow.countUserName }}</span>
</div>
<CountFlowSection :planStatus="3" />
<el-divider />
<WarehouseDetailPanel ref="whPanel"
:planId="currentRow.planId"
:planStatus="3"
@process-disc="handleProcessDisc"
/>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
<PlanDetailPanel ref="detailPanel"
:planId="currentRow ? currentRow.planId : null"
subtitle="Discrepancy Processing"
@process-disc="handleProcessDisc"
@archive="handleArchive"
/>
</div>
</template>
</DragResizePanel>
@@ -59,20 +39,19 @@
</template>
<script>
import { listCountPlan, getCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import { listCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import { updateCountDiscrepancy } from "@/api/flow/countDiscrepancy";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import CountFlowSection from "./components/CountFlowSection.vue";
import WarehouseDetailPanel from "./components/WarehouseDetailPanel.vue";
import PlanDetailPanel from "./components/PlanDetailPanel.vue";
import { parseTime } from '@/utils/klp'
export default {
name: "InvCountExecute",
components: { DragResizePanel, CountFlowSection, WarehouseDetailPanel },
components: { DragResizePanel, PlanDetailPanel },
data() {
return {
loading: false, detailLoading: false, total: 0,
queryParams: { pageNum: 1, pageSize: 10, planCode: undefined, planStatus: 3 },
loading: false, total: 0,
queryParams: { pageNum: 1, pageSize: 10, planCode: undefined, planStatus: 4 },
dataList: [], currentRow: null
};
},
@@ -81,8 +60,8 @@ export default {
parseTime,
getList() { var self = this; this.loading = true; listCountPlan(this.queryParams).then(function(r) { self.dataList = r.rows; self.total = r.total; }).finally(function() { self.loading = false; }); },
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
handleRowClick(row) { this.currentRow = row; this.detailLoading = true; getCountPlan(row.planId).then(function(r) { self.currentRow = r.data; }).finally(function() { self.detailLoading = false; }); },
handleRefreshDetail() { if (this.currentRow) { var self = this; this.detailLoading = true; getCountPlan(this.currentRow.planId).then(function(r) { self.currentRow = r.data; }).finally(function() { self.detailLoading = false; }); } },
handleRowClick(row) { this.currentRow = row; },
handleRefreshDetail() { if (this.currentRow) this.currentRow = Object.assign({}, this.currentRow); },
handleProcessDisc({ row, relId }) {
var self = this;
this.$modal.confirm('确认将该差异项标记为已处理?', '处理确认', { confirmButtonText: '确认处理', cancelButtonText: '取消', type: 'warning' }).then(function() {
@@ -91,13 +70,19 @@ export default {
return updateCountDiscrepancy({
discrepancyId: row.discrepancyId,
processResult: row._processResult || '',
processStatus: 2,
processStatus: 2, processUserId: self.$store.getters.id, processUserName: self.$store.getters.nickName,
processTime: now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate()) + ' ' + pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds())
});
}).then(function() {
self.$modal.msgSuccess('已处理');
self.$refs.whPanel.refreshOneDisc(relId);
self.$refs.detailPanel.$refs.whPanel.refreshOneDisc(relId);
}).catch(function(err) { if (err !== 'cancel') self.$modal.msgError('处理失败'); });
},
handleArchive() {
var self = this;
this.$modal.confirm('确认将盘库计划归档封存?').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 5 });
}).then(function() { self.$modal.msgSuccess("已归档"); self.currentRow = null; self.getList(); });
}
}
};

View File

@@ -11,10 +11,11 @@
</div>
<el-select v-model="queryParams.planStatus" placeholder="全部状态" clearable size="mini" @change="handleQuery" class="header-filter">
<el-option label="草稿" :value="0" />
<el-option label="待审批" :value="1" />
<el-option label="差异处理中" :value="2" />
<el-option label="差异处理中" :value="3" />
<el-option label="已归档" :value="4" />
<el-option label="计划待审批" :value="1" />
<el-option label="盘库执行" :value="2" />
<el-option label="差异待审批" :value="3" />
<el-option label="差异处理" :value="4" />
<el-option label="已归档" :value="5" />
</el-select>
</div>
@@ -35,10 +36,11 @@
</div>
<div class="item-meta">
<el-tag v-if="item.planStatus === 0" type="info" size="mini">草稿</el-tag>
<el-tag v-else-if="item.planStatus === 1" 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="danger" size="mini">差异处理</el-tag>
<el-tag v-else-if="item.planStatus === 4" type="success" size="mini">已归档</el-tag>
<el-tag v-else-if="item.planStatus === 1" 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" size="mini">差异审批</el-tag>
<el-tag v-else-if="item.planStatus === 4" type="danger" size="mini">差异处理中</el-tag>
<el-tag v-else-if="item.planStatus === 5" type="success" size="mini">已归档</el-tag>
</div>
<div class="item-actions">
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleUpdate(item)"></el-button>
@@ -60,87 +62,21 @@
<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.planCode }}</div>
<div class="doc-subtitle">Inventory Count Plan</div>
</div>
<div class="doc-header-right">
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail" title="刷新详情">刷新</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(currentRow)" v-if="currentRow.planStatus === 0">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(currentRow)" v-if="currentRow.planStatus === 0">删除</el-button>
</div>
</div>
<div class="doc-status-row">
<span class="doc-status-label">Status / 状态</span>
<el-tag v-if="currentRow.planStatus === 0" type="info" size="small">草稿</el-tag>
<el-tag v-else-if="currentRow.planStatus === 1" 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="danger" size="small">差异处理中</el-tag>
<el-tag v-else-if="currentRow.planStatus === 4" type="success" size="small">已归档</el-tag>
</div>
</div>
<div class="detail-meta">
<span><i class="el-icon-document"></i>{{ currentRow.planName }}</span>
<span v-if="currentRow.countDate"><i class="el-icon-date"></i>盘库日期: {{ parseTime(currentRow.countDate, '{y}-{m}-{d}') }}</span>
<span v-if="currentRow.deadlineTime"><i class="el-icon-time"></i>截止: {{ parseTime(currentRow.deadlineTime, '{y}-{m}-{d} {h}:{i}') }}</span>
<span v-if="currentRow.countUserName"><i class="el-icon-user-solid"></i>盘点人: {{ currentRow.countUserName }}</span>
<span v-if="currentRow.principalUserName"><i class="el-icon-s-custom"></i>负责人: {{ currentRow.principalUserName }}</span>
<span v-if="currentRow.participantNames"><i class="el-icon-user"></i>参与人: {{ currentRow.participantNames }}</span>
</div>
<CountFlowSection :planStatus="currentRow.planStatus" />
<el-divider />
<div class="section-title">
<span>流程操作 <span class="en-sub">· Workflow Actions</span></span>
</div>
<div class="flow-actions">
<el-button v-if="currentRow.planStatus === 0" type="primary" size="small" icon="el-icon-s-promotion" @click="handleSubmitApproval">提交审批</el-button>
<el-button v-if="currentRow.planStatus === 1" type="primary" size="small" icon="el-icon-s-promotion" @click="handleStartCount">审批通过开始盘库</el-button>
<el-button v-if="currentRow.planStatus === 3" type="success" size="small" icon="el-icon-circle-check" @click="handleArchive">归档封存</el-button>
</div>
<el-divider />
<div class="section-title">
库区盘点明细 <span class="en-sub">· Warehouse Count Details</span>
<el-button v-if="currentRow.planStatus === 0" size="mini" type="primary" plain icon="el-icon-plus" style="margin-left: 8px;" @click="handleAddWarehouse">绑定库区</el-button>
</div>
<WarehouseDetailPanel ref="whPanel"
:planId="currentRow.planId"
:planStatus="currentRow.planStatus"
@submit-approval="handleSubmitApproval"
@approve="handleStartCount"
@archive="handleArchive"
@process-disc="handleProcessFromPanel"
/>
<!-- Excel 快照预览弹窗 -->
<el-dialog title="快照预览" :visible.sync="previewDialogVisible" width="80%" top="5vh" append-to-body
:before-close="function() { previewDialogVisible = false; previewUrl = ''; }">
<div slot="title">{{ previewTitle }}</div>
<div style="height:75vh;">
<vue-office-excel v-if="previewUrl" :src="previewUrl" @render-error="handleExcelError" />
<span v-else style="color:#909399;">加载中...</span>
</div>
</el-dialog>
<div class="section-gap" />
<div class="section-title">备注 <span class="en-sub">· Remarks</span></div>
<div class="remark-content">{{ currentRow.remark || '无' }}</div>
</div>
<PlanDetailPanel ref="detailPanel"
:planId="currentRow ? currentRow.planId : null"
subtitle="Inventory Count Plan"
emptyText="请在左侧列表中选择一条盘库计划查看详情"
@submit-approval="handleSubmitApproval"
@approve="handlePlanApprove"
@diff-approve="handleDiffApprove"
@archive="handleArchive"
@edit-plan="handleUpdate"
@delete-plan="handleDelete"
@bind-warehouse="handleAddWarehouse"
@reject="handleReject"
@process-disc="handleProcessFromPanel"
@submit-disc-approval="handleSubmitDiscApproval"
/>
</div>
</template>
</DragResizePanel>
@@ -275,13 +211,13 @@
<script>
import { listCountPlan, getCountPlan, addCountPlan, updateCountPlan, delCountPlan } from "@/api/flow/countPlan";
import { listCountPlanWarehouse, addCountPlanWarehouse, delCountPlanWarehouse, updateCountPlanWarehouse } from "@/api/flow/countPlanWarehouse";
import { listCountDiscrepancy, updateCountDiscrepancy } from "@/api/flow/countDiscrepancy";
import { addCountPlanWarehouse, delCountPlanWarehouse, updateCountPlanWarehouse } from "@/api/flow/countPlanWarehouse";
import { updateCountDiscrepancy } from "@/api/flow/countDiscrepancy";
import { exportCoilWithAll } from "@/api/wms/coil";
import { uploadFile } from "@/api/system/oss";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import CountFlowSection from "./components/CountFlowSection.vue";
import WarehouseDetailPanel from "./components/WarehouseDetailPanel.vue";
import PlanDetailPanel from "./components/PlanDetailPanel.vue";
import WarehouseSelect from "@/components/KLPService/WarehouseSelect/index.vue";
import ActualWarehouseL1L2Select from "@/components/KLPService/ActualWarehouseL1L2Select/index.vue";
import { parseTime } from '@/utils/klp'
@@ -357,7 +293,7 @@ const SNAPSHOT_COL_MAP = {
export default {
name: "CountPlan",
components: { DragResizePanel, WarehouseSelect, ActualWarehouseL1L2Select, CountFlowSection, VueOfficeExcel, WarehouseDetailPanel },
components: { DragResizePanel, WarehouseSelect, ActualWarehouseL1L2Select, CountFlowSection, VueOfficeExcel, PlanDetailPanel },
data() {
return {
buttonLoading: false,
@@ -444,51 +380,32 @@ export default {
},
handleRowClick(row) {
this.currentRow = row;
this.loadDetail(row.planId);
},
loadDetail(planId) {
this.detailLoading = true;
var self = this;
getCountPlan(planId).then(function(response) {
self.currentRow = response.data;
self.loadWarehouseList(planId);
}).finally(function() { self.detailLoading = false; });
},
loadWarehouseList(planId) {
this.warehouseLoading = true;
var self = this;
listCountPlanWarehouse({ planId: planId, pageNum: 1, pageSize: 999 }).then(function(r) {
self.warehouseList = r.rows || [];
}).finally(function() { self.warehouseLoading = false; });
},
handleViewDiscrepancy(row) {
var relId = row.relId;
if (this.discrepancyMap[relId]) {
return;
}
this.$set(this.discrepancyLoadingMap, relId, true);
var self = this;
listCountDiscrepancy({ relId: relId, pageNum: 1, pageSize: 999 }).then(function(r) {
self.$set(self.discrepancyMap, relId, r.rows || []);
}).finally(function() {
self.$set(self.discrepancyLoadingMap, relId, false);
});
},
handleRefreshDetail() {
if (this.currentRow && this.currentRow.planId) {
this.loadDetail(this.currentRow.planId);
this.$refs.detailPanel.refresh();
}
},
handleSubmitApproval() {
var self = this;
this.$modal.confirm('确认提交盘库计划"' + this.currentRow.planCode + '"进行审批?').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 1 });
}).then(function() {
self.$modal.msgSuccess("提交审批成功");
self.$refs.detailPanel.refresh();
self.getList();
}).catch(function() { });
},
// 处理阶段差异处理(来自组件事件)
handleProcessFromPanel({ row, relId }) {
var self = this;
this.$modal.confirm('确认将该差异项标记为已处理?', '处理确认', { confirmButtonText: '确认处理', cancelButtonText: '取消', type: 'warning' }).then(function() {
var now = new Date(); var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
return updateCountDiscrepancy({ discrepancyId: row.discrepancyId, processResult: row._processResult || '', processStatus: 2, processTime: now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + ' ' + pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()) });
}).then(function() { self.$message.success('已处理'); self.$refs.whPanel.refreshOneDisc(relId); }).catch(function(err) { if (err !== 'cancel') self.$message.error('处理失败'); });
return updateCountDiscrepancy({ discrepancyId: row.discrepancyId, processResult: row._processResult || '', processStatus: 2, processUserId: self.$store.getters.id, processUserName: self.$store.getters.nickName, processTime: now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + ' ' + pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()) });
}).then(function() { self.$message.success('已处理'); self.$refs.detailPanel.$refs.whPanel.refreshOneDisc(relId); }).catch(function(err) { if (err !== 'cancel') self.$message.error('处理失败'); });
},
handleAdd() {
this.reset();
this.form = { planName: new Date().toLocaleDateString().replace(/\//g, '-') + '盘库计划' };
this.open = true;
this.title = "新增盘库计划";
},
@@ -536,7 +453,7 @@ export default {
self.open = false;
self.getList();
if (self.currentRow && self.currentRow.planId === self.form.planId) {
self.loadDetail(self.currentRow.planId);
self.$refs.detailPanel.refresh();
}
}).finally(function() { self.buttonLoading = false; });
} else {
@@ -565,30 +482,44 @@ export default {
self.$modal.msgSuccess("删除成功");
}).catch(function() { }).finally(function() { self.loading = false; });
},
handleSubmitApproval() {
handlePlanApprove() {
var self = this;
if (this.warehouseList.length === 0) {
this.$modal.msgWarning("请先绑定库区");
return;
}
this.$modal.confirm('确认提交盘库计划"' + this.currentRow.planCode + '"进行审批?').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 1 });
this.$modal.confirm('确认通过"' + this.currentRow.planCode + '"的计划审批?通过后进入盘库执行阶段。').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 2 });
}).then(function() {
self.$modal.msgSuccess("提交审批成功");
self.loadDetail(self.currentRow.planId);
self.$modal.msgSuccess("审批通过,已进入盘库执行");
self.$refs.detailPanel.refresh();
self.getList();
}).catch(function() { });
},
handleStartCount() {
handleDiffApprove() {
var self = this;
this.$modal.confirm('确认开始执行盘库计划"' + this.currentRow.planCode + '"?系统将自动保存当前时间节点的库存快照并统计系统总重量和总数量。').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 3 });
this.$modal.confirm('确认通过"' + this.currentRow.planCode + '"的差异审批?通过后进入差异处理阶段。').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 4 });
}).then(function() {
self.$modal.msgSuccess("盘库已开始执行,系统已生成库存快照");
self.loadDetail(self.currentRow.planId);
self.$modal.msgSuccess("差异审批通过,已进入差异处理");
self.$refs.detailPanel.refresh();
self.getList();
}).catch(function() { });
},
handleSubmitDiscApproval() {
var self = this;
this.$modal.confirm('确认提交"' + this.currentRow.planCode + '"的差异审批?').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 3 });
}).then(function() {
self.$modal.msgSuccess("已提交差异审批");
self.$refs.detailPanel.refresh();
self.getList();
}).catch(function() { });
},
handleReject() {
var self = this;
var targetStatus = self.currentRow.planStatus === 3 ? 2 : 0;
var rejectMsg = targetStatus === 2 ? '驳回至盘库执行' : '驳回至草稿';
this.$prompt('请输入驳回原因', '驳回审批', { inputType: 'textarea', inputValidator: function(v) { return v ? true : '驳回原因不能为空'; } }).then(function({ value }) {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: targetStatus, remark: value });
}).then(function() { self.$modal.msgSuccess("已" + rejectMsg); self.currentRow = null; self.getList(); });
},
// 打开导入实盘Excel对话框
handleUploadExcel(wh) {
this.importWarehouse = wh;
@@ -859,10 +790,10 @@ export default {
handleArchive() {
var self = this;
this.$modal.confirm('确认将盘库计划"' + this.currentRow.planCode + '"归档封存?归档后数据将不可修改。').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 4 });
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 5 });
}).then(function() {
self.$modal.msgSuccess("盘库计划已归档");
self.loadDetail(self.currentRow.planId);
self.$refs.detailPanel.refresh();
self.getList();
}).catch(function() { });
},
@@ -875,7 +806,7 @@ export default {
return self.generateWarehouseSnapshot(wh);
}).then(function() {
self.$modal.msgSuccess('库区"' + label + '"快照已生成');
self.loadWarehouseList(self.currentRow.planId);
self.$refs.detailPanel.refresh();
}).catch(function(err) {
if (err !== 'cancel') {
self.$modal.msgError(err.message || '快照生成失败');
@@ -892,7 +823,7 @@ export default {
return self.createSnapshot();
}).then(function() {
self.$modal.msgSuccess('全部库区快照已生成');
self.loadWarehouseList(self.currentRow.planId);
self.$refs.detailPanel.refresh();
}).catch(function(err) {
if (err !== 'cancel') {
self.$modal.msgError(err.message || '快照生成失败');
@@ -1050,7 +981,7 @@ export default {
addCountPlanWarehouse(data).then(function() {
self.$modal.msgSuccess("绑定库区成功");
self.warehouseDialogVisible = false;
self.loadWarehouseList(self.currentRow.planId);
self.$refs.detailPanel.refresh();
}).finally(function() { self.whButtonLoading = false; });
},
// 选择逻辑库区时记录名称
@@ -1079,7 +1010,7 @@ export default {
return delCountPlanWarehouse(row.relId);
}).then(function() {
self.$modal.msgSuccess("移除成功");
self.loadWarehouseList(self.currentRow.planId);
self.$refs.detailPanel.refresh();
}).catch(function() { });
},
handleEditDiscrepancy(row) {
@@ -1464,108 +1395,6 @@ export default {
}
/* ===== 自定义 Tab — 完全原生 DOM ===== */
/* 主Tab仓库切换 */
.doc-tabs {
font-family: 'Microsoft YaHei', '微软雅黑', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.doc-tabs-header {
display: flex;
align-items: stretch;
border-bottom: 1px solid #d4d0c8;
margin-bottom: 14px;
gap: 0;
}
.doc-tab-item {
position: relative;
padding: 8px 18px;
cursor: pointer;
color: #8c8c8c;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.3px;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
display: flex;
align-items: center;
gap: 6px;
user-select: none;
}
.doc-tab-item:hover {
color: #1a3c6e;
}
.doc-tab-item.active {
color: #1a3c6e;
font-weight: 600;
border-bottom-color: #1a3c6e;
}
.doc-tab-label {
white-space: nowrap;
}
.doc-tab-badge {
font-size: 11px;
color: #909399;
font-weight: 400;
}
.doc-tab-item.active .doc-tab-badge {
color: #1a3c6e;
}
.doc-tabs-empty {
padding: 8px 0;
color: #909399;
font-size: 13px;
}
.doc-tabs-content {
min-height: 100px;
}
/* 子Tab概览 / 快照 / 差异明细 */
.doc-sub-tabs {
font-family: 'Microsoft YaHei', '微软雅黑', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.doc-sub-tabs-header {
display: flex;
align-items: stretch;
border-bottom: 1px solid #e0dcd6;
margin-bottom: 0;
gap: 0;
padding-left: 0;
}
.doc-sub-tab-item {
position: relative;
padding: 5px 18px;
cursor: pointer;
color: #8c8c8c;
font-size: 12px;
font-weight: 400;
letter-spacing: 0.3px;
background: #f7f5f0;
border: 1px solid #e0dcd6;
border-bottom: 1px solid #e0dcd6;
margin-bottom: -1px;
margin-right: -1px;
transition: color 0.2s, background 0.2s, border-bottom-color 0.2s;
user-select: none;
}
.doc-sub-tab-item:first-child {
border-radius: 2px 0 0 0;
}
.doc-sub-tab-item:last-child {
border-radius: 0 2px 0 0;
margin-right: 0;
}
.doc-sub-tab-item:hover {
color: #1a3c6e;
}
.doc-sub-tab-item.active {
color: #1a3c6e;
font-weight: 600;
background: #ffffff;
border-bottom-color: #ffffff;
}
.doc-sub-tabs-content {
padding: 14px 0 0;
}
/* 概览/快照中 el-descriptions 与文档风格统一 */
.right-panel .el-descriptions {

View File

@@ -0,0 +1,94 @@
<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-check"></i><span>计划审批</span><el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表" /></div>
</div>
<div class="search-row"><el-input v-model="queryParams.planCode" 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.planCode }}</span><span class="item-sub">{{ item.planName }}</span></div>
<div class="item-meta"><el-tag size="mini">计划待审批</el-tag></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">
<PlanDetailPanel ref="detailPanel"
:planId="currentRow ? currentRow.planId : null"
subtitle="Plan Approval"
emptyText="请在左侧列表中选择一条盘库计划进行审批"
@approve="handleApprove"
@reject="handleReject"
/>
</div>
</template>
</DragResizePanel>
</div>
</template>
<script>
import { listCountPlan, updateCountPlan } from "@/api/flow/countPlan";
import DragResizePanel from "@/components/DragResizePanel/index.vue";
import PlanDetailPanel from "./components/PlanDetailPanel.vue";
import { parseTime } from '@/utils/klp'
export default {
name: "InvCountPlanApproval",
components: { DragResizePanel, PlanDetailPanel },
data() {
return {
loading: false, total: 0,
dataList: [], currentRow: null,
queryParams: { pageNum: 1, pageSize: 10, planCode: undefined, planStatus: 1 }
};
},
created() { this.getList(); },
methods: {
parseTime,
getList() { var self = this; this.loading = true; listCountPlan(this.queryParams).then(function(r) { self.dataList = r.rows; self.total = r.total; }).finally(function() { self.loading = false; }); },
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
handleRowClick(row) { this.currentRow = row; },
handleApprove() {
var self = this;
this.$modal.confirm('确认通过"' + this.currentRow.planCode + '"的计划审批?通过后进入盘库执行阶段。').then(function() {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 2 });
}).then(function() { self.$modal.msgSuccess("审批通过,已进入盘库执行"); self.currentRow = null; self.getList(); });
},
handleReject() {
var self = this;
this.$prompt('请输入驳回原因', '驳回审批', { inputType: 'textarea', inputValidator: function(v) { return v ? true : '驳回原因不能为空'; } }).then(function({ value }) {
return updateCountPlan({ planId: self.currentRow.planId, planStatus: 0, remark: value });
}).then(function() { self.$modal.msgSuccess("已驳回至草稿"); self.currentRow = null; self.getList(); });
}
}
};
</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; }
.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; }
.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; }
.empty-tip { display: flex; align-items: center; justify-content: center; height: 100%; color: #909399; font-size: 14px; gap: 8px; }
</style>

View File

@@ -57,7 +57,10 @@
<div class="detail-card">
<div class="detail-card-header">
<span>产需单信息</span>
<button class="header-btn" @click="handleEdit(currentReq)">编辑</button>
<div style="display:flex; gap:6px;">
<button class="header-btn" :disabled="currentReq.scheduleStatus !== 0 && currentReq.scheduleStatus !== 3" @click="handleEdit(currentReq)">编辑</button>
<button v-if="currentReq.scheduleStatus === 0 || currentReq.scheduleStatus === 3" class="header-btn" style="background:rgba(255,255,255,0.4);border-color:rgba(255,255,255,0.7);" @click="handleDispatch">{{ currentReq.scheduleStatus === 3 ? '重新下发' : '下发' }}</button>
</div>
</div>
<div class="detail-card-body">
<table class="req-info-table">
@@ -134,6 +137,10 @@
<td class="req-td-label">备注</td>
<td class="req-td-value" colspan="3">{{ currentReq.remark }}</td>
</tr>
<tr v-if="currentReq.returnReason">
<td class="req-td-label" style="color:#e74c3c;">驳回原因</td>
<td class="req-td-value" colspan="3" style="color:#e74c3c;background:#fdecea;">{{ currentReq.returnReason }}</td>
</tr>
</tbody>
</table>
</div>
@@ -141,8 +148,8 @@
<div class="detail-card">
<div class="detail-card-header">
<span>已绑定的销售订单{{ boundOrderList.length }} </span>
<button class="header-btn" @click="openBindDialog">+ 添加订单</button>
<span>已绑定的销售订单{{ (currentReq.orderList || []).length }} </span>
<button v-if="canEdit" class="header-btn" @click="openBindDialog">+ 添加订单</button>
</div>
<div class="detail-card-body" v-loading="boundLoading">
<div v-if="(currentReq.orderList || []).length === 0"
@@ -151,7 +158,7 @@
<div v-for="order in currentReq.orderList" :key="order.orderId" class="bound-order-card">
<div class="bound-order-card-header">
<span>{{ order.orderCode || ('订单 #' + order.orderId) }}</span>
<button class="link-btn" @click="handleUnbindByOrderId(order.orderId)">解绑</button>
<button v-if="canEdit" class="link-btn" @click="handleUnbindByOrderId(order.orderId)">解绑</button>
</div>
<div class="bound-order-card-body">
<div style="font-size:12px; color:#7f8c8d; margin-bottom:3px;">
@@ -172,6 +179,7 @@
<div class="detail-card">
<div class="detail-card-header">
<span>排产明细{{ requirementDetailList.length }} 合计 {{ detailTotalWeight }} T</span>
<button v-if="canEdit" class="header-btn" @click="handleDetailAdd">+ 新增</button>
</div>
<div class="detail-card-body" style="padding:0;">
<el-table :data="requirementDetailList" border size="small" v-loading="detailLoading" class="aps-table">
@@ -179,6 +187,14 @@
<el-table-column label="材质" prop="material" width="90" align="center" />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" />
<el-table-column label="备注" prop="remark" min-width="120" />
<el-table-column label="操作" width="130" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button v-if="canEdit" type="text" size="small" style="color:#409EFF;" @click="handleDetailEdit(scope.row)">编辑</el-button>
<el-button v-if="canEdit" type="text" size="small" style="color:#ff4d4f;" @click="handleDetailDelete(scope.row)">删除</el-button>
</span>
</template>
</el-table-column>
</el-table>
<el-empty v-if="requirementDetailList.length === 0 && !detailLoading" description="暂无排产明细" />
</div>
@@ -348,6 +364,29 @@
<el-button @click="bindDialogVisible = false"> </el-button>
</div>
</el-dialog>
<!-- 排产明细新增/编辑对话框 -->
<el-dialog :title="detailDialogTitle" :visible.sync="detailDialogVisible" width="500px" append-to-body
:close-on-click-modal="false">
<el-form ref="detailForm" :model="detailForm" :rules="detailFormRules" label-width="100px" size="small">
<el-form-item label="规格" prop="spec">
<el-input v-model="detailForm.spec" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="材质" prop="material">
<el-input v-model="detailForm.material" placeholder="请输入材质" />
</el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight">
<el-input-number v-model="detailForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="detailForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="detailBtnLoading" type="danger" @click="submitDetailForm"> </el-button>
<el-button @click="detailDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
@@ -360,7 +399,9 @@ import {
delRequirement,
addRel,
delRel,
addRequirementDetail
addRequirementDetail,
updateRequirementDetail,
delRequirementDetail
} from '@/api/aps/requirement'
import { listCrmOrder } from '@/api/aps/order'
import { parseProductContent } from '@/utils/productContent'
@@ -385,7 +426,6 @@ export default {
statusBadgeMap: { 0: 'gray', 1: 'blue', 2: 'green', 3: 'red' },
// 绑定订单
boundOrderList: [],
bindDialogVisible: false,
bindQuery: { keyword: '', pageNum: 1, pageSize: 50 },
candidateOrderList: [],
@@ -402,6 +442,17 @@ export default {
detailLoading: false,
requirementDetailList: [],
// 排产明细新增/编辑对话框
detailDialogVisible: false,
detailDialogTitle: '新增排产明细',
detailBtnLoading: false,
detailForm: this.getEmptyDetailForm(),
detailFormRules: {
spec: [{ required: true, message: '规格不能为空', trigger: 'blur' }],
material: [{ required: true, message: '材质不能为空', trigger: 'blur' }],
scheduleWeight: [{ required: true, message: '排产吨数不能为空', trigger: 'change' }]
},
// 新增/编辑对话框
dialogVisible: false,
dialogTitle: '新增产需单',
@@ -414,6 +465,9 @@ export default {
}
},
computed: {
canEdit() {
return this.currentReq && (this.currentReq.scheduleStatus === 0 || this.currentReq.scheduleStatus === 3)
},
detailTotalWeight() {
return this.requirementDetailList.reduce((sum, item) => {
const n = Number(item.scheduleWeight)
@@ -452,6 +506,19 @@ export default {
}
},
getEmptyDetailForm() {
return {
scheduleDetailId: undefined,
scheduleId: undefined,
orderDetailId: undefined,
spec: '',
material: '',
scheduleWeight: 0,
productType: '',
remark: ''
}
},
handleSearch() {
this.queryParams.pageNum = 1
this.getList()
@@ -651,8 +718,8 @@ export default {
},
handleUnbindByOrderId(orderId) {
const rel = this.boundOrderList.find(r => r.orderId === orderId)
if (!rel) {
const order = (this.currentReq.orderList || []).find(o => o.orderId === orderId)
if (!order) {
this.$message.warning('未找到关联记录')
return
}
@@ -661,12 +728,120 @@ export default {
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delRel(rel.relId).then(() => {
delRel(order.relId || orderId).then(() => {
this.$modal.msgSuccess('解绑成功')
this.handleReqClick(this.currentReq)
})
}).catch(() => { })
},
// ====== 下发 ======
handleDispatch() {
this.$confirm('确认下发该产需单吗?下发后将进入审核流程。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
updateRequirement({
scheduleId: this.currentReq.scheduleId,
scheduleStatus: 1
}).then(() => {
this.$modal.msgSuccess('下发成功')
this.getList()
this.handleReqClick(this.currentReq)
}).catch(() => {
this.$modal.msgError('下发失败')
})
}).catch(() => { })
},
// ====== 排产明细新增/编辑/删除 ======
resetDetailForm() {
this.detailForm = this.getEmptyDetailForm()
},
handleDetailAdd() {
if (!this.currentReq) {
this.$message.warning('请先选择一个产需单')
return
}
this.resetDetailForm()
this.detailForm.scheduleId = this.currentReq.scheduleId
this.detailDialogTitle = '新增排产明细'
this.detailDialogVisible = true
this.$nextTick(() => {
this.$refs.detailForm && this.$refs.detailForm.clearValidate()
})
},
handleDetailEdit(row) {
this.detailForm = { ...this.getEmptyDetailForm(), ...row }
this.detailDialogTitle = '编辑排产明细'
this.detailDialogVisible = true
this.$nextTick(() => {
this.$refs.detailForm && this.$refs.detailForm.clearValidate()
})
},
submitDetailForm() {
this.$refs.detailForm.validate(valid => {
if (!valid) return
this.detailBtnLoading = true
const formData = { ...this.detailForm }
if (this.detailForm.scheduleDetailId) {
// 更新
updateRequirementDetail(formData).then(() => {
this.$modal.msgSuccess('修改成功')
this.detailDialogVisible = false
this.refreshDetailList()
}).catch(() => {
this.$modal.msgError('修改失败')
}).finally(() => {
this.detailBtnLoading = false
})
} else {
// 新增
addRequirementDetail(formData).then(() => {
this.$modal.msgSuccess('新增成功')
this.detailDialogVisible = false
this.refreshDetailList()
}).catch(() => {
this.$modal.msgError('新增失败')
}).finally(() => {
this.detailBtnLoading = false
})
}
})
},
handleDetailDelete(row) {
this.$confirm(`确认删除排产明细「${row.spec} / ${row.material}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delRequirementDetail(row.scheduleDetailId).then(() => {
this.$modal.msgSuccess('删除成功')
this.refreshDetailList()
}).catch(() => {
this.$modal.msgError('删除失败')
})
}).catch(() => { })
},
refreshDetailList() {
if (!this.currentReq) return
this.detailLoading = true
getRequirement(this.currentReq.scheduleId).then(res => {
if (res.data) {
this.requirementDetailList = res.data.detailList || []
}
}).catch(() => {
this.requirementDetailList = []
}).finally(() => {
this.detailLoading = false
})
},
}
}
</script>
@@ -849,6 +1024,11 @@ export default {
border-color: rgba(255, 255, 255, 0.7);
}
.detail-card-header .header-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.detail-card-body {
padding: 0;
}

View File

@@ -13,55 +13,115 @@
@change="handleDateChange"
/>
<el-button size="small" class="aps-btn-red" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-tabs v-model="activeTab" size="small" style="margin:0 0 0 16px;" @tab-click="handleQuery">
<el-tab-pane label="待审核" name="pending" />
<el-tab-pane label="已排产" name="scheduled" />
</el-tabs>
<div class="aps-sch-summary" v-if="summaryText">
<span>{{ summaryText }}</span>
</div>
</div>
<!-- 排产明细卡片 -->
<div class="detail-card aps-sch-card">
<!-- 待审核 Tab -->
<div class="detail-card aps-sch-card" v-show="activeTab === 'pending'">
<div class="detail-card-header">
<span>排产明细</span>
<span v-if="detailList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ scheduleList.length }} 个产需单{{ detailList.length }} 条明细
<span>待审核产需单明细</span>
<span v-if="pendingScheduleList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ pendingScheduleList.length }} 个产需单
</span>
</div>
<div class="detail-card-body" style="padding:0;" v-loading="loading">
<el-table
v-if="pendingScheduleList.length > 0"
:data="pendingScheduleList"
border
size="small"
class="aps-table"
@row-click="handleScheduleClick"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
row-key="scheduleId"
>
<el-table-column type="expand" width="40">
<template slot-scope="{ row }">
<el-table :data="expandDetailMap[row.scheduleId] || []" border size="mini" style="margin:6px 0;" @row-click.stop="handleRowClick">
<el-table-column label="规格" prop="spec" min-width="120" />
<el-table-column label="材质" prop="material" width="90" align="center" />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" />
<el-table-column label="品名" prop="productType" min-width="100" />
<el-table-column label="备注" prop="remark" min-width="100" />
</el-table>
</template>
</el-table-column>
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" />
<el-table-column label="订货单位" prop="customerName" min-width="140" />
<el-table-column label="业务员" prop="businessUser" width="80" align="center" />
<el-table-column label="明细数量" width="80" align="center">
<template slot-scope="{ row }">
{{ (row.detailList || []).length }}
</template>
</el-table-column>
<el-table-column label="排产总吨数" width="100" align="right">
<template slot-scope="{ row }">
{{ scheduleTotalWeight(row) }}
</template>
</el-table-column>
<el-table-column label="交货期(天)" prop="deliveryCycle" width="90" align="center" />
<el-table-column label="操作" width="140" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button type="text" size="small" style="color:#52c41a;" @click.stop="handleAccept(scope.row)">接收</el-button>
<el-button type="text" size="small" style="color:#ff4d4f;" @click.stop="handleReject(scope.row)">驳回</el-button>
</span>
</template>
</el-table-column>
</el-table>
<div v-else-if="!loading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无待审核产需单' : '请选择日期查询' }}
</div>
</div>
</div>
<!-- 已排产 Tab -->
<div class="detail-card aps-sch-card" v-show="activeTab === 'scheduled'">
<div class="detail-card-header">
<span>已排产明细</span>
<span v-if="scheduledItemList.length > 0" style="font-weight:normal;font-size:12px;opacity:0.8;">
{{ scheduledItemList.length }}
</span>
</div>
<div class="detail-card-body" style="padding:0;" v-loading="schLoading">
<el-table
v-if="detailList.length > 0"
:data="detailList"
v-if="scheduledItemList.length > 0"
:data="scheduledItemList"
border
size="small"
class="aps-table"
@row-click="handleRowClick"
>
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" fixed="left">
<template slot-scope="{ row }">
<span class="sch-link-text">{{ row.scheduleNo }}</span>
</template>
</el-table-column>
<el-table-column label="排产单号" prop="scheduleNo" min-width="140" />
<el-table-column label="规格" prop="spec" min-width="120" />
<el-table-column label="材质" prop="material" width="90" align="center" />
<el-table-column label="排产吨数" prop="scheduleWeight" width="100" align="right" />
<el-table-column label="品名" prop="productType" min-width="100" />
<el-table-column label="订货单位" prop="customerName" min-width="140" />
<el-table-column label="业务员" prop="businessUser" width="80" align="center" />
<el-table-column label="交货期(天)" prop="deliveryCycle" width="90" align="center" />
<el-table-column label="备注" prop="remark" min-width="140" />
<el-table-column label="备注" prop="remark" min-width="100" />
<el-table-column label="操作" width="110" align="center" fixed="right">
<template slot-scope="scope">
<span style="white-space:nowrap;">
<el-button type="text" size="small" style="color:#409EFF;" @click="handleEditScheduled(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color:#ff4d4f;" @click="handleDeleteScheduled(scope.row)">删除</el-button>
</span>
</template>
</el-table-column>
</el-table>
<div v-else-if="!schLoading" style="padding:40px;text-align:center;color:#909399;">
{{ hasQueried ? '该日期暂无排产数据' : '请选择日期查询排产数据' }}
{{ hasQueried ? '该日期暂无排产数据' : '请选择日期查询' }}
</div>
</div>
</div>
<!-- 下钻弹窗 -->
<el-dialog
title="来源订单信息"
:visible.sync="drillDialogVisible"
width="600px"
append-to-body
>
<!-- 下钻弹窗来源订单信息 -->
<el-dialog title="来源订单信息" :visible.sync="drillDialogVisible" width="600px" append-to-body>
<div v-if="drillOrder" class="detail-card" style="border:none;box-shadow:none;">
<div class="detail-card-body">
<div class="form-grid-2">
@@ -80,12 +140,66 @@
<el-empty description="未找到订单信息" />
</div>
</el-dialog>
<!-- 驳回理由对话框 -->
<el-dialog title="驳回产需单" :visible.sync="rejectDialogVisible" width="450px" append-to-body :close-on-click-modal="false">
<el-form ref="rejectForm" :model="rejectForm" label-width="90px" size="small">
<el-form-item label="排产单号">
<span style="color:#2c3e50;font-weight:500;">{{ rejectForm.scheduleNo }}</span>
</el-form-item>
<el-form-item label="驳回理由" prop="returnReason">
<el-input v-model="rejectForm.returnReason" type="textarea" :rows="4" placeholder="请填写驳回理由" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="rejectBtnLoading" type="danger" @click="confirmReject"> </el-button>
<el-button @click="rejectDialogVisible = false"> </el-button>
</div>
</el-dialog>
<!-- 已排产明细编辑对话框 -->
<el-dialog :title="editDialogTitle" :visible.sync="editDialogVisible" width="500px" append-to-body
:close-on-click-modal="false">
<el-form ref="editForm" :model="editForm" label-width="100px" size="small">
<el-form-item label="排产单号">
<span style="color:#2c3e50;">{{ editForm.scheduleNo }}</span>
</el-form-item>
<el-form-item label="规格" prop="spec">
<el-input v-model="editForm.spec" />
</el-form-item>
<el-form-item label="材质" prop="material">
<el-input v-model="editForm.material" />
</el-form-item>
<el-form-item label="排产吨数" prop="scheduleWeight">
<el-input-number v-model="editForm.scheduleWeight" :min="0" :precision="3" :controls="false" style="width:100%" />
</el-form-item>
<el-form-item label="品名" prop="productType">
<el-input v-model="editForm.productType" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="editBtnLoading" type="danger" @click="submitEditForm"> </el-button>
<el-button @click="editDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listRequirement } from '@/api/aps/requirement'
import { getCrmOrderInfo } from '@/api/aps/schedule'
import {
listRequirement,
updateRequirement
} from '@/api/aps/requirement'
import {
getCrmOrderInfo,
listScheduleItem,
addScheduleItem,
updateScheduleItem,
delScheduleItem
} from '@/api/aps/schedule'
export default {
name: 'ApsSchedule',
@@ -96,11 +210,36 @@ export default {
const d = String(today.getDate()).padStart(2, '0')
return {
queryDate: `${y}-${m}-${d}`,
activeTab: 'pending',
loading: false,
schLoading: false,
hasQueried: false,
scheduleList: [],
detailList: [],
// 待审核
pendingScheduleList: [],
expandedRowKeys: [],
expandDetailMap: {},
summaryText: '',
// 已排产
scheduledItemList: [],
// 驳回
rejectDialogVisible: false,
rejectBtnLoading: false,
rejectForm: {
scheduleId: null,
scheduleNo: '',
returnReason: ''
},
// 已排产编辑
editDialogVisible: false,
editDialogTitle: '编辑排产明细',
editBtnLoading: false,
editForm: this.getEmptyEditForm(),
// 下钻
drillDialogVisible: false,
drillOrder: null
}
@@ -109,59 +248,233 @@ export default {
this.handleQuery()
},
methods: {
getEmptyEditForm() {
return {
itemId: undefined,
scheduleId: undefined,
scheduleNo: '',
spec: '',
material: '',
scheduleWeight: 0,
productType: '',
customerName: '',
remark: ''
}
},
handleDateChange() {
this.handleQuery()
},
handleQuery() {
if (!this.queryDate) {
this.$message.warning('请选择生产日期')
return
}
this.schLoading = true
this.hasQueried = true
this.detailList = []
this.scheduleList = []
if (this.activeTab === 'pending') {
this.queryPending()
} else {
this.queryScheduled()
}
},
// 后端 list 接口已通过 fillDetailList 填充 detailList
listRequirement({ prodDate: this.queryDate, pageNum: 1, pageSize: 999 }).then(res => {
// ====== 待审核 ======
queryPending() {
this.loading = true
this.pendingScheduleList = []
listRequirement({
prodDate: this.queryDate,
scheduleStatus: 1,
pageNum: 1,
pageSize: 999
}).then(res => {
const list = res.rows || []
this.scheduleList = list
this.pendingScheduleList = list
// 扁平化所有 detailList
const merged = []
// 构建展开映射
const map = {}
list.forEach(sch => {
const details = sch.detailList || []
details.forEach(d => {
merged.push({
...d,
scheduleNo: sch.scheduleNo,
customerName: sch.customerName,
businessUser: sch.businessUser,
deliveryCycle: sch.deliveryCycle,
_scheduleId: sch.scheduleId
})
})
map[sch.scheduleId] = (sch.detailList || []).map(d => ({
...d,
scheduleId: sch.scheduleId,
productType: sch.productType || d.productType || ''
}))
})
this.detailList = merged
this.expandDetailMap = map
const totalWeight = merged.reduce((sum, d) => sum + (parseFloat(d.scheduleWeight) || 0), 0)
this.summaryText = `${list.length} 个产需单,${merged.length} 条明细,排产总吨数 ${totalWeight.toFixed(3)}`
const totalWeight = list.reduce((sum, sch) => {
const details = sch.detailList || []
return sum + details.reduce((s, d) => s + (parseFloat(d.scheduleWeight) || 0), 0)
}, 0)
const totalCount = list.reduce((sum, sch) => sum + (sch.detailList || []).length, 0)
this.summaryText = `${list.length} 个产需单,${totalCount} 条明细,排产总吨数 ${totalWeight.toFixed(3)}`
}).catch(() => {
this.detailList = []
this.pendingScheduleList = []
this.summaryText = ''
}).finally(() => {
this.loading = false
})
},
handleAccept(sch) {
this.$confirm(
`确认接收产需单「${sch.scheduleNo}」的全部排产明细吗?`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(() => {
const details = sch.detailList || []
if (details.length === 0) {
this.$message.warning('该产需单无可排产的明细')
return
}
const promises = details.map(detail =>
addScheduleItem({
scheduleId: sch.scheduleId,
scheduleNo: sch.scheduleNo,
spec: detail.spec || '',
material: detail.material || '',
scheduleWeight: detail.scheduleWeight || 0,
productType: sch.productType || detail.productType || '',
customerName: sch.customerName || '',
remark: detail.remark || ''
})
)
// 更新产需单状态为 2已接收
promises.push(
updateRequirement({
scheduleId: sch.scheduleId,
scheduleStatus: 2
})
)
this.$message.info('正在处理接收...')
Promise.all(promises).then(() => {
this.$modal.msgSuccess('接收成功,排产明细已写入')
this.queryPending()
}).catch(() => {
this.$modal.msgError('接收失败')
})
}).catch(() => {})
},
handleReject(sch) {
this.rejectForm.scheduleId = sch.scheduleId
this.rejectForm.scheduleNo = sch.scheduleNo
this.rejectForm.returnReason = ''
this.rejectDialogVisible = true
this.$nextTick(() => {
this.$refs.rejectForm && this.$refs.rejectForm.clearValidate()
})
},
confirmReject() {
if (!this.rejectForm.returnReason || !this.rejectForm.returnReason.trim()) {
this.$message.warning('请填写驳回理由')
return
}
this.rejectBtnLoading = true
updateRequirement({
scheduleId: this.rejectForm.scheduleId,
scheduleStatus: 3,
returnReason: this.rejectForm.returnReason.trim()
}).then(() => {
this.$modal.msgSuccess('已驳回')
this.rejectDialogVisible = false
this.queryPending()
}).catch(() => {
this.$modal.msgError('驳回失败')
}).finally(() => {
this.rejectBtnLoading = false
})
},
// ====== 已排产 ======
queryScheduled() {
this.schLoading = true
this.scheduledItemList = []
listScheduleItem({ prodDate: this.queryDate, pageNum: 1, pageSize: 999 }).then(res => {
this.scheduledItemList = res.rows || []
const totalWeight = this.scheduledItemList.reduce((sum, d) => sum + (parseFloat(d.scheduleWeight) || 0), 0)
this.summaryText = `${this.scheduledItemList.length} 条明细,排产总吨数 ${totalWeight.toFixed(3)}`
}).catch(() => {
this.scheduledItemList = []
this.summaryText = ''
}).finally(() => {
this.schLoading = false
})
},
handleEditScheduled(row) {
this.editForm = { ...this.getEmptyEditForm(), ...row }
this.editDialogTitle = '编辑排产明细'
this.editDialogVisible = true
this.$nextTick(() => {
this.$refs.editForm && this.$refs.editForm.clearValidate()
})
},
submitEditForm() {
this.editBtnLoading = true
updateScheduleItem(this.editForm).then(() => {
this.$modal.msgSuccess('修改成功')
this.editDialogVisible = false
this.queryScheduled()
}).catch(() => {
this.$modal.msgError('修改失败')
}).finally(() => {
this.editBtnLoading = false
})
},
handleDeleteScheduled(row) {
this.$confirm(`确认删除排产明细「${row.spec} / ${row.material}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delScheduleItem(row.itemId || row.scheduleItemId).then(() => {
this.$modal.msgSuccess('删除成功')
this.queryScheduled()
}).catch(() => {
this.$modal.msgError('删除失败')
})
}).catch(() => {})
},
// ====== 下钻 ======
// ====== 待审核辅助方法 ======
scheduleTotalWeight(sch) {
const details = sch.detailList || []
const total = details.reduce((sum, d) => sum + (parseFloat(d.scheduleWeight) || 0), 0)
return total.toFixed(3)
},
handleScheduleClick(sch) {
// 父行单击展开/折叠
const idx = this.expandedRowKeys.indexOf(sch.scheduleId)
if (idx >= 0) {
this.expandedRowKeys.splice(idx, 1)
} else {
this.expandedRowKeys.push(sch.scheduleId)
}
},
handleExpandChange() {
// noop
},
handleRowClick(row) {
// 通过 _scheduleId 找到产需单,然后找到关联的订单
const sch = this.scheduleList.find(s => s.scheduleId === row._scheduleId)
// 子行(明细行)点击查看来源订单
const sch = this.pendingScheduleList.find(s => s.scheduleId === row.scheduleId)
if (!sch || !sch.orderList || sch.orderList.length === 0) {
this.$message.warning('未找到关联订单')
return
}
// 取第一个关联订单展示
const order = sch.orderList[0]
getCrmOrderInfo(order.orderId).then(res => {
this.drillOrder = res.data
@@ -198,6 +511,7 @@ export default {
border-radius: $aps-radius;
box-shadow: $aps-shadow-sm;
flex-shrink: 0;
flex-wrap: wrap;
}
.aps-sch-label {
@@ -252,15 +566,6 @@ export default {
}
}
.sch-link-text {
color: $aps-red-2;
font-weight: 500;
&:hover {
color: $aps-red-3;
text-decoration: underline;
}
}
// 复用卡片/网格变量
.aps-btn-red {
@include aps-btn-red;
@@ -283,6 +588,7 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.detail-card-body {
@@ -313,4 +619,22 @@ export default {
padding: 4px 0;
border-bottom: 1px solid $aps-silver-mid;
}
// Tabs 覆盖
::v-deep .el-tabs__item {
font-size: 13px;
padding: 0 14px;
}
::v-deep .el-tabs__header {
margin: 0;
}
::v-deep .el-tabs__active-bar {
background-color: $aps-red-2 !important;
}
::v-deep .el-tabs__item.is-active {
color: $aps-red-2 !important;
}
</style>