1581 lines
54 KiB
Vue
1581 lines
54 KiB
Vue
<template>
|
||
<div class="aps-page">
|
||
<div class="aps-calendar-flat">
|
||
<aps-factory-calendar-board :embedded="true" :height="360" :external-query="calendarQuery" />
|
||
</div>
|
||
<!-- 顶部:计划操作区 -->
|
||
<el-card class="aps-header" shadow="never">
|
||
<div class="aps-header-main">
|
||
<div class="aps-header-left">
|
||
<div class="aps-header-sub">
|
||
一键完成产品直排或库存订单排产,支持按产线进行调度
|
||
</div>
|
||
<div class="aps-header-current" v-if="form.orderCode || form.orderId">
|
||
当前库存订单:{{ form.orderCode || form.orderId }}
|
||
</div>
|
||
</div>
|
||
<div class="aps-header-actions">
|
||
<el-button size="small" type="primary" plain icon="el-icon-date" @click="$router.push('/aps/factory-calendar')">工厂总日历</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="aps-header-filter">
|
||
<div class="dispatch-panel">
|
||
<div class="dispatch-left">
|
||
<div class="dispatch-title">快速排产</div>
|
||
<div class="dispatch-source-row">
|
||
<el-radio-group v-model="quick.sourceType" size="small">
|
||
<el-radio-button label="product">产品直排</el-radio-button>
|
||
<el-radio-button label="wmsOrder">库存订单排产</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
<div class="dispatch-form-row" v-if="quick.sourceType === 'product'">
|
||
<el-input :value="quick.productName || quick.productId" placeholder="请选择产品" readonly style="width: 220px" />
|
||
<el-button size="mini" @click="openProductSelector('quick')">选择产品</el-button>
|
||
<el-input-number v-model="quick.productQty" :min="1" :step="1" style="width: 130px" />
|
||
<el-button size="mini" type="primary" plain @click="addQuickProductItem">加入清单</el-button>
|
||
</div>
|
||
|
||
<div class="dispatch-form-row" v-else>
|
||
<el-input :value="quick.orderCode || quick.orderId" placeholder="请在右侧库存订单池点击选择" readonly style="width: 260px" />
|
||
<el-tag v-if="quick.orderId" type="success" size="mini">已选库存订单</el-tag>
|
||
</div>
|
||
<div class="dispatch-form-row">
|
||
<el-select
|
||
v-model="quick.lineId"
|
||
placeholder="选择目标产线(必选)"
|
||
clearable
|
||
filterable
|
||
:loading="lineLoading"
|
||
style="width: 220px"
|
||
@visible-change="onQuickLineVisible"
|
||
@change="onLineSelected"
|
||
>
|
||
<el-option v-for="line in lineOptions" :key="line.lineId" :label="line.lineName || line.lineCode || ('编号' + line.lineId)" :value="line.lineId" />
|
||
</el-select>
|
||
<el-date-picker
|
||
v-model="quick.range"
|
||
type="datetimerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
range-separator="至"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
:default-time="['00:00:00', '23:59:59']"
|
||
style="width: 360px"
|
||
/>
|
||
<el-button type="primary" icon="el-icon-magic-stick" :loading="loading.oneKey" @click="handleQuickSchedule">一键排产并查看</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="dispatch-right">
|
||
<template v-if="quick.sourceType === 'wmsOrder'">
|
||
<div class="pending-order-head">
|
||
<span class="pending-order-title">库存订单池(未完成)</span>
|
||
<el-input v-model="pendingOrderKeyword" size="mini" clearable placeholder="订单号/客户" style="width: 180px" @keyup.enter.native="loadPendingOrders" />
|
||
<el-button size="mini" @click="loadPendingOrders">刷新</el-button>
|
||
</div>
|
||
<div class="pending-order-list" v-loading="pendingOrdersLoading">
|
||
<el-empty v-if="!pendingOrderList.length" :image-size="40" description="暂无未完成库存订单" />
|
||
<el-tag
|
||
v-for="item in pendingOrderList"
|
||
:key="item.orderId"
|
||
size="small"
|
||
:type="String(quick.orderId) === String(item.orderId) ? 'primary' : 'info'"
|
||
effect="plain"
|
||
class="pending-order-tag"
|
||
@click="pickPendingOrder(item)"
|
||
>
|
||
{{ item.orderCode || item.orderId }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="pending-order-head">
|
||
<span class="pending-order-title">已选产品清单</span>
|
||
</div>
|
||
<div class="product-pool-list product-pool-inline">
|
||
<el-empty v-if="!quick.productItems.length" :image-size="40" description="暂无已选产品" />
|
||
<div class="product-card product-card-inline" v-for="(p, idx) in quick.productItems" :key="p.productId + '_picked_' + idx">
|
||
<div class="p-inline-main">
|
||
<span class="p-title">{{ p.productName || p.productId }}</span>
|
||
<span class="p-sub">规格:{{ p.specification || '-' }}</span>
|
||
<span class="p-sub">材质:{{ p.material || '-' }}</span>
|
||
</div>
|
||
<div class="p-inline-op">
|
||
<el-input-number v-model="p.quantity" :min="1" :step="1" size="mini" style="width: 94px" />
|
||
<el-button type="text" size="mini" :disabled="idx===0" @click="moveQuickProductItem(idx,-1)">↑</el-button>
|
||
<el-button type="text" size="mini" :disabled="idx===quick.productItems.length-1" @click="moveQuickProductItem(idx,1)">↓</el-button>
|
||
<el-button type="text" size="mini" @click="removeQuickProductItem(idx)">移除</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<el-form :inline="true" size="small" :model="form">
|
||
<el-form-item label="时间范围">
|
||
<el-date-picker
|
||
v-model="form.range"
|
||
type="datetimerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
range-separator="至"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
:default-time="['00:00:00', '23:59:59']"
|
||
style="width: 380px"
|
||
@change="handleQueryGantt"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button icon="el-icon-refresh" @click="resetFilter">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<template v-if="ganttList && ganttList.length">
|
||
<div class="result-toolbar">
|
||
<el-button size="small" type="primary" icon="el-icon-download" @click="goSheet">下载排产表</el-button>
|
||
</div>
|
||
<div class="result-head">排产甘特图</div>
|
||
<div class="gantt-wrap" v-loading="loading.gantt">
|
||
<div class="gantt-axis">
|
||
<span>{{ ganttChartStartLabel }}</span>
|
||
<span>{{ ganttChartEndLabel }}</span>
|
||
</div>
|
||
<div class="gantt-line" v-for="line in ganttLineRows" :key="line.lineId || line.lineName">
|
||
<div class="gantt-line-name">{{ line.lineName }}</div>
|
||
<div class="gantt-track">
|
||
<div
|
||
class="gantt-bar"
|
||
v-for="task in line.tasks"
|
||
:key="task.operationId || (task.startTime + '_' + task.endTime + '_' + task.productName)"
|
||
:style="ganttBarStyle(task)"
|
||
:title="ganttTaskLabel(task) + ' ' + (task.startTime || '') + ' ~ ' + (task.endTime || '')"
|
||
@click="openReschedule(task)"
|
||
>
|
||
{{ ganttTaskLabel(task) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="aps-empty">
|
||
<el-empty>
|
||
<template slot="description">
|
||
<p>当前条件下暂无排产记录。</p>
|
||
<p>如需为加急订单排产,可先执行自动排产或在生成结果后通过“重排”进行手动调整。</p>
|
||
</template>
|
||
<el-button
|
||
v-if="form.orderId"
|
||
type="primary"
|
||
size="mini"
|
||
:loading="loading.oneKey"
|
||
@click="handleOneKeySchedule"
|
||
>
|
||
为当前订单生成排产
|
||
</el-button>
|
||
</el-empty>
|
||
</div>
|
||
</template>
|
||
</el-card>
|
||
|
||
<!-- 创建计划弹窗 -->
|
||
<el-dialog
|
||
title="创建排产计划"
|
||
:visible.sync="createDialog.visible"
|
||
width="520px"
|
||
>
|
||
<el-form
|
||
ref="createFormRef"
|
||
:model="createDialog.form"
|
||
:rules="createRules"
|
||
label-width="90px"
|
||
size="small"
|
||
>
|
||
<el-form-item label="排产来源">
|
||
<el-radio-group v-model="createDialog.form.sourceType">
|
||
<el-radio label="product">产品直排</el-radio>
|
||
<el-radio label="wmsOrder">库存订单排产</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item v-if="createDialog.form.sourceType === 'product'" label="产品" prop="productId">
|
||
<el-input :value="createDialog.form.productName || createDialog.form.productId" placeholder="请选择产品" readonly style="width: calc(100% - 88px); margin-right: 8px" />
|
||
<el-button @click="openProductSelector('create')">选择</el-button>
|
||
</el-form-item>
|
||
<el-form-item v-if="createDialog.form.sourceType === 'product'" label="排产数量" prop="productQty">
|
||
<el-input-number v-model="createDialog.form.productQty" :min="1" :step="1" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item v-if="createDialog.form.sourceType === 'wmsOrder'" label="库存订单" prop="orderId">
|
||
<el-input :value="createDialog.form.orderCode || createDialog.form.orderId" placeholder="请选择库存订单" readonly style="width: calc(100% - 88px); margin-right: 8px" />
|
||
<el-button @click="openOrderSelector('create')">选择</el-button>
|
||
</el-form-item>
|
||
<el-form-item label="目标产线" prop="lineId">
|
||
<el-select v-model="createDialog.form.lineId" placeholder="请选择产线" clearable filterable style="width: 100%">
|
||
<el-option v-for="line in lineOptions" :key="line.lineId" :label="line.lineName || line.lineCode || ('产线' + line.lineId)" :value="line.lineId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="优先级">
|
||
<el-select v-model="createDialog.form.priority" placeholder="默认:中">
|
||
<el-option :value="0" label="低" />
|
||
<el-option :value="1" label="中" />
|
||
<el-option :value="2" label="高" />
|
||
<el-option :value="3" label="VIP" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="计划时间">
|
||
<el-date-picker
|
||
v-model="createDialog.form.range"
|
||
type="datetimerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
range-separator="至"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
:default-time="['00:00:00', '23:59:59']"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="createDialog.form.remark"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="可选"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button size="small" @click="createDialog.visible = false">取 消</el-button>
|
||
<el-button size="small" type="primary" @click="submitCreatePlan">确 定</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<el-dialog title="选择库存订单" :visible.sync="orderSelector.visible" width="980px" append-to-body>
|
||
<el-form :inline="true" size="small">
|
||
<el-form-item label="订单关键字">
|
||
<el-input v-model="orderSelector.keyword" placeholder="输入库存订单号/客户名称" @keyup.enter.native="fetchOrderOptions" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" size="mini" @click="fetchOrderOptions">搜索</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table v-loading="orderSelector.loading" :data="orderSelector.list" size="mini" border height="360">
|
||
<el-table-column prop="orderCode" label="库存订单号" min-width="150" />
|
||
<el-table-column prop="customerName" label="客户" min-width="140" />
|
||
<el-table-column prop="orderStatus" label="状态" width="90">
|
||
<template slot-scope="scope">
|
||
<el-tag v-if="Number(scope.row.orderStatus) === 0" size="mini" type="info">新建</el-tag>
|
||
<el-tag v-else-if="Number(scope.row.orderStatus) === 1" size="mini" type="warning">生产中</el-tag>
|
||
<el-tag v-else-if="Number(scope.row.orderStatus) === 2" size="mini" type="success">已完成</el-tag>
|
||
<el-tag v-else size="mini">{{ scope.row.orderStatus }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button size="mini" type="text" @click="selectOrder(scope.row)">选择</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<pagination v-show="orderSelector.total > 0" :total="orderSelector.total" :page.sync="orderSelector.pageNum" :limit.sync="orderSelector.pageSize" @pagination="fetchOrderOptions" />
|
||
</el-dialog>
|
||
|
||
<el-dialog title="选择产品" :visible.sync="productSelector.visible" width="980px" append-to-body>
|
||
<el-form :inline="true" size="small">
|
||
<el-form-item label="产品关键字">
|
||
<el-input v-model="productSelector.keyword" placeholder="输入产品名称/编号" @keyup.enter.native="fetchProductOptions" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" size="mini" @click="fetchProductOptions">搜索</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="product-dialog-toolbar">
|
||
<el-button size="mini" type="primary" plain :disabled="!selectedProductRows.length" @click="batchAddSelectedProducts">批量加入清单</el-button>
|
||
<span class="dialog-tip">已选 {{ selectedProductRows.length }} 条</span>
|
||
</div>
|
||
<el-table v-loading="productSelector.loading" :data="productSelector.list" size="mini" border @selection-change="onProductSelectionChange">
|
||
<el-table-column type="selection" width="45" />
|
||
<el-table-column prop="productCode" label="产品编号" min-width="120" />
|
||
<el-table-column prop="productName" label="产品名称" min-width="150" />
|
||
<el-table-column prop="material" label="材质" min-width="100" show-overflow-tooltip>
|
||
<template slot-scope="scope">{{ scope.row.material || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="specification" label="规格" min-width="120" show-overflow-tooltip />
|
||
<el-table-column prop="unit" label="单位" width="70">
|
||
<template slot-scope="scope">{{ scope.row.unit || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="90" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button size="mini" type="text" @click="selectProduct(scope.row)">选择</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<pagination v-show="productSelector.total > 0" :total="productSelector.total" :page.sync="productSelector.pageNum" :limit.sync="productSelector.pageSize" @pagination="fetchProductOptions" />
|
||
</el-dialog>
|
||
|
||
<!-- 重排弹窗 -->
|
||
<el-dialog
|
||
title="单工序重排"
|
||
:visible.sync="rescheduleDialog.visible"
|
||
width="520px"
|
||
>
|
||
<div class="dialog-tip">
|
||
手动排产步骤:1)选择目标产线;2)选择目标时间范围;3)填写调整原因并确认。
|
||
</div>
|
||
<el-form
|
||
ref="rescheduleFormRef"
|
||
:model="rescheduleDialog.form"
|
||
:rules="rescheduleRules"
|
||
label-width="110px"
|
||
size="small"
|
||
>
|
||
<el-form-item label="当前产线">
|
||
<span>{{ rescheduleDialog.meta.lineName }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="目标产线" prop="targetLineId">
|
||
<el-select
|
||
v-model="rescheduleDialog.form.targetLineId"
|
||
placeholder="选择产线"
|
||
filterable
|
||
@change="onLineSelected"
|
||
>
|
||
<el-option
|
||
v-for="line in lineOptions"
|
||
:key="line.lineId"
|
||
:label="line.lineName || ('产线' + line.lineId)"
|
||
:value="line.lineId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="目标时间" prop="range">
|
||
<el-date-picker
|
||
v-model="rescheduleDialog.form.range"
|
||
type="datetimerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
range-separator="至"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
:default-time="['00:00:00', '23:59:59']"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="重排原因" prop="reason">
|
||
<el-input
|
||
v-model="rescheduleDialog.form.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button size="small" @click="rescheduleDialog.visible = false">取 消</el-button>
|
||
<el-button size="small" type="primary" @click="submitReschedule">确 定</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<!-- 锁定/解锁弹窗 -->
|
||
<el-dialog
|
||
title="锁定时间窗 / 工序"
|
||
:visible.sync="lockDialog.visible"
|
||
width="520px"
|
||
>
|
||
<el-form
|
||
ref="lockFormRef"
|
||
:model="lockDialog.form"
|
||
:rules="lockRules"
|
||
label-width="110px"
|
||
size="small"
|
||
>
|
||
<el-form-item label="锁定类型" prop="lockType">
|
||
<el-radio-group v-model="lockDialog.form.lockType">
|
||
<el-radio :label="3">工序</el-radio>
|
||
<el-radio :label="2">产线时间窗</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="计划编号">
|
||
<span>{{ form.planId || '-' }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="产线">
|
||
<span>{{ lockDialog.meta.lineName }}</span>
|
||
</el-form-item>
|
||
<el-form-item
|
||
v-if="lockDialog.form.lockType === 2"
|
||
label="锁定时间"
|
||
prop="range"
|
||
>
|
||
<el-date-picker
|
||
v-model="lockDialog.form.range"
|
||
type="datetimerange"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
range-separator="至"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
:default-time="['00:00:00', '23:59:59']"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="锁定原因" prop="lockReason">
|
||
<el-input
|
||
v-model="lockDialog.form.lockReason"
|
||
type="textarea"
|
||
:rows="3"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button size="small" @click="lockDialog.visible = false">取 消</el-button>
|
||
<el-button size="small" type="primary" @click="submitLock">确 定</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<!-- 解锁确认弹窗 -->
|
||
<el-dialog
|
||
title="解锁提示"
|
||
:visible.sync="unlockDialog.visible"
|
||
width="420px"
|
||
>
|
||
<div>确定要解除该工序/时间窗的锁定吗?</div>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button size="small" @click="unlockDialog.visible = false">取 消</el-button>
|
||
<el-button size="small" type="primary" @click="submitUnlock">解 锁</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { convertFromProducts, createPlan, autoSchedule, publishPlan, fetchGantt, rescheduleOperation, createLock, releaseLock } from '@/api/aps/aps'
|
||
import { listProductionLine } from '@/api/wms/productionLine'
|
||
import { listOrder as listWmsOrder } from '@/api/wms/order'
|
||
import { listProduct } from '@/api/wms/product'
|
||
import { parseTime } from '@/utils/klp'
|
||
import ApsFactoryCalendarBoard from './calendar/index.vue'
|
||
|
||
export default {
|
||
name: 'ApsPlanGantt',
|
||
components: {
|
||
ApsFactoryCalendarBoard
|
||
},
|
||
data() {
|
||
return {
|
||
loading: {
|
||
gantt: false,
|
||
creating: false,
|
||
auto: false,
|
||
publish: false,
|
||
oneKey: false
|
||
},
|
||
form: {
|
||
planId: '',
|
||
orderId: '',
|
||
orderCode: '',
|
||
lineId: null,
|
||
range: []
|
||
},
|
||
lineOptions: [],
|
||
lineLoading: false,
|
||
orderSelectorTarget: 'filter',
|
||
orderSelector: {
|
||
visible: false,
|
||
loading: false,
|
||
keyword: '',
|
||
pageNum: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
list: []
|
||
},
|
||
lineHasMore: false,
|
||
linePageNum: 1,
|
||
lineKeyword: '',
|
||
pendingOrdersLoading: false,
|
||
pendingOrderKeyword: '',
|
||
pendingOrderList: [],
|
||
recentStorageKey: 'aps_plan_recent',
|
||
ganttList: [],
|
||
tableHeight: 520,
|
||
quick: {
|
||
sourceType: 'product',
|
||
orderId: '',
|
||
orderCode: '',
|
||
productId: null,
|
||
productName: '',
|
||
productSpec: '',
|
||
productMaterial: '',
|
||
productQty: 1,
|
||
productItems: [],
|
||
lineId: null,
|
||
range: []
|
||
},
|
||
createDialog: {
|
||
visible: false,
|
||
form: {
|
||
sourceType: 'product',
|
||
orderId: '',
|
||
orderCode: '',
|
||
productId: null,
|
||
productName: '',
|
||
productQty: 1,
|
||
priority: 1,
|
||
range: [],
|
||
remark: ''
|
||
}
|
||
},
|
||
productSelector: {
|
||
target: 'create',
|
||
visible: false,
|
||
loading: false,
|
||
keyword: '',
|
||
pageNum: 1,
|
||
pageSize:20,
|
||
total: 0,
|
||
list: []
|
||
},
|
||
selectedProductRows: [],
|
||
rescheduleDialog: {
|
||
visible: false,
|
||
meta: {
|
||
operationId: null,
|
||
lineName: ''
|
||
},
|
||
form: {
|
||
targetLineId: null,
|
||
range: [],
|
||
reason: ''
|
||
}
|
||
},
|
||
lockDialog: {
|
||
visible: false,
|
||
meta: {
|
||
operationId: null,
|
||
lineId: null,
|
||
lineName: ''
|
||
},
|
||
form: {
|
||
lockType: 3,
|
||
range: [],
|
||
lockReason: ''
|
||
}
|
||
},
|
||
unlockDialog: {
|
||
visible: false,
|
||
lockId: null,
|
||
operationId: null
|
||
},
|
||
adjustMode: false,
|
||
adjustSnapshot: [],
|
||
createRules: {
|
||
orderId: [{ required: true, message: '库存订单不能为空', trigger: 'blur' }],
|
||
productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
|
||
productQty: [{ required: true, message: '数量不能为空', trigger: 'blur' }],
|
||
lineId: [{ required: true, message: '请选择产线', trigger: 'change' }]
|
||
},
|
||
rescheduleRules: {
|
||
targetLineId: [{ required: true, message: '请选择目标产线', trigger: 'change' }],
|
||
range: [{ type: 'array', required: true, message: '请选择目标时间范围', trigger: 'change' }],
|
||
reason: [{ required: true, message: '请填写重排原因', trigger: 'blur' }]
|
||
},
|
||
lockRules: {
|
||
lockType: [{ required: true, message: '请选择锁定类型', trigger: 'change' }],
|
||
lockReason: [{ required: true, message: '请填写锁定原因', trigger: 'blur' }]
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
calendarQuery() {
|
||
const range = (this.form.range && this.form.range.length === 2)
|
||
? [this.form.range[0].slice(0, 10), this.form.range[1].slice(0, 10)]
|
||
: []
|
||
return {
|
||
range,
|
||
lineId: this.form.lineId || undefined,
|
||
orderId: this.form.orderId || undefined,
|
||
keyword: this.form.orderCode || ''
|
||
}
|
||
},
|
||
quickReady() {
|
||
if (!this.quick.lineId) return false
|
||
if (this.quick.sourceType === 'product') {
|
||
return (this.quick.productItems || []).length > 0
|
||
}
|
||
return !!this.quick.orderId
|
||
},
|
||
ganttTimes() {
|
||
const times = (this.ganttList || [])
|
||
.flatMap(i => [i.startTime, i.endTime])
|
||
.filter(Boolean)
|
||
.map(t => new Date(t).getTime())
|
||
.filter(v => !Number.isNaN(v))
|
||
if (!times.length) return { start: null, end: null }
|
||
return { start: Math.min(...times), end: Math.max(...times) }
|
||
},
|
||
ganttChartStartLabel() {
|
||
if (!this.ganttTimes.start) return '-'
|
||
return this.formatDateTime(new Date(this.ganttTimes.start))
|
||
},
|
||
ganttChartEndLabel() {
|
||
if (!this.ganttTimes.end) return '-'
|
||
return this.formatDateTime(new Date(this.ganttTimes.end))
|
||
},
|
||
ganttLineRows() {
|
||
const map = {}
|
||
;(this.ganttList || []).forEach(item => {
|
||
const key = String(item.lineId || item.lineName || '未知产线')
|
||
if (!map[key]) {
|
||
map[key] = {
|
||
lineId: item.lineId,
|
||
lineName: item.lineName || ('产线' + (item.lineId || '-')),
|
||
tasks: []
|
||
}
|
||
}
|
||
map[key].tasks.push(item)
|
||
})
|
||
return Object.values(map).map(i => {
|
||
i.tasks.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime())
|
||
return i
|
||
})
|
||
}
|
||
},
|
||
created() {
|
||
this.tableHeight = document.documentElement.clientHeight - 260
|
||
this.initDefaultRange()
|
||
this.loadLines()
|
||
this.loadPendingOrders()
|
||
this.fetchProductOptions()
|
||
this.applyRouteQuery()
|
||
this.$nextTick(() => {
|
||
if (!this.loading.oneKey) {
|
||
this.handleQueryGantt()
|
||
}
|
||
})
|
||
},
|
||
methods: {
|
||
applyRouteQuery() {
|
||
const q = (this.$route && this.$route.query) || {}
|
||
if (q.orderId) {
|
||
this.form.orderId = q.orderId
|
||
if (q.orderCode) {
|
||
this.form.orderCode = q.orderCode
|
||
}
|
||
this.openCreatePlan()
|
||
this.$nextTick(() => {
|
||
if (this.createDialog && this.createDialog.form) {
|
||
this.createDialog.form.orderId = String(q.orderId)
|
||
this.createDialog.form.orderCode = q.orderCode || ''
|
||
}
|
||
if (q.autoOneKey === '1') {
|
||
this.handleOneKeySchedule()
|
||
}
|
||
})
|
||
}
|
||
},
|
||
openOrderSelector(target = 'filter') {
|
||
this.orderSelectorTarget = target
|
||
this.orderSelector.visible = true
|
||
this.orderSelector.pageNum = 1
|
||
this.fetchOrderOptions()
|
||
},
|
||
openProductSelector(target = 'create') {
|
||
this.productSelector.target = target
|
||
this.productSelector.visible = true
|
||
this.productSelector.pageNum = 1
|
||
this.fetchProductOptions()
|
||
},
|
||
fetchOrderOptions() {
|
||
this.orderSelector.loading = true
|
||
const params = {
|
||
pageNum: this.orderSelector.pageNum,
|
||
pageSize: this.orderSelector.pageSize,
|
||
orderCode: this.orderSelector.keyword || undefined
|
||
}
|
||
listWmsOrder(params).then(res => {
|
||
this.orderSelector.list = (res.rows || []).filter(item => {
|
||
const status = Number(item.orderStatus)
|
||
return status !== 2 && status !== 3
|
||
})
|
||
this.orderSelector.total = res.total || 0
|
||
}).finally(() => {
|
||
this.orderSelector.loading = false
|
||
})
|
||
},
|
||
loadPendingOrders() {
|
||
this.pendingOrdersLoading = true
|
||
listWmsOrder({
|
||
pageNum: 1,
|
||
pageSize: 200,
|
||
orderCode: this.pendingOrderKeyword || undefined
|
||
}).then(res => {
|
||
const rows = res.rows || []
|
||
this.pendingOrderList = rows.filter(item => {
|
||
const status = Number(item.orderStatus)
|
||
return status !== 2 && status !== 3
|
||
})
|
||
}).finally(() => {
|
||
this.pendingOrdersLoading = false
|
||
})
|
||
},
|
||
pickPendingOrder(item) {
|
||
const orderId = item.orderId || item.id
|
||
this.quick.orderId = String(orderId)
|
||
this.quick.orderCode = item.orderCode || ''
|
||
this.quick.sourceType = 'wmsOrder'
|
||
this.$message.success('已选择库存订单:' + (item.orderCode || item.orderId))
|
||
},
|
||
|
||
fetchProductOptions() {
|
||
this.productSelector.loading = true
|
||
listProduct({
|
||
pageNum: this.productSelector.pageNum,
|
||
pageSize: this.productSelector.pageSize,
|
||
productName: this.productSelector.keyword || undefined,
|
||
productCode: this.productSelector.keyword || undefined
|
||
}).then(res => {
|
||
this.productSelector.list = res.rows || []
|
||
this.productSelector.total = res.total || 0
|
||
}).finally(() => {
|
||
this.productSelector.loading = false
|
||
})
|
||
},
|
||
selectOrder(row) {
|
||
const orderId = row.orderId || row.id
|
||
if (!orderId) {
|
||
this.$message.warning('订单数据缺少 orderId')
|
||
return
|
||
}
|
||
const orderCode = row.orderCode || ''
|
||
if (this.orderSelectorTarget === 'create') {
|
||
this.createDialog.form.orderId = String(orderId)
|
||
this.createDialog.form.orderCode = orderCode
|
||
} else {
|
||
this.quick.orderId = String(orderId)
|
||
this.quick.orderCode = orderCode
|
||
}
|
||
this.orderSelector.visible = false
|
||
},
|
||
selectProduct(row) {
|
||
if (this.productSelector.target === 'quick') {
|
||
this.quick.productId = row.productId
|
||
this.quick.productName = row.productName || row.productCode || ''
|
||
this.quick.productSpec = row.specification || ''
|
||
this.quick.productMaterial = row.material || ''
|
||
} else {
|
||
this.createDialog.form.productId = row.productId
|
||
this.createDialog.form.productName = row.productName || row.productCode || ''
|
||
}
|
||
this.productSelector.visible = false
|
||
},
|
||
onProductSelectionChange(rows) {
|
||
this.selectedProductRows = rows || []
|
||
},
|
||
batchAddSelectedProducts() {
|
||
if (this.productSelector.target !== 'quick') {
|
||
this.$message.warning('当前模式不支持批量加入')
|
||
return
|
||
}
|
||
if (!this.selectedProductRows.length) {
|
||
this.$message.warning('请先勾选产品')
|
||
return
|
||
}
|
||
const qty = Number(this.quick.productQty || 1)
|
||
this.selectedProductRows.forEach(row => {
|
||
const existed = (this.quick.productItems || []).find(i => String(i.productId) === String(row.productId))
|
||
if (existed) {
|
||
existed.quantity = Number(existed.quantity || 0) + qty
|
||
} else {
|
||
this.quick.productItems.push({
|
||
productId: row.productId,
|
||
productName: row.productName || row.productCode || String(row.productId),
|
||
specification: row.specification || '',
|
||
material: row.material || '',
|
||
quantity: qty
|
||
})
|
||
}
|
||
})
|
||
this.$message.success('已批量加入 ' + this.selectedProductRows.length + ' 个产品')
|
||
this.productSelector.visible = false
|
||
this.selectedProductRows = []
|
||
},
|
||
addQuickProductItem() {
|
||
if (!this.quick.productId) {
|
||
this.$message.warning('请先选择产品')
|
||
return
|
||
}
|
||
const qty = Number(this.quick.productQty || 0)
|
||
if (qty <= 0) {
|
||
this.$message.warning('数量必须大于 0')
|
||
return
|
||
}
|
||
const existed = (this.quick.productItems || []).find(i => String(i.productId) === String(this.quick.productId))
|
||
if (existed) {
|
||
existed.quantity = Number(existed.quantity || 0) + qty
|
||
} else {
|
||
this.quick.productItems.push({
|
||
productId: this.quick.productId,
|
||
productName: this.quick.productName || String(this.quick.productId),
|
||
specification: this.quick.productSpec || '',
|
||
material: this.quick.productMaterial || '',
|
||
quantity: qty
|
||
})
|
||
}
|
||
this.quick.productQty = 1
|
||
},
|
||
removeQuickProductItem(index) {
|
||
this.quick.productItems.splice(index, 1)
|
||
},
|
||
moveQuickProductItem(index, step) {
|
||
const target = index + step
|
||
if (target < 0 || target >= this.quick.productItems.length) return
|
||
const arr = this.quick.productItems
|
||
const tmp = arr[index]
|
||
this.$set(arr, index, arr[target])
|
||
this.$set(arr, target, tmp)
|
||
},
|
||
goSheet() {
|
||
const q = {
|
||
templateKey: 'cold_rolling',
|
||
planId: this.form.planId || undefined,
|
||
orderId: this.form.orderId || undefined,
|
||
lineId: this.form.lineId || undefined
|
||
}
|
||
if (this.form.range && this.form.range.length === 2) {
|
||
q.queryStart = this.form.range[0]
|
||
q.queryEnd = this.form.range[1]
|
||
}
|
||
this.$router.push({ path: '/aps/sheet', query: q }).catch(() => {})
|
||
},
|
||
initDefaultRange() {
|
||
const now = new Date()
|
||
const y = now.getFullYear()
|
||
const m = now.getMonth()
|
||
const d = now.getDate()
|
||
const start = new Date(y, m, d, 0, 0, 0)
|
||
const end = new Date(y, m, d + 7, 23, 59, 59)
|
||
this.form.range = [parseTime(start), parseTime(end)]
|
||
this.quick.range = [parseTime(start), parseTime(end)]
|
||
},
|
||
formatDateTime(d) {
|
||
if (!d) return '-'
|
||
const y = d.getFullYear()
|
||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||
const day = `${d.getDate()}`.padStart(2, '0')
|
||
const hh = `${d.getHours()}`.padStart(2, '0')
|
||
const mm = `${d.getMinutes()}`.padStart(2, '0')
|
||
return `${y}-${m}-${day} ${hh}:${mm}`
|
||
},
|
||
ganttTaskLabel(task) {
|
||
const p = task.productName || task.productCode || '产品'
|
||
const m = task.material || task.productMaterial || '-'
|
||
const s = task.specification || task.productSpec || '-'
|
||
return `${p}-${m}-${s}`
|
||
},
|
||
ganttBarStyle(task) {
|
||
const start = new Date(task.startTime).getTime()
|
||
const end = new Date(task.endTime).getTime()
|
||
const min = this.ganttTimes.start
|
||
const max = this.ganttTimes.end
|
||
if (!min || !max || Number.isNaN(start) || Number.isNaN(end) || max <= min) {
|
||
return { left: '0%', width: '2%' }
|
||
}
|
||
const left = ((start - min) / (max - min)) * 100
|
||
const width = Math.max(((end - start) / (max - min)) * 100, 1.2)
|
||
return { left: `${left}%`, width: `${width}%` }
|
||
},
|
||
loadLines(keyword) {
|
||
this.lineLoading = true
|
||
listProductionLine({ pageNum: 1, pageSize: 500, lineName: keyword || undefined }).then(res => {
|
||
const rows = (res.rows || res.data || []).map(r => ({
|
||
lineId: r.lineId,
|
||
lineName: r.lineName || r.lineCode
|
||
}))
|
||
this.lineOptions = this.sortByRecent(rows, 'lineId')
|
||
this.applyDefaultSelections()
|
||
}).catch(() => {}).finally(() => {
|
||
this.lineLoading = false
|
||
})
|
||
},
|
||
searchLines(keyword) {
|
||
this.loadLines(keyword)
|
||
},
|
||
loadMoreLines() {},
|
||
onLineSelectVisible() {},
|
||
getRecentSelections() {
|
||
try {
|
||
const raw = localStorage.getItem(this.recentStorageKey)
|
||
if (!raw) {
|
||
return { lineIdList: [], defaultLineId: null }
|
||
}
|
||
const parsed = JSON.parse(raw)
|
||
return {
|
||
lineIdList: parsed.lineIdList || [],
|
||
defaultLineId: parsed.defaultLineId || null
|
||
}
|
||
} catch (e) {
|
||
return { lineIdList: [], defaultLineId: null }
|
||
}
|
||
},
|
||
saveRecentSelections(state) {
|
||
localStorage.setItem(this.recentStorageKey, JSON.stringify(state))
|
||
},
|
||
rememberSelection(lineId) {
|
||
if (!lineId) return
|
||
const state = this.getRecentSelections()
|
||
state.lineIdList = [lineId, ...(state.lineIdList || []).filter(id => id !== lineId)].slice(0, 8)
|
||
state.defaultLineId = lineId
|
||
this.saveRecentSelections(state)
|
||
},
|
||
sortByRecent(options, idKey) {
|
||
const state = this.getRecentSelections()
|
||
const recent = state.lineIdList || []
|
||
const recentSet = new Set(recent)
|
||
const unique = []
|
||
const seen = new Set()
|
||
;(options || []).forEach(item => {
|
||
const key = item && item[idKey]
|
||
if (!seen.has(key)) {
|
||
seen.add(key)
|
||
unique.push(item)
|
||
}
|
||
})
|
||
const recentPart = []
|
||
recent.forEach(id => {
|
||
const matched = unique.find(item => item && item[idKey] === id)
|
||
if (matched) {
|
||
recentPart.push(matched)
|
||
}
|
||
})
|
||
const normalPart = unique.filter(item => !recentSet.has(item && item[idKey]))
|
||
return recentPart.concat(normalPart)
|
||
},
|
||
applyDefaultSelections() {
|
||
const state = this.getRecentSelections()
|
||
if (!this.quick.lineId && state.defaultLineId) {
|
||
this.quick.lineId = state.defaultLineId
|
||
}
|
||
},
|
||
onQuickLineVisible(visible) {
|
||
if (visible && (!this.lineOptions || !this.lineOptions.length)) {
|
||
this.loadLines()
|
||
}
|
||
},
|
||
onLineSelected(value) {
|
||
this.rememberSelection(value)
|
||
this.quick.lineId = value
|
||
this.form.lineId = value
|
||
},
|
||
resetFilter() {
|
||
this.form.planId = ''
|
||
this.form.orderId = ''
|
||
this.form.orderCode = ''
|
||
this.form.lineId = null
|
||
this.initDefaultRange()
|
||
this.applyDefaultSelections()
|
||
this.ganttList = []
|
||
},
|
||
handleQueryGantt() {
|
||
if (!this.form.range || this.form.range.length !== 2) {
|
||
this.$message.warning('请先选择时间范围')
|
||
return
|
||
}
|
||
if (!this.form.orderId && !this.form.planId && !this.form.lineId) {
|
||
// 保守起见,允许仅按时间与产线/订单过滤,此处不强制订单编号
|
||
}
|
||
const params = {
|
||
queryStart: this.form.range[0],
|
||
queryEnd: this.form.range[1],
|
||
lineId: this.form.lineId || undefined,
|
||
planId: this.form.planId || undefined,
|
||
orderId: this.form.orderId || undefined
|
||
}
|
||
this.loading.gantt = true
|
||
fetchGantt(params)
|
||
.then(res => {
|
||
this.ganttList = (res.data || []).map((r, idx) => ({
|
||
...r,
|
||
_editStartTime: r.startTime,
|
||
_editEndTime: r.endTime,
|
||
_editSequenceNo: r.sequenceNo || idx + 1
|
||
}))
|
||
})
|
||
.finally(() => {
|
||
this.loading.gantt = false
|
||
})
|
||
},
|
||
openCreatePlan() {
|
||
this.createDialog.visible = true
|
||
this.$nextTick(() => {
|
||
this.$refs.createFormRef && this.$refs.createFormRef.resetFields()
|
||
this.createDialog.form.sourceType = 'product'
|
||
this.createDialog.form.priority = 1
|
||
this.createDialog.form.productQty = 1
|
||
this.createDialog.form.lineId = this.form.lineId || null
|
||
this.createDialog.form.range = []
|
||
})
|
||
},
|
||
submitCreatePlan() {
|
||
this.$refs.createFormRef.validate(valid => {
|
||
if (!valid) return
|
||
const sourceType = this.createDialog.form.sourceType || 'product'
|
||
this.loading.creating = true
|
||
const convertReq = sourceType === 'product'
|
||
? convertFromProducts({
|
||
items: [{
|
||
productId: this.createDialog.form.productId,
|
||
quantity: Number(this.createDialog.form.productQty || 0),
|
||
productName: this.createDialog.form.productName
|
||
}],
|
||
remark: this.createDialog.form.remark
|
||
})
|
||
: Promise.resolve({ data: { wmsOrderId: String(this.createDialog.form.orderId) } })
|
||
|
||
convertReq
|
||
.then(conv => {
|
||
const wmsOrderId = conv && conv.data && conv.data.wmsOrderId
|
||
if (!wmsOrderId) throw new Error('未获取到库存订单ID')
|
||
this.form.orderId = String(wmsOrderId)
|
||
this.form.orderCode = this.createDialog.form.orderCode || this.form.orderCode
|
||
this.form.lineId = this.createDialog.form.lineId || this.form.lineId
|
||
const payload = {
|
||
orderId: String(wmsOrderId),
|
||
lineId: this.createDialog.form.lineId,
|
||
priority: this.createDialog.form.priority,
|
||
startDate: this.createDialog.form.range && this.createDialog.form.range[0],
|
||
endDate: this.createDialog.form.range && this.createDialog.form.range[1],
|
||
remark: this.createDialog.form.remark
|
||
}
|
||
return createPlan(payload)
|
||
})
|
||
.then(res => {
|
||
this.form.planId = res.data
|
||
this.$message.success('创建计划成功,planId=' + res.data)
|
||
this.createDialog.visible = false
|
||
})
|
||
.finally(() => {
|
||
this.loading.creating = false
|
||
})
|
||
})
|
||
},
|
||
handleQuickSchedule() {
|
||
if (!this.quick.lineId) {
|
||
this.$message.warning('请选择要排产的目标产线')
|
||
return
|
||
}
|
||
this.createDialog.form.sourceType = this.quick.sourceType
|
||
this.createDialog.form.productId = this.quick.productId
|
||
this.createDialog.form.productName = this.quick.productName
|
||
this.createDialog.form.productQty = this.quick.productQty
|
||
this.createDialog.form.orderId = this.quick.orderId
|
||
this.createDialog.form.orderCode = this.quick.orderCode
|
||
this.createDialog.form.lineId = this.quick.lineId
|
||
this.form.lineId = this.quick.lineId
|
||
this.createDialog.form.range = this.quick.range
|
||
this.handleOneKeySchedule()
|
||
},
|
||
handleOneKeySchedule() {
|
||
const sourceType = (this.createDialog.form && this.createDialog.form.sourceType) || 'product'
|
||
const orderId = this.createDialog.form && this.createDialog.form.orderId
|
||
const hasProductItems = (this.quick.productItems || []).some(i => i && i.productId && Number(i.quantity || 0) > 0)
|
||
if (sourceType === 'product' && !hasProductItems) {
|
||
this.$message.warning('请先添加产品到排产清单')
|
||
return
|
||
}
|
||
if (sourceType === 'wmsOrder' && !orderId) {
|
||
this.$message.warning('请先选择库存订单')
|
||
return
|
||
}
|
||
this.loading.oneKey = true
|
||
const ensurePlan = () => {
|
||
if (this.form.planId) return Promise.resolve(this.form.planId)
|
||
const convertReq = sourceType === 'product'
|
||
? (() => {
|
||
const items = (this.quick.productItems || []).map(i => ({
|
||
productId: i.productId,
|
||
quantity: Number(i.quantity || 0),
|
||
productName: i.productName
|
||
})).filter(i => i.productId && i.quantity > 0)
|
||
if (!items.length) {
|
||
throw new Error('请先添加至少一个产品到排产清单')
|
||
}
|
||
return convertFromProducts({ items, remark: this.createDialog.form.remark })
|
||
})()
|
||
: Promise.resolve({ data: { wmsOrderId: String(orderId) } })
|
||
|
||
return convertReq.then(conv => {
|
||
const wmsOrderId = conv && conv.data && conv.data.wmsOrderId
|
||
this.form.orderId = String(wmsOrderId)
|
||
this.form.orderCode = this.createDialog.form.orderCode || this.form.orderCode
|
||
const payload = {
|
||
orderId: String(wmsOrderId),
|
||
lineId: this.createDialog.form.lineId,
|
||
priority: this.createDialog.form.priority || 1,
|
||
startDate: this.createDialog.form.range && this.createDialog.form.range[0],
|
||
endDate: this.createDialog.form.range && this.createDialog.form.range[1],
|
||
remark: this.createDialog.form.remark
|
||
}
|
||
return createPlan(payload)
|
||
}).then(res => {
|
||
this.form.planId = res.data
|
||
return res.data
|
||
})
|
||
}
|
||
|
||
ensurePlan()
|
||
.then(planId => autoSchedule({ planId: String(planId), clearOld: true, remark: 'ONE_KEY_SCHEDULE' }))
|
||
.then(() => {
|
||
this.$message.success('一键排产完成')
|
||
if (this.quick.range && this.quick.range.length === 2) this.form.range = this.quick.range
|
||
if (!this.form.range || this.form.range.length !== 2) this.initDefaultRange()
|
||
this.handleQueryGantt()
|
||
})
|
||
.finally(() => {
|
||
this.loading.oneKey = false
|
||
})
|
||
},
|
||
handleAutoSchedule() {
|
||
if (!this.form.planId) {
|
||
this.$message.warning('当前暂无排产计划,请先通过“创建计划”或“一键排产并查看”生成计划')
|
||
return
|
||
}
|
||
this.$confirm('将对计划 ID=' + this.form.planId + ' 执行自动排程,是否继续?', '提示', {
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.loading.auto = true
|
||
autoSchedule({
|
||
planId: String(this.form.planId),
|
||
clearOld: true,
|
||
remark: 'FE_TRIGGER'
|
||
}).then(() => {
|
||
this.$message.success('自动排程成功')
|
||
this.handleQueryGantt()
|
||
}).finally(() => {
|
||
this.loading.auto = false
|
||
})
|
||
}).catch(() => {})
|
||
},
|
||
handlePublish() {
|
||
if (!this.form.planId) {
|
||
this.$message.warning('当前暂无排产计划,请先完成排产')
|
||
return
|
||
}
|
||
this.$confirm('发布计划后将进入执行状态,是否继续?', '提示', {
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.loading.publish = true
|
||
publishPlan(String(this.form.planId))
|
||
.then(() => {
|
||
this.$message.success('发布成功')
|
||
})
|
||
.finally(() => {
|
||
this.loading.publish = false
|
||
})
|
||
}).catch(() => {})
|
||
},
|
||
enterAdjustMode() {
|
||
if (!this.ganttList.length) return
|
||
this.adjustMode = true
|
||
this.adjustSnapshot = JSON.parse(JSON.stringify(this.ganttList))
|
||
},
|
||
cancelAdjustMode() {
|
||
this.adjustMode = false
|
||
this.ganttList = JSON.parse(JSON.stringify(this.adjustSnapshot || []))
|
||
},
|
||
moveGanttRow(index, step) {
|
||
const target = index + step
|
||
if (target < 0 || target >= this.ganttList.length) return
|
||
const arr = this.ganttList
|
||
const tmp = arr[index]
|
||
this.$set(arr, index, arr[target])
|
||
this.$set(arr, target, tmp)
|
||
arr.forEach((r, i) => { r._editSequenceNo = i + 1 })
|
||
},
|
||
saveAdjustments() {
|
||
if (!this.adjustMode) return
|
||
const changed = (this.ganttList || []).filter((r, idx) => {
|
||
const old = (this.adjustSnapshot || [])[idx]
|
||
if (!old) return true
|
||
return String(r._editStartTime || '') !== String(old._editStartTime || '') ||
|
||
String(r._editEndTime || '') !== String(old._editEndTime || '') ||
|
||
Number(r._editSequenceNo || 0) !== Number(old._editSequenceNo || 0)
|
||
})
|
||
if (!changed.length) {
|
||
this.$message.info('没有检测到调整')
|
||
this.adjustMode = false
|
||
return
|
||
}
|
||
|
||
const jobs = changed.map(r => {
|
||
const payload = {
|
||
operationId: r.operationId,
|
||
targetLineId: r.lineId,
|
||
targetStartTime: r._editStartTime,
|
||
targetEndTime: r._editEndTime,
|
||
reason: 'MANUAL_ADJUST_SAVE'
|
||
}
|
||
return rescheduleOperation(payload)
|
||
})
|
||
|
||
Promise.all(jobs).then(() => {
|
||
this.$message.success('调整已保存')
|
||
this.adjustMode = false
|
||
this.handleQueryGantt()
|
||
})
|
||
},
|
||
openReschedule(row) {
|
||
this.rescheduleDialog.visible = true
|
||
this.rescheduleDialog.meta.operationId = row.operationId
|
||
this.rescheduleDialog.meta.lineName = row.lineName || ('产线' + row.lineId)
|
||
this.$nextTick(() => {
|
||
this.$refs.rescheduleFormRef && this.$refs.rescheduleFormRef.resetFields()
|
||
const state = this.getRecentSelections()
|
||
if (state.defaultLineId) {
|
||
this.rescheduleDialog.form.targetLineId = state.defaultLineId
|
||
}
|
||
})
|
||
},
|
||
submitReschedule() {
|
||
this.$refs.rescheduleFormRef.validate(valid => {
|
||
if (!valid) return
|
||
const [start, end] = this.rescheduleDialog.form.range || []
|
||
const payload = {
|
||
operationId: this.rescheduleDialog.meta.operationId,
|
||
targetLineId: this.rescheduleDialog.form.targetLineId,
|
||
targetStartTime: start,
|
||
targetEndTime: end,
|
||
reason: this.rescheduleDialog.form.reason
|
||
}
|
||
rescheduleOperation(payload).then(() => {
|
||
this.$message.success('重排成功')
|
||
this.rescheduleDialog.visible = false
|
||
this.handleQueryGantt()
|
||
})
|
||
})
|
||
},
|
||
openLock(row) {
|
||
this.lockDialog.visible = true
|
||
this.lockDialog.meta.operationId = row.operationId
|
||
this.lockDialog.meta.lineId = row.lineId
|
||
this.lockDialog.meta.lineName = row.lineName || ('产线' + row.lineId)
|
||
this.$nextTick(() => {
|
||
this.$refs.lockFormRef && this.$refs.lockFormRef.resetFields()
|
||
this.lockDialog.form.lockType = 3
|
||
})
|
||
},
|
||
submitLock() {
|
||
this.$refs.lockFormRef.validate(valid => {
|
||
if (!valid) return
|
||
const [start, end] = this.lockDialog.form.range || []
|
||
const payload = {
|
||
lockType: this.lockDialog.form.lockType,
|
||
planId: this.form.planId ? String(this.form.planId) : null,
|
||
lineId: this.lockDialog.meta.lineId,
|
||
operationId: this.lockDialog.form.lockType === 3 ? this.lockDialog.meta.operationId : null,
|
||
lockStartTime: this.lockDialog.form.lockType === 2 ? start : null,
|
||
lockEndTime: this.lockDialog.form.lockType === 2 ? end : null,
|
||
lockReason: this.lockDialog.form.lockReason
|
||
}
|
||
createLock(payload).then(() => {
|
||
this.$message.success('锁定成功')
|
||
this.lockDialog.visible = false
|
||
this.handleQueryGantt()
|
||
})
|
||
})
|
||
},
|
||
openUnlock(row) {
|
||
this.unlockDialog.lockId = row.lockId || null
|
||
this.unlockDialog.operationId = row.operationId || null
|
||
this.unlockDialog.visible = true
|
||
},
|
||
submitUnlock() {
|
||
if (!this.unlockDialog.lockId) {
|
||
this.$message.warning('当前记录未返回 lockId,暂无法直接解锁,请到锁管理页面处理')
|
||
this.unlockDialog.visible = false
|
||
return
|
||
}
|
||
releaseLock(this.unlockDialog.lockId).then(() => {
|
||
this.$message.success('解锁成功')
|
||
this.unlockDialog.visible = false
|
||
this.handleQueryGantt()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.aps-page {
|
||
padding: 10px;
|
||
background: #f7f9fc;
|
||
}
|
||
|
||
.aps-calendar-flat {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
::v-deep .aps-header.el-card,
|
||
::v-deep .aps-body.el-card {
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.aps-header,
|
||
.aps-body,
|
||
.aps-calendar-flat {
|
||
border: 1px solid #eef2f7;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.aps-header {
|
||
margin-bottom: 10px;
|
||
|
||
.aps-header-main {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.aps-header-sub {
|
||
margin-top: 4px;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
|
||
.aps-header-current {
|
||
margin-top: 6px;
|
||
font-size: 13px;
|
||
color: #409EFF;
|
||
}
|
||
|
||
.aps-header-actions > .el-button + .el-button {
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
|
||
.aps-header-filter {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.dispatch-panel {
|
||
margin-bottom: 10px;
|
||
padding: 12px;
|
||
border: 1px solid #edf1f7;
|
||
border-radius: 10px;
|
||
background: #ffffff;
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
.dispatch-panel-single {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.dispatch-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.dispatch-source-row,
|
||
.dispatch-form-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.dispatch-right {
|
||
border-left: 1px solid #f0f2f6;
|
||
padding-left: 10px;
|
||
}
|
||
|
||
.pending-order-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pending-order-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.pending-order-list {
|
||
min-height: 58px;
|
||
max-height: 126px;
|
||
overflow: auto;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.pending-order-list::-webkit-scrollbar,
|
||
.product-pool-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
.pending-order-list::-webkit-scrollbar-thumb,
|
||
.product-pool-list::-webkit-scrollbar-thumb {
|
||
background: #d9e2ef;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.pending-order-list::-webkit-scrollbar-track,
|
||
.product-pool-list::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.pending-order-tag {
|
||
cursor: pointer;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.product-pool-list {
|
||
max-height: 190px;
|
||
overflow: auto;
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 6px;
|
||
}
|
||
|
||
.product-pool-inline {
|
||
max-height: none;
|
||
overflow: visible;
|
||
}
|
||
|
||
.product-card {
|
||
border: 1px solid #edf1f7;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
background: #fff;
|
||
}
|
||
|
||
.product-card-inline {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.p-inline-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.p-inline-op {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.product-card:hover {
|
||
border-color: #bcd2f7;
|
||
background: #f7fbff;
|
||
}
|
||
|
||
.product-dialog-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.p-title {
|
||
font-size: 13px;
|
||
color: #303133;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.p-sub {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.aps-body {
|
||
.el-table {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.result-toolbar {
|
||
margin: 0 0 8px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.result-head {
|
||
font-size: 12px;
|
||
color: #606266;
|
||
margin: 0 0 8px;
|
||
}
|
||
|
||
.gantt-wrap {
|
||
border: 1px solid #eef2f7;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.gantt-axis {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.gantt-line {
|
||
display: grid;
|
||
grid-template-columns: 120px 1fr;
|
||
gap: 8px;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.gantt-line-name {
|
||
font-size: 12px;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.gantt-track {
|
||
position: relative;
|
||
height: 28px;
|
||
border: 1px solid #f0f2f6;
|
||
border-radius: 4px;
|
||
background: repeating-linear-gradient(to right, #fafbfd 0, #fafbfd 24px, #fff 24px, #fff 48px);
|
||
}
|
||
|
||
.gantt-bar {
|
||
position: absolute;
|
||
top: 3px;
|
||
height: 22px;
|
||
border-radius: 4px;
|
||
background: #409eff;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
line-height: 22px;
|
||
padding: 0 6px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.aps-empty {
|
||
padding: 40px 0;
|
||
}
|
||
|
||
.dialog-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin: 0 0 8px;
|
||
}
|
||
</style>
|
||
|