1661 lines
53 KiB
Vue
1661 lines
53 KiB
Vue
<template>
|
||
<div class="plan-board-page">
|
||
<!-- 顶部工具栏 -->
|
||
<div class="board-toolbar">
|
||
<n-space :size="8" align="center">
|
||
<n-button type="primary" size="small" @click="handleAdd">
|
||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||
新增工序
|
||
</n-button>
|
||
<n-button size="small" type="success" ghost disabled title="待后端接口支持">
|
||
<template #icon><n-icon><CheckmarkCircleOutline /></n-icon></template>
|
||
齐套自动下达
|
||
</n-button>
|
||
<n-button size="small" type="error" ghost disabled title="待后端接口支持">批量删除</n-button>
|
||
<n-button size="small" type="primary" ghost disabled title="待后端接口支持">批量下达</n-button>
|
||
<n-button size="small" quaternary disabled title="待后端接口支持">合并为组工单</n-button>
|
||
<n-dropdown :options="batchOptions" trigger="click" @select="onBatchSelect">
|
||
<n-button size="small" quaternary>
|
||
批量操作
|
||
<n-icon style="margin-left: 4px"><ChevronDownOutline /></n-icon>
|
||
</n-button>
|
||
</n-dropdown>
|
||
</n-space>
|
||
<n-space :size="8" align="center">
|
||
<n-button size="small" quaternary @click="importModalVisible = true">
|
||
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
||
导入
|
||
</n-button>
|
||
<n-button size="small" quaternary @click="handleExport">
|
||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||
导出
|
||
</n-button>
|
||
<n-button size="small" quaternary @click="loadData">
|
||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||
同步
|
||
</n-button>
|
||
</n-space>
|
||
</div>
|
||
|
||
<!-- 筛选 -->
|
||
<div class="board-filter">
|
||
<n-form inline :model="searchForm" label-placement="left" size="small">
|
||
<n-form-item label="工序名称">
|
||
<n-input v-model:value="searchForm.name" placeholder="工序名称" clearable style="width: 140px" />
|
||
</n-form-item>
|
||
<n-form-item label="工序编码">
|
||
<n-input v-model:value="searchForm.code" placeholder="工序编码" clearable style="width: 140px" />
|
||
</n-form-item>
|
||
<n-form-item>
|
||
<n-space>
|
||
<n-button type="primary" size="small" @click="handleSearch">
|
||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||
搜索
|
||
</n-button>
|
||
<n-button size="small" @click="handleReset">重置</n-button>
|
||
</n-space>
|
||
</n-form-item>
|
||
</n-form>
|
||
</div>
|
||
|
||
<div class="board-table-wrap">
|
||
<!-- 表头 -->
|
||
<div class="board-table-head">
|
||
<div class="col-check" />
|
||
<div class="col-product">产品 / 工单编号</div>
|
||
<div class="col-qty">数量</div>
|
||
<div class="col-mode">模式</div>
|
||
<div class="col-kit">齐套率</div>
|
||
<div class="col-flow">工序</div>
|
||
<div class="col-plan">计划开始 / 结束</div>
|
||
<div class="col-life">生命周期</div>
|
||
<div class="col-actions">操作</div>
|
||
</div>
|
||
|
||
<!-- 列表 -->
|
||
<n-spin :show="loading">
|
||
<div v-if="!tableData.length && !loading" class="board-empty">
|
||
<n-empty description="暂无工序计划数据" />
|
||
</div>
|
||
|
||
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
|
||
<div class="order-row" :class="{ expanded: isExpanded(order) }">
|
||
<div class="col-check">
|
||
<n-checkbox
|
||
:checked="selectedOrderIds.includes(orderRowKey(order))"
|
||
@update:checked="(v: boolean) => toggleSelect(orderRowKey(order), v)"
|
||
/>
|
||
</div>
|
||
<div class="col-product">
|
||
<div class="product-name">{{ order.materialName || '-' }}</div>
|
||
<div class="product-code">
|
||
<span>{{ formatWorkOrderCodes(order) }}</span>
|
||
<n-tag v-if="isOverdue(order)" size="tiny" type="error" :bordered="false">逾期</n-tag>
|
||
<n-tag v-if="hasOutsource(order)" size="tiny" type="warning" :bordered="false">委外</n-tag>
|
||
</div>
|
||
</div>
|
||
<div class="col-qty">{{ order.quantity ?? '-' }}</div>
|
||
<div class="col-mode">
|
||
<n-tag v-if="order.routeCode" size="tiny" :bordered="false">工艺型</n-tag>
|
||
<n-tag v-if="order.workshopName" size="tiny" type="info" :bordered="false">{{ order.workshopName }}</n-tag>
|
||
</div>
|
||
<div class="col-kit">-</div>
|
||
<div class="col-flow" :class="{ 'flow-expanded': isExpanded(order) }">
|
||
<ProcessFlowStepper
|
||
:list="order.processList"
|
||
clickable
|
||
@click="toggleExpand(order)"
|
||
/>
|
||
</div>
|
||
<div class="col-plan">
|
||
<div>{{ formatShortTime(order.beginTime) }}</div>
|
||
<div class="sub-text">{{ formatShortTime(order.endTime) }}</div>
|
||
</div>
|
||
<div class="col-life">
|
||
<div class="life-cell">
|
||
<n-icon :color="lifeIcon(order).color" size="18">
|
||
<component :is="lifeIcon(order).icon" />
|
||
</n-icon>
|
||
<span>{{ lifeLabel(order) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-actions">
|
||
<n-button text type="primary" size="tiny" @click="openDetail(order)">详情</n-button>
|
||
<n-button text type="info" size="tiny" @click="openPlmModel(order)">模型</n-button>
|
||
<!-- 订单级编辑:打开工序顺序重排弹窗(不进单工序编辑) -->
|
||
<n-button
|
||
text
|
||
type="primary"
|
||
size="tiny"
|
||
:disabled="!hasPermission('biz:orderProcessPlan:edit')"
|
||
@click="openReorder(order)"
|
||
>编辑</n-button>
|
||
<n-dropdown :options="moreOptions" trigger="click" @select="(k) => onMoreSelect(k, order)">
|
||
<n-button text size="tiny">更多</n-button>
|
||
</n-dropdown>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isExpanded(order)" class="order-expand">
|
||
<div v-if="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
|
||
<div v-else class="card-flow-scroll">
|
||
<ProcessCard
|
||
v-for="(proc, idx) in sortedProcessList(order.processList)"
|
||
:key="proc.planId ?? idx"
|
||
:process="proc"
|
||
:material-code="order.materialCode"
|
||
:order-code="order.orderCode"
|
||
:order-begin-time="order.beginTime"
|
||
:order-end-time="order.endTime"
|
||
:workshop-name="order.workshopName"
|
||
:is-last="idx === (order.processList?.length ?? 0) - 1"
|
||
:can-edit="hasPermission('biz:orderItem:edit')"
|
||
:can-assign="hasPermission('biz:orderItem:assign')"
|
||
:can-outsource="hasPermission('biz:orderProcessPlan:add')"
|
||
@edit="(p) => handleEdit(p, order)"
|
||
@assign="(p) => disphand(p, order)"
|
||
@outsource="(p) => openOutsource(order, p)"
|
||
@view-model="(p) => openPlmModel(order, p)"
|
||
@submit-detail = "(p) => openDetailModel(order,p)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</n-spin>
|
||
</div>
|
||
|
||
<!--模型抽屉 -->
|
||
<PlmModelDrawer ref="plmModelDrawerRef" />
|
||
|
||
<div class="board-pagination">
|
||
<n-pagination
|
||
v-model:page="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:item-count="pagination.itemCount"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
show-size-picker
|
||
show-quick-jumper
|
||
@update:page="handlePageChange"
|
||
@update:page-size="handlePageSizeChange"
|
||
>
|
||
<template #prefix>共 {{ pagination.itemCount }} 条</template>
|
||
</n-pagination>
|
||
</div>
|
||
|
||
<!-- 详情弹窗 -->
|
||
<n-modal v-model:show="detailVisible" preset="card" title="工序计划详情" style="width: 900px">
|
||
<n-spin :show="detailLoading">
|
||
<template v-if="detailData">
|
||
<n-descriptions :column="3" bordered size="small" label-placement="left">
|
||
<n-descriptions-item label="生产令号">{{ detailData.mainCode || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="工单号">{{ formatWorkOrderCodes(detailData) }}</n-descriptions-item>
|
||
<n-descriptions-item label="物料">{{ detailData.materialName || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="数量">{{ detailData.quantity ?? '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="工艺路线">{{ detailData.routeCode || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="车间">{{ detailData.workshopName || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="计划开工">{{ detailData.beginTime || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="计划完工">{{ detailData.endTime || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="订单状态">{{ lifeLabel(detailData) }}</n-descriptions-item>
|
||
</n-descriptions>
|
||
<div class="detail-cards">
|
||
<ProcessCard
|
||
v-for="(proc, idx) in (detailData.processList)"
|
||
:key="proc.planId ?? idx"
|
||
:process="proc"
|
||
:material-code="detailData.materialCode"
|
||
:order-code="detailData.orderCode"
|
||
:order-begin-time="detailData.beginTime"
|
||
:order-end-time="detailData.endTime"
|
||
:workshop-name="detailData.workshopName"
|
||
:is-last="idx === (detailData.processList?.length ?? 0) - 1"
|
||
:can-edit="hasPermission('biz:orderItem:edit')"
|
||
:can-assign="hasPermission('biz:orderItem:assign')"
|
||
:can-outsource="hasPermission('biz:orderProcessPlan:add')"
|
||
:can-submitDetail="hasPermission('biz:submitLog:detail')"
|
||
@edit="(p) => handleEdit(p, detailData!)"
|
||
@assign="(p) => disphand(p, detailData!)"
|
||
@outsource="(p) => openOutsource(detailData!, p)"
|
||
@view-model="(p) => openPlmModel(detailData!, p)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</n-spin>
|
||
</n-modal>
|
||
|
||
<!-- 订单级工序顺序重排:拖拽 / 上移下移,只提交 planId 顺序 -->
|
||
<n-modal
|
||
v-model:show="reorderVisible"
|
||
preset="card"
|
||
title="调整工序顺序"
|
||
style="width: 640px"
|
||
:mask-closable="false"
|
||
@after-leave="resetReorder"
|
||
>
|
||
<n-spin :show="reorderLoading">
|
||
<n-alert type="info" style="margin-bottom: 12px">
|
||
拖拽行或使用上移/下移调整顺序;保存后按 10、20、30… 重写工序号。存在派工工单或已有转出数量的工序不可调整。
|
||
</n-alert>
|
||
<div v-if="reorderOrderMeta" class="reorder-meta">
|
||
<span>令号:{{ reorderOrderMeta.mainCode || '-' }}</span>
|
||
<span>物料:{{ reorderOrderMeta.materialName || '-' }}</span>
|
||
<span>数量:{{ reorderOrderMeta.quantity ?? '-' }}</span>
|
||
</div>
|
||
<div v-if="!reorderList.length && !reorderLoading" class="reorder-empty">暂无工序可调整</div>
|
||
<VueDraggable
|
||
v-else
|
||
v-model="reorderList"
|
||
:animation="150"
|
||
handle=".reorder-drag-handle"
|
||
class="reorder-list"
|
||
>
|
||
<div
|
||
v-for="(proc, idx) in reorderList"
|
||
:key="proc.planId ?? idx"
|
||
class="reorder-item"
|
||
>
|
||
<n-icon class="reorder-drag-handle" size="18" :depth="3">
|
||
<MenuOutline />
|
||
</n-icon>
|
||
<span class="reorder-index">{{ idx + 1 }}</span>
|
||
<div class="reorder-body">
|
||
<div class="reorder-title">
|
||
<em v-if="proc.operNumber != null">{{ proc.operNumber }}</em>
|
||
{{ proc.processName || '-' }}
|
||
</div>
|
||
<div class="reorder-sub">
|
||
{{ proc.processCode || '-' }} · {{ proc.workOrderCode || '-' }}
|
||
</div>
|
||
</div>
|
||
<n-space :size="4">
|
||
<n-button
|
||
size="tiny"
|
||
quaternary
|
||
:disabled="idx === 0"
|
||
@click="moveReorderItem(idx, -1)"
|
||
>上移</n-button>
|
||
<n-button
|
||
size="tiny"
|
||
quaternary
|
||
:disabled="idx === reorderList.length - 1"
|
||
@click="moveReorderItem(idx, 1)"
|
||
>下移</n-button>
|
||
</n-space>
|
||
</div>
|
||
</VueDraggable>
|
||
</n-spin>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button @click="reorderVisible = false">取消</n-button>
|
||
<n-button
|
||
type="primary"
|
||
:loading="reorderSubmitLoading"
|
||
:disabled="!reorderList.length"
|
||
@click="submitReorder"
|
||
>保存顺序</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<!-- 新增/编辑(单工序字段;顺序请用上方重排弹窗) -->
|
||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px">
|
||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="120px">
|
||
<n-grid :cols="2">
|
||
<n-gi>
|
||
<n-form-item label="工序名称" path="name">
|
||
<n-input v-model:value="formData.name" placeholder="请输入工序名称" />
|
||
</n-form-item>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-form-item label="工序编码" path="code">
|
||
<n-input v-model:value="formData.code" placeholder="请输入工序编码" />
|
||
</n-form-item>
|
||
</n-gi>
|
||
<n-gi>
|
||
<!-- 顺序请走订单行「编辑」重排,避免单条改 sort 撞号断链 -->
|
||
<n-form-item label="工序顺序" path="sort">
|
||
<n-input v-model:value="formData.sort" placeholder="请通过订单编辑调整顺序" disabled />
|
||
</n-form-item>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-form-item label="生产订单id" path="orderItemId">
|
||
<n-input v-model:value="formData.orderItemId" placeholder="请输入生产订单id" />
|
||
</n-form-item>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-form-item label="生产开始时间" path="beginTime">
|
||
<n-date-picker v-model:value="formData.beginTime" type="datetime" clearable style="width: 100%" />
|
||
</n-form-item>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-form-item label="生产结束时间" path="endTime">
|
||
<n-date-picker v-model:value="formData.endTime" type="datetime" clearable style="width: 100%" />
|
||
</n-form-item>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-form-item label="生产总数" path="quantity">
|
||
<n-input v-model:value="formData.quantity" placeholder="请输入生产总数" />
|
||
</n-form-item>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-form-item label="转入数量" path="transferNum">
|
||
<n-input v-model:value="formData.transferNum" placeholder="请输入转入数量" />
|
||
</n-form-item>
|
||
</n-gi>
|
||
</n-grid>
|
||
</n-form>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button @click="modalVisible = false">取消</n-button>
|
||
<n-button type="primary" @click="handleSubmit">确定</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<!-- 工序工单拆单(整单:全部工序一起按数量拆) -->
|
||
<SplitProcessDialog
|
||
v-model:show="splitVisible"
|
||
:plan-id="splitPlanId"
|
||
@success="onSplitSuccess"
|
||
/>
|
||
|
||
<!-- 派工 -->
|
||
<n-modal v-model:show="dispatchmodal" title="工序派工" preset="card" style="width: 800px" :mask-closable="false">
|
||
<n-divider />
|
||
<n-card>
|
||
<n-grid :cols="2">
|
||
<n-gi>
|
||
<span class="pgClass">当前工序:</span>
|
||
<span>{{dispatchform.name}}</span>
|
||
</n-gi>
|
||
<n-gi>
|
||
<span class="pgClass">计划数量:</span>
|
||
<span>{{dispatchform.quantity}}</span>
|
||
</n-gi>
|
||
</n-grid>
|
||
<n-grid :cols="2">
|
||
<n-gi>
|
||
<span class="pgClass">已完成:</span>
|
||
<span>{{dispatchform.completedQty ?? 0}}</span>
|
||
</n-gi>
|
||
<n-gi>
|
||
<span class="pgClass">本次待派:</span>
|
||
<span style="color: #2080f0; font-weight: 600">{{dispatchform.remainQty}}</span>
|
||
</n-gi>
|
||
</n-grid>
|
||
<n-grid :cols="2">
|
||
<n-gi>
|
||
<span class="pgClass">开始时间:</span>
|
||
<span>{{dispatchform.beginTime}}</span>
|
||
</n-gi>
|
||
<n-gi>
|
||
<span class="pgClass">结束时间:</span>
|
||
<span>{{dispatchform.endTime}}</span>
|
||
</n-gi>
|
||
</n-grid>
|
||
</n-card>
|
||
<n-form ref="dispatchformRef" :model="dispatchform"
|
||
label-placement="left"
|
||
:rules="dispatchrules"
|
||
style="margin-top: 10px;"
|
||
label-width="80">
|
||
<n-grid :cols="2">
|
||
<n-gi :span="24">
|
||
<Pgitem
|
||
v-for="(n, i) in dispatchform.list"
|
||
:key="i"
|
||
:obj="n"
|
||
:index="i"
|
||
:section-id="dispatchform.sectionId"
|
||
:workshop-id="dispatchform.workshopId"
|
||
listname="list"
|
||
@addhand="addpgnum"
|
||
@delehand="deletenum(i)"
|
||
/>
|
||
</n-gi>
|
||
</n-grid>
|
||
</n-form>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button size="small" @click="dispatchmodal = false">取消</n-button>
|
||
<n-button size="small" type="primary" :loading="dispatchLoading" @click="dispatchSubmit">确定</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<!-- 工序委外 -->
|
||
<n-modal
|
||
v-model:show="outsourceModalVisible"
|
||
preset="card"
|
||
:title="outsourceStep === 1 ? '创建工序委外 - 选择工序' : '创建工序委外 - 填写信息'"
|
||
style="width: 720px"
|
||
:mask-closable="false"
|
||
@after-leave="resetOutsourceForm"
|
||
>
|
||
<n-spin :show="outsourceOptionsLoading">
|
||
<template v-if="outsourceStep === 1 && outsourceOptions">
|
||
<n-descriptions :column="2" bordered size="small" label-placement="left" style="margin-bottom: 16px">
|
||
<n-descriptions-item label="生产令号">{{ outsourceOptions.mainCode || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="订单编码">{{ outsourceOptions.orderCode || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="物料编码">{{ outsourceOptions.materialCode || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="物料名称">{{ outsourceOptions.materialName || '-' }}</n-descriptions-item>
|
||
<n-descriptions-item label="生产数量">{{ outsourceOptions.quantity ?? '-' }}</n-descriptions-item>
|
||
</n-descriptions>
|
||
<n-radio-group v-model:value="outsourceSelectedPlanId" style="width: 100%">
|
||
<n-space vertical :size="8" style="width: 100%">
|
||
<div
|
||
v-for="proc in outsourceOptions.processList"
|
||
:key="proc.planId"
|
||
class="outsource-process-item"
|
||
:class="{ active: outsourceSelectedPlanId === proc.planId }"
|
||
@click="outsourceSelectedPlanId = proc.planId!"
|
||
>
|
||
<n-radio :value="proc.planId" />
|
||
<div class="outsource-process-body">
|
||
<div class="outsource-process-title">
|
||
<span>{{ proc.processName || '-' }}</span>
|
||
<n-tag v-if="proc.current" size="tiny" type="info" :bordered="false">当前</n-tag>
|
||
<n-tag v-if="proc.outsource === 1" size="tiny" type="warning" :bordered="false">已委外</n-tag>
|
||
</div>
|
||
<div class="outsource-process-meta">
|
||
<span>工单号:{{ proc.workOrderCode || '-' }}</span>
|
||
<span>工序号:{{ proc.operNumber ?? '-' }}</span>
|
||
<span>数量:{{ proc.quantity ?? '-' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</n-space>
|
||
</n-radio-group>
|
||
</template>
|
||
|
||
<template v-else-if="outsourceStep === 2">
|
||
<n-card size="small" style="margin-bottom: 16px">
|
||
<n-grid :cols="2" :x-gap="12">
|
||
<n-gi>
|
||
<span class="pgClass">委外工序:</span>
|
||
<span>{{ selectedOutsourceProcess?.processName || '-' }}</span>
|
||
</n-gi>
|
||
<n-gi>
|
||
<span class="pgClass">工单号:</span>
|
||
<span>{{ selectedOutsourceProcess?.workOrderCode || '-' }}</span>
|
||
</n-gi>
|
||
<n-gi>
|
||
<span class="pgClass">物料:</span>
|
||
<span>{{ outsourceOptions?.materialName || '-' }}</span>
|
||
</n-gi>
|
||
<n-gi>
|
||
<span class="pgClass">工序数量:</span>
|
||
<span>{{ selectedOutsourceProcess?.quantity ?? '-' }}</span>
|
||
</n-gi>
|
||
</n-grid>
|
||
</n-card>
|
||
<n-form
|
||
ref="outsourceFormRef"
|
||
:model="outsourceForm"
|
||
:rules="outsourceRules"
|
||
label-placement="left"
|
||
label-width="110"
|
||
>
|
||
<n-form-item label="供应商" path="supplierName">
|
||
<n-input v-model:value="outsourceForm.supplierName" placeholder="请输入供应商名称" />
|
||
</n-form-item>
|
||
<n-form-item label="委外数量" path="quantity">
|
||
<n-input-number
|
||
v-model:value="outsourceForm.quantity"
|
||
:min="1"
|
||
:max="selectedOutsourceProcess?.quantity ?? undefined"
|
||
:precision="0"
|
||
placeholder="请输入委外数量"
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="单价" path="unitPrice">
|
||
<n-input-number
|
||
v-model:value="outsourceForm.unitPrice"
|
||
:min="0"
|
||
:precision="2"
|
||
placeholder="请输入单价(选填)"
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="计划开始时间" path="planStartTime">
|
||
<n-date-picker
|
||
v-model:value="outsourceForm.planStartTime"
|
||
type="datetime"
|
||
clearable
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="计划结束时间" path="planEndTime">
|
||
<n-date-picker
|
||
v-model:value="outsourceForm.planEndTime"
|
||
type="datetime"
|
||
clearable
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="备注" path="remark">
|
||
<n-input
|
||
v-model:value="outsourceForm.remark"
|
||
type="textarea"
|
||
placeholder="请输入备注(选填)"
|
||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||
/>
|
||
</n-form-item>
|
||
</n-form>
|
||
</template>
|
||
</n-spin>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button @click="outsourceModalVisible = false">取消</n-button>
|
||
<n-button v-if="outsourceStep === 2" @click="outsourceStep = 1">上一步</n-button>
|
||
<n-button
|
||
v-if="outsourceStep === 1"
|
||
type="primary"
|
||
:disabled="!outsourceSelectedPlanId"
|
||
@click="goOutsourceStep2"
|
||
>下一步</n-button>
|
||
<n-button
|
||
v-else
|
||
type="primary"
|
||
:loading="outsourceSubmitLoading"
|
||
@click="submitOutsource"
|
||
>确认创建</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<!-- 导入 -->
|
||
<n-modal v-model:show="importModalVisible" preset="card" title="导入工序计划" style="width: 500px">
|
||
<n-space vertical>
|
||
<n-alert type="info">
|
||
<template #header>导入说明</template>
|
||
请先下载模板,按格式填写后上传 .xlsx / .xls 文件。
|
||
</n-alert>
|
||
<n-button type="primary" @click="handleDownloadTemplate">
|
||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||
下载模板
|
||
</n-button>
|
||
<n-upload :max="1" accept=".xlsx,.xls" :custom-request="handleImportUpload">
|
||
<n-upload-dragger>
|
||
<n-text>点击或拖拽文件到此处上传</n-text>
|
||
</n-upload-dragger>
|
||
</n-upload>
|
||
</n-space>
|
||
</n-modal>
|
||
|
||
|
||
<!-- 汇报详情弹窗 -->
|
||
<n-modal v-model:show="submitLogModalVisible" preset="card" title="工序汇报详情" style="width: 2000px">
|
||
<ReportDetailPage
|
||
:planId="processId"
|
||
/>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button size="small" @click="submitLogModalVisible = false">取消</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
|
||
|
||
<n-modal v-model:show="submitModalVisible" preset="card" title="工单汇报详情" style="width: 2000px">
|
||
<OrderReportDetailPage
|
||
:orderItemId="orderItemId"
|
||
/>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button size="small" @click="submitLogModalVisible = false">取消</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, h, computed, type Component } from 'vue'
|
||
import {
|
||
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
|
||
NInput, NInputNumber, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
|
||
NDescriptionsItem, NAlert, NUpload, NText, NCard, NRadioGroup, NRadio, NDivider, useMessage, useDialog,
|
||
type UploadCustomRequestOptions, type FormRules,
|
||
} from 'naive-ui'
|
||
import {
|
||
AddOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
|
||
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
||
EllipseOutline, TimeOutline, PrintOutline, PeopleOutline,
|
||
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
|
||
MenuOutline,
|
||
} from '@vicons/ionicons5'
|
||
import { VueDraggable } from 'vue-draggable-plus'
|
||
import {
|
||
orderProcessPlanApi,
|
||
toOrderProcessPlanPayload,
|
||
processItemToEntity,
|
||
ORDER_STATUS_MAP,
|
||
type OrderProcessPlanVO,
|
||
type ProcessPlanItemVO,
|
||
type OutsourcingOptions,
|
||
type OutsourcingProcessOption,
|
||
} from '@/api/orderProcessPlan'
|
||
import { useUserStore } from '@/stores/user'
|
||
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
|
||
import ProcessCard from './components/ProcessCard.vue'
|
||
import SplitProcessDialog from './components/SplitProcessDialog.vue'
|
||
import PlmModelDrawer from '@/components/PlmModelDrawer.vue'
|
||
import type { PlmModelOpenContext } from '@/api/plmModel'
|
||
import Pgitem from './pgitem.vue'
|
||
import ReportDetailPage from "@/views/biz/orderProcessPlan/components/ReportDetail.vue";
|
||
import OrderReportDetailPage from "@/views/biz/orderProcessPlan/components/OrderReportDetail.vue";
|
||
|
||
|
||
const message = useMessage()
|
||
const dialog = useDialog()
|
||
const userStore = useUserStore()
|
||
const hasPermission = (p: string) => userStore.hasPermission(p)
|
||
|
||
const searchForm = reactive({
|
||
name: '',
|
||
code: '',
|
||
})
|
||
|
||
const tableData = ref<OrderProcessPlanVO[]>([])
|
||
const expandedIds = ref<Set<string>>(new Set())
|
||
const selectedOrderIds = ref<string[]>([])
|
||
const loading = ref(false)
|
||
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
|
||
|
||
const detailVisible = ref(false)
|
||
const detailLoading = ref(false)
|
||
const detailData = ref<OrderProcessPlanVO | null>(null)
|
||
|
||
/** 工序工单拆单 */
|
||
const splitVisible = ref(false)
|
||
const splitPlanId = ref<number | null>(null)
|
||
|
||
/** 订单级工序重排弹窗状态 */
|
||
const reorderVisible = ref(false)
|
||
const reorderLoading = ref(false)
|
||
const reorderSubmitLoading = ref(false)
|
||
const reorderOrderItemId = ref<number | null>(null)
|
||
const reorderOrderMeta = ref<OrderProcessPlanVO | null>(null)
|
||
/** 弹窗内可拖拽的工序列表(按当前展示顺序) */
|
||
const reorderList = ref<ProcessPlanItemVO[]>([])
|
||
|
||
const modalVisible = ref(false)
|
||
const modalTitle = ref('')
|
||
const formRef = ref()
|
||
const defaultFormData = {
|
||
name: '', code: '', sort: '', orderItemId: '',
|
||
beginTime: null as number | null, endTime: null as number | null,
|
||
quantity: '', transferNum: '', transferOutNum: '',
|
||
procurement: '', outsource: '', qualityInspection: '', storageEntry: '',
|
||
}
|
||
const formData = reactive<any>({ ...defaultFormData })
|
||
const formRules = {}
|
||
|
||
const dispatchmodal = ref(false)
|
||
const dispatchLoading = ref(false)
|
||
const dispatchformRef = ref()
|
||
const dispatchform = reactive<any>({
|
||
id: '', name: '', beginTime: '', endTime: '', quantity: '', sectionId: null as number | null, workshopId: null as number | null,
|
||
list: [{ sectionId:'',deviceId:'', quantity: '', ncId:'' }],
|
||
})
|
||
const dispatchrules = {}
|
||
|
||
const importModalVisible = ref(false)
|
||
const plmModelDrawerRef = ref<InstanceType<typeof PlmModelDrawer> | null>(null)
|
||
|
||
const submitModalVisible = ref<Boolean>(false)
|
||
|
||
const outsourceModalVisible = ref(false)
|
||
const outsourceOptionsLoading = ref(false)
|
||
const outsourceSubmitLoading = ref(false)
|
||
const outsourceStep = ref(1)
|
||
const outsourceOptions = ref<OutsourcingOptions | null>(null)
|
||
const outsourceSelectedPlanId = ref<number | null>(null)
|
||
const outsourceFormRef = ref()
|
||
const outsourceForm = reactive({
|
||
supplierName: '',
|
||
quantity: null as number | null,
|
||
unitPrice: null as number | null,
|
||
planStartTime: null as number | null,
|
||
planEndTime: null as number | null,
|
||
remark: '',
|
||
})
|
||
const outsourceRules: FormRules = {
|
||
supplierName: [{ required: true, message: '请输入供应商', trigger: 'blur' }],
|
||
quantity: [{ required: true, type: 'number', message: '请输入委外数量', trigger: ['blur', 'change'] }],
|
||
}
|
||
|
||
const selectedOutsourceProcess = computed<OutsourcingProcessOption | null>(() => {
|
||
if (!outsourceSelectedPlanId.value) return null
|
||
return outsourceOptions.value?.processList?.find(p => p.planId === outsourceSelectedPlanId.value) ?? null
|
||
})
|
||
|
||
function openPlmModel(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
|
||
const ctx: PlmModelOpenContext = {
|
||
orderItemId: order.orderItemId,
|
||
materialCode: order.materialCode,
|
||
materialName: order.materialName,
|
||
drawingNo: order.materialCode,
|
||
title: process
|
||
? `三维模型 · ${process.processName || process.processCode || ''}`
|
||
: `三维模型 · ${order.materialName || order.materialCode || ''}`,
|
||
}
|
||
if (process?.planId) ctx.processPlanId = process.planId
|
||
plmModelDrawerRef.value?.open(ctx)
|
||
}
|
||
|
||
//汇报记录详情
|
||
const submitLogModalVisible = ref<Boolean>(false)
|
||
const processId = ref<number | null>(null)
|
||
|
||
//汇报详情
|
||
function openDetailModel(order:ProcessPlanItemVO,process?:OrderProcessPlanVO) {
|
||
|
||
submitLogModalVisible.value = true
|
||
console.log(process.planId)
|
||
processId.value = process.planId
|
||
|
||
}
|
||
|
||
const batchOptions = [
|
||
{ label: '全部展开', key: 'expandAll' },
|
||
{ label: '全部收起', key: 'collapseAll' },
|
||
]
|
||
|
||
function dropdownIcon(icon: Component) {
|
||
return () => h(NIcon, null, { default: () => h(icon) })
|
||
}
|
||
|
||
function dangerDropdownItem(label: string, key: string, icon: Component) {
|
||
return {
|
||
label,
|
||
key,
|
||
icon: () => h(NIcon, { color: '#d03050' }, { default: () => h(icon) }),
|
||
props: { style: { color: '#d03050' } },
|
||
}
|
||
}
|
||
|
||
const moreOptions = [
|
||
{
|
||
type: 'group',
|
||
label: '查看与编辑',
|
||
key: 'group-view-edit',
|
||
children: [
|
||
{ label: '打印', key: 'print', icon: dropdownIcon(PrintOutline) },
|
||
],
|
||
},
|
||
{
|
||
type: 'group',
|
||
label: '派生操作',
|
||
key: 'group-derived',
|
||
children: [
|
||
{ label: '创建工序委外', key: 'createOutsource', icon: dropdownIcon(PeopleOutline) },
|
||
{ label: '拆分工单', key: 'splitWorkOrder', icon: dropdownIcon(GitBranchOutline) },
|
||
{ label: '汇报详情', key: 'reportDetailView', icon: dropdownIcon(EyeSharp) },
|
||
],
|
||
},
|
||
{
|
||
type: 'group',
|
||
label: '状态控制',
|
||
key: 'group-status',
|
||
children: [
|
||
dangerDropdownItem('撤回', 'withdraw', CloseCircleOutline),
|
||
dangerDropdownItem('冻结工单', 'freeze', LockClosedOutline),
|
||
],
|
||
},
|
||
{
|
||
type: 'group',
|
||
label: '危险操作',
|
||
key: 'group-danger',
|
||
children: [
|
||
dangerDropdownItem('删除', 'delete', TrashOutline),
|
||
],
|
||
},
|
||
]
|
||
|
||
/** 列表行主键:按工单号(拆单后同订单多工单分行) */
|
||
function orderRowKey(row: OrderProcessPlanVO) {
|
||
if (row.workOrderCode) return row.workOrderCode
|
||
return `oi-${row.orderItemId ?? 'x'}`
|
||
}
|
||
|
||
/** 订单级工单号展示 */
|
||
function formatWorkOrderCodes(order?: OrderProcessPlanVO | null) {
|
||
if (order?.workOrderCode) return order.workOrderCode
|
||
const codes = (order?.processList ?? [])
|
||
.map((p) => p.workOrderCode)
|
||
.filter((c): c is string => !!c)
|
||
const unique = [...new Set(codes)]
|
||
return unique.length ? unique.join('、') : '-'
|
||
}
|
||
|
||
function sortedProcessList(list?: ProcessPlanItemVO[]) {
|
||
return [...(list ?? [])].sort((a, b) => {
|
||
const oa = a.operNumber ?? 0
|
||
const ob = b.operNumber ?? 0
|
||
if (oa !== ob) return oa - ob
|
||
return (a.planId ?? 0) - (b.planId ?? 0)
|
||
})
|
||
}
|
||
|
||
function isExpanded(order: OrderProcessPlanVO) {
|
||
return expandedIds.value.has(orderRowKey(order))
|
||
}
|
||
|
||
function toggleExpand(order: OrderProcessPlanVO) {
|
||
const key = orderRowKey(order)
|
||
const next = new Set(expandedIds.value)
|
||
if (next.has(key)) next.delete(key)
|
||
else next.add(key)
|
||
expandedIds.value = next
|
||
}
|
||
|
||
/** 整单拆分入口:有计划数量且未全部完工即可点开 */
|
||
function canSplitWorkOrder(order: OrderProcessPlanVO) {
|
||
const qty = order.quantity ?? 0
|
||
if (qty <= 0) return false
|
||
if (order.orderStatus === 2) return false
|
||
return (order.processList?.length ?? 0) > 0
|
||
}
|
||
|
||
function openSplitFromOrder(order: OrderProcessPlanVO) {
|
||
if (!hasPermission('biz:orderProcessPlan:edit')) {
|
||
message.warning('无拆单权限')
|
||
return
|
||
}
|
||
const list = sortedProcessList(order.processList)
|
||
const seed = list.find((p) => p.planId != null)
|
||
if (!seed?.planId) {
|
||
message.warning('当前工单暂无工序,无法拆单')
|
||
return
|
||
}
|
||
if (!canSplitWorkOrder(order)) {
|
||
message.warning('该工单不可拆(无数量或已完工)')
|
||
return
|
||
}
|
||
splitPlanId.value = seed.planId
|
||
splitVisible.value = true
|
||
}
|
||
|
||
function onSplitSuccess() {
|
||
loadData()
|
||
if (detailVisible.value && detailData.value?.orderItemId) {
|
||
openDetail(detailData.value)
|
||
}
|
||
}
|
||
|
||
function toggleSelect(id: string, checked: boolean) {
|
||
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
|
||
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
|
||
}
|
||
|
||
function formatShortTime(val?: string | null) {
|
||
if (!val) return '-'
|
||
return val.length > 16 ? val.slice(0, 16) : val
|
||
}
|
||
|
||
function isOverdue(order: OrderProcessPlanVO) {
|
||
if (order.orderStatus === 2 || !order.endTime) return false
|
||
return new Date(order.endTime.replace(' ', 'T')).getTime() < Date.now()
|
||
}
|
||
|
||
function hasOutsource(order: OrderProcessPlanVO) {
|
||
return order.processList?.some(p => p.outsource === 1)
|
||
}
|
||
|
||
function lifeLabel(order: OrderProcessPlanVO) {
|
||
const s = order.orderStatus
|
||
if (s == null) return '-'
|
||
return ORDER_STATUS_MAP[s]?.label ?? String(s)
|
||
}
|
||
|
||
function lifeIcon(order: OrderProcessPlanVO) {
|
||
const s = order.orderStatus
|
||
if (s === 2) return { icon: CheckmarkCircle, color: '#18a058' }
|
||
if (s === 1) return { icon: TimeOutline, color: '#2080f0' }
|
||
return { icon: EllipseOutline, color: '#8c8c8c' }
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await orderProcessPlanApi.page({
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize,
|
||
name: searchForm.name || undefined,
|
||
code: searchForm.code || undefined,
|
||
})
|
||
tableData.value = res.list ?? []
|
||
pagination.itemCount = res.total
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
function handleReset() {
|
||
searchForm.name = ''
|
||
searchForm.code = ''
|
||
handleSearch()
|
||
}
|
||
|
||
function handlePageChange(page: number) {
|
||
pagination.page = page
|
||
loadData()
|
||
}
|
||
|
||
function handlePageSizeChange(pageSize: number) {
|
||
pagination.pageSize = pageSize
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
function onBatchSelect(key: string) {
|
||
if (key === 'expandAll') {
|
||
expandedIds.value = new Set(tableData.value.map(o => orderRowKey(o)))
|
||
} else if (key === 'collapseAll') {
|
||
expandedIds.value = new Set()
|
||
}
|
||
}
|
||
|
||
function onMoreSelect(key: string, order: OrderProcessPlanVO) {
|
||
if (key === 'createOutsource') {
|
||
openOutsource(order)
|
||
return
|
||
}
|
||
if (key === 'reportDetailView') {
|
||
openReportDetail(order)
|
||
return;
|
||
}
|
||
if (key === 'splitWorkOrder') {
|
||
openSplitFromOrder(order)
|
||
return
|
||
}
|
||
|
||
const labels: Record<string, string> = {
|
||
print: '打印',
|
||
withdraw: '撤回',
|
||
freeze: '冻结工单',
|
||
delete: '删除',
|
||
}
|
||
if (labels[key]) message.info(`${labels[key]}功能待后端接口支持`)
|
||
}
|
||
|
||
function resolveDefaultPlanId(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
|
||
if (process?.planId) return process.planId
|
||
const list = sortedProcessList(order.processList)
|
||
if (!list.length) return null
|
||
const current = list.find(p => p.operNumber === order.currentOperNumber)
|
||
return current?.planId ?? list[0].planId ?? null
|
||
}
|
||
|
||
function resetOutsourceForm() {
|
||
outsourceStep.value = 1
|
||
outsourceOptions.value = null
|
||
outsourceSelectedPlanId.value = null
|
||
outsourceForm.supplierName = ''
|
||
outsourceForm.quantity = null
|
||
outsourceForm.unitPrice = null
|
||
outsourceForm.planStartTime = null
|
||
outsourceForm.planEndTime = null
|
||
outsourceForm.remark = ''
|
||
outsourceFormRef.value?.restoreValidation()
|
||
}
|
||
|
||
function fillOutsourceFormFromProcess(proc?: OutsourcingProcessOption | null) {
|
||
if (!proc) return
|
||
outsourceForm.quantity = proc.quantity ?? null
|
||
outsourceForm.planStartTime = parseDateTime(proc.planStartTime)
|
||
outsourceForm.planEndTime = parseDateTime(proc.planFinishTime)
|
||
}
|
||
|
||
async function openOutsource(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
|
||
const planId = resolveDefaultPlanId(order, process)
|
||
if (!planId) {
|
||
message.warning('当前订单暂无可委外工序')
|
||
return
|
||
}
|
||
resetOutsourceForm()
|
||
outsourceModalVisible.value = true
|
||
outsourceOptionsLoading.value = true
|
||
try {
|
||
const options = await orderProcessPlanApi.getOutsourcingOptions(planId)
|
||
outsourceOptions.value = options
|
||
const preferredId = process?.planId ?? options.currentProcessPlanId ?? options.processList?.[0]?.planId ?? null
|
||
outsourceSelectedPlanId.value = preferredId
|
||
if (process?.planId) {
|
||
outsourceStep.value = 2
|
||
fillOutsourceFormFromProcess(selectedOutsourceProcess.value)
|
||
}
|
||
} catch {
|
||
outsourceModalVisible.value = false
|
||
} finally {
|
||
outsourceOptionsLoading.value = false
|
||
}
|
||
}
|
||
|
||
function goOutsourceStep2() {
|
||
if (!outsourceSelectedPlanId.value) {
|
||
message.warning('请选择委外工序')
|
||
return
|
||
}
|
||
fillOutsourceFormFromProcess(selectedOutsourceProcess.value)
|
||
outsourceStep.value = 2
|
||
}
|
||
|
||
function formatSubmitDateTime(ts: number | null) {
|
||
if (!ts) return undefined
|
||
return new Date(ts).toISOString().slice(0, 19).replace('T', ' ')
|
||
}
|
||
|
||
async function submitOutsource() {
|
||
await outsourceFormRef.value?.validate()
|
||
if (!outsourceSelectedPlanId.value) {
|
||
message.warning('请选择委外工序')
|
||
return
|
||
}
|
||
const maxQty = selectedOutsourceProcess.value?.quantity
|
||
if (maxQty != null && (outsourceForm.quantity ?? 0) > maxQty) {
|
||
message.error(`委外数量不能大于工序数量 ${maxQty}`)
|
||
return
|
||
}
|
||
if (outsourceForm.planStartTime && outsourceForm.planEndTime
|
||
&& outsourceForm.planEndTime < outsourceForm.planStartTime) {
|
||
message.error('计划结束时间不能早于计划开始时间')
|
||
return
|
||
}
|
||
outsourceSubmitLoading.value = true
|
||
try {
|
||
await orderProcessPlanApi.createOutsourcing({
|
||
processPlanId: outsourceSelectedPlanId.value,
|
||
supplierName: outsourceForm.supplierName.trim(),
|
||
quantity: outsourceForm.quantity!,
|
||
unitPrice: outsourceForm.unitPrice ?? undefined,
|
||
planStartTime: formatSubmitDateTime(outsourceForm.planStartTime),
|
||
planEndTime: formatSubmitDateTime(outsourceForm.planEndTime),
|
||
remark: outsourceForm.remark || undefined,
|
||
})
|
||
message.success('委外单创建成功')
|
||
outsourceModalVisible.value = false
|
||
loadData()
|
||
} finally {
|
||
outsourceSubmitLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openDetail(order: OrderProcessPlanVO) {
|
||
if (!order.orderItemId) return
|
||
detailVisible.value = true
|
||
detailLoading.value = true
|
||
detailData.value = null
|
||
try {
|
||
detailData.value = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
|
||
} catch {
|
||
detailData.value = order
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 打开订单级工序顺序调整(拉详情,按 operNumber 排序后供拖拽) */
|
||
async function openReorder(order: OrderProcessPlanVO) {
|
||
if (!order.orderItemId) {
|
||
message.warning('缺少生产订单id')
|
||
return
|
||
}
|
||
reorderVisible.value = true
|
||
reorderLoading.value = true
|
||
reorderOrderItemId.value = order.orderItemId
|
||
reorderOrderMeta.value = order
|
||
reorderList.value = []
|
||
try {
|
||
const detail = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
|
||
reorderOrderMeta.value = detail
|
||
// 仅当前工单工序,按 operNumber 升序
|
||
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
|
||
if (!reorderList.value.length) {
|
||
message.warning('当前工单暂无工序计划')
|
||
}
|
||
} catch {
|
||
// 详情失败时降级用列表行数据,仍可本地排序后提交
|
||
reorderList.value = sortedProcessList(order.processList).map((p) => ({ ...p }))
|
||
} finally {
|
||
reorderLoading.value = false
|
||
}
|
||
}
|
||
|
||
function resetReorder() {
|
||
reorderOrderItemId.value = null
|
||
reorderOrderMeta.value = null
|
||
reorderList.value = []
|
||
reorderSubmitLoading.value = false
|
||
}
|
||
|
||
/** 上移 / 下移(delta: -1 上移,+1 下移) */
|
||
function moveReorderItem(index: number, delta: number) {
|
||
const next = index + delta
|
||
if (next < 0 || next >= reorderList.value.length) return
|
||
const list = [...reorderList.value]
|
||
const [item] = list.splice(index, 1)
|
||
list.splice(next, 0, item)
|
||
reorderList.value = list
|
||
}
|
||
|
||
/** 提交重排:只传 orderItemId + planId 顺序 */
|
||
async function submitReorder() {
|
||
if (!reorderOrderItemId.value) {
|
||
message.warning('缺少生产订单id')
|
||
return
|
||
}
|
||
if (!reorderList.value.length) {
|
||
message.warning('暂无工序可保存')
|
||
return
|
||
}
|
||
const missing = reorderList.value.some((p) => p.planId == null)
|
||
if (missing) {
|
||
message.error('存在缺少 planId 的工序,请刷新后重试')
|
||
return
|
||
}
|
||
reorderSubmitLoading.value = true
|
||
try {
|
||
await orderProcessPlanApi.reorder({
|
||
orderItemId: reorderOrderItemId.value,
|
||
items: reorderList.value.map((p) => ({ planId: p.planId! })),
|
||
})
|
||
message.success('工序顺序已更新')
|
||
reorderVisible.value = false
|
||
loadData()
|
||
} finally {
|
||
reorderSubmitLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleAdd() {
|
||
modalTitle.value = '新增工序计划'
|
||
Object.assign(formData, defaultFormData)
|
||
modalVisible.value = true
|
||
}
|
||
|
||
function parseDateTime(val: string | null | undefined) {
|
||
if (!val) return null
|
||
const ts = new Date(val.replace(' ', 'T')).getTime()
|
||
return Number.isNaN(ts) ? null : ts
|
||
}
|
||
|
||
function handleEdit(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
||
modalTitle.value = '编辑工序计划'
|
||
const entity = processItemToEntity(process, order?.orderItemId)
|
||
Object.assign(formData, {
|
||
...defaultFormData,
|
||
...entity,
|
||
sort: entity.sort != null ? String(entity.sort) : '',
|
||
orderItemId: entity.orderItemId != null ? String(entity.orderItemId) : '',
|
||
quantity: entity.quantity != null ? String(entity.quantity) : '',
|
||
transferNum: entity.transferNum != null ? String(entity.transferNum) : '',
|
||
beginTime: parseDateTime(entity.beginTime),
|
||
endTime: parseDateTime(entity.endTime),
|
||
})
|
||
modalVisible.value = true
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
await formRef.value?.validate()
|
||
const submitData = toOrderProcessPlanPayload({ ...formData })
|
||
if (typeof submitData.beginTime === 'number') {
|
||
submitData.beginTime = new Date(submitData.beginTime).toISOString().slice(0, 19).replace('T', ' ')
|
||
}
|
||
if (typeof submitData.endTime === 'number') {
|
||
submitData.endTime = new Date(submitData.endTime).toISOString().slice(0, 19).replace('T', ' ')
|
||
}
|
||
if (submitData.id) {
|
||
await orderProcessPlanApi.update(submitData)
|
||
message.success('修改成功')
|
||
} else {
|
||
await orderProcessPlanApi.create(submitData)
|
||
message.success('新增成功')
|
||
}
|
||
modalVisible.value = false
|
||
loadData()
|
||
}
|
||
|
||
function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
||
const entity = processItemToEntity(process, order?.orderItemId)
|
||
|
||
const planQty = Number(entity.quantity) || 0
|
||
const completedQty = Number(process.completedQty) || 0
|
||
const remainQty = Math.max(0, planQty - completedQty)
|
||
dispatchmodal.value = true
|
||
dispatchformRef.value?.restoreValidation()
|
||
dispatchform.id = entity.id
|
||
dispatchform.name = entity.name
|
||
// 工序计划时间优先,没有则用订单头 beginTime/endTime
|
||
dispatchform.beginTime = entity.beginTime || order?.beginTime || ''
|
||
dispatchform.endTime = entity.endTime || order?.endTime || ''
|
||
// quantity 保持工序计划总数,提交时不能改掉计划数量
|
||
dispatchform.quantity = entity.quantity
|
||
dispatchform.completedQty = completedQty
|
||
dispatchform.remainQty = remainQty
|
||
dispatchform.orderItemId = entity.orderItemId
|
||
dispatchform.sort = entity.sort
|
||
dispatchform.sectionId = entity.sectionId ?? process.sectionId ?? null
|
||
dispatchform.workshopId = entity.workshopId ?? process.workshopId ?? order?.workshopId ?? null
|
||
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty || '',ncId:'' }]
|
||
}
|
||
|
||
function addpgnum() {
|
||
dispatchform.list.push({ sectionId: '', deviceId: '', quantity: '',ncId:'' })
|
||
}
|
||
|
||
function deletenum(index: number) {
|
||
dispatchform.list.splice(index, 1)
|
||
}
|
||
|
||
function dispatchSubmit() {
|
||
let total = 0
|
||
dispatchform.list.forEach((n: any) => {
|
||
total += Number(n.quantity) || 0
|
||
})
|
||
const remainQty = Number(dispatchform.remainQty)
|
||
if (remainQty <= 0) {
|
||
message.warning('当前工序已全部完成,无需再派工')
|
||
return
|
||
}
|
||
|
||
dispatchformRef.value?.validate((errors: any) => {
|
||
// Naive UI:有 errors 表示校验失败
|
||
if (errors) return
|
||
if (total !== remainQty) {
|
||
message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已完成 ${dispatchform.completedQty ?? 0})`, {
|
||
duration: 4000,
|
||
})
|
||
return
|
||
}
|
||
|
||
dispatchLoading.value = true
|
||
// 提交时 quantity 仍传计划总数,避免把工序计划数量改成待派数
|
||
orderProcessPlanApi.assignWork(dispatchform).then(() => {
|
||
message.success('派工成功')
|
||
dispatchmodal.value = false
|
||
loadData()
|
||
}).finally(() => { dispatchLoading.value = false })
|
||
})
|
||
}
|
||
|
||
async function handleExport() {
|
||
const blob = await orderProcessPlanApi.export({
|
||
name: searchForm.name || undefined,
|
||
code: searchForm.code || undefined,
|
||
})
|
||
const url = window.URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = '工序计划数据.xlsx'
|
||
link.click()
|
||
window.URL.revokeObjectURL(url)
|
||
}
|
||
|
||
async function handleDownloadTemplate() {
|
||
const blob = await orderProcessPlanApi.downloadTemplate()
|
||
const url = window.URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = '工序计划导入模板.xlsx'
|
||
link.click()
|
||
window.URL.revokeObjectURL(url)
|
||
}
|
||
|
||
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
|
||
if (!file.file) return
|
||
const result = await orderProcessPlanApi.importData(file.file)
|
||
if (result.fail > 0) {
|
||
dialog.warning({
|
||
title: '导入结果',
|
||
content: `成功 ${result.success} 条,失败 ${result.fail} 条`,
|
||
positiveText: '确定',
|
||
})
|
||
} else {
|
||
message.success(`导入成功,共 ${result.success} 条`)
|
||
importModalVisible.value = false
|
||
}
|
||
loadData()
|
||
}
|
||
|
||
|
||
const orderItemId = ref<number>(null)
|
||
|
||
function openReportDetail(order) {
|
||
submitModalVisible.value = true
|
||
orderItemId.value = order.orderItemId
|
||
|
||
}
|
||
|
||
onMounted(loadData)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.plan-board-page {
|
||
padding: 0;
|
||
background: #f5f7fa;
|
||
min-height: 100%;
|
||
}
|
||
|
||
.board-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
background: #fff;
|
||
border-radius: 8px 8px 0 0;
|
||
border-bottom: 1px solid #eef0f3;
|
||
}
|
||
|
||
.board-filter {
|
||
padding: 12px 16px;
|
||
background: #fff;
|
||
border-bottom: 1px solid #eef0f3;
|
||
}
|
||
|
||
.board-table-wrap {
|
||
overflow-x: auto;
|
||
background: #fff;
|
||
}
|
||
|
||
.board-table-head,
|
||
.order-row {
|
||
display: grid;
|
||
/* 勾选 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
|
||
grid-template-columns:
|
||
40px
|
||
minmax(220px, 1.6fr)
|
||
64px
|
||
108px
|
||
72px
|
||
minmax(200px, 1.4fr)
|
||
168px
|
||
96px
|
||
140px;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 0 16px;
|
||
min-width: 1280px;
|
||
}
|
||
|
||
.board-table-head {
|
||
height: 40px;
|
||
background: #fafbfc;
|
||
border-bottom: 1px solid #e8eaed;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #8c8c8c;
|
||
}
|
||
|
||
.order-block {
|
||
background: #fff;
|
||
border-bottom: 1px solid #eef0f3;
|
||
}
|
||
|
||
.order-row {
|
||
min-height: 64px;
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.order-row:hover {
|
||
background: #f8faff;
|
||
}
|
||
|
||
.order-row.expanded {
|
||
background: rgba(32, 128, 240, 0.04);
|
||
}
|
||
|
||
.col-check {
|
||
display: flex;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.col-product {
|
||
min-width: 0;
|
||
}
|
||
|
||
.col-qty,
|
||
.col-kit {
|
||
text-align: center;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.product-code {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
}
|
||
|
||
.col-mode {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.col-plan {
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.col-plan .sub-text {
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.col-life {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.col-flow {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.col-flow.flow-expanded :deep(.flow-stepper) {
|
||
background: rgba(32, 128, 240, 0.1);
|
||
}
|
||
|
||
.life-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.col-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: nowrap;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.order-expand {
|
||
padding: 12px 16px 16px 56px;
|
||
background: linear-gradient(180deg, rgba(32, 128, 240, 0.03) 0%, #fff 100%);
|
||
border-top: 1px dashed #e8eaed;
|
||
}
|
||
|
||
.card-flow-scroll,
|
||
.detail-cards {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0;
|
||
overflow-x: auto;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.detail-cards {
|
||
margin-top: 16px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.expand-empty {
|
||
font-size: 13px;
|
||
color: #8c8c8c;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.board-empty {
|
||
padding: 48px;
|
||
background: #fff;
|
||
}
|
||
|
||
.board-pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px 16px;
|
||
background: #fff;
|
||
border-radius: 0 0 8px 8px;
|
||
}
|
||
.pgClass {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.outsource-process-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 12px;
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s, background 0.2s;
|
||
}
|
||
|
||
.outsource-process-item:hover,
|
||
.outsource-process-item.active {
|
||
border-color: #2080f0;
|
||
background: rgba(32, 128, 240, 0.04);
|
||
}
|
||
|
||
.outsource-process-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.outsource-process-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
}
|
||
|
||
.outsource-process-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
}
|
||
|
||
.reorder-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
color: #595959;
|
||
}
|
||
|
||
.reorder-empty {
|
||
padding: 24px 0;
|
||
text-align: center;
|
||
color: #8c8c8c;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.reorder-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
max-height: 420px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.reorder-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
background: #fafbfc;
|
||
border: 1px solid #eef0f3;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.reorder-drag-handle {
|
||
cursor: grab;
|
||
flex-shrink: 0;
|
||
color: #8c8c8c;
|
||
}
|
||
|
||
.reorder-drag-handle:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.reorder-index {
|
||
width: 22px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #2080f0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.reorder-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.reorder-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.reorder-title em {
|
||
font-style: normal;
|
||
margin-right: 6px;
|
||
color: #2080f0;
|
||
}
|
||
|
||
.reorder-sub {
|
||
margin-top: 2px;
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
}
|
||
</style>
|