evoToK3Cloud-vue/src/views/system/orderPro/index.vue
2026-08-11 14:46:49 +08:00

6168 lines
207 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="order-pro-page app-container">
<div class="kpi-panel" v-loading="overviewStatsLoading" element-loading-background="rgba(255, 255, 255, 0.75)">
<div class="kpi-card kpi-card--total">
<i class="el-icon-folder-opened kpi-card__icon"></i>
<div class="kpi-card__body">
<span class="kpi-card__value">{{ overviewStats.total }}</span>
<span class="kpi-card__label">项目总数</span>
</div>
</div>
<div class="kpi-card kpi-card--delay">
<i class="el-icon-warning-outline kpi-card__icon"></i>
<div class="kpi-card__body">
<span class="kpi-card__value">{{ overviewStats.delayed }}</span>
<span class="kpi-card__label">延期数</span>
</div>
</div>
<div class="kpi-card kpi-card--change">
<i class="el-icon-refresh kpi-card__icon"></i>
<div class="kpi-card__body">
<span class="kpi-card__value">{{ overviewStats.change }}</span>
<span class="kpi-card__label">变更数</span>
</div>
</div>
<div class="kpi-card kpi-card--production">
<i class="el-icon-s-operation kpi-card__icon"></i>
<div class="kpi-card__body">
<span class="kpi-card__value">{{ overviewStats.inProduction }}</span>
<span class="kpi-card__label">生产中数量</span>
</div>
</div>
</div>
<el-card class="search-panel" shadow="never" v-show="showSearch" :body-style="{ padding: '14px 20px' }">
<div class="search-form" ref="queryForm">
<div class="search-form__main">
<div class="search-field">
<span class="search-field__label">生产令号</span>
<el-input
v-model="queryParams.productionOrderNo"
size="small"
clearable
placeholder="请输入生产令号"
@keyup.enter.native="handleQuery"
/>
</div>
<div class="search-field">
<span class="search-field__label">项目名称</span>
<el-input
v-model="queryParams.productionName"
size="small"
clearable
placeholder="请输入项目名称"
@keyup.enter.native="handleQuery"
/>
</div>
<div class="search-field">
<span class="search-field__label">来源</span>
<el-select v-model="queryParams.source" size="small" clearable placeholder="请选择">
<el-option label="手动" value="手动" />
<el-option label="PLM" value="PLM" />
</el-select>
</div>
<div class="search-form__actions">
<el-button type="primary" icon="el-icon-search" size="small" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="small" @click="resetQuery">重置</el-button>
<button
type="button"
class="search-form__toggle"
@click="showAdvancedSearch = !showAdvancedSearch"
>
<span>{{ showAdvancedSearch ? '收起筛选' : '更多筛选' }}</span>
<i :class="showAdvancedSearch ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
<em v-if="!showAdvancedSearch && advancedFilterCount > 0" class="search-form__count">{{ advancedFilterCount }}</em>
</button>
</div>
</div>
<transition name="el-fade-in-linear">
<div v-if="showAdvancedSearch" class="search-form__advanced">
<div class="search-form__advanced-head">
<span class="search-form__advanced-title">更多筛选</span>
<span class="search-form__advanced-tip">支持组合条件过滤项目</span>
</div>
<div class="search-form__advanced-grid">
<div class="search-field">
<span class="search-field__label">图号</span>
<el-input
v-model="queryParams.drawingNo"
size="small"
clearable
placeholder="请输入图号"
@keyup.enter.native="handleQuery"
/>
</div>
<div class="search-field">
<span class="search-field__label">名称</span>
<el-input
v-model="queryParams.drawingName"
size="small"
clearable
placeholder="请输入名称"
@keyup.enter.native="handleQuery"
/>
</div>
<div class="search-field">
<span class="search-field__label">是否延期</span>
<el-select v-model="queryParams.whetherDelay" size="small" clearable placeholder="请选择">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</div>
<div class="search-field">
<span class="search-field__label">项目状态</span>
<el-select v-model="queryParams.bomStatus" size="small" clearable placeholder="请选择项目状态">
<el-option
v-for="dict in dict.type.process_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</div>
<div class="search-field">
<span class="search-field__label search-field__label--wide">工艺审核</span>
<el-select v-model="queryParams.routeStatus" size="small" clearable placeholder="请选择">
<el-option label="未审核" :value="0" />
<el-option label="已审核" :value="1" />
<el-option label="已拒绝" :value="2" />
</el-select>
</div>
<div class="search-field">
<span class="search-field__label search-field__label--wide">计划表发送</span>
<el-select v-model="queryParams.routePlanSendFilter" size="small" clearable placeholder="请选择">
<el-option label="未发送 (0次)" value="0" />
<el-option label="已发送 (≥1次)" value="sent" />
<el-option label="1次" value="1" />
<el-option label="2次" value="2" />
<el-option label="3次及以上" value="ge3" />
</el-select>
</div>
<div class="search-field">
<span class="search-field__label search-field__label--wide">是否做工艺</span>
<el-select v-model="queryParams.needProcess" size="small" clearable placeholder="请选择">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</div>
<div class="search-field search-field--type">
<span class="search-field__label">项目类型</span>
<el-select
v-model="queryParams.isEnterpriseStandardList"
size="small"
clearable
multiple
collapse-tags
placeholder="请选择"
>
<el-option label="产品 (CP)" value="产品" />
<el-option label="非标 (FB)" value="非标" />
<el-option label="变更 (BG)" value="变更" />
<el-option label="研发 (EY)" value="研发" />
<el-option label="企标 (QB)" value="企标" />
<el-option label="库存 (KC)" value="库存" />
<el-option label="固资 (GZ)" value="固资" />
<el-option label="展品 (ZP)" value="展品" />
</el-select>
</div>
</div>
</div>
</transition>
</div>
</el-card>
<div class="toolbar-panel">
<div class="toolbar-panel__left">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
v-hasPermi="['system:orderPro:add']">新增
</el-button>
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate"
v-hasPermi="['system:orderPro:edit']">修改
</el-button>
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
v-hasPermi="['system:orderPro:remove']">删除
</el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['system:orderPro:export']">导出
</el-button>
<el-button type="info" plain icon="el-icon-s-flag" size="mini" :disabled="multiple"
@click="handleSetStatus" v-hasPermi="['system:orderPro:edit']">设置状态
</el-button>
</div>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</div>
<el-card class="table-panel" shadow="never">
<el-table
v-loading="loading"
:data="orderProList"
ref="orderTable"
row-key="id"
stripe
border
size="small"
class="order-pro-table"
:header-cell-style="orderTableHeaderStyle"
:row-class-name="getOrderRowClass"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="48" align="center" />
<el-table-column label="生产令号" align="left" prop="productionOrderNo" min-width="140" fixed="left">
<template slot-scope="scope">
<el-link class="order-no-link" @click="productionCodeHandle(scope.row)" type="primary">
<i class="el-icon-document"></i>
{{ scope.row.productionOrderNo }}
</el-link>
</template>
</el-table-column>
<el-table-column label="执行单图" align="center" min-width="120">
<template slot-scope="scope">
<div v-if="getImageList(scope.row).length" class="exec-image-entry">
<el-button type="text" size="mini" class="exec-image-trigger" @click="openExecImagePreview(scope.row)">
<i class="el-icon-picture-outline"></i>
查看图片
</el-button>
</div>
<span v-else class="text-muted">暂无执行单图</span>
</template>
</el-table-column>
<el-table-column label="项目状态" align="center" prop="imCategory" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.process_status" :value="scope.row.bomStatus" />
</template>
</el-table-column>
<el-table-column label="工艺审核" align="center" prop="routeStatus" width="96">
<template slot-scope="scope">
<span
class="status-pill"
:class="routeStatusPillClass(scope.row.routeStatus)"
>{{ formatRouteStatus(scope.row.routeStatus) }}</span>
</template>
</el-table-column>
<el-table-column label="生产完成" align="center" prop="productionComplete" width="96">
<template slot-scope="scope">
<span
v-if="scope.row.productionComplete === true"
class="status-pill status-pill--success"
>已完成</span>
<span
v-else-if="scope.row.productionComplete === false"
class="status-pill status-pill--warning"
>未完成</span>
<span v-else class="status-pill status-pill--muted">无计划</span>
</template>
</el-table-column>
<el-table-column label="项目名称" align="left" prop="productionName" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">
<span class="project-name-cell">{{ scope.row.productionName }}</span>
</template>
</el-table-column>
<el-table-column label="来源" align="center" prop="source" width="80">
<template slot-scope="scope">
<span class="source-badge" :class="'source-badge--' + getSourceKey(scope.row.source)">
{{ formatSource(scope.row.source) }}
</span>
</template>
</el-table-column>
<el-table-column label="是否延期" align="center" prop="whetherDelay" width="88">
<template slot-scope="scope">
<span
class="status-pill"
:class="Number(scope.row.whetherDelay) === 1 ? 'status-pill--danger' : 'status-pill--success'"
>
{{ Number(scope.row.whetherDelay) === 1 ? '延期' : '正常' }}
</span>
</template>
</el-table-column>
<el-table-column label="金蝶执行" align="center" min-width="168" class-name="kingdee-need-col">
<template slot-scope="scope">
<div
class="kingdee-need-group"
:class="{ 'kingdee-need-group--clickable': hasAnyKingdeeExecuted(scope.row) }"
@click="hasAnyKingdeeExecuted(scope.row) && openKingdeeDrawer(scope.row)"
>
<span
class="kingdee-need-tag kingdee-need-tag--mo"
:class="isKingdeeExecuted(getKingdeeField(scope.row, 'prd')) ? 'kingdee-need-tag--active' : 'kingdee-need-tag--idle'"
:title="isKingdeeExecuted(getKingdeeField(scope.row, 'prd')) ? '生产订单已执行,点击查看' : '生产订单未执行'"
@click.stop="handleKingdeeChipClick(scope.row, 'prd', $event)"
>
<i class="el-icon-s-cooperation"></i>
<em>生产</em>
<b>{{ formatKingdeeExecStatus(getKingdeeField(scope.row, 'prd')) }}</b>
</span>
<span
class="kingdee-need-tag kingdee-need-tag--po"
:class="isKingdeeExecuted(getKingdeeField(scope.row, 'po')) ? 'kingdee-need-tag--active' : 'kingdee-need-tag--idle'"
:title="isKingdeeExecuted(getKingdeeField(scope.row, 'po')) ? '采购订单已执行,点击查看' : '采购订单未执行'"
@click.stop="handleKingdeeChipClick(scope.row, 'po', $event)"
>
<i class="el-icon-shopping-cart-2"></i>
<em>采购</em>
<b>{{ formatKingdeeExecStatus(getKingdeeField(scope.row, 'po')) }}</b>
</span>
</div>
</template>
</el-table-column>
<el-table-column label="出图时间" prop="drawTime" min-width="160">
<template slot-scope="scope">
<span>{{ formatStepTime(scope.row, 'drawTime', '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="延期天数" align="center" prop="delayDay" width="88">
<template slot-scope="scope">
<span class="delay-days" :class="{ 'delay-days--active': scope.row.delayDay > 0 }">
{{ scope.row.delayDay != null ? scope.row.delayDay : '—' }}
</span>
</template>
</el-table-column>
<el-table-column label="计量单位" align="center" prop="unit" width="80" />
<el-table-column label="保留零件图" align="center" prop="quantity" width="96">
<template slot-scope="scope">
<el-tag size="mini" :type="Number(scope.row.quantity) === 1 ? 'success' : 'info'" effect="plain">
{{ Number(scope.row.quantity) === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="下载图纸" align="center" prop="isDownload" width="88">
<template slot-scope="scope">
<el-tag size="mini" :type="Number(scope.row.isDownload) === 1 ? 'success' : 'info'" effect="plain">
{{ Number(scope.row.isDownload) === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="是否做工艺" align="center" prop="needProcess" width="96">
<template slot-scope="scope">
<el-tag size="mini" :type="Number(scope.row.needProcess) === 1 ? 'success' : 'info'" effect="plain">
{{ Number(scope.row.needProcess) === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="项目类型" align="center" prop="isEnterpriseStandard" width="96">
<template slot-scope="scope">
<span class="type-badge" :class="'type-badge--' + getProjectTypeKey(scope.row.isEnterpriseStandard)">
{{ getProjectTypeLabel(scope.row.isEnterpriseStandard) }}
</span>
</template>
</el-table-column>
<el-table-column label="计划表发送" align="center" width="96">
<template slot-scope="scope">
<span v-if="getRowRoutePlanSendCount(scope.row) === null" class="text-muted">—</span>
<span
v-else
class="send-count-badge"
:class="{ 'send-count-badge--sent': getRowRoutePlanSendCount(scope.row) > 0 }"
>
{{ getRowRoutePlanSendCount(scope.row) }}次
</span>
</template>
</el-table-column>
<el-table-column label="创建人" align="center" prop="createBy" width="90" show-overflow-tooltip />
<el-table-column label="出图时间" align="center" prop="drawingTime" width="110">
<template slot-scope="scope">
<span class="date-cell">{{ drawingTime(scope.row, 'drawingTime', '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="项目完成时间" align="center" prop="projectEndTime" width="110">
<template slot-scope="scope">
<span class="date-cell">{{ formatStepTime(scope.row, 'projectEndTime', '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span class="date-cell">{{ formatStepTime(scope.row, 'createTime', '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="168">
<template slot-scope="scope">
<el-dropdown
:split-button="true"
size="small"
type="primary"
class="action-dropdown"
@click="handleDetail(scope.row.productionOrderNo)"
v-hasPermi="['system:orderPro:remove']"
@command="(command) => handleCommand(command, scope.row)"
>
查看工艺
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="生产计划">生产计划</el-dropdown-item>
<el-dropdown-item command="BOM" v-hasPermi="['system:orderPro:remove']">查看BOM</el-dropdown-item>
<el-dropdown-item :divided="true" command="修改" v-hasPermi="['system:orderPro:edit']">修改
</el-dropdown-item>
<el-dropdown-item :divided="true" command="删除" v-hasPermi="['system:orderPro:remove']">删除
</el-dropdown-item>
<el-dropdown-item :divided="true" command="清空项目" v-hasPermi="['system:orderPro:clear']">清空项目
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<div class="table-panel__footer">
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
</div>
</el-card>
<el-drawer
:title="getDrawerTitle()"
:visible.sync="drawer"
:direction="direction"
:before-close="handleClose"
size="90%"
append-to-body
:modal-append-to-body="true"
:destroy-on-close="false"
custom-class="order-pro-drawer"
>
<div class="drawer-body">
<el-tabs v-model="drawerActiveTab" class="drawer-main-tabs" @tab-click="handleDrawerTabClick">
<el-tab-pane label="项目出图" name="project">
<div class="drawer-section-title drawer-section-title--green drawer-section-title--block">
<i class="el-icon-picture"></i>
<span>选择产品</span>
</div>
<!-- 第一部分:销齿链型号表格管理 -->
<div class="drawer-toolbar">
<el-button type="primary" @click="openAddChainDialog" size="mini"
v-hasPermi="['system:save:add']">新增</el-button>
<el-upload :action="dwgUploadUrl" :headers="uploadHeaders" :show-file-list="false" accept=".dwg"
:http-request="batchUploadDwg" :before-upload="beforeBatchDwgUpload">
<el-button type="success" size="mini" :disabled="!selectedChainIds.length"
v-hasPermi="['system:orderPro:uploadDwg']">
<i class="el-icon-upload2"></i> 上传图纸
</el-button>
</el-upload>
<el-button
type="primary"
size="mini"
:disabled="!selectedChainIds.length"
@click="downloadSelectedDwgZip"
v-hasPermi="['system:orderPro:downloadZip']"
>
<i class="el-icon-download"></i> 下载图纸ZIP
</el-button>
<el-button
type="warning"
size="mini"
@click="pushDingTalkCard"
v-hasPermi="['system:orderPro:pushDingTalkCard']"
>
<i class="el-icon-message"></i> 推送钉钉
</el-button>
<el-upload
:show-file-list="false"
:http-request="importProducts"
:before-upload="beforeElectricalBomUpload"
accept=".xls,.xlsx"
>
<el-button
type="info"
size="mini"
v-hasPermi="['system:save:import']"
>
<i class="el-icon-upload"></i> 导入产品
</el-button>
</el-upload>
<!-- 出图状态展示按钮 -->
<el-button
:type="plotStatusType"
size="mini"
@click="fetchPlotStatus"
style="margin-left: 10px;"
title="点击刷新出图状态"
>
<i class="el-icon-picture-outline" style="margin-right: 4px;"></i>
出图状态:{{ plotStatusText }}
<span v-if="plotProjectName" style="margin-left: 8px;">项目:{{ plotProjectName }}</span>
<el-tooltip v-if="plotOutputPath || plotTargetDrawing || plotStateMsg" effect="dark" :content="plotDetailText" placement="top">
<i class="el-icon-info" style="margin-left: 6px;"></i>
</el-tooltip>
<i v-if="plotStatusLoading" class="el-icon-loading" style="margin-left: 4px;"></i>
</el-button>
</div>
<el-table :data="chainTable" border stripe size="small" style="width: 100%; margin-bottom: 10px"
highlight-current-row :header-cell-style="{ fontWeight: 'bold', background: '#f5f7fa' }"
@selection-change="handleChainSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="figureNumber" label="产品型号" min-width="100" />
<el-table-column prop="figureName" label="产品名称" min-width="140" />
<el-table-column prop="jdInventory" label="行程" min-width="80" />
<el-table-column prop="figureNum" label="数量" min-width="60" />
<el-table-column prop="drawPath" label="文件路径" min-width="120" :show-overflow-tooltip="true" />
<el-table-column prop="productType" label="产品类型" min-width="100" />
<el-table-column prop="fileType" label="文件类型" min-width="100" />
<el-table-column label="出图人" prop="drawBy" min-width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.plan_proer" :value="scope.row.drawBy" />
</template>
</el-table-column>
<el-table-column label="是否延期" prop="whetherDelay" min-width="90" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="Number(scope.row.whetherDelay) === 1 ? 'danger' : 'success'">
{{ Number(scope.row.whetherDelay) === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="delayDay" label="延期天数" min-width="90" align="right" />
<el-table-column label="上传图纸时间" prop="uploadTime" min-width="160">
<template slot-scope="scope">
<span>{{ formatStepTime(scope.row, 'uploadTime', '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="创建人" min-width="100" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span>{{ scope.row.createBy || scope.row.create_by || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="修改人" min-width="100" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span>{{ scope.row.updateBy || scope.row.update_by || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<div style="display: flex; align-items: center; gap: 8px">
<el-button type="text" size="mini" @click="editChain(scope.$index, scope.row)"
v-hasPermi="['system:save:edit']">
<i class="el-icon-edit"></i> 修改
</el-button>
<el-button type="text" size="mini" @click="removeChain(scope.$index)"
v-hasPermi="['system:save:remove']">
<i class="el-icon-delete"></i> 删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-divider content-position="center" class="drawer-divider">
<div class="drawer-section-title drawer-section-title--green">
<i class="el-icon-picture"></i>
<span>执行出图</span>
</div>
</el-divider>
<!-- 第二部分:执行出图命令及日志展示 -->
<div class="drawer-action-block">
<div class="drawer-toolbar drawer-toolbar--wrap">
<el-button type="success" :loading="drawLoading" @click="executeDrawCommand"
v-hasPermi="['system:orderPro:executDrawing']">
执行出图
</el-button>
<el-button type="success" plain :loading="sendProcessRoutePlanLoading" @click="sendProcessRoutePlan"
v-hasPermi="['system:orderPro:sendProcessRoutePlanExcel']"
title="发送工艺计划表(钉钉推送分类 BOM Excel括号内为已成功发送次数">
发送计划表({{ routePlanSendCount }}次)
</el-button>
<el-button type="primary" @click="downloadPDF" v-hasPermi="['system:orderPro:uploadPDF']">
下载PDF
</el-button>
<el-button type="warning" @click="downloadRoute" v-hasPermi="['system:orderPro:exportRoute2']">
下载金蝶工艺计划表
</el-button>
<el-button type="primary" @click="downloadRoute3" v-hasPermi="['system:orderPro:exportRoute3']">
导出工艺计划表
</el-button>
<el-button
type="warning"
plain
:loading="drawingWorkloadLoading"
@click="syncDrawingWorkload"
v-hasPermi="['system:orderPro:drawingWorkload']"
>
读取图纸工作量
</el-button>
<el-button type="danger" @click="pushProcessInbound" v-hasPermi="['system:route:updateProductionOrders']">
推送工序计划单入库
</el-button>
<el-button
type="warning"
:loading="downloadPlmDwgLoading"
@click="downloadPlmDwgZip"
v-hasPermi="['system:orderPro:downloadZip']"
>
下载PLMDWG图纸
</el-button>
<el-button type="success" @click="pushPurchaseInbound" v-hasPermi="['system:route:updateProductionOrders']">
推送入库采购申请
</el-button>
<el-upload :show-file-list="false" :http-request="uploadElectricalBom"
:before-upload="beforeElectricalBomUpload" accept=".xls,.xlsx" style="display: inline-block">
<el-button type="info" v-hasPermi="['system:details:importElectricalBom']">
导入电气BOM
</el-button>
</el-upload>
</div>
<el-progress :text-inside="true" :stroke-width="26" :percentage="percentage" class="draw-progress"></el-progress>
<el-card shadow="never" class="draw-log-card">
<div class="draw-log-content">
<template v-if="drawLog">{{ drawLog }}</template>
<template v-else>暂无日志</template>
</div>
</el-card>
</div>
<el-divider content-position="center" class="drawer-divider">
<div class="drawer-section-title drawer-section-title--blue">
<i class="el-icon-data-analysis"></i>
<span>MRP运算结果复查</span>
</div>
</el-divider>
<!-- 第三部分:预留 -->
<div style="color: #aaa; font-size: 13px"></div>
<!-- 第三部分MRP运算结果复查表格 -->
<el-card class="box-card mrp-result-card" style="margin-bottom: 20px">
<div slot="header" class="clearfix">
<span style="font-weight: 600; color: #303133">
<i class="el-icon-data-analysis" style="margin-right: 8px; color: #409eff"></i>
MRP运算结果复查
</span>
<div style="
float: right;
display: flex;
align-items: center;
gap: 12px;
">
<!-- 运算状态显示 -->
<div v-if="loadingMRP" class="mrp-status-indicator">
<i class="el-icon-loading" style="margin-right: 6px"></i>
<span>{{ mrpOperationStatus || "MRP运算进行中..." }}</span>
</div>
<!-- 运算结果按钮 -->
<el-button type="primary" size="medium" :loading="loadingMRP" :disabled="loadingMRP"
@click="getMRPResults()" class="mrp-calculate-btn">
{{ loadingMRP ? "核算中..." : "开始核算" }}
</el-button>
<!-- 搜索输入框 -->
<el-input v-model="mrpSearchKeyword" placeholder="搜索物料名称或编码" style="width: 220px" size="small" clearable
@input="handleMrpSearch" @clear="clearMrpSearch" class="mrp-search-input">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
<!-- 导出按钮 -->
<el-button type="success" size="small" icon="el-icon-download" @click="handleExportMrp"
:loading="exportLoading" :disabled="!productionObj || !productionObj.id" class="mrp-export-btn" :title="productionObj && productionObj.id
? `导出${productionObj.productionOrderNo}的MRP数据`
: '请先选择生产令号'
">
导出Excel
</el-button>
<el-button type="text" @click="handleRefreshMrpData">
<i class="el-icon-refresh"></i> 刷新
</el-button>
</div>
</div>
<!-- 统计信息 -->
<div style="
margin-bottom: 15px;
padding: 15px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-radius: 8px;
border-left: 4px solid #409eff;
">
<el-row :gutter="20">
<el-col :span="6">
<div style="text-align: center">
<div style="font-size: 24px; font-weight: bold; color: #409eff">
{{ mrpStats.totalMaterials }}
</div>
<div style="font-size: 12px; color: #606266">总物料数</div>
</div>
</el-col>
<el-col :span="6">
<div style="text-align: center">
<div style="font-size: 24px; font-weight: bold; color: #67c23a">
{{ mrpStats.positiveStock }}
</div>
<div style="font-size: 12px; color: #606266">正库存</div>
</div>
</el-col>
<el-col :span="6">
<div style="text-align: center">
<div style="font-size: 24px; font-weight: bold; color: #f56c6c">
{{ mrpStats.negativeStock }}
</div>
<div style="font-size: 12px; color: #606266">负库存</div>
</div>
</el-col>
<el-col :span="6">
<div style="text-align: center">
<div style="font-size: 24px; font-weight: bold; color: #e6a23c">
{{ mrpStats.zeroStock }}
</div>
<div style="font-size: 12px; color: #606266">零库存</div>
</div>
</el-col>
</el-row>
</div>
<el-table :data="mrpResultData" style="width: 100%"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }" :row-class-name="getMrpRowClass" border
stripe size="small" v-loading="mrpLoading">
<el-table-column prop="materialCode" label="物料编码" min-width="120" show-overflow-tooltip />
<el-table-column prop="materialName" label="物料名称" min-width="100" show-overflow-tooltip />
<el-table-column label="项目本批数" width="130" align="right">
<template slot-scope="scope">
<span style="color: #e6a23c">
{{ formatFraction(scope.row.purchaseNotInStock) }}
</span>
</template>
</el-table-column>
<el-table-column prop="availableStock" label="本项目可用库存" width="120" align="right">
<template slot-scope="scope">
<span :style="{
color: scope.row.availableStock < 0 ? '#f56c6c' : '#67c23a',
}">
{{ scope.row.availableStock.toFixed(3) }}
</span>
</template>
</el-table-column>
<el-table-column prop="realTimeStock" label="当前可用库存" width="100" align="right">
<template slot-scope="scope">
<span :style="{
color: scope.row.realTimeStock < 0 ? '#f56c6c' : '#67c23a',
}">
{{ scope.row.realTimeStock.toFixed(3) }}
</span>
</template>
</el-table-column>
<el-table-column prop="prodNotInStock" label="本次生产未入库" width="130" align="right">
<template slot-scope="scope">
<span style="color: #e6a23c">{{
scope.row.prodNotInStock.toFixed(3)
}}</span>
</template>
</el-table-column>
<el-table-column prop="purchaseRequestQty" label="本项目采购申请" width="130" align="right">
<template slot-scope="scope">
<span style="color: #409eff">{{
scope.row.purchaseRequestQty.toFixed(3)
}}</span>
</template>
</el-table-column>
<el-table-column prop="childNotPickedQty" label="子项未领料数量" width="130" align="right">
<template slot-scope="scope">
<span style="color: #f56c6c">{{
scope.row.childNotPickedQty.toFixed(3)
}}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" align="center">
<template slot-scope="scope">
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" width="160" align="center">
<template slot-scope="scope">
{{ formatDateTime(scope.row.updateTime) }}
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination @size-change="handleMrpSizeChange" @current-change="handleMrpCurrentChange"
:current-page="mrpQueryParams.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="mrpQueryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="mrpTotal"
style="margin-top: 20px; text-align: right" />
</el-card>
<!-- 第四部分 工艺上传日志 -->
<el-card shadow="never" style="margin-top: 20px">
<div slot="header" class="clearfix">
<span style="font-weight: 600; font-size: 16px">工艺上传日志</span>
</div>
<!-- 概览统计 -->
<div style="
margin-bottom: 16px;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
display: flex;
gap: 24px;
align-items: center;
flex-wrap: wrap;
">
<div style="display: flex; align-items: center; gap: 8px">
<el-tag type="success" size="small">成功</el-tag>
<span style="font-weight: 600; color: #67c23a">{{
processLogCounts.success
}}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px">
<el-tag type="warning" size="small">重复</el-tag>
<span style="font-weight: 600; color: #e6a23c">{{
processLogCounts.duplicate
}}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px">
<el-tag type="danger" size="small">失败</el-tag>
<span style="font-weight: 600; color: #f56c6c">{{
processLogCounts.failed
}}</span>
</div>
<div style="margin-left: auto; color: #606266; font-size: 13px">
<i class="el-icon-time" style="margin-right: 4px"></i>
<span v-if="processUploadLog.time">{{
processUploadLog.time
}}</span>
<span v-else style="color: #909399">暂无时间信息</span>
</div>
</div>
<!-- 拉取按钮 -->
<div style="margin-bottom: 16px">
<el-button type="primary" size="small" @click="fetchProcessLog" :loading="processLogLoading"
:disabled="!productionObj || !productionObj.id" icon="el-icon-refresh">
拉取最新日志
</el-button>
<el-button size="small" @click="clearProcessLog" icon="el-icon-delete"
style="margin-left: 8px">清空</el-button>
</div>
<!-- 成功上传结构化显示 -->
<div v-if="
processUploadLog.successfulRoutes &&
processUploadLog.successfulRoutes.length
" style="margin-bottom: 20px">
<div style="
margin-bottom: 12px;
padding: 8px 12px;
background: #f0f9ff;
border-left: 4px solid #67c23a;
border-radius: 4px;
">
<span style="font-weight: 600; color: #67c23a; font-size: 14px">
<i class="el-icon-success" style="margin-right: 6px"></i>
成功上传({{ processUploadLog.successfulRoutes.length }}
</span>
<span style="margin-left: 12px; color: #909399; font-size: 12px">工序行数:{{ successFlatRows.length }}</span>
</div>
<el-table :data="successFlatPagedRows" size="small" border stripe style="width: 100%">
<el-table-column prop="materialCode" label="物料编码" width="160" show-overflow-tooltip />
<el-table-column prop="materialName" label="物料名称" width="180" show-overflow-tooltip />
<el-table-column prop="processNo" label="工序号" width="90" align="center" />
<el-table-column prop="workCenter" label="工作中心" width="120" show-overflow-tooltip />
<el-table-column prop="processName" label="工序名称" width="120" show-overflow-tooltip />
<el-table-column prop="activityUnit" label="工序单位" width="80" align="center" />
<el-table-column prop="processControl" label="工序控制" width="120" show-overflow-tooltip />
<el-table-column prop="activityDuration" label="工序时长" width="100" align="center" />
<el-table-column prop="processDescription" label="工序描述" min-width="150" show-overflow-tooltip />
<el-table-column prop="processNameDescription" label="材质/备注" width="140" show-overflow-tooltip />
</el-table>
<pagination v-show="successFlatRows.length > 0" :total="successFlatRows.length"
:page.sync="processSuccessPage.pageNum" :limit.sync="processSuccessPage.pageSize" @pagination="() => { }"
style="margin-top: 12px" />
</div>
<!-- 重复 -->
<div v-if="
processUploadLog.duplicateRoutes &&
processUploadLog.duplicateRoutes.length
" style="margin-bottom: 20px">
<div style="
margin-bottom: 12px;
padding: 8px 12px;
background: #fdf6ec;
border-left: 4px solid #e6a23c;
border-radius: 4px;
">
<span style="font-weight: 600; color: #e6a23c; font-size: 14px">
<i class="el-icon-warning" style="margin-right: 6px"></i>
重复({{ processUploadLog.duplicateRoutes.length }}
</span>
</div>
<div style="padding: 12px; background: #fafafa; border-radius: 4px">
<el-tag v-for="(dup, idx) in processUploadLog.duplicateRoutes" :key="idx" type="warning" size="small"
style="margin-right: 8px; margin-bottom: 8px; padding: 4px 12px">
{{ dup }}
</el-tag>
</div>
</div>
<!-- 失败 -->
<div v-if="
processUploadLog.failedRoutes &&
processUploadLog.failedRoutes.length
">
<div style="
margin-bottom: 12px;
padding: 8px 12px;
background: #fef0f0;
border-left: 4px solid #f56c6c;
border-radius: 4px;
">
<span style="font-weight: 600; color: #f56c6c; font-size: 14px">
<i class="el-icon-error" style="margin-right: 6px"></i>
失败({{ processUploadLog.failedRoutes.length }}
</span>
</div>
<el-table :data="processFailedPagedItems" size="small" border stripe style="width: 100%">
<el-table-column prop="materialCode" label="物料编码" width="160" show-overflow-tooltip />
<el-table-column prop="errorMessage" label="错误信息" min-width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span style="color: #f56c6c">{{
scope.row.errorMessage
}}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="processUploadLog.failedRoutes &&
processUploadLog.failedRoutes.length > 0
" :total="processUploadLog.failedRoutes.length" :page.sync="processFailedPage.pageNum"
:limit.sync="processFailedPage.pageSize" @pagination="() => { }" style="margin-top: 12px" />
</div>
<!-- 空状态 -->
<el-empty v-if="
(!processUploadLog.successfulRoutes ||
processUploadLog.successfulRoutes.length === 0) &&
(!processUploadLog.duplicateRoutes ||
processUploadLog.duplicateRoutes.length === 0) &&
(!processUploadLog.failedRoutes ||
processUploadLog.failedRoutes.length === 0)
" description="暂无工艺上传日志数据" :image-size="100" style="padding: 40px 0">
</el-empty>
</el-card>
<!-- 第五部分 BOM上传日志 -->
<el-card shadow="never" style="margin-top: 20px">
<div slot="header" class="clearfix">
<span style="font-weight: 600; font-size: 16px">BOM上传日志</span>
</div>
<!-- 概览统计 -->
<div style="
margin-bottom: 16px;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
display: flex;
gap: 24px;
align-items: center;
flex-wrap: wrap;
">
<div style="display: flex; align-items: center; gap: 8px">
<el-tag type="success" size="small">成功</el-tag>
<span style="font-weight: 600; color: #67c23a">{{
bomLogCounts.success
}}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px">
<el-tag type="info" size="small">已存在</el-tag>
<span style="font-weight: 600; color: #909399">{{
bomLogCounts.exists
}}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px">
<el-tag type="danger" size="small">失败</el-tag>
<span style="font-weight: 600; color: #f56c6c">{{
bomLogCounts.failed
}}</span>
</div>
<div style="margin-left: auto; color: #606266; font-size: 13px">
<span>总计:{{ bomLogCounts.total }}</span>
</div>
</div>
<!-- 拉取按钮 -->
<div style="margin-bottom: 16px">
<el-button type="primary" size="small" @click="fetchBomLog" :loading="bomLogLoading"
:disabled="!productionObj || !productionObj.productionOrderNo" icon="el-icon-refresh">
拉取最新日志
</el-button>
<el-button size="small" @click="clearBomLog" icon="el-icon-delete" style="margin-left: 8px">清空</el-button>
</div>
<!-- 成功上传 -->
<div v-if="bomLogCounts.success > 0" style="margin-bottom: 20px">
<div style="
margin-bottom: 12px;
padding: 8px 12px;
background: #f0f9ff;
border-left: 4px solid #67c23a;
border-radius: 4px;
">
<span style="font-weight: 600; color: #67c23a; font-size: 14px">
<i class="el-icon-success" style="margin-right: 6px"></i>
成功上传({{ bomLogCounts.success }}
</span>
</div>
<el-table :data="bomSuccessPagedItems" size="small" border stripe style="width: 100%">
<el-table-column prop="materialCode" label="物料编码" width="160" show-overflow-tooltip />
<el-table-column prop="reason" label="说明" min-width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span style="color: #67c23a">{{ scope.row.reason }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="bomLogCounts.success > 0" :total="bomLogCounts.success"
:page.sync="bomSuccessPage.pageNum" :limit.sync="bomSuccessPage.pageSize" @pagination="() => { }"
style="margin-top: 12px" />
</div>
<!-- 已存在 -->
<div v-if="bomLogCounts.exists > 0" style="margin-bottom: 20px">
<div style="
margin-bottom: 12px;
padding: 8px 12px;
background: #f4f4f5;
border-left: 4px solid #909399;
border-radius: 4px;
">
<span style="font-weight: 600; color: #909399; font-size: 14px">
<i class="el-icon-info" style="margin-right: 6px"></i>
已存在且一致({{ bomLogCounts.exists }}
</span>
</div>
<el-table :data="bomExistsPagedItems" size="small" border stripe style="width: 100%">
<el-table-column prop="materialCode" label="物料编码" width="160" show-overflow-tooltip />
<el-table-column prop="reason" label="说明" min-width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span style="color: #909399">{{ scope.row.reason }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="bomLogCounts.exists > 0" :total="bomLogCounts.exists" :page.sync="bomExistsPage.pageNum"
:limit.sync="bomExistsPage.pageSize" @pagination="() => { }" style="margin-top: 12px" />
</div>
<!-- 失败 -->
<div v-if="bomLogCounts.failed > 0" style="margin-bottom: 20px">
<div style="
margin-bottom: 12px;
padding: 8px 12px;
background: #fef0f0;
border-left: 4px solid #f56c6c;
border-radius: 4px;
">
<span style="font-weight: 600; color: #f56c6c; font-size: 14px">
<i class="el-icon-error" style="margin-right: 6px"></i>
失败({{ bomLogCounts.failed }}
</span>
</div>
<el-table :data="bomFailedPagedItems" size="small" border stripe style="width: 100%">
<el-table-column prop="materialCode" label="物料编码" width="160" show-overflow-tooltip />
<el-table-column prop="reason" label="错误信息" min-width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span style="color: #f56c6c">{{
formatBomErrorReason(scope.row.reason)
}}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="bomLogCounts.failed > 0" :total="bomLogCounts.failed" :page.sync="bomFailedPage.pageNum"
:limit.sync="bomFailedPage.pageSize" @pagination="() => { }" style="margin-top: 12px" />
</div>
<!-- 空状态 -->
<el-empty v-if="bomLogCounts.total === 0" description="暂无BOM上传日志数据" :image-size="100" style="padding: 40px 0">
</el-empty>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="isEditChain ? '编辑销齿链型号' : '新增销齿链型号'" :visible.sync="chainDialogVisible" width="420px"
append-to-body>
<el-form :model="chainForm" label-width="100px">
<div style="
margin-bottom: 16px;
padding: 12px;
background-color: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
color: #409eff;
font-size: 13px;
">
<i class="el-icon-info" style="margin-right: 6px"></i>
<strong>操作说明:</strong>可以先搜索选择已有型号自动填充,或直接手动填写所有必填字段(带*号)
</div>
<el-form-item label="销齿链型号">
<div style="margin-bottom: 8px; color: #909399; font-size: 12px">
可选择已有型号或手动填写所有信息
</div>
<el-select v-model="chainForm.id" filterable remote reserve-keyword :placeholder="isEditChain
? '编辑模式不使用此项'
: '请输入行程/类型/名称/产品名(可选)'
" :remote-method="searchRigidChain" :loading="chainLoading" @change="onRigidChainSelect"
popper-class="chain-autocomplete-dropdown" :default-first-option="false" :disabled="isEditChain"
clearable>
<el-option v-for="item in rigidChainOptions" :key="item.id" :label="item.productName" :value="item.id">
<div class="chain-suggestion-item">
<div class="chain-name">{{ item.figureNumber }}</div>
<div class="chain-info">
<span>编码: {{ item.typeName }}</span>
<span>产品名: {{ item.productName }}</span>
<span>类型: {{ item.type }}</span>
<span>行程: {{ item.journey }}</span>
</div>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="生产令号">
<el-input v-model="chainForm.productionCode" placeholder="请输入生产令号" disabled />
</el-form-item>
<el-form-item label="物料类型">
<el-select v-model="chainForm.productType" placeholder="请选择类型" clearable>
<el-option label="30S" value="30S" />
<el-option label="30D" value="30D" />
<el-option label="35R" value="35R" />
<el-option label="40S" value="40S" />
<el-option label="40R" value="40R" />
<el-option label="60R" value="60R" />
<el-option label="80R" value="80R" />
<el-option label="100R" value="100R" />
<el-option label="125R" value="125R" />
<el-option label="150R" value="150R" />
</el-select>
</el-form-item>
<el-form-item label="物料编码" required>
<el-input v-model="chainForm.figureNumber" placeholder="请输入图号" />
</el-form-item>
<el-form-item label="物料名称" required>
<el-input v-model="chainForm.figureName" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="物料数量" required>
<el-input v-model="chainForm.figureNum" placeholder="请输入数量" type="number" min="1" />
</el-form-item>
<!-- 文件路径和DWG上传企标可编辑非企标只读+可上传 -->
<el-form-item label="文件路径">
<div>
<el-select v-model="chainForm.selectedFileType" placeholder="请选择文件类型" @change="onConfigIniSelect"
filterable clearable>
<el-option v-for="item in configIniOptions" :key="item.fileType" :label="item.fileType"
:value="item.fileType" />
</el-select>
</div>
<div>
<el-input v-model="chainForm.drawPath" placeholder="请输入文件路径" />
</div>
</el-form-item>
<el-form-item label="文件类型">
<el-select v-model="chainForm.fileType" placeholder="请选择文件类型" clearable>
<el-option label="二维" value="二维" />
<el-option label="三维" value="三维" />
</el-select>
</el-form-item>
<el-form-item label="出图人">
<el-select v-model="chainForm.drawBy" placeholder="请选择出图人" clearable>
<el-option
v-for="d in dict.type.plan_proer || []"
:key="d.value"
:label="d.label"
:value="d.value"
/>
</el-select>
</el-form-item>
<el-form-item label="出图时间">
<el-date-picker
clearable
v-model="chainForm.drawTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
:default-time="'09:00:00'"
placeholder="请选择出图时间"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer" style="text-align: right">
<el-button @click="chainDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="saveChainForm">确 定</el-button>
</div>
</el-dialog>
</el-tab-pane>
<el-tab-pane label="生产订单" name="prdMo">
<div class="kingdee-tab-toolbar">
<el-button type="primary" size="mini" icon="el-icon-refresh" :loading="kingdeePrdMoLoading" @click="fetchKingdeePrdMo">
刷新
</el-button>
</div>
<el-table
:data="kingdeePrdMoList"
v-loading="kingdeePrdMoLoading"
border
stripe
size="small"
style="width: 100%"
empty-text="暂无生产订单数据"
>
<el-table-column label="生产订单编号" min-width="130" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FBillNo || '—' }}</template>
</el-table-column>
<el-table-column label="工艺路线编码" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FRoutingId.FNumber'] || '—' }}</template>
</el-table-column>
<el-table-column label="生产车间" min-width="100" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FWorkShopID.FName'] || '—' }}</template>
</el-table-column>
<el-table-column label="物料编码" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FMaterialId.FNumber'] || '—' }}</template>
</el-table-column>
<el-table-column label="物料名称" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FMaterialName || '—' }}</template>
</el-table-column>
<el-table-column label="数量" width="80" align="right">
<template slot-scope="scope">{{ scope.row.FQty != null ? scope.row.FQty : '—' }}</template>
</el-table-column>
<el-table-column label="材质" min-width="90" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.F_UCHN_BaseProperty_qtr || '—' }}</template>
</el-table-column>
<el-table-column label="单位" width="70" align="center">
<template slot-scope="scope">{{ scope.row['FUnitId.FName'] || '—' }}</template>
</el-table-column>
<el-table-column label="计划开工" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FPlanStartDate) }}</template>
</el-table-column>
<el-table-column label="计划完工" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FPlanFinishDate) }}</template>
</el-table-column>
<el-table-column label="领料状态" width="100" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="getPickMtrlStatusTag(scope.row.FPickMtrlStatus)">
{{ formatPickMtrlStatus(scope.row.FPickMtrlStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="采购订单" name="purchaseOrder">
<div class="kingdee-tab-toolbar">
<el-button type="primary" size="mini" icon="el-icon-refresh" :loading="kingdeePurchaseLoading" @click="fetchKingdeePurchaseOrder">
刷新
</el-button>
</div>
<el-table
:data="kingdeePurchaseOrderList"
v-loading="kingdeePurchaseLoading"
border
stripe
size="small"
style="width: 100%"
empty-text="暂无采购订单数据"
>
<el-table-column label="采购订单编号" min-width="130" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FBillNo || '—' }}</template>
</el-table-column>
<el-table-column label="物料编码" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FMaterialId.FNumber'] || '—' }}</template>
</el-table-column>
<el-table-column label="物料名称" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FMaterialName || '—' }}</template>
</el-table-column>
<el-table-column label="采购数量" width="90" align="right">
<template slot-scope="scope">{{ scope.row.FQty != null ? scope.row.FQty : '—' }}</template>
</el-table-column>
<el-table-column label="订单日期" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FDate) }}</template>
</el-table-column>
<el-table-column label="交货日期" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FDeliveryDate) }}</template>
</el-table-column>
<el-table-column label="剩余未入库" width="100" align="right">
<template slot-scope="scope">{{ scope.row.FRemainStockINQty != null ? scope.row.FRemainStockINQty : '—' }}</template>
</el-table-column>
<el-table-column label="MRP关闭状态" width="110" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.FMRPCloseStatus === 'B' ? 'info' : 'success'">
{{ formatMrpCloseStatus(scope.row.FMRPCloseStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="供应商" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FSupplierId.FName'] || '—' }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</el-drawer>
<el-drawer
:title="getKingdeeDrawerTitle()"
:visible.sync="kingdeeDrawer"
direction="rtl"
size="78%"
append-to-body
:modal-append-to-body="true"
:before-close="handleKingdeeDrawerClose"
custom-class="order-pro-drawer kingdee-order-drawer"
>
<div class="kingdee-drawer-body">
<div v-if="productionObj" class="kingdee-drawer-summary">
<div class="kingdee-drawer-summary__item">
<span class="kingdee-drawer-summary__label">生产令号</span>
<span class="kingdee-drawer-summary__value">{{ productionObj.productionOrderNo || '—' }}</span>
</div>
<div class="kingdee-drawer-summary__item">
<span class="kingdee-drawer-summary__label">项目名称</span>
<span class="kingdee-drawer-summary__value">{{ productionObj.productionName || '—' }}</span>
</div>
<div class="kingdee-drawer-summary__flags">
<span
class="kingdee-drawer-flag"
:class="isKingdeeExecuted(getKingdeeField(productionObj, 'prd')) ? 'kingdee-drawer-flag--done' : 'kingdee-drawer-flag--idle'"
>
<i class="el-icon-s-cooperation"></i>
生产订单 {{ formatKingdeeExecStatus(getKingdeeField(productionObj, 'prd')) }}
</span>
<span
class="kingdee-drawer-flag"
:class="isKingdeeExecuted(getKingdeeField(productionObj, 'po')) ? 'kingdee-drawer-flag--done' : 'kingdee-drawer-flag--idle'"
>
<i class="el-icon-shopping-cart-2"></i>
采购订单 {{ formatKingdeeExecStatus(getKingdeeField(productionObj, 'po')) }}
</span>
</div>
</div>
<el-tabs v-model="kingdeeActiveTab" class="kingdee-order-tabs" @tab-click="handleKingdeeTabClick">
<el-tab-pane name="prdMo">
<span slot="label" class="kingdee-tab-label">
<i class="el-icon-s-cooperation"></i>
生产订单
<span v-if="isKingdeeExecuted(getKingdeeField(productionObj, 'prd'))" class="kingdee-tab-dot"></span>
</span>
<div class="kingdee-tab-toolbar">
<el-button type="primary" size="mini" icon="el-icon-refresh" :loading="kingdeePrdMoLoading" @click="fetchKingdeePrdMo">
刷新
</el-button>
</div>
<el-table
:data="kingdeePrdMoList"
v-loading="kingdeePrdMoLoading"
border
stripe
size="small"
class="kingdee-order-table"
empty-text="暂无生产订单数据"
>
<el-table-column label="生产订单编号" min-width="130" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FBillNo || '—' }}</template>
</el-table-column>
<el-table-column label="工艺路线编码" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FRoutingId.FNumber'] || '—' }}</template>
</el-table-column>
<el-table-column label="生产车间" min-width="100" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FWorkShopID.FName'] || '—' }}</template>
</el-table-column>
<el-table-column label="物料编码" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FMaterialId.FNumber'] || '—' }}</template>
</el-table-column>
<el-table-column label="物料名称" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FMaterialName || '—' }}</template>
</el-table-column>
<el-table-column label="数量" width="80" align="right">
<template slot-scope="scope">{{ scope.row.FQty != null ? scope.row.FQty : '—' }}</template>
</el-table-column>
<el-table-column label="材质" min-width="90" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.F_UCHN_BaseProperty_qtr || '—' }}</template>
</el-table-column>
<el-table-column label="单位" width="70" align="center">
<template slot-scope="scope">{{ scope.row['FUnitId.FName'] || '—' }}</template>
</el-table-column>
<el-table-column label="计划开工" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FPlanStartDate) }}</template>
</el-table-column>
<el-table-column label="计划完工" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FPlanFinishDate) }}</template>
</el-table-column>
<el-table-column label="领料状态" width="100" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="getPickMtrlStatusTag(scope.row.FPickMtrlStatus)">
{{ formatPickMtrlStatus(scope.row.FPickMtrlStatus) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane name="purchaseOrder">
<span slot="label" class="kingdee-tab-label">
<i class="el-icon-shopping-cart-2"></i>
采购订单
<span v-if="isKingdeeExecuted(getKingdeeField(productionObj, 'po'))" class="kingdee-tab-dot"></span>
</span>
<div class="kingdee-tab-toolbar">
<el-button type="primary" size="mini" icon="el-icon-refresh" :loading="kingdeePurchaseLoading" @click="fetchKingdeePurchaseOrder">
刷新
</el-button>
</div>
<el-table
:data="kingdeePurchaseOrderList"
v-loading="kingdeePurchaseLoading"
border
stripe
size="small"
class="kingdee-order-table"
empty-text="暂无采购订单数据"
>
<el-table-column label="采购订单编号" min-width="130" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FBillNo || '—' }}</template>
</el-table-column>
<el-table-column label="物料编码" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FMaterialId.FNumber'] || '—' }}</template>
</el-table-column>
<el-table-column label="物料名称" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.FMaterialName || '—' }}</template>
</el-table-column>
<el-table-column label="采购数量" width="90" align="right">
<template slot-scope="scope">{{ scope.row.FQty != null ? scope.row.FQty : '—' }}</template>
</el-table-column>
<el-table-column label="订单日期" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FDate) }}</template>
</el-table-column>
<el-table-column label="交货日期" min-width="110" align="center">
<template slot-scope="scope">{{ formatKingdeeDate(scope.row.FDeliveryDate) }}</template>
</el-table-column>
<el-table-column label="剩余未入库" width="100" align="right">
<template slot-scope="scope">{{ scope.row.FRemainStockINQty != null ? scope.row.FRemainStockINQty : '—' }}</template>
</el-table-column>
<el-table-column label="MRP关闭状态" width="110" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.FMRPCloseStatus === 'B' ? 'info' : 'success'">
{{ formatMrpCloseStatus(scope.row.FMRPCloseStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="供应商" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row['FSupplierId.FName'] || '—' }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</el-drawer>
<el-drawer
:title="bomDrawerTitle"
:visible.sync="bomDrawer"
direction="rtl"
size="92%"
append-to-body
:modal-append-to-body="true"
custom-class="order-pro-drawer bom-detail-drawer"
>
<div class="drawer-body drawer-body--bom">
<bomInfo v-if="bomDrawer" :params="bomParams" />
</div>
</el-drawer>
<el-drawer
title="工艺路线"
:visible.sync="processDrawer"
direction="rtl"
size="90%"
append-to-body
:modal-append-to-body="true"
custom-class="order-pro-drawer"
>
<div class="drawer-body">
<processInfo v-if="processDrawer" :params="processParams" />
</div>
</el-drawer>
<!-- 添加或修改项目令号对话框 -->
<el-dialog
:visible.sync="open"
width="680px"
append-to-body
custom-class="order-pro-form-dialog"
:close-on-click-modal="false"
>
<div slot="title" class="order-form-dialog__title">
<span class="order-form-dialog__title-icon">
<i :class="form.id != null ? 'el-icon-edit-outline' : 'el-icon-folder-add'"></i>
</span>
<div class="order-form-dialog__title-text">
<h3>{{ title }}</h3>
<p>{{ form.id != null ? '更新项目基础信息与图纸配置' : '创建新项目,金蝶执行状态默认为未执行' }}</p>
</div>
</div>
<div class="order-form-dialog__body">
<el-form ref="form" :model="form" :rules="rules" label-width="108px" class="order-form-dialog__form">
<section class="order-form-section">
<header class="order-form-section__head">
<i class="el-icon-document"></i>
<span>基本信息</span>
</header>
<div class="order-form-section__grid order-form-section__grid--2">
<el-form-item label="生产令号" prop="productionOrderNo">
<el-input v-model="form.productionOrderNo" placeholder="请输入生产令号" prefix-icon="el-icon-tickets" />
</el-form-item>
<el-form-item label="项目名称" prop="productionName">
<el-input v-model="form.productionName" placeholder="请输入项目名称" prefix-icon="el-icon-office-building" />
</el-form-item>
<el-form-item label="来源" prop="source">
<el-select v-model="form.source" placeholder="请选择来源" style="width: 100%">
<el-option label="手动" value="手动" />
<el-option label="PLM" value="PLM" />
</el-select>
</el-form-item>
<el-form-item label="计量单位" prop="unit">
<el-input v-model="form.unit" placeholder="请输入计量单位" prefix-icon="el-icon-c-scale-to-original" />
</el-form-item>
</div>
</section>
<section class="order-form-section">
<header class="order-form-section__head order-form-section__head--green">
<i class="el-icon-picture-outline"></i>
<span>图纸设置</span>
</header>
<el-form-item label="执行单图" class="order-form-item--full">
<div class="order-form-upload-wrap">
<image-upload v-model="form.drawingPath" :limit="9" :file-size="5" />
</div>
</el-form-item>
<div class="order-form-toggle-grid">
<div class="order-form-toggle-card">
<div class="order-form-toggle-card__info">
<i class="el-icon-files"></i>
<div>
<strong>保留零件图</strong>
<span>是否在系统中保留零件图纸</span>
</div>
</div>
<el-switch
v-model="form.quantity"
:active-value="1"
:inactive-value="0"
active-text="是"
inactive-text="否"
/>
</div>
<div class="order-form-toggle-card">
<div class="order-form-toggle-card__info">
<i class="el-icon-download"></i>
<div>
<strong>下载图纸</strong>
<span>是否允许下载项目图纸</span>
</div>
</div>
<el-switch
v-model="form.isDownload"
:active-value="1"
:inactive-value="0"
active-text="是"
inactive-text="否"
/>
</div>
<div class="order-form-toggle-card">
<div class="order-form-toggle-card__info">
<i class="el-icon-s-operation"></i>
<div>
<strong>是否做工艺</strong>
<span>该项目是否需要编制工艺</span>
</div>
</div>
<el-switch
v-model="form.needProcess"
:active-value="1"
:inactive-value="0"
active-text="是"
inactive-text="否"
/>
</div>
</div>
</section>
<section class="order-form-section order-form-section--last">
<header class="order-form-section__head order-form-section__head--slate">
<i class="el-icon-time"></i>
<span>时间与进度</span>
</header>
<div class="order-form-section__grid order-form-section__grid--2">
<el-form-item label="完成时间" prop="projectEndTime" class="order-form-item--full-span">
<el-date-picker
clearable
v-model="form.projectEndTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择项目完成时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="工艺上传更新" prop="routeUptime">
<el-date-picker
clearable
v-model="form.routeUptime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="排产计划更新" prop="planUptime">
<el-date-picker
clearable
v-model="form.planUptime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="BOM/物料更新" prop="bomUptime" class="order-form-item--full-span">
<el-date-picker
clearable
v-model="form.bomUptime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择时间"
style="width: 100%"
/>
</el-form-item>
</div>
</section>
</el-form>
</div>
<div slot="footer" class="order-form-dialog__footer">
<el-button class="order-form-dialog__btn-cancel" @click="cancel">取 消</el-button>
<el-button :loading="buttonLoading" type="primary" class="order-form-dialog__btn-submit" @click="submitForm">
<i v-if="!buttonLoading" class="el-icon-check"></i>
{{ form.id != null ? '保存修改' : '确认新增' }}
</el-button>
</div>
</el-dialog>
<el-dialog
:visible.sync="execImagePreviewVisible"
:title="execImagePreviewTitle"
width="860px"
append-to-body
custom-class="exec-image-dialog"
>
<div v-if="execImagePreviewList.length" class="exec-image-dialog__grid">
<el-image
v-for="(img, idx) in execImagePreviewList"
:key="idx"
:src="img"
:preview-src-list="execImagePreviewList"
fit="cover"
class="exec-image-dialog__thumb"
/>
</div>
<el-empty v-else description="暂无执行单图" :image-size="72" />
</el-dialog>
<el-dialog
:visible.sync="drawingWorkloadVisible"
:title="drawingWorkloadTitle"
width="960px"
append-to-body
top="6vh"
>
<div v-if="drawingWorkload" class="drawing-workload">
<div class="drawing-workload__meta">
<span>生产令号:{{ drawingWorkload.productionOrderNo || '-' }}</span>
<span>同步时间:{{ drawingWorkload.syncTime || '-' }}</span>
<span>明细:{{ drawingWorkload.itemCount || 0 }} 条</span>
<span>等效 A4 合计:<b>{{ drawingWorkload.equivalentA4Total != null ? drawingWorkload.equivalentA4Total : '-' }}</b></span>
</div>
<el-table
:data="drawingWorkloadSummaryRows"
border
size="mini"
style="width: 100%; margin-bottom: 14px"
>
<el-table-column
v-for="col in drawingWorkloadSummaryCols"
:key="col.prop"
:prop="col.prop"
:label="col.label"
align="center"
min-width="88"
/>
</el-table>
<el-table
:data="drawingWorkload.items || []"
border
size="mini"
max-height="420"
style="width: 100%"
>
<el-table-column type="index" label="#" width="50" align="center" />
<el-table-column prop="drawingNo" label="图号" min-width="160" show-overflow-tooltip />
<el-table-column prop="drawingName" label="名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="frameName" label="图框名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="paperSize" label="图纸幅面" width="110" align="center" />
<el-table-column prop="equivalentA4" label="等效A4" width="90" align="center" />
</el-table>
</div>
<el-empty v-else description="暂无图纸工作量数据" :image-size="72" />
<div slot="footer" class="dialog-footer">
<el-button
type="primary"
:loading="drawingWorkloadLoading"
@click="syncDrawingWorkload"
v-hasPermi="['system:orderPro:drawingWorkload']"
>重新读取</el-button>
<el-button @click="drawingWorkloadVisible = false">关 闭</el-button>
</div>
</el-dialog>
<!-- 批量设置项目状态 -->
<el-dialog
title="设置项目状态"
:visible.sync="statusDialogVisible"
width="420px"
append-to-body
@closed="statusForm.bomStatus = undefined"
>
<el-form ref="statusForm" :model="statusForm" :rules="statusRules" label-width="90px">
<el-form-item label="已选项目">
<span>{{ ids.length }} 个</span>
</el-form-item>
<el-form-item label="项目状态" prop="bomStatus">
<el-select
v-model="statusForm.bomStatus"
placeholder="请选择项目状态"
clearable
style="width: 100%"
>
<el-option
v-for="dict in dict.type.process_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="statusSubmitting" @click="submitStatusForm">确 定</el-button>
<el-button @click="statusDialogVisible = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<style>
/* ── 新增/修改项目对话框 ── */
.order-pro-form-dialog {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(26, 41, 66, 0.18), 0 4px 16px rgba(26, 41, 66, 0.08);
}
.order-pro-form-dialog .el-dialog__header {
margin: 0;
padding: 20px 24px 18px;
background: linear-gradient(135deg, #1a2942 0%, #2c5282 100%);
border-bottom: none;
}
.order-pro-form-dialog .el-dialog__headerbtn {
top: 18px;
right: 18px;
}
.order-pro-form-dialog .el-dialog__headerbtn .el-dialog__close {
color: rgba(255, 255, 255, 0.75);
font-size: 18px;
}
.order-pro-form-dialog .el-dialog__headerbtn:hover .el-dialog__close {
color: #fff;
}
.order-pro-form-dialog .el-dialog__body {
padding: 0;
background: #f4f6f9;
}
.order-pro-form-dialog .el-dialog__footer {
padding: 14px 24px 18px;
background: #fff;
border-top: 1px solid #e4e9f0;
}
.order-form-dialog__title {
display: flex;
align-items: center;
gap: 14px;
}
.order-form-dialog__title-icon {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.18);
color: #fff;
font-size: 22px;
flex-shrink: 0;
}
.order-form-dialog__title-text h3 {
margin: 0 0 4px;
font-size: 17px;
font-weight: 600;
color: #fff;
letter-spacing: 0.02em;
}
.order-form-dialog__title-text p {
margin: 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.65);
line-height: 1.4;
}
.order-form-dialog__body {
max-height: calc(88vh - 180px);
overflow-y: auto;
padding: 18px 20px 8px;
}
.order-form-dialog__body::-webkit-scrollbar {
width: 6px;
}
.order-form-dialog__body::-webkit-scrollbar-thumb {
background: #c5cdd8;
border-radius: 3px;
}
.order-form-dialog__form .el-form-item {
margin-bottom: 16px;
}
.order-form-dialog__form .el-form-item__label {
color: #3d4f66;
font-weight: 500;
line-height: 32px;
}
.order-form-dialog__form .el-input__inner,
.order-form-dialog__form .el-select .el-input__inner {
border-radius: 8px;
border-color: #dce3ec;
transition: border-color 0.2s, box-shadow 0.2s;
}
.order-form-dialog__form .el-input__inner:focus,
.order-form-dialog__form .el-select .el-input.is-focus .el-input__inner {
border-color: #2c5282;
box-shadow: 0 0 0 2px rgba(44, 82, 130, 0.12);
}
.order-form-section {
margin-bottom: 14px;
padding: 16px 18px 4px;
background: #fff;
border: 1px solid #e4e9f0;
border-radius: 10px;
box-shadow: 0 1px 4px rgba(26, 41, 66, 0.04);
animation: orderFormSectionIn 0.35s ease-out both;
}
.order-form-section:nth-child(2) { animation-delay: 0.05s; }
.order-form-section:nth-child(3) { animation-delay: 0.1s; }
.order-form-section:nth-child(4) { animation-delay: 0.15s; }
.order-form-section--last {
margin-bottom: 6px;
}
@keyframes orderFormSectionIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.order-form-section__head {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 14px;
padding-bottom: 10px;
border-bottom: 1px solid #eef2f6;
font-size: 13px;
font-weight: 600;
color: #2c5282;
letter-spacing: 0.04em;
}
.order-form-section__head i {
font-size: 15px;
opacity: 0.85;
}
.order-form-section__head--green { color: #1e7e4a; }
.order-form-section__head--amber { color: #b45309; }
.order-form-section__head--slate { color: #475569; }
.order-form-section__grid {
display: grid;
gap: 0 16px;
}
.order-form-section__grid--2 {
grid-template-columns: 1fr 1fr;
}
.order-form-item--full {
margin-bottom: 12px;
}
.order-form-item--full >>> .el-form-item__content {
line-height: normal;
}
.order-form-item--full-span {
grid-column: 1 / -1;
}
.order-form-upload-wrap {
padding: 12px;
border-radius: 8px;
background: #f8fafc;
border: 1px dashed #dce3ec;
}
.order-form-toggle-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 12px;
}
.order-form-toggle-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-radius: 8px;
background: #f8fafc;
border: 1px solid #e4e9f0;
transition: border-color 0.2s, background 0.2s;
}
.order-form-toggle-card:hover {
border-color: #b3cce8;
background: #f0f5fa;
}
.order-form-toggle-card__info {
display: flex;
align-items: flex-start;
gap: 10px;
min-width: 0;
}
.order-form-toggle-card__info > i {
margin-top: 2px;
font-size: 16px;
color: #2c5282;
flex-shrink: 0;
}
.order-form-toggle-card__info strong {
display: block;
font-size: 13px;
color: #1a2942;
margin-bottom: 2px;
}
.order-form-toggle-card__info span {
display: block;
font-size: 11px;
color: #909399;
line-height: 1.35;
}
.order-form-kingdee-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 8px;
}
.order-form-kingdee-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
border-radius: 10px;
border: 1px solid #e4e9f0;
background: #fafbfc;
transition: border-color 0.25s, box-shadow 0.25s, background 0.25s;
}
.order-form-kingdee-card--mo.order-form-kingdee-card--on {
border-color: #93c5fd;
background: linear-gradient(160deg, #eff6ff 0%, #dbeafe 100%);
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.12);
}
.order-form-kingdee-card--po.order-form-kingdee-card--on {
border-color: #fcd34d;
background: linear-gradient(160deg, #fffbeb 0%, #fef3c7 100%);
box-shadow: 0 4px 14px rgba(217, 119, 6, 0.12);
}
.order-form-kingdee-card__icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 16px;
}
.order-form-kingdee-card--mo .order-form-kingdee-card__icon {
color: #2563eb;
background: rgba(37, 99, 235, 0.12);
}
.order-form-kingdee-card--po .order-form-kingdee-card__icon {
color: #d97706;
background: rgba(217, 119, 6, 0.12);
}
.order-form-kingdee-card__body strong {
display: block;
font-size: 13px;
color: #1a2942;
margin-bottom: 3px;
}
.order-form-kingdee-card__body span {
display: block;
font-size: 11px;
color: #909399;
line-height: 1.4;
}
.order-form-kingdee-card >>> .el-switch {
align-self: flex-end;
}
.order-form-dialog__footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.order-form-dialog__btn-cancel {
border-radius: 8px;
padding: 10px 22px;
border-color: #dce3ec;
color: #5c6b7f;
}
.order-form-dialog__btn-cancel:hover {
color: #2c5282;
border-color: #b3cce8;
background: #f0f5fa;
}
.order-form-dialog__btn-submit {
border-radius: 8px;
padding: 10px 24px;
background: linear-gradient(135deg, #2c5282 0%, #3a6ea5 100%);
border: none;
font-weight: 600;
letter-spacing: 0.03em;
box-shadow: 0 4px 12px rgba(44, 82, 130, 0.28);
transition: box-shadow 0.2s, transform 0.15s;
}
.order-form-dialog__btn-submit:hover,
.order-form-dialog__btn-submit:focus {
background: linear-gradient(135deg, #234a75 0%, #2c5282 100%);
box-shadow: 0 6px 16px rgba(44, 82, 130, 0.35);
}
.order-form-dialog__btn-submit:active {
transform: translateY(1px);
}
@media (max-width: 720px) {
.order-pro-form-dialog {
width: 94vw !important;
}
.order-form-section__grid--2,
.order-form-toggle-grid,
.order-form-kingdee-grid {
grid-template-columns: 1fr;
}
}
.order-pro-page .action-dropdown.el-dropdown {
vertical-align: middle;
}
.order-pro-page .el-dropdown + .el-dropdown {
margin-left: 15px;
}
.order-pro-page .el-icon-arrow-down {
font-size: 12px;
}
.order-pro-drawer.el-drawer.rtl {
display: flex;
flex-direction: column;
max-width: 1500px;
}
.order-pro-drawer .el-drawer__header {
flex-shrink: 0;
margin-bottom: 0;
padding: 18px 24px;
border-bottom: 1px solid #e4e9f0;
background: linear-gradient(135deg, #f8fafc 0%, #eef2f7 100%);
color: #1a2942;
font-weight: 600;
font-size: 16px;
}
.order-pro-drawer .el-drawer__body {
flex: 1;
min-height: 0;
padding: 0;
background: #f4f6f9;
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
</style>
<script>
import {
listOrderPro,
getOrderPro,
delOrderPro,
addOrderPro,
updateOrderPro,
getDrawingTypeDict,
listSave,
executDrawing,
delSave,
uploadPDF,
downloadPlmDwg,
exportRoute,
exportRoute3,
sendProcessRoutePlanExcel,
getRoutePlanSendCount,
syncDrawingWorkload as syncDrawingWorkloadApi,
getMRPResults,
getRouteLog,
getBomUploadStatus,
getPlotState,
getKingdeePrdMo,
getKingdeePurchaseOrder,
} from "@/api/system/orderPro";
import { processList } from "../../../api/system/orderPro";
import { listRigidChain } from "@/api/system/pcRigidChain";
import { addProduct, updateProduct } from "@/api/system/orderPro";
import { listConfigIni } from "@/api/system/configIni";
import ImageUpload from "@/components/ImageUpload";
import { getToken } from "@/utils/auth";
import axios from "axios";
import bomInfo from "@/views/system/details/index.vue"; //bom明细
import processInfo from "@/views/system/route/index.vue"; //工艺信息
import { listMrp, exportMrp } from "@/api/system/mrp";
import { importElectricalBom } from "@/api/system/details";
import { updateProductionOrders, updateCgOrders } from "@/api/system/route";
import dayjs from "dayjs";
export default {
name: "OrderPro",
dicts: ["process_status", "plan_proer"],
components: {
ImageUpload,
bomInfo,
processInfo,
},
data() {
return {
// 定时器
time: null,
// 进度
percentage: 0,
drawer: false,
direction: "rtl",
drawerActiveTab: "project",
kingdeeDrawer: false,
kingdeeActiveTab: "prdMo",
kingdeePrdMoList: [],
kingdeePrdMoLoading: false,
kingdeePurchaseOrderList: [],
kingdeePurchaseLoading: false,
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
showAdvancedSearch: false,
// 总条数
total: 0,
// 项目令号表格数据
orderProList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
execImagePreviewVisible: false,
execImagePreviewTitle: "执行单图",
execImagePreviewList: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
productionOrderNo: undefined,
productionName: undefined,
drawingNo: undefined,
drawingName: undefined,
bomStatus: undefined,
routePlanSendFilter: undefined,
routeStatus: undefined,
planEndTime: undefined,
planStartTime: undefined,
drawingType: undefined,
projectEndTime: undefined,
createBy: undefined,
unit: undefined,
quantity: undefined,
isEnterpriseStandard: undefined,
isEnterpriseStandardList: undefined,
whetherDelay: undefined,
source: undefined,
needProcess: undefined,
drawingPath: undefined,
routeUptime: undefined,
planUptime: undefined,
bomUptime: undefined,
drawingTime: undefined,
bomCount: undefined,
materialCount: undefined,
routeCount: undefined,
drawingBy: undefined,
},
// 表单参数
form: {},
//bom明细
bomDrawer: false,
bomParams: {},
//工艺信息
processDrawer: false,
processParams: {},
// 图纸类型选项
drawingTypeOptions: [],
// MRP运算结果复查相关数据
mrpResultData: [],
mrpLoading: false,
mrpTotal: 0,
mrpSearchKeyword: "",
mrpQueryParams: {
pageNum: 1,
pageSize: 10,
materialName: "",
materialCode: "",
orderProId: null,
},
loadingMRP: false,
mrpOperationStatus: "", // MRP运算状态
exportLoading: false, // 导出加载状态
mrpStats: {
totalMaterials: 0,
positiveStock: 0,
negativeStock: 0,
zeroStock: 0,
},
// 表单校验
rules: {
productionOrderNo: [
{ required: true, message: "生产令号不能为空", trigger: "blur" },
],
productionName: [
{ required: true, message: "项目名称不能为空", trigger: "blur" },
],
drawingNo: [
{ required: true, message: "图号不能为空", trigger: "blur" },
],
drawingName: [
{ required: true, message: "名称不能为空", trigger: "blur" },
],
drawingType: [
{
validator: (rule, value, callback) => {
if (this.form.isEnterpriseStandard === 1 && !value) {
callback(new Error("图纸类型不能为空"));
} else {
callback();
}
},
trigger: "change",
},
],
projectEndTime: [
{ required: true, message: "项目完成时间不能为空", trigger: "blur" },
],
unit: [
{ required: true, message: "计量单位不能为空", trigger: "blur" },
],
quantity: [
{ required: true, message: "数量不能为空", trigger: "blur" },
],
isDownload: [
{ required: true, message: "是否允许下载不能为空", trigger: "change" },
],
isEnterpriseStandard: [
{
required: true,
message: "是否企标 (0-否, 1-是)不能为空",
trigger: "blur",
},
],
/* drawingPath: [
{
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('图纸路径不能为空'));
}
// 你可以加更多条件
callback();
},
trigger: "blur"
}
]*/
},
productionObj: null,
id: "",
// 销齿链型号表单
chainTable: [], // 销齿链型号表格数据
chainForm: {
pid: "",
productionCode: "",
productType: "",
figureNumber: "",
figureName: "",
figureNum: "",
jdInventory: "",
axialType: "",
boxType: "",
selectedFileType: "",
drawPath: "",
fileType: "",
drawBy: "",
drawTime: "",
id: undefined,
},
chainDialogVisible: false, // 控制新增/编辑弹窗
isEditChain: false, // 区分新增/编辑
editChainIndex: null, // 编辑时的索引
rigidChainOptions: [],
typeOptions: [],
journeyOptions: [],
chainLoading: false,
selectedChainIds: [],
drawLog: "", // 出图日志内容
drawLoading: false, // 出图按钮 loading
sendProcessRoutePlanLoading: false,
downloadPlmDwgLoading: false,
drawingWorkloadLoading: false,
drawingWorkloadVisible: false,
drawingWorkload: null,
statusDialogVisible: false,
statusSubmitting: false,
statusForm: {
bomStatus: undefined,
},
statusRules: {
bomStatus: [
{ required: true, message: "请选择项目状态", trigger: "change" },
],
},
/** 工艺计划表钉钉已成功发送次数,默认 0当前抽屉项目 */
routePlanSendCount: 0,
/** 列表各行计划表发送次数key 为项目 id */
routePlanSendCountMap: {},
/** 顶部概览统计 */
overviewStatsLoading: false,
overviewStats: {
total: 0,
delayed: 0,
change: 0,
inProduction: 0,
},
plotStatusText: "待获取",
plotStatusType: "info",
plotStatusLoading: false,
plotOutputPath: "",
plotProjectName: "",
plotTargetDrawing: "",
plotStateMsg: "",
currentPid: null, // 当前项目id
configIniOptions: [], // 文件类型下拉选项
dwgUploadUrl: process.env.VUE_APP_BASE_API + "/system/orderPro/uploadDwg",
uploadHeaders: {
Authorization: "Bearer " + getToken(),
},
// DWG 上传大小阈值MB用于前端预校验与提示
dwgMaxSizeMB: 100,
// 工艺上传日志
processUploadLog: {
time: null,
successfulRoutes: [],
duplicateRoutes: [],
failedRoutes: [],
},
processLogLoading: false,
// 工艺日志分页
processSuccessPage: { pageNum: 1, pageSize: 10 },
processFailedPage: { pageNum: 1, pageSize: 10 },
// BOM上传日志
bomUploadLog: {
items: [],
},
bomLogLoading: false,
// BOM日志分页
bomSuccessPage: { pageNum: 1, pageSize: 10 },
bomExistsPage: { pageNum: 1, pageSize: 10 },
bomFailedPage: { pageNum: 1, pageSize: 10 },
};
},
created() {
this.getList();
this.fetchOverviewStats();
this.getDrawingTypeOptions();
this.getConfigIniOptions(); // 新增
this.$api = { addProduct, updateProduct };
this.fetchPlotStatus();
},
watch: {
"dict.type.process_status"() {
this.fetchOverviewStats();
},
},
computed: {
/** 折叠区内已选筛选项数量(收起时角标) */
advancedFilterCount() {
const q = this.queryParams || {};
let count = 0;
if (q.drawingNo) count++;
if (q.drawingName) count++;
if (q.whetherDelay === 0 || q.whetherDelay === 1) count++;
if (q.bomStatus !== undefined && q.bomStatus !== null && q.bomStatus !== "") count++;
if (q.routeStatus === 0 || q.routeStatus === 1 || q.routeStatus === 2) count++;
if (q.routePlanSendFilter) count++;
if (q.needProcess === 0 || q.needProcess === 1) count++;
if (Array.isArray(q.isEnterpriseStandardList) && q.isEnterpriseStandardList.length > 0) count++;
return count;
},
orderTableHeaderStyle() {
return {
background: "#eef2f7",
color: "#1a2942",
fontWeight: "600",
fontSize: "13px",
borderBottom: "2px solid #c9a227",
};
},
drawingWorkloadTitle() {
const no = this.drawingWorkload && this.drawingWorkload.productionOrderNo;
return no ? `图纸工作量 · ${no}` : "图纸工作量";
},
drawingWorkloadSummaryCols() {
return [
{ prop: "a4Count", label: "A4" },
{ prop: "a3Count", label: "A3" },
{ prop: "a3HalfCount", label: "A3+1/2" },
{ prop: "a3Plus1Count", label: "A3+1" },
{ prop: "a2Count", label: "A2" },
{ prop: "a2HalfCount", label: "A2+1/2" },
{ prop: "a1Count", label: "A1" },
{ prop: "a1HalfCount", label: "A1+1/2" },
{ prop: "a0Count", label: "A0" },
{ prop: "a0HalfCount", label: "A0+1/2" },
{ prop: "equivalentA4Total", label: "等效A4合计" },
];
},
drawingWorkloadSummaryRows() {
if (!this.drawingWorkload) {
return [];
}
const w = this.drawingWorkload;
return [{
a4Count: w.a4Count,
a3Count: w.a3Count,
a3HalfCount: w.a3HalfCount,
a3Plus1Count: w.a3Plus1Count,
a2Count: w.a2Count,
a2HalfCount: w.a2HalfCount,
a1Count: w.a1Count,
a1HalfCount: w.a1HalfCount,
a0Count: w.a0Count,
a0HalfCount: w.a0HalfCount,
equivalentA4Total: w.equivalentA4Total,
}];
},
bomDrawerTitle() {
const orderNo = this.bomParams.productionOrderNo || (this.productionObj && this.productionObj.productionOrderNo);
const name = this.bomParams.productionName || (this.productionObj && this.productionObj.productionName);
if (orderNo && name) {
return "BOM明细 · " + orderNo + " · " + name;
}
return orderNo ? "BOM明细 · " + orderNo : "BOM明细";
},
processLogCounts() {
const success = Array.isArray(this.processUploadLog?.successfulRoutes)
? this.processUploadLog.successfulRoutes.length
: 0;
const duplicate = Array.isArray(this.processUploadLog?.duplicateRoutes)
? this.processUploadLog.duplicateRoutes.length
: 0;
const failed = Array.isArray(this.processUploadLog?.failedRoutes)
? this.processUploadLog.failedRoutes.length
: 0;
return { success, duplicate, failed };
},
successFlatRows() {
const rows = [];
const list = this.processUploadLog?.successfulRoutes || [];
list.forEach((item) => {
const base = {
materialCode: item.materialCode || "",
materialName: item.materialName || "",
};
const details = Array.isArray(item.processRouteDT)
? item.processRouteDT
: [];
details.forEach((dt) => {
// 兼容两种字段命名:
// 1) 我们的前端命名processNo / workCenter / processName / activityUnit / processControl / activityDuration / processDescription / processNameDescription
// 2) 后端K3字段命名FOperNumber / FWorkCenterId.FName / FProcessId.FName / FActivity1UnitID.FName / FOptCtrlCodeId.FName / FActivity1Qty / FOperDescription / FNumber
const processNo = dt.processNo ?? dt.FOperNumber ?? dt["FOperNumber"];
const workCenter =
dt.workCenter ??
(dt.FWorkCenterId && dt.FWorkCenterId.FName) ??
dt["FWorkCenterId.FName"];
const processName =
dt.processName ??
(dt.FProcessId && dt.FProcessId.FName) ??
dt["FProcessId.FName"];
const activityUnit =
dt.activityUnit ??
(dt.FActivity1UnitID && dt.FActivity1UnitID.FName) ??
dt["FActivity1UnitID.FName"];
const processControl =
dt.processControl ??
(dt.FOptCtrlCodeId && dt.FOptCtrlCodeId.FName) ??
dt["FOptCtrlCodeId.FName"];
const activityDuration =
dt.activityDuration ?? dt.FActivity1Qty ?? dt["FActivity1Qty"];
const processDescription =
dt.processDescription ??
dt.FOperDescription ??
dt["FOperDescription"];
const processNameDescription =
dt.processNameDescription ?? dt.FNumber ?? dt["FNumber"];
rows.push({
...base,
processNo,
workCenter,
processName,
activityUnit,
processControl,
activityDuration,
processDescription,
processNameDescription,
});
});
});
return rows;
},
// 工艺成功项(分页)
successFlatPagedRows() {
const rows = this.successFlatRows;
const start =
(this.processSuccessPage.pageNum - 1) *
this.processSuccessPage.pageSize;
const end = start + this.processSuccessPage.pageSize;
return rows.slice(start, end);
},
// 工艺失败项(分页)
processFailedPagedItems() {
const items = Array.isArray(this.processUploadLog?.failedRoutes)
? this.processUploadLog.failedRoutes
: [];
const start =
(this.processFailedPage.pageNum - 1) * this.processFailedPage.pageSize;
const end = start + this.processFailedPage.pageSize;
return items.slice(start, end);
},
// BOM上传日志统计
bomLogCounts() {
const items = Array.isArray(this.bomUploadLog?.items)
? this.bomUploadLog.items
: [];
const success = items.filter((item) => item.code === "200").length;
const exists = items.filter((item) => item.code === "100").length;
const failed = items.filter(
(item) =>
item.code === "300" || (item.code !== "200" && item.code !== "100")
).length;
return {
success,
exists,
failed,
total: items.length,
};
},
// BOM成功项
bomSuccessItems() {
const items = Array.isArray(this.bomUploadLog?.items)
? this.bomUploadLog.items
: [];
return items.filter((item) => item.code === "200");
},
// BOM已存在项
bomExistsItems() {
const items = Array.isArray(this.bomUploadLog?.items)
? this.bomUploadLog.items
: [];
return items.filter((item) => item.code === "100");
},
// BOM失败项
bomFailedItems() {
const items = Array.isArray(this.bomUploadLog?.items)
? this.bomUploadLog.items
: [];
return items.filter(
(item) =>
item.code === "300" || (item.code !== "200" && item.code !== "100")
);
},
// BOM成功项分页
bomSuccessPagedItems() {
const items = this.bomSuccessItems;
const start =
(this.bomSuccessPage.pageNum - 1) * this.bomSuccessPage.pageSize;
const end = start + this.bomSuccessPage.pageSize;
return items.slice(start, end);
},
// BOM已存在项分页
bomExistsPagedItems() {
const items = this.bomExistsItems;
const start =
(this.bomExistsPage.pageNum - 1) * this.bomExistsPage.pageSize;
const end = start + this.bomExistsPage.pageSize;
return items.slice(start, end);
},
// BOM失败项分页
bomFailedPagedItems() {
const items = this.bomFailedItems;
const start =
(this.bomFailedPage.pageNum - 1) * this.bomFailedPage.pageSize;
const end = start + this.bomFailedPage.pageSize;
return items.slice(start, end);
},
plotDetailText() {
const parts = [];
if (this.plotOutputPath) parts.push(`路径: ${this.plotOutputPath}`);
if (this.plotProjectName) parts.push(`项目: ${this.plotProjectName}`);
if (this.plotTargetDrawing) parts.push(`图纸: ${this.plotTargetDrawing}`);
if (this.plotStateMsg) parts.push(`提示: ${this.plotStateMsg}`);
return parts.join(' | ');
},
},
methods: {
// 解析 /system/orderPro/getstate 返回的出图状态(兼容拦截器与字符串 JSON
parsePlotStateResponse(res) {
if (!res) {
return null;
}
let stateObj = null;
if (res.code === 200 && typeof res.data === "string") {
try {
stateObj = JSON.parse(res.data);
} catch (e) {
console.error("Failed to parse getstate data:", e);
}
} else if (typeof res === "string") {
try {
stateObj = JSON.parse(res);
} catch (e) {
stateObj = null;
}
} else if (res.data && typeof res.data === "object") {
stateObj = res.data;
} else if (res.processstate !== undefined) {
stateObj = res;
}
return stateObj;
},
normalizePlotProcessState(state) {
if (state === null || state === undefined || state === "") {
return null;
}
const n = Number(state);
return Number.isFinite(n) ? n : null;
},
clearDrawProgressTimer() {
if (this.time) {
clearInterval(this.time);
this.time = null;
}
},
applyPlotProgressFromMsg(msg) {
if (!msg) {
return;
}
const parts = String(msg).split(",");
if (parts.length <= 1) {
return;
}
const progress = parts[1]
.replace(".", "")
.replace(/\s/g, "")
.split("/");
const current = Number(progress[0]);
const total = Number(progress[1]);
if (total > 0 && Number.isFinite(current)) {
this.percentage = Number(((current / total) * 100).toFixed(2));
}
},
// 获取出图状态
fetchPlotStatus() {
this.plotStatusLoading = true;
getPlotState().then(res => {
this.plotStatusLoading = false;
const stateObj = this.parsePlotStateResponse(res);
if (stateObj && stateObj.processstate !== undefined) {
this.plotOutputPath = stateObj.outputpath || "";
const path = String(this.plotOutputPath || "");
const normalized = path.replace(/\\\\/g, "/");
this.plotProjectName = normalized.split("/").filter(Boolean).pop() || "";
const msg = stateObj.msg || "";
this.plotStateMsg = msg || "";
const match = String(msg).match(/未找到装配图[:]\s*([^,,。\s"]+)/);
this.plotTargetDrawing = (match && match[1]) ? match[1] : (stateObj.sourcedrawing || "");
const status = stateObj.processstate;
switch (String(status)) {
case '0':
this.plotStatusText = '无任务';
this.plotStatusType = 'info';
break;
case '1':
this.plotStatusText = '生成图纸中';
this.plotStatusType = 'primary';
break;
case '2':
this.plotStatusText = '导出明细中';
this.plotStatusType = 'primary';
break;
case '3':
this.plotStatusText = '导出PDF中';
this.plotStatusType = 'primary';
break;
case '4':
this.plotStatusText = '已完成';
this.plotStatusType = 'success';
break;
case '5':
this.plotStatusText = '出图失败';
this.plotStatusType = 'danger';
break;
default:
this.plotStatusText = '异常';
this.plotStatusType = 'warning';
}
} else {
this.plotStatusText = '解析状态失败';
this.plotStatusType = 'warning';
}
}).catch(err => {
this.plotStatusLoading = false;
this.plotStatusText = '获取失败';
this.plotStatusType = 'danger';
});
},
async fetchProcessLog() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.error("请先选择生产令号");
return;
}
this.processLogLoading = true;
try {
const res = await getRouteLog(this.productionObj.id);
if (res && (res.code === 200 || res.success)) {
// 后端返回的数据在 msg 字段中JSON 字符串),如果 msg 为空则尝试 data
const jsonStr = res.msg || res.data || "";
// 如果返回的字符串为空,提示无数据
if (!jsonStr || !jsonStr.trim()) {
this.$message.info("暂无工艺上传日志");
this.clearProcessLog();
return;
}
// 解析 JSON 字符串
let parsed = null;
try {
parsed = JSON.parse(jsonStr);
} catch (parseError) {
console.error("JSON 解析失败", parseError);
this.$message.error("日志数据格式错误,无法解析");
this.clearProcessLog();
return;
}
// 处理不同的数据格式
let latest = null;
// 如果是数组,选择时间最新的一条记录
if (Array.isArray(parsed)) {
if (parsed.length === 0) {
this.$message.info("暂无工艺上传日志");
this.clearProcessLog();
return;
}
latest = parsed.reduce((acc, cur) => {
const accTime =
acc && acc.time ? new Date(acc.time).getTime() : 0;
const curTime =
cur && cur.time ? new Date(cur.time).getTime() : 0;
return curTime >= accTime ? cur : acc;
});
}
// 如果是单个对象,直接使用
else if (parsed && typeof parsed === "object") {
latest = parsed;
} else {
this.$message.error("日志数据格式不正确");
this.clearProcessLog();
return;
}
if (!latest) {
this.$message.info("暂无工艺上传日志");
this.clearProcessLog();
return;
}
// 直接使用解析后的数据结构
// 处理时间dayjs 支持时间戳(毫秒)和日期字符串
this.processUploadLog = {
time: latest.time
? dayjs(latest.time).format("YYYY-MM-DD HH:mm:ss")
: null,
successfulRoutes: latest.successfulRoutes || [],
duplicateRoutes: latest.duplicateRoutes || [],
failedRoutes: latest.failedRoutes || [],
};
} else {
this.$message.error(
res && res.msg ? res.msg : "获取工艺上传日志失败"
);
}
} catch (e) {
this.$message.error("获取工艺上传日志失败");
} finally {
this.processLogLoading = false;
}
},
clearProcessLog() {
this.processUploadLog = {
time: null,
successfulRoutes: [],
duplicateRoutes: [],
failedRoutes: [],
};
// 重置分页
this.processSuccessPage = { pageNum: 1, pageSize: 10 };
this.processFailedPage = { pageNum: 1, pageSize: 10 };
},
// 获取BOM上传日志
async fetchBomLog() {
if (!this.productionObj || !this.productionObj.productionOrderNo) {
this.$message.error("请先选择生产令号");
return;
}
this.bomLogLoading = true;
try {
const res = await getBomUploadStatus(
this.productionObj.productionOrderNo
);
if (res && (res.code === 200 || res.success)) {
// 后端返回的数据在 data 字段中JSON 字符串),如果 data 为空则尝试 msg
const jsonStr = res.data || res.msg || "";
// 如果返回的字符串为空,提示无数据
if (!jsonStr || !jsonStr.trim()) {
this.$message.info("暂无BOM上传日志");
this.clearBomLog();
return;
}
// 解析 JSON 字符串
let parsed = null;
try {
parsed = JSON.parse(jsonStr);
} catch (parseError) {
console.error("JSON 解析失败", parseError);
this.clearBomLog();
return;
}
// 处理数据:如果是数组,直接使用;如果是对象且有 items 属性,使用 items
let items = [];
if (Array.isArray(parsed)) {
items = parsed;
} else if (
parsed &&
typeof parsed === "object" &&
Array.isArray(parsed.items)
) {
items = parsed.items;
}
this.bomUploadLog = {
items: items,
};
} else {
this.$message.error(res && res.msg ? res.msg : "获取BOM上传日志失败");
}
} catch (e) {
this.$message.error("获取BOM上传日志失败");
} finally {
this.bomLogLoading = false;
}
},
// 清空BOM日志
clearBomLog() {
this.bomUploadLog = {
items: [],
};
// 重置分页
this.bomSuccessPage = { pageNum: 1, pageSize: 10 };
this.bomExistsPage = { pageNum: 1, pageSize: 10 };
this.bomFailedPage = { pageNum: 1, pageSize: 10 };
},
// 格式化BOM错误信息
formatBomErrorReason(reason) {
if (!reason) return "";
// 如果 reason 是 JSON 字符串,尝试解析并格式化
try {
const parsed = JSON.parse(reason);
if (Array.isArray(parsed)) {
return parsed
.map((item) => {
const field = item.FieldName || "";
const message = item.Message || "";
return field ? `${field}: ${message}` : message;
})
.join("; ");
}
return reason;
} catch (e) {
// 如果不是 JSON直接返回原字符串
return reason;
}
},
formatFraction(value) {
// 如果本身就是分数字符串,直接返回
if (typeof value === "string" && value.includes("/")) {
return value;
}
// 如果是数字或数字字符串,直接原样返回(或保留三位小数)
if (!isNaN(value)) {
return parseFloat(value).toFixed(3);
}
return value; // 兜底
},
// 处理分数值,用于数据处理阶段
formatFractionValue(value) {
// 如果本身就是分数字符串,直接返回
if (typeof value === "string" && value.includes("/")) {
return value;
}
// 如果是数字,转换为数字
if (!isNaN(value) && value !== null && value !== undefined) {
return Number(value);
}
return 0; // 兜底
},
handleCommand(command, row) {
if ("生产计划" === command) {
this.$router.push({
path: "/system/operationPlanningMain",
query: {
hbytSclh: row && row.productionOrderNo ? row.productionOrderNo : undefined,
batchNo: row && row.productionOrderNo ? row.productionOrderNo : undefined
}
});
return;
}
if ("BOM" === command) {
this.handleBOM(row.productionOrderNo);
}
if ("修改" === command) {
this.handleUpdate(row);
}
if ("删除" === command) {
this.handleDelete(row);
}
if ("清空项目" === command) {
this.clearProject(row);
}
},
handleClose(done) {
this.drawerActiveTab = "project";
this.kingdeePrdMoList = [];
this.kingdeePurchaseOrderList = [];
done();
},
/** 获取图纸类型选项 */
getDrawingTypeOptions() {
getDrawingTypeDict()
.then((response) => {
this.drawingTypeOptions = response.rows.map((item) => ({
fileType: item.fileType,
fileUrl: item.fileUrl,
}));
})
.catch(() => {
this.drawingTypeOptions = [
{ fileType: "机械图纸", fileUrl: "mechanical" },
{ fileType: "电气图纸", fileUrl: "electrical" },
{ fileType: "工艺图纸", fileUrl: "process" },
{ fileType: "装配图纸", fileUrl: "assembly" },
];
});
},
productionCodeHandle(row) {
this.productionObj = row;
this.drawerActiveTab = "project";
this.kingdeePrdMoList = [];
this.kingdeePurchaseOrderList = [];
this.routePlanSendCount = Object.prototype.hasOwnProperty.call(this.routePlanSendCountMap, row.id)
? this.routePlanSendCountMap[row.id]
: 0;
this.drawer = true;
// 打开抽屉后加载表格数据
this.loadChainTableData();
// 自动加载工艺上传日志
if (row && row.id) {
this.fetchRoutePlanSendCount();
this.fetchProcessLog();
}
// 自动加载BOM上传日志
if (row && row.productionOrderNo) {
this.fetchBomLog();
}
},
/** 项目类型缩写文本 */
getProjectTypeLabel(code) {
if (!code) return "-";
const map = {
CP: "产品",
FB: "非标",
BG: "变更",
QB: "研发",
EY: "研发",
GZ: "固资",
ZP: "展品",
KC: "库存",
产品: "产品",
非标: "非标",
变更: "变更",
研发: "研发",
企标: "企标",
库存: "库存",
固资: "固资",
展品: "展品",
};
return map[String(code)] || code;
},
/** 项目类型样式 key */
getProjectTypeKey(code) {
const label = this.getProjectTypeLabel(code);
const keyMap = {
产品: "product",
非标: "custom",
变更: "change",
研发: "rd",
企标: "standard",
库存: "stock",
固资: "asset",
展品: "exhibit",
};
return keyMap[label] || "default";
},
/** 来源显示文本 */
formatSource(value) {
const text = value != null ? String(value).trim() : "";
return text || "手动";
},
/** 工艺审核状态文案0未审核 1已审核 2已拒绝 */
formatRouteStatus(value) {
const n = value === null || value === undefined || value === "" ? null : Number(value);
if (n === 0) return "未审核";
if (n === 1) return "已审核";
if (n === 2) return "已拒绝";
return "-";
},
routeStatusPillClass(value) {
const n = value === null || value === undefined || value === "" ? null : Number(value);
if (n === 1) return "status-pill--success";
if (n === 2) return "status-pill--danger";
if (n === 0) return "status-pill--warning";
return "status-pill--muted";
},
/** 来源样式 key */
getSourceKey(value) {
return String(value || "手动").trim().toUpperCase() === "PLM" ? "plm" : "manual";
},
/** 解析计划表发送次数接口返回值 */
parseRoutePlanSendCount(res) {
const n = res && res.data !== undefined && res.data !== null ? Number(res.data) : 0;
return Number.isFinite(n) && n >= 0 ? n : 0;
},
/** 解析「生产中」对应的项目状态字典值 */
resolveProductionBomStatus() {
const options = (this.dict && this.dict.type && this.dict.type.process_status) || [];
const hit = options.find((o) => String(o.label || "").includes("生产中"));
return hit ? hit.value : undefined;
},
/** 加载顶部概览统计(全库,不受当前筛选影响) */
async fetchOverviewStats() {
this.overviewStatsLoading = true;
try {
const base = { pageNum: 1, pageSize: 1 };
const productionStatus = this.resolveProductionBomStatus();
const tasks = [
listOrderPro({ ...base }),
listOrderPro({ ...base, whetherDelay: 1 }),
listOrderPro({ ...base, isEnterpriseStandardList: "变更" }),
];
if (productionStatus !== undefined && productionStatus !== null && productionStatus !== "") {
tasks.push(listOrderPro({ ...base, bomStatus: productionStatus }));
}
const results = await Promise.all(tasks);
this.overviewStats = {
total: results[0].total || 0,
delayed: results[1].total || 0,
change: results[2].total || 0,
inProduction: results[3] ? results[3].total || 0 : 0,
};
} catch (e) {
/* 统计失败时不阻断页面 */
} finally {
this.overviewStatsLoading = false;
}
},
/** 列表行计划表发送次数,未加载完返回 null */
getRowRoutePlanSendCount(row) {
if (!row || row.id == null) return null;
if (!Object.prototype.hasOwnProperty.call(this.routePlanSendCountMap, row.id)) {
return null;
}
return this.routePlanSendCountMap[row.id];
},
/** 批量加载当前页各行计划表发送次数 */
async loadRoutePlanSendCountsForList(rows) {
const list = (rows || []).filter((r) => r && r.id != null);
if (!list.length) return;
await Promise.all(
list.map(async (row) => {
try {
const res = await getRoutePlanSendCount(row.id);
this.$set(this.routePlanSendCountMap, row.id, this.parseRoutePlanSendCount(res));
} catch (e) {
this.$set(this.routePlanSendCountMap, row.id, 0);
}
})
);
},
/** 表格行样式:延期高亮 */
getOrderRowClass({ row }) {
if (Number(row.whetherDelay) === 1) {
return "order-row--delayed";
}
return "";
},
/** 组装列表/导出查询参数(含计划表发送筛选项映射) */
buildOrderProQueryParams() {
const params = { ...this.queryParams };
if (Array.isArray(params.isEnterpriseStandardList) && params.isEnterpriseStandardList.length > 0) {
params.isEnterpriseStandardList = params.isEnterpriseStandardList.join(",");
}
delete params.isEnterpriseStandard;
const sendFilter = params.routePlanSendFilter;
delete params.routePlanSendFilter;
delete params.routePlanSendCount;
delete params.routePlanSendCountMin;
if (sendFilter === "0") {
params.routePlanSendCount = 0;
} else if (sendFilter === "sent") {
params.routePlanSendCountMin = 1;
} else if (sendFilter === "1" || sendFilter === "2") {
params.routePlanSendCount = Number(sendFilter);
} else if (sendFilter === "ge3") {
params.routePlanSendCountMin = 3;
}
return params;
},
/** 查询项目令号列表 */
async getList() {
this.loading = true;
try {
const base = process.env.VUE_APP_BASE_API || "";
const url = `${base}/system/orderPro/refreshDelayStatus`;
await axios.get(url, { headers: { ...(this.uploadHeaders || {}) } });
} catch (e) {
/* ignore refresh errors to not block list */
}
try {
const response = await listOrderPro(this.buildOrderProQueryParams());
this.orderProList = response.rows;
this.total = response.total;
this.loadRoutePlanSendCountsForList(response.rows);
} finally {
this.loading = false;
}
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
id: undefined,
productionOrderNo: undefined,
productionName: undefined,
drawingNo: undefined,
drawingName: undefined,
createTime: undefined,
updateTime: undefined,
createBy: undefined,
updateBy: undefined,
bomStatus: undefined,
routeStatus: undefined,
planEndTime: undefined,
planStartTime: undefined,
drawingType: undefined,
projectEndTime: undefined,
unit: undefined,
source: "手动",
quantity: 0,
isDownload: 0,
needProcess: 1,
isEnterpriseStandard: 0, // 默认为否
drawingPath: undefined,
drawer: undefined,
drawUptime: undefined,
routeUptime: undefined,
planUptime: undefined,
bomUptime: undefined
};
this.resetForm("form");
},
handleDetail(productionOrderNo) {
this.processParams = { productionOrderNo: productionOrderNo };
this.processDrawer = true;
},
handleBOM(productionOrderNo) {
this.bomParams = {
productionOrderNo: productionOrderNo,
productionName: this.productionObj && this.productionObj.productionName,
};
this.bomDrawer = true;
},
/** MRP运算结果复查相关方法 */
// 获取MRP运算结果
async getMRPResults() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.error("请先选择生产令号");
return;
}
// 只显示一次开始提示,不重复弹窗
this.$message({
message:
"MRP运算已开始由于数据量较大可能需要较长时间请耐心等待...",
type: "info",
duration: 6000, // 减少到6秒
showClose: true,
});
this.loadingMRP = true;
this.mrpOperationStatus = "MRP运算已开始正在处理数据...";
// 只更新状态,不弹窗提醒
const progressTimer = setInterval(() => {
this.mrpOperationStatus = "MRP运算仍在进行中请继续等待...";
// 移除弹窗提示,只更新状态显示
}, 30000);
try {
const response = await getMRPResults(this.productionObj.id);
// 清除定时器
clearInterval(progressTimer);
if (response && response.code === 200) {
this.$message.success("MRP运算完成");
// 运算完成后刷新MRP数据
await this.getMrpData();
} else {
this.$message.error(response?.msg || "MRP运算失败");
}
} catch (error) {
// 清除定时器
clearInterval(progressTimer);
// 简化错误提示,避免重复弹窗
if (
error.code === "ECONNABORTED" ||
error.message.includes("timeout")
) {
this.$message.error("MRP运算超时请稍后重试");
} else {
this.$message.error("MRP运算失败请重试");
}
} finally {
this.loadingMRP = false;
this.mrpOperationStatus = ""; // 清空状态
}
},
// 获取MRP数据
async getMrpData() {
this.mrpLoading = true;
try {
this.mrpQueryParams.orderProId = this.productionObj.id;
const response = await listMrp(this.mrpQueryParams);
if (response && response.rows) {
// 统一转Number避免 toFixed 报错
const processedRows = response.rows.map((item) => ({
...item,
availableStock: Number(item.availableStock) || 0,
realTimeStock: Number(item.realTimeStock) || 0,
prodNotInStock: Number(item.prodNotInStock) || 0,
purchaseRequestQty: Number(item.purchaseRequestQty) || 0,
purchaseNotInStock: this.formatFractionValue(
item.purchaseNotInStock
),
childNotPickedQty: Number(item.childNotPickedQty) || 0,
createTime: item.createTime
? dayjs(item.createTime).format("YYYY-MM-DD HH:mm:ss")
: "",
updateTime: item.updateTime
? dayjs(item.updateTime).format("YYYY-MM-DD HH:mm:ss")
: "",
}));
this.mrpResultData = processedRows;
this.mrpTotal = response.total || processedRows.length;
// 统计计算也用处理过的数字数据
this.calculateMrpStats(processedRows);
}
} catch (error) {
console.error("获取MRP数据失败:", error);
this.$message.error("获取MRP数据失败");
} finally {
this.mrpLoading = false;
}
},
// MRP分页大小改变
handleMrpSizeChange(size) {
this.mrpQueryParams.pageSize = size;
this.mrpQueryParams.pageNum = 1;
this.getMrpData();
},
// MRP当前页改变
handleMrpCurrentChange(page) {
this.mrpQueryParams.pageNum = page;
this.getMrpData();
},
// 格式化日期时间
formatDateTime(date) {
if (!date) return "-";
if (typeof date === "string") {
date = new Date(date);
}
if (isNaN(date.getTime())) return "-";
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
normalizeIncomingDate(value) {
if (!value) return "";
const s = typeof value === "string" ? value.trim() : value;
if (
typeof s === "string" &&
(s === "0000-00-00 00:00:00" || s.startsWith("1970-01-01"))
) {
return "";
}
const d = dayjs(s);
if (!d.isValid()) return "";
return d.format("YYYY-MM-DD HH:mm:ss");
},
normalizeDateTime(value) {
if (!value) return null;
if (typeof value === "string") {
const v = value.trim();
if (v === "" || v === "0000-00-00 00:00:00") return null;
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
const now = dayjs();
return `${v} ${now.format("HH:mm:ss")}`;
}
const d = dayjs(v);
if (!d.isValid()) return null;
return d.format("YYYY-MM-DD HH:mm:ss");
}
const d = dayjs(value);
if (!d.isValid()) return null;
return d.format("YYYY-MM-DD HH:mm:ss");
},
normalizeDateTimeNonZero(value) {
const s = this.normalizeDateTime(value);
if (!s) return s;
if (/^\d{4}-\d{2}-\d{2} 00:00:00$/.test(s)) {
const now = dayjs();
return `${s.slice(0, 10)} ${now.format("HH:mm:ss")}`;
}
return s;
},
// MRP搜索处理
handleMrpSearch() {
this.mrpQueryParams.pageNum = 1;
this.mrpQueryParams.materialCode = this.mrpSearchKeyword;
console.log("MRP搜索参数:", this.mrpQueryParams);
this.getMrpData();
},
// 清空MRP搜索
clearMrpSearch() {
this.mrpSearchKeyword = "";
this.mrpQueryParams.materialCode = "";
this.mrpQueryParams.pageNum = 1;
this.getMrpData();
},
// 过滤MRP数据前端过滤作为备用方案
filterMrpData(data) {
if (!this.mrpSearchKeyword) {
return data;
}
const keyword = this.mrpSearchKeyword.toLowerCase();
return data.filter(
(item) =>
item.materialName.toLowerCase().includes(keyword) ||
item.materialCode.toLowerCase().includes(keyword)
);
},
// 计算MRP统计信息
calculateMrpStats(data) {
const total = data.length;
const positive = data.filter((item) => item.availableStock > 0).length;
const negative = data.filter((item) => item.availableStock < 0).length;
const zero = data.filter((item) => item.availableStock === 0).length;
this.mrpStats = {
totalMaterials: total,
positiveStock: positive,
negativeStock: negative,
zeroStock: zero,
};
},
// 获取抽屉标题
getDrawerTitle() {
if (!this.productionObj) {
return "项目详情";
}
const orderNo = this.productionObj.productionOrderNo || "";
const projectName = this.productionObj.productionName || "";
if (orderNo && projectName) {
return `${orderNo} | ${projectName}`;
} else if (orderNo) {
return orderNo;
} else if (projectName) {
return projectName;
} else {
return "项目详情";
}
},
handleDrawerTabClick(tab) {
if (!this.productionObj || !this.productionObj.id) return;
if (tab.name === "prdMo" && !this.kingdeePrdMoList.length && !this.kingdeePrdMoLoading) {
this.fetchKingdeePrdMo();
}
if (tab.name === "purchaseOrder" && !this.kingdeePurchaseOrderList.length && !this.kingdeePurchaseLoading) {
this.fetchKingdeePurchaseOrder();
}
},
getKingdeeField(row, type) {
if (!row) return 0;
if (type === "prd") {
const raw = row.need_kingdee_prd_mo != null ? row.need_kingdee_prd_mo : row.needKingdeePrdMo;
return raw != null ? Number(raw) : 0;
}
const raw = row.need_kingdee_purchase_order != null
? row.need_kingdee_purchase_order
: row.needKingdeePurchaseOrder;
return raw != null ? Number(raw) : 0;
},
isKingdeeExecuted(value) {
return Number(value) === 1;
},
formatKingdeeExecStatus(value) {
return this.isKingdeeExecuted(value) ? "已执行" : "未执行";
},
hasAnyKingdeeExecuted(row) {
if (!row) return false;
return this.isKingdeeExecuted(this.getKingdeeField(row, "prd"))
|| this.isKingdeeExecuted(this.getKingdeeField(row, "po"));
},
getKingdeeDrawerTitle() {
if (!this.productionObj) return "金蝶订单";
const orderNo = this.productionObj.productionOrderNo || "";
const projectName = this.productionObj.productionName || "";
if (orderNo && projectName) return `金蝶订单 · ${orderNo} | ${projectName}`;
if (orderNo) return `金蝶订单 · ${orderNo}`;
return "金蝶订单";
},
openKingdeeDrawer(row, tab) {
if (!row || !this.hasAnyKingdeeExecuted(row)) return;
this.productionObj = row;
this.kingdeeActiveTab = tab
|| (this.isKingdeeExecuted(this.getKingdeeField(row, "prd")) ? "prdMo" : "purchaseOrder");
this.kingdeePrdMoList = [];
this.kingdeePurchaseOrderList = [];
this.kingdeeDrawer = true;
this.$nextTick(() => {
this.loadKingdeeDrawerTabData(this.kingdeeActiveTab);
});
},
handleKingdeeChipClick(row, type, event) {
if (event) event.stopPropagation();
if (!this.isKingdeeExecuted(this.getKingdeeField(row, type === "prd" ? "prd" : "po"))) return;
const tab = type === "prd" ? "prdMo" : "purchaseOrder";
this.openKingdeeDrawer(row, tab);
},
loadKingdeeDrawerTabData(tabName) {
if (!this.productionObj || !this.productionObj.id) return;
if (tabName === "prdMo" && !this.kingdeePrdMoList.length && !this.kingdeePrdMoLoading) {
this.fetchKingdeePrdMo();
}
if (tabName === "purchaseOrder" && !this.kingdeePurchaseOrderList.length && !this.kingdeePurchaseLoading) {
this.fetchKingdeePurchaseOrder();
}
},
handleKingdeeTabClick(tab) {
this.loadKingdeeDrawerTabData(tab.name);
},
handleKingdeeDrawerClose(done) {
this.kingdeeActiveTab = "prdMo";
this.kingdeePrdMoList = [];
this.kingdeePurchaseOrderList = [];
done();
},
/** 解析金蝶接口 data 数组 */
parseKingdeeListResponse(res) {
if (Array.isArray(res)) return res;
if (Array.isArray(res && res.data)) return res.data;
return [];
},
formatKingdeeDate(value) {
if (!value) return "—";
const d = dayjs(value);
return d.isValid() ? d.format("YYYY-MM-DD") : String(value);
},
formatPickMtrlStatus(value) {
const map = {
1: "未领料",
2: "部分领料",
3: "全部领料",
4: "超额领料",
};
const key = value != null ? String(value) : "";
return map[key] || value || "—";
},
getPickMtrlStatusTag(value) {
const key = value != null ? String(value) : "";
if (key === "3") return "success";
if (key === "2") return "warning";
if (key === "4") return "danger";
return "info";
},
formatMrpCloseStatus(value) {
const map = { A: "未关闭", B: "已关闭" };
const key = value != null ? String(value).toUpperCase() : "";
return map[key] || value || "—";
},
async fetchKingdeePrdMo() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.warning("请先选择项目");
return;
}
this.kingdeePrdMoLoading = true;
try {
const res = await getKingdeePrdMo(this.productionObj.id);
this.kingdeePrdMoList = this.parseKingdeeListResponse(res);
} catch (e) {
this.kingdeePrdMoList = [];
} finally {
this.kingdeePrdMoLoading = false;
}
},
async fetchKingdeePurchaseOrder() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.warning("请先选择项目");
return;
}
this.kingdeePurchaseLoading = true;
try {
const res = await getKingdeePurchaseOrder(this.productionObj.id);
this.kingdeePurchaseOrderList = this.parseKingdeeListResponse(res);
} catch (e) {
this.kingdeePurchaseOrderList = [];
} finally {
this.kingdeePurchaseLoading = false;
}
},
// 刷新MRP数据
handleRefreshMrpData() {
this.mrpQueryParams.pageNum = 1;
this.getMrpData();
},
// 导出MRP数据
async handleExportMrp() {
// 增强参数验证
if (!this.productionObj) {
this.$message.error("请先选择生产令号");
return;
}
if (!this.productionObj.id) {
this.$message.error("生产令号ID无效请重新选择");
return;
}
if (!this.productionObj.productionOrderNo) {
this.$message.error("生产令号信息不完整,请重新选择");
return;
}
// 调试信息
console.log("productionObj:", this.productionObj);
console.log("productionObj.id:", this.productionObj.id);
console.log("mrpResultData.length:", this.mrpResultData?.length || 0);
// 显示导出确认信息
const exportInfo = `即将导出生产令号"${this.productionObj.productionOrderNo}"的MRP数据`;
const searchInfo = this.mrpSearchKeyword
? `,包含搜索关键词"${this.mrpSearchKeyword}"`
: "";
const dataInfo =
this.mrpResultData && this.mrpResultData.length > 0
? `,当前显示${this.mrpResultData.length}条记录`
: "(后台将查询所有相关数据)";
try {
await this.$confirm(
`${exportInfo}${searchInfo}${dataInfo}\n\n确定要导出吗`,
"确认导出",
{
confirmButtonText: "确定导出",
cancelButtonText: "取消",
type: "info",
}
);
} catch (error) {
// 用户取消导出
return;
}
this.exportLoading = true;
try {
// 准备导出参数主要传递orderProId给后台
const exportParams = {
orderProId: this.productionObj.id, // 核心参数生产令号ID
materialCode: this.mrpSearchKeyword || "", // 可选的物料编码搜索
materialName: "", // 清空名称搜索,避免冲突
pageSize: 9999, // 后台查询时获取所有数据
pageNum: 1,
};
// 详细调试信息
console.log("导出参数:", exportParams);
console.log("orderProId类型:", typeof exportParams.orderProId);
console.log("orderProId值:", exportParams.orderProId);
console.log("materialCode:", exportParams.materialCode);
const response = await exportMrp(exportParams);
// 创建下载链接
const blob = new Blob([response], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `MRP运算结果_${this.productionObj.productionOrderNo
}_${new Date().getTime()}.xlsx`;
link.click();
window.URL.revokeObjectURL(url);
this.$message.success(
`导出成功文件已下载MRP运算结果_${this.productionObj.productionOrderNo
}_${new Date().getTime()}.xlsx`
);
} catch (error) {
console.error("导出失败:", error);
this.$message.error("导出失败: " + (error.message || error));
} finally {
this.exportLoading = false;
}
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
const pageSize = this.queryParams.pageSize;
this.queryParams = {
pageNum: 1,
pageSize: pageSize || 10,
productionOrderNo: undefined,
productionName: undefined,
drawingNo: undefined,
drawingName: undefined,
bomStatus: undefined,
routePlanSendFilter: undefined,
routeStatus: undefined,
planEndTime: undefined,
planStartTime: undefined,
drawingType: undefined,
projectEndTime: undefined,
createBy: undefined,
unit: undefined,
quantity: undefined,
isEnterpriseStandard: undefined,
isEnterpriseStandardList: undefined,
whetherDelay: undefined,
source: undefined,
needProcess: undefined,
drawingPath: undefined,
routeUptime: undefined,
planUptime: undefined,
bomUptime: undefined,
drawingTime: undefined,
bomCount: undefined,
materialCount: undefined,
routeCount: undefined,
drawingBy: undefined,
};
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.id);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 打开批量设置项目状态弹窗 */
handleSetStatus() {
if (!this.ids || !this.ids.length) {
this.$modal.msgWarning("请先选择要设置状态的项目");
return;
}
this.statusForm.bomStatus = undefined;
this.statusDialogVisible = true;
this.$nextTick(() => {
if (this.$refs.statusForm) {
this.$refs.statusForm.clearValidate();
}
});
},
/** 提交批量设置项目状态 */
submitStatusForm() {
this.$refs.statusForm.validate((valid) => {
if (!valid) return;
const bomStatus = Number(this.statusForm.bomStatus);
if (Number.isNaN(bomStatus)) {
this.$modal.msgWarning("项目状态无效");
return;
}
const statusLabel =
((this.dict.type.process_status || []).find(
(d) => String(d.value) === String(this.statusForm.bomStatus)
) || {}).label || this.statusForm.bomStatus;
this.$modal
.confirm(
`确认将选中的 ${this.ids.length} 个项目状态设置为「${statusLabel}」?`
)
.then(() => {
this.statusSubmitting = true;
const tasks = this.ids.map((id) =>
updateOrderPro({ id, bomStatus })
);
return Promise.all(tasks);
})
.then(() => {
this.$modal.msgSuccess("状态设置成功");
this.statusDialogVisible = false;
this.getList();
this.fetchOverviewStats();
})
.catch(() => {})
.finally(() => {
this.statusSubmitting = false;
});
});
},
/** 新增按钮操作 */
handleAdd() {
this.currentPid = null; // 新增时,不依赖当前行
this.chainForm.productionCode = "";
this.reset();
this.open = true;
this.title = "添加项目";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const id = row.id || this.ids;
getOrderPro(id).then((response) => {
this.loading = false;
this.form = response.data || {};
this.form.isEnterpriseStandard =
this.form.isEnterpriseStandard != null
? Number(this.form.isEnterpriseStandard)
: 0;
this.form.isDownload =
this.form.isDownload != null ? Number(this.form.isDownload) : 1;
this.form.needProcess =
this.form.needProcess != null ? Number(this.form.needProcess) : 1;
this.form.source = this.form.source || "手动";
this.form.drawer = this.parseDesignerArray(this.form && this.form.drawingBy);
this.form.drawUptime = this.normalizeIncomingDate(
this.form && this.form.drawingTime
);
this.form.projectEndTime = this.normalizeIncomingDate(
this.form && this.form.projectEndTime
);
this.form.routeUptime = this.normalizeIncomingDate(
this.form && this.form.routeUptime
);
this.form.planUptime = this.normalizeIncomingDate(
this.form && this.form.planUptime
);
this.form.bomUptime = this.normalizeIncomingDate(
this.form && this.form.bomUptime
);
// 保持后端返回的 drawingPath图片ID串原样避免将 URL 误传给 ImageUpload
this.open = true;
this.title = "修改项目令号";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate((valid) => {
if (valid) {
this.buttonLoading = true;
const payload = {
...this.form,
isEnterpriseStandard:
this.form.isEnterpriseStandard != null
? Number(this.form.isEnterpriseStandard)
: 0,
isDownload:
this.form.isDownload != null
? Number(this.form.isDownload)
: 1,
needProcess:
this.form.needProcess != null
? Number(this.form.needProcess)
: 1,
drawingBy: Array.isArray(this.form.drawer)
? this.form.drawer.join(",")
: this.form.drawer,
drawingTime: this.normalizeDateTime(this.form.drawUptime),
projectEndTime: this.normalizeDateTime(this.form.projectEndTime),
routeUptime: this.normalizeDateTime(this.form.routeUptime),
planUptime: this.normalizeDateTime(this.form.planUptime),
bomUptime: this.normalizeDateTime(this.form.bomUptime),
drawingPath: this.form.drawingPath || "",
};
delete payload.needKingdeePrdMo;
delete payload.needKingdeePurchaseOrder;
delete payload.need_kingdee_prd_mo;
delete payload.need_kingdee_purchase_order;
if (this.form.id != null) {
updateOrderPro(payload)
.then((response) => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
this.fetchOverviewStats();
})
.finally(() => {
this.buttonLoading = false;
});
} else {
addOrderPro({
...payload,
need_kingdee_prd_mo: 0,
need_kingdee_purchase_order: 0,
})
.then((response) => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
this.fetchOverviewStats();
})
.finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal
.confirm('是否确认删除项目令号编号为"' + ids + '"的数据项?')
.then(() => {
this.loading = true;
return delOrderPro(ids);
})
.then(() => {
this.loading = false;
this.getList();
this.fetchOverviewStats();
this.$modal.msgSuccess("删除成功");
})
.catch(() => { })
.finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport() {
this.download(
"system/orderPro/export",
this.buildOrderProQueryParams(),
`orderPro_${new Date().getTime()}.xlsx`
);
},
openAddChainDialog() {
this.isEditChain = false;
this.chainForm = {
pid: this.id,
productionCode: this.productionObj.productionOrderNo,
productType: "",
figureNumber: "",
figureName: "",
figureNum: "",
jdInventory: "",
axialType: "",
boxType: "",
selectedFileType: "",
drawPath: "",
fileType: "",
drawBy: "",
drawTime: "",
id: undefined,
};
this.chainDialogVisible = true;
},
editChain(index, row) {
this.isEditChain = true;
this.editChainIndex = index;
this.chainForm = {
pid: this.productionObj && this.productionObj.id,
productionCode:
this.productionObj && this.productionObj.productionOrderNo,
productType: (row && (row.productType || row.type)) || "",
figureNumber: (row && row.figureNumber) || "",
figureName: (row && (row.figureName || row.productName)) || "",
figureNum: (row && row.figureNum) || "",
jdInventory: (row && row.jdInventory) || "",
axialType: (row && row.axialType) || "",
boxType: (row && row.boxType) || "",
selectedFileType: (row && (row.fileType || row.selectedFileType)) || "",
drawPath: (row && row.drawPath) || "",
fileType: (row && row.fileType) || "",
drawBy: (row && row.drawBy) || "",
drawTime: (row && row.drawTime) || "",
id: row && row.id,
};
// 同步文件类型选择:优先使用后端返回的 fileType若无则根据当前路径匹配配置项
if (!this.chainForm.selectedFileType) {
this.setSelectedFileTypeFromPath(this.chainForm.drawPath);
}
this.chainDialogVisible = true;
},
removeChain(index) {
const row = this.chainTable[index];
this.$modal
.confirm("是否确认删除该销齿链型号?")
.then(() => {
return delSave(row.id);
})
.then(() => {
this.$modal.msgSuccess("删除成功");
// 删除成功后刷新表格数据
this.loadChainTableData();
})
.catch(() => {
// 删除失败时不显示错误信息,因为用户可能取消了操作
});
},
handleChainSelectionChange(selection) {
this.selectedChainIds = selection.map((item) => item.id).filter(Boolean);
},
beforeBatchDwgUpload(file) {
const name = file.name || "";
const suffix = name.substring(name.lastIndexOf(".") + 1).toLowerCase();
if (!this.selectedChainIds.length) {
this.$message.warning("请先勾选需要上传的行");
return false;
}
if (suffix !== "dwg") {
this.$message.error("仅允许上传 DWG 文件");
return false;
}
return true;
},
async batchUploadDwg({ file }) {
try {
const formData = new FormData();
// Spring 接收 List<Long> ids使用重复键形式提交
this.selectedChainIds.forEach((id) => formData.append("ids", id));
formData.append("file", file);
const res = await axios.post(this.dwgUploadUrl, formData, {
headers: {
...this.uploadHeaders,
},
});
const payload = res && res.data ? res.data : {};
const ok = payload.code === 200 || payload.success === true;
const msg = payload.msg || "上传完成";
if (ok) {
this.$message.success(msg);
this.loadChainTableData();
} else {
this.$message.error(msg || "上传失败");
}
} catch (e) {
const status = e && e.response && e.response.status;
const serverMsg =
(e &&
e.response &&
e.response.data &&
(e.response.data.msg ||
e.response.data.message ||
e.response.data)) ||
"";
if (status === 413) {
this.$message.error("文件过大,已超过服务器限制");
} else if (status === 401) {
this.$message.error("未认证或登录已过期");
} else if (status === 403) {
this.$message.error("权限不足,无法上传");
} else if (status === 404) {
this.$message.error("上传接口不存在或路径错误");
} else if (status === 415) {
this.$message.error("服务器不支持当前文件类型");
} else if (status >= 500) {
this.$message.error(
"服务器内部错误:" + (serverMsg || "请联系管理员")
);
} else {
this.$message.error(
"上传失败:" + (serverMsg || e.message || "请重试")
);
}
}
},
beforeElectricalBomUpload(file) {
const name = file.name || "";
const suffix = name.substring(name.lastIndexOf(".") + 1).toLowerCase();
if (!(suffix === "xls" || suffix === "xlsx")) {
this.$message.error("仅支持Excel文件(xls/xlsx)");
return false;
}
return true;
},
async uploadElectricalBom({ file }) {
try {
const formData = new FormData();
formData.append("file", file);
const res = await importElectricalBom(formData);
if (res && (res.code === 200 || res.success)) {
let payload = res.data || res.msg || {};
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (_) {
payload = {};
}
}
const total = payload.totalRows ?? payload.total ?? 0;
const parsed = payload.parsedRows ?? 0;
const saved = payload.savedRows ?? payload.saved ?? 0;
const message = payload.message || "导入完成";
this.$message.success(
`${message}(总:${total} 解析:${parsed} 保存:${saved}`
);
this.fetchBomLog();
} else {
this.$message.error(res?.msg || "导入失败");
}
} catch (e) {
this.$message.error("导入失败: " + (e.message || e));
}
},
async importProducts({ file }) {
try {
const formData = new FormData();
formData.append("file", file);
const base = process.env.VUE_APP_BASE_API || "";
const url = `${base}/system/save/importData`;
const res = await axios.post(`${url}?updateSupport=false`, formData, {
headers: {
"Content-Type": "multipart/form-data",
...(this.uploadHeaders || {}),
},
});
const data = res && res.data ? res.data : {};
if (data && (data.code === 200 || data.success)) {
const msg = data.msg || data.message || data.data || "导入成功";
this.$message.success(msg);
this.loadChainTableData();
} else {
this.$message.error(data.msg || data.message || "导入失败");
}
} catch (e) {
this.$message.error("导入失败: " + (e.message || e));
}
},
async downloadSelectedDwgZip() {
if (!Array.isArray(this.selectedChainIds) || this.selectedChainIds.length === 0) {
this.$message.warning("请先勾选需要下载的行");
return;
}
try {
const base = process.env.VUE_APP_BASE_API || "";
const firstId = this.selectedChainIds[0];
const url = `${base}/system/orderPro/downloadDwg/${firstId}`;
const res = await axios.get(url, {
params: { ids: this.selectedChainIds },
responseType: "blob",
headers: { ...(this.uploadHeaders || {}) },
});
const contentType = res && res.headers && res.headers["content-type"];
if (contentType && contentType.includes("application/json")) {
// 错误信息
const text = await new Response(res.data).text();
try {
const json = JSON.parse(text);
this.$message.error(json.msg || json.message || "下载失败");
} catch (_) {
this.$message.error(text || "下载失败");
}
return;
}
// 提取文件名
let filename = `${(this.productionObj && this.productionObj.productionOrderNo) || "DWG"}_ZIP.zip`;
const cd = res && res.headers && res.headers["content-disposition"];
if (cd) {
const m = cd.match(/filename\*?=UTF-8''([^;]+)|filename="?([^";]+)"?/i);
const raw = (m && (m[1] || m[2])) || "";
if (raw) {
try {
filename = decodeURIComponent(raw.replace(/\+/g, "%20"));
} catch (_) {
filename = raw;
}
}
}
const blob = new Blob([res.data], { type: "application/zip" });
const link = document.createElement("a");
const urlObj = window.URL.createObjectURL(blob);
link.href = urlObj;
link.download = filename;
link.click();
window.URL.revokeObjectURL(urlObj);
this.$message.success("图纸ZIP下载成功");
} catch (e) {
this.$message.error("下载失败:" + (e.message || e));
}
},
async pushDingTalkCard() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.warning("请先选择生产令号");
return;
}
try {
const base = process.env.VUE_APP_BASE_API || "";
const url = `${base}/system/orderPro/pushDingTalkCard/${this.productionObj.id}`;
const res = await axios.post(url, null, { headers: { ...(this.uploadHeaders || {}) } });
const ct = res && res.headers && res.headers["content-type"];
if (ct && ct.includes("application/json")) {
const data = res.data;
if (data && (data.code === 200 || data.success)) {
this.$message.success("已推送钉钉卡片");
} else {
this.$message.error((data && (data.msg || data.message)) || "推送失败");
}
} else {
this.$message.success("已推送钉钉卡片");
}
} catch (e) {
this.$message.error("推送失败:" + (e.message || e));
}
},
async clearProject(row) {
if (!row || !row.id) {
this.$message.warning("未获取到项目ID");
return;
}
try {
await this.$confirm(
`确认清空项目【${row.productionOrderNo || row.id}】的关联数据?(保留项目本身)`,
"提示",
{ type: "warning" }
);
} catch (e) {
return;
}
try {
const base = process.env.VUE_APP_BASE_API || "";
const url = `${base}/system/orderPro/clear/${row.id}`;
await axios.delete(url, { headers: { ...(this.uploadHeaders || {}) } });
this.$message.success("清空项目数据成功");
this.getList();
this.fetchOverviewStats();
} catch (e) {
this.$message.error("清空失败:" + (e.message || e));
}
},
async saveChainForm() {
if (!this.chainForm.productionCode) {
this.$message.warning("生产令号不能为空");
return;
}
if (!this.chainForm.figureNumber) {
this.$message.warning("图号不能为空");
return;
}
if (!this.chainForm.figureName) {
this.$message.warning("名称不能为空");
return;
}
if (!this.chainForm.figureNum || Number(this.chainForm.figureNum) < 1) {
this.$message.warning("数量不能为空且必须大于0");
return;
}
const payload = {
id: this.chainForm.id,
pid: this.productionObj.id,
productionCode: this.chainForm.productionCode,
productType: this.chainForm.productType,
figureNumber: this.chainForm.figureNumber,
figureName: this.chainForm.figureName,
figureNum: this.chainForm.figureNum,
jdInventory: this.chainForm.jdInventory || undefined,
axialType: this.chainForm.axialType || undefined,
boxType: this.chainForm.boxType || undefined,
selectedFileType: this.chainForm.selectedFileType || "",
drawPath: this.chainForm.drawPath || "",
fileType: this.chainForm.fileType || "",
drawBy: this.chainForm.drawBy || "",
drawTime: this.normalizeDateTimeNonZero(this.chainForm.drawTime) || null,
};
if (this.isEditChain && this.chainForm.id) {
payload.id = this.chainForm.id;
}
console.log("saveChainForm........", payload);
try {
if (this.isEditChain) {
await this.$api.updateProduct(payload);
this.$message.success("修改成功");
} else {
await this.$api.addProduct(payload);
this.$message.success("添加成功");
}
this.chainDialogVisible = false;
this.loadChainTableData();
} catch (e) {
const msg = this.isEditChain ? "修改失败" : "添加失败";
this.$message.error(msg);
}
},
searchRigidChain(query) {
if (!query) {
this.rigidChainOptions = [];
this.typeOptions = [];
this.journeyOptions = [];
return;
}
this.chainLoading = true;
listRigidChain({ typeName: query })
.then((res) => {
this.rigidChainOptions = (res.rows || []).slice(0, 10);
const types = new Set();
const journeys = new Set();
this.rigidChainOptions.forEach((item) => {
if (item.type) types.add(item.type);
if (item.typeName) types.add(item.typeName);
if (item.journey) journeys.add(item.journey);
});
this.typeOptions = Array.from(types);
this.journeyOptions = Array.from(journeys);
this.chainLoading = false;
})
.catch(() => {
this.chainLoading = false;
});
},
onRigidChainSelect(id) {
if (!id) {
this.chainForm.productType = "";
this.chainForm.figureNumber = "";
this.chainForm.figureName = "";
this.chainForm.drawPath = "";
return;
}
const selected = this.rigidChainOptions.find((item) => item.id === id);
if (selected) {
this.chainForm.productType = selected.type || "";
this.chainForm.figureNumber =
selected.typeName || selected.figureNumber || "";
this.chainForm.figureName = selected.productName || "";
this.chainForm.drawPath = selected.drawPath || "";
if (!this.chainForm.figureNum) {
this.chainForm.figureNum = 1;
}
if (!this.isEditChain) {
this.chainForm.id = undefined;
}
}
},
async executeDrawCommand() {
this.drawLoading = true;
this.drawLog = "";
this.percentage = 0;
this.clearDrawProgressTimer();
try {
const res = await executDrawing(this.productionObj);
if (res.code !== 200) {
this.drawLog = res.msg || "出图请求失败";
this.drawLoading = false;
return;
}
this.time = setInterval(async () => {
try {
const stateObj = this.parsePlotStateResponse(await getPlotState());
if (!stateObj || stateObj.processstate === undefined) {
return;
}
this.drawLog = stateObj.msg || "";
const ps = this.normalizePlotProcessState(stateObj.processstate);
if (ps === 3) {
this.applyPlotProgressFromMsg(stateObj.msg);
}
// 完成 / 失败:兼容 processstate 为字符串 "4"/"5"
if (ps === 4) {
this.percentage = 100;
this.clearDrawProgressTimer();
this.drawLoading = false;
this.fetchPlotStatus();
} else if (ps === 5) {
this.clearDrawProgressTimer();
this.drawLoading = false;
this.drawLog = stateObj.msg || "出图失败";
this.fetchPlotStatus();
}
} catch (pollErr) {
console.error("出图进度轮询失败:", pollErr);
}
}, 1000);
} catch (e) {
this.drawLog = "出图失败:" + (e.msg || e.message || e);
this.drawLoading = false;
this.clearDrawProgressTimer();
}
},
// 加载销齿链型号表格数据
async loadChainTableData() {
if (!this.productionObj || !this.productionObj.id) {
this.chainTable = [];
return;
}
try {
this.getMrpData(this.productionObj.id); // 获取MRP数据
const response = await listSave({ pid: this.productionObj.id });
this.chainTable = response.rows || [];
} catch (error) {
console.error("加载表格数据失败:", error);
this.chainTable = [];
}
},
getConfigIniOptions() {
listConfigIni().then((res) => {
this.configIniOptions = (res.rows || []).map((item) => ({
fileType: item.fileType,
fileUrl: item.fileUrl,
}));
});
},
onConfigIniSelect(fileType) {
const selected = this.configIniOptions.find(
(item) => item.fileType === fileType
);
if (selected) {
this.chainForm.drawPath = selected.fileUrl;
}
},
setSelectedFileTypeFromPath(path) {
if (!path) {
this.chainForm.selectedFileType = "";
return;
}
const match =
this.configIniOptions &&
this.configIniOptions.find((opt) => opt.fileUrl === path);
if (match) {
this.chainForm.selectedFileType = match.fileType;
} else if (!this.configIniOptions || this.configIniOptions.length === 0) {
// 配置尚未加载,拉取后再匹配
this.getConfigIniOptions();
this.$nextTick(() => {
const m = this.configIniOptions.find((opt) => opt.fileUrl === path);
if (m) this.chainForm.selectedFileType = m.fileType;
});
}
},
getFullImageUrl(row) {
if (!row) return "";
let url = row.drawingPathUrl || row.drawingPath || "";
if (!url) return "";
if (typeof url === "string") {
url = url.replace(/[`'"]/g, "").trim();
}
return url;
},
getImageList(row) {
if (!row) return [];
const raw = row.drawingPathUrl || row.drawingPath || "";
if (!raw) return [];
if (Array.isArray(raw)) return raw.filter(Boolean).map((s) => String(s).trim());
if (typeof raw === "string") {
return raw
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
return [];
},
openExecImagePreview(row) {
const list = this.getImageList(row);
if (!list.length) {
this.$message.warning("暂无执行单图");
return;
}
this.execImagePreviewList = list;
this.execImagePreviewTitle = `执行单图${row && row.productionOrderNo ? ` - ${row.productionOrderNo}` : ""}`;
this.execImagePreviewVisible = true;
},
beforeDwgUpload(file, row) {
// 验证文件格式
const ext = file.name.split(".").pop().toLowerCase();
if (ext !== "dwg") {
this.$message.error("仅支持dwg格式文件上传");
return false;
}
// 验证行数据
if (!row || !row.id) {
this.$message.error("行数据无效,无法上传");
return false;
}
// 验证文件大小(根据前端阈值,默认 100MB提示包含所选文件大小
const limitMB = Number(this.dwgMaxSizeMB) || 100;
const maxSize = limitMB * 1024 * 1024;
const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
if (file.size > maxSize) {
this.$message.error(
`文件过大(所选 ${sizeMB}MB限制 ${limitMB}MB。请压缩后重试或联系管理员调整服务器限制`
);
return false;
}
return true;
},
handleDwgUploadSuccess(response, file, row) {
if (response && response.code === 200) {
// 刷新列表
this.loadChainTableData();
this.$message.success(`图纸上传成功:${file.name}`);
// 可以在这里更新行的drawPath字段
if (response.data && response.data.filePath) {
row.drawPath = response.data.filePath;
}
} else {
this.$message.error((response && response.msg) || "上传失败");
}
},
handleDwgUploadError(error, file, row) {
console.error("图纸上传失败:", error);
const status =
(error &&
(error.status || (error.response && error.response.status))) ||
0;
if (status === 413) {
this.$message.error(
"文件过大(超出服务器限制)。请联系管理员提高上传大小限制或压缩文件后重试"
);
return;
}
this.$message.error(`图纸上传失败:${file.name}`);
},
async downloadPDF() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.error("请先选择生产令号");
return;
}
const loading = this.$loading({
text: "正在生成PDF请稍候...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
try {
const response = await uploadPDF(this.productionObj.id);
if (!(response instanceof Blob)) {
this.$message.error("PDF下载失败服务器返回异常");
return;
}
// 业务失败:后端返回 application/json解析并展示真实原因
const contentType = (response.type || "").toLowerCase();
if (contentType.includes("application/json")) {
const text = await response.text();
try {
const json = JSON.parse(text);
this.$message.error(json.msg || "PDF下载失败");
} catch (_) {
this.$message.error(text || "PDF下载失败");
}
return;
}
if (response.size === 0) {
this.$message.error("PDF下载失败文件为空");
return;
}
const blob = new Blob([response], { type: "application/zip" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${this.productionObj.productionOrderNo}_PDF.zip`;
link.click();
window.URL.revokeObjectURL(url);
this.$message.success("PDF文件下载成功");
} catch (error) {
let msg = "PDF下载失败";
try {
const data = error && error.response && error.response.data;
if (data instanceof Blob) {
const text = await data.text();
const json = JSON.parse(text);
msg = json.msg || msg;
} else if (error && error.message && !/status code|系统接口\d+异常/i.test(error.message)) {
msg = error.message;
}
} catch (_) {
// ignore parse error
}
this.$message.error(msg);
} finally {
loading.close();
}
},
async downloadPlmDwgZip() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.error("请先选择生产令号");
return;
}
if (Number(this.productionObj.isDownload) === 0) {
this.$message.warning("当前项目不允许下载图纸");
return;
}
this.downloadPlmDwgLoading = true;
try {
const response = await downloadPlmDwg(this.productionObj.id);
if (!(response instanceof Blob)) {
this.$message.error("下载失败:服务器返回异常");
return;
}
const contentType = (response.type || "").toLowerCase();
if (contentType.includes("application/json")) {
const text = await response.text();
try {
const json = JSON.parse(text);
this.$message.error(json.msg || "下载失败");
} catch (_) {
this.$message.error(text || "下载失败");
}
return;
}
if (response.size === 0) {
this.$message.error("下载失败:文件为空");
return;
}
const blob = new Blob([response], { type: "application/zip" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${this.productionObj.productionOrderNo || "PLM"}_PLM_DWG.zip`;
link.click();
window.URL.revokeObjectURL(url);
this.$message.success("PLM DWG图纸下载成功");
} catch (error) {
let msg = "下载失败";
try {
const data = error && error.response && error.response.data;
if (data instanceof Blob) {
const text = await data.text();
const json = JSON.parse(text);
msg = json.msg || msg;
} else if (error && error.message && !/status code|系统接口\d+异常/i.test(error.message)) {
msg = error.message;
}
} catch (_) {
// ignore
}
this.$message.error(msg);
} finally {
this.downloadPlmDwgLoading = false;
}
},
downloadRoute() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.error("请先选择生产令号");
return;
}
this.$message.success("开始下载生产工艺计划表");
console.log("开始下载生产令号ID:", this.productionObj.id);
// 调用API获取文件流
exportRoute(this.productionObj.id)
.then((response) => {
// 检查文件大小
if (response.size === 0) {
this.$message.error("文件为空或下载失败");
return;
}
// 创建blob对象并下载
let blob = new Blob([response], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
let url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${this.productionObj.productionOrderNo}_生产工艺计划表.xlsx`;
link.click();
window.URL.revokeObjectURL(url);
this.$message.success("生产工艺计划表下载成功");
})
.catch((error) => {
console.error("下载失败:", error);
// 后端异常会直接抛出,显示错误信息
this.$message.error("下载失败: " + (error.message || "项目未出图"));
});
},
downloadRoute3() {
this.$message.success("开始导出工艺计划表");
console.log("开始下载生产令号ID:", this.productionObj.id);
exportRoute3(this.productionObj.id)
.then((response) => {
if (response.size === 0) {
this.$message.error("文件为空或下载失败");
return;
}
const blob = new Blob([response], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${this.productionObj.productionOrderNo}_工艺计划表.xlsx`;
link.click();
window.URL.revokeObjectURL(url);
this.$message.success("工艺计划表下载成功");
})
.catch((error) => {
console.error("下载失败:", error);
this.$message.error("下载失败: " + (error.message || "项目未出图"));
});
},
async syncDrawingWorkload() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.warning("请先选择生产令号");
return;
}
this.drawingWorkloadLoading = true;
try {
const res = await syncDrawingWorkloadApi(this.productionObj.id);
this.drawingWorkload = (res && res.data) || null;
this.drawingWorkloadVisible = true;
this.$message.success((res && res.msg) || "图纸工作量读取成功");
} catch (e) {
/* 拦截器已提示 */
} finally {
this.drawingWorkloadLoading = false;
}
},
async fetchRoutePlanSendCount() {
if (!this.productionObj || !this.productionObj.id) {
this.routePlanSendCount = 0;
return;
}
try {
const res = await getRoutePlanSendCount(this.productionObj.id);
this.routePlanSendCount = this.parseRoutePlanSendCount(res);
this.$set(this.routePlanSendCountMap, this.productionObj.id, this.routePlanSendCount);
} catch (e) {
this.routePlanSendCount = 0;
this.$set(this.routePlanSendCountMap, this.productionObj.id, 0);
}
},
async sendProcessRoutePlan() {
if (!this.productionObj || !this.productionObj.id) {
this.$message.warning("请先选择生产令号");
return;
}
this.sendProcessRoutePlanLoading = true;
try {
const res = await sendProcessRoutePlanExcel(this.productionObj.id);
this.$message.success((res && res.msg) || "工艺计划表已发送");
await this.fetchRoutePlanSendCount();
} catch (e) {
/* 500 等已由 request 拦截器提示 */
} finally {
this.sendProcessRoutePlanLoading = false;
}
},
async pushProcessInbound() {
if (!this.productionObj || !this.productionObj.productionOrderNo) {
this.$message.error("请先选择生产令号");
return;
}
try {
const { value } = await this.$prompt(
"请输入入库编码(仓库编号)",
"推送工序计划单入库",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: "例如CK001",
inputValidator: (val) => !!(val && String(val).trim()),
inputErrorMessage: "入库编码不能为空",
}
);
const code = String(value).trim();
const res = await updateProductionOrders(
this.productionObj.productionOrderNo,
code
);
if (res && (res.code === 200 || res.success)) {
const list = res.data || [];
const msg = Array.isArray(list)
? `成功:${list.length}`
: "推送成功";
this.$message.success(msg);
} else {
this.$message.error(res?.msg || "推送失败");
}
} catch (e) {
// 用户取消或异常
if (e && e === "cancel") return;
this.$message.error("推送失败:" + (e.message || e));
}
},
async pushPurchaseInbound() {
if (!this.productionObj || !this.productionObj.productionOrderNo) {
this.$message.error("请先选择生产令号");
return;
}
try {
const { value } = await this.$prompt(
"请输入入库编码(仓库编号)",
"推送入库采购申请",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: "例如CK001",
inputValidator: (val) => !!(val && String(val).trim()),
inputErrorMessage: "入库编码不能为空",
}
);
const code = String(value).trim();
const res = await updateCgOrders(
this.productionObj.productionOrderNo,
code
);
if (res && (res.code === 200 || res.success)) {
const list = res.data || [];
const msg = Array.isArray(list)
? `成功:${list.length}`
: "推送成功";
this.$message.success(msg);
} else {
this.$message.error(res?.msg || "推送失败");
}
} catch (e) {
if (e && e === "cancel") return;
this.$message.error("推送失败:" + (e.message || e));
}
},
formatStepTime(row, field, fmt) {
const val = row && row[field];
if (!this.isValidTime(val)) return "—";
const format = fmt || "{y}-{m}-{d} {h}:{i}:{s}";
return this.parseTime(val, format);
}, drawingTime(row, field, fmt) {
const val = row && row[field];
if (!this.isValidTime(val)) return "—";
const format = fmt || "{y}-{m}-{d} {h}:{i}:{s}";
return this.parseTime(val, format);
},
isValidTime(val) {
if (!val) return false;
const d = new Date(val);
if (isNaN(d.getTime())) return false;
const year = d.getFullYear();
return year > 1971;
},
parseDesignerArray(v) {
if (!v) return [];
if (Array.isArray(v)) return v.filter(Boolean).map((x) => String(x));
if (typeof v === "string") {
return v
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
return [];
},
getDesignerLabels(value) {
const opts =
(this.dict && this.dict.type && this.dict.type.plan_proer) || [];
const codes = this.parseDesignerArray(value);
return codes.map((c) => {
const m =
opts.find(
(o) =>
String(o.value) === String(c) ||
String(o.dictValue || "") === String(c)
) || null;
return m ? m.label || m.dictLabel || c : c;
});
},
},
beforeMount() {
this.clearDrawProgressTimer();
},
};
</script>
<style scoped>
/* ── Design tokens: industrial refined ── */
.order-pro-page {
--op-bg: #f0f2f5;
--op-surface: #ffffff;
--op-ink: #1a2942;
--op-ink-muted: #5c6b7f;
--op-accent: #c9a227;
--op-accent-soft: rgba(201, 162, 39, 0.12);
--op-steel: #2c5282;
--op-steel-dark: #1a2942;
--op-border: #dce3ec;
--op-danger: #c0392b;
--op-danger-soft: #fdf2f0;
--op-success: #1e7e4a;
--op-success-soft: #edf7f1;
--op-radius: 10px;
--op-shadow: 0 1px 3px rgba(26, 41, 66, 0.06), 0 8px 24px rgba(26, 41, 66, 0.04);
animation: opPageIn 0.45s ease-out both;
}
@keyframes opPageIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* ── KPI 概览卡片 ── */
.kpi-panel {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 12px;
margin-bottom: 16px;
animation: opPageIn 0.45s ease-out both;
}
.kpi-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 18px;
border-radius: var(--op-radius);
border: 1px solid var(--op-border);
background: var(--op-surface);
box-shadow: var(--op-shadow);
transition: box-shadow 0.2s, border-color 0.2s;
}
.kpi-card:hover {
box-shadow: 0 4px 16px rgba(26, 41, 66, 0.08);
border-color: #c5d0de;
}
.kpi-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
font-size: 20px;
flex-shrink: 0;
}
.kpi-card__body {
min-width: 0;
}
.kpi-card__value {
display: block;
font-size: 24px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.15;
color: var(--op-ink);
}
.kpi-card__label {
display: block;
margin-top: 4px;
font-size: 12px;
color: var(--op-ink-muted);
white-space: nowrap;
}
.kpi-card--total .kpi-card__icon {
color: var(--op-steel);
background: #e8f0fa;
}
.kpi-card--delay .kpi-card__icon {
color: var(--op-danger);
background: var(--op-danger-soft);
}
.kpi-card--delay .kpi-card__value {
color: var(--op-danger);
}
.kpi-card--change .kpi-card__icon {
color: #7c3aed;
background: #f3e8ff;
}
.kpi-card--change .kpi-card__value {
color: #7c3aed;
}
.kpi-card--production .kpi-card__icon {
color: #b45309;
background: #fef3e2;
}
.kpi-card--production .kpi-card__value {
color: #b45309;
}
@media (max-width: 1200px) {
.kpi-panel {
grid-template-columns: repeat(2, 1fr);
}
}
/* ── Search panel ── */
.search-panel {
margin-bottom: 16px;
border: 1px solid var(--op-border);
border-radius: var(--op-radius);
box-shadow: var(--op-shadow);
animation: opPageIn 0.5s ease-out 0.05s both;
}
.search-panel ::v-deep .el-card__body {
padding: 14px 20px;
overflow: visible;
}
.search-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.search-form__main {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) 320px;
gap: 12px 18px;
align-items: center;
}
.search-form__advanced {
padding: 14px 16px 16px;
border: 1px solid #e8edf5;
border-radius: 10px;
background: linear-gradient(180deg, #fbfcff 0%, #f7faff 100%);
}
.search-form__advanced-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.search-form__advanced-title {
font-size: 13px;
font-weight: 600;
color: #2f3a4a;
}
.search-form__advanced-tip {
font-size: 12px;
color: #8b95a7;
}
.search-form__advanced-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px 18px;
}
.search-field {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
align-items: center;
align-items: center;
min-height: 32px;
}
.search-field__label {
display: inline-flex;
align-items: center;
justify-content: flex-end;
width: 100%;
min-height: 32px;
padding-right: 8px;
color: #606266;
font-size: 13px;
line-height: 32px;
white-space: nowrap;
text-align: right;
flex-shrink: 0;
}
.search-field__label--wide {
width: 100%;
}
.search-field ::v-deep .el-input,
.search-field ::v-deep .el-select {
width: 100%;
}
.search-field--type ::v-deep .el-select {
width: 100%;
}
.search-field ::v-deep .el-input__inner,
.search-field ::v-deep .el-select .el-input__inner {
height: 32px;
line-height: 32px;
border-radius: 6px;
}
.search-field ::v-deep .el-input__icon {
line-height: 32px;
}
.search-field--actions {
min-height: 32px;
}
.search-form__actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
min-height: 32px;
}
.search-form__actions ::v-deep .el-button {
margin-left: 0;
height: 32px;
padding: 0 14px;
line-height: 30px;
}
.search-form__actions ::v-deep .el-button--primary {
background: linear-gradient(135deg, var(--op-steel) 0%, #3a6ea5 100%);
border-color: var(--op-steel);
}
.search-form__toggle {
display: inline-flex;
align-items: center;
gap: 4px;
height: 32px;
margin: 0;
padding: 0 4px;
border: 0;
background: transparent;
color: var(--op-steel);
font-size: 13px;
line-height: 1;
cursor: pointer;
}
.search-form__toggle:hover {
color: #3a6ea5;
}
.search-form__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: var(--op-steel);
color: #fff;
font-size: 11px;
font-style: normal;
line-height: 1;
}
@media (min-width: 1280px) {
.search-field--type {
grid-column: span 2;
}
}
@media (max-width: 1440px) {
.search-form__main {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.search-form__actions {
grid-column: span 2;
justify-content: flex-end;
}
.search-form__advanced-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.search-form__main {
grid-template-columns: 1fr;
}
.search-form__actions {
grid-column: auto;
justify-content: flex-start;
}
.search-field {
grid-template-columns: 82px minmax(0, 1fr);
}
.search-form__advanced-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.search-field--type {
grid-column: auto;
}
}
@media (max-width: 768px) {
.search-form__advanced {
padding: 12px;
}
.search-form__advanced-head {
align-items: flex-start;
flex-direction: column;
margin-bottom: 10px;
}
.search-form__advanced-grid {
grid-template-columns: 1fr;
}
.search-form__actions {
justify-content: flex-start;
}
}
/* ── Toolbar ── */
.toolbar-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
animation: opPageIn 0.5s ease-out 0.1s both;
}
.toolbar-panel__left {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.toolbar-panel__left >>> .el-button {
border-radius: 6px;
font-weight: 500;
transition: box-shadow 0.15s;
}
.toolbar-panel__left >>> .el-button:hover:not(:disabled) {
box-shadow: 0 3px 8px rgba(26, 41, 66, 0.1);
}
/* ── Table panel ── */
.table-panel {
border: 1px solid var(--op-border);
border-radius: var(--op-radius);
box-shadow: var(--op-shadow);
animation: opPageIn 0.5s ease-out 0.15s both;
}
.table-panel >>> .el-card__body {
padding: 0;
overflow-x: auto;
}
.table-panel__footer {
padding: 12px 16px;
border-top: 1px solid var(--op-border);
background: #fafbfc;
}
.order-pro-table >>> .el-table__body tr:hover > td {
background-color: #f0f5fa !important;
}
.order-pro-table >>> .order-row--delayed > td {
background-color: var(--op-danger-soft) !important;
}
.order-pro-table >>> .order-row--delayed:hover > td {
background-color: #fce8e4 !important;
}
.order-no-link {
font-weight: 600;
font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 13px;
}
.order-no-link i {
margin-right: 4px;
font-size: 12px;
opacity: 0.7;
}
.project-name-cell {
color: var(--op-ink);
font-weight: 500;
}
.exec-image-entry {
display: inline-flex;
align-items: center;
gap: 6px;
}
.exec-image-trigger {
padding: 0;
}
.exec-image-count {
font-size: 12px;
color: #606266;
white-space: nowrap;
}
.exec-image-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
}
.exec-image-thumb {
width: 72px;
height: 72px;
border-radius: 6px;
border: 1px solid var(--op-border);
transition: transform 0.2s, box-shadow 0.2s;
cursor: zoom-in;
}
.exec-image-thumb:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(26, 41, 66, 0.15);
}
.exec-image-dialog__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
max-height: 62vh;
overflow-y: auto;
}
.exec-image-dialog__thumb {
width: 100%;
height: 180px;
border-radius: 8px;
border: 1px solid var(--op-border);
background: #f8fafc;
cursor: zoom-in;
}
.drawing-workload__meta {
display: flex;
flex-wrap: wrap;
gap: 12px 20px;
margin-bottom: 12px;
padding: 10px 12px;
background: #f5f7fa;
border-radius: 6px;
color: #606266;
font-size: 13px;
}
.drawing-workload__meta b {
color: #303133;
}
.text-muted {
color: #909399;
font-size: 12px;
}
.status-pill {
display: inline-block;
padding: 2px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
}
.status-pill--danger {
background: var(--op-danger-soft);
color: var(--op-danger);
border: 1px solid rgba(192, 57, 43, 0.25);
}
.status-pill--success {
background: var(--op-success-soft);
color: var(--op-success);
border: 1px solid rgba(30, 126, 74, 0.25);
}
.status-pill--warning {
background: rgba(217, 148, 42, 0.12);
color: #b87a12;
border: 1px solid rgba(217, 148, 42, 0.3);
}
.status-pill--muted {
background: rgba(120, 130, 140, 0.1);
color: var(--op-ink-muted);
border: 1px solid rgba(120, 130, 140, 0.2);
}
.delay-days {
font-variant-numeric: tabular-nums;
color: var(--op-ink-muted);
font-weight: 500;
}
.delay-days--active {
color: var(--op-danger);
font-weight: 700;
}
.date-cell {
font-size: 12px;
color: var(--op-ink-muted);
font-variant-numeric: tabular-nums;
}
.type-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
border: 1px solid transparent;
}
.type-badge--product { background: #e8f4fd; color: #1a6fb5; border-color: #b3d9f2; }
.type-badge--custom { background: #fef3e2; color: #b45309; border-color: #fcd9a8; }
.type-badge--change { background: #f3e8ff; color: #7c3aed; border-color: #ddd6fe; }
.type-badge--rd { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
.type-badge--standard { background: #f1f5f9; color: #475569; border-color: #cbd5e1; }
.type-badge--stock { background: #fff7ed; color: #c2410c; border-color: #fed7aa; }
.type-badge--asset { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
.type-badge--exhibit { background: #fdf4ff; color: #a21caf; border-color: #f5d0fe; }
.type-badge--default { background: #f4f6f9; color: #5c6b7f; border-color: #dce3ec; }
.source-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
line-height: 1.4;
border: 1px solid transparent;
}
.source-badge--plm {
color: #6d28d9;
background: #f3e8ff;
border-color: #ddd6fe;
}
.source-badge--manual {
color: #475569;
background: #f1f5f9;
border-color: #cbd5e1;
}
.send-count-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: #909399;
background: #f4f4f5;
border: 1px solid #e4e7ed;
}
.send-count-badge--sent {
color: #2c5282;
background: #e8f0fa;
border-color: #b3cce8;
}
.action-dropdown >>> .el-button-group .el-button {
border-radius: 6px 0 0 6px;
}
/* ── Drawer ── */
.drawer-body {
flex: 1;
min-height: 0;
padding: 12px 24px 32px;
overflow-x: auto;
overflow-y: auto;
}
.drawer-body--bom {
padding: 0;
overflow: hidden;
}
.bom-detail-drawer .el-drawer__body {
display: flex;
flex-direction: column;
}
.drawer-main-tabs >>> .el-tabs__header {
margin-bottom: 16px;
}
.drawer-main-tabs >>> .el-tabs__item {
font-weight: 500;
}
.kingdee-tab-toolbar {
margin-bottom: 12px;
}
/* ── 金蝶待建列表列 ── */
.kingdee-need-group {
display: inline-flex;
flex-direction: column;
gap: 4px;
padding: 2px 0;
}
.kingdee-need-group--clickable {
cursor: pointer;
}
.kingdee-need-group--clickable:hover .kingdee-need-tag--active {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(44, 82, 130, 0.18);
}
.kingdee-need-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 6px;
font-size: 11px;
line-height: 1.4;
border: 1px solid transparent;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
user-select: none;
}
.kingdee-need-tag i {
font-size: 12px;
opacity: 0.85;
}
.kingdee-need-tag em {
font-style: normal;
font-weight: 500;
}
.kingdee-need-tag b {
font-size: 10px;
font-weight: 600;
min-width: 38px;
text-align: center;
padding: 1px 5px;
border-radius: 4px;
font-style: normal;
}
.kingdee-need-tag--mo.kingdee-need-tag--active {
color: #1a6fb5;
background: linear-gradient(135deg, #e8f4fd 0%, #dbeafe 100%);
border-color: #93c5fd;
}
.kingdee-need-tag--mo.kingdee-need-tag--active b {
color: #fff;
background: #2563eb;
}
.kingdee-need-tag--po.kingdee-need-tag--active {
color: #b45309;
background: linear-gradient(135deg, #fef3e2 0%, #fde68a 100%);
border-color: #fcd34d;
}
.kingdee-need-tag--po.kingdee-need-tag--active b {
color: #fff;
background: #d97706;
}
.kingdee-need-tag--idle {
color: #94a3b8;
background: #f8fafc;
border-color: #e2e8f0;
}
.kingdee-need-tag--idle b {
color: #64748b;
background: #e2e8f0;
}
.kingdee-need-tag--active {
cursor: pointer;
}
/* ── 金蝶订单独立抽屉 ── */
.kingdee-order-drawer >>> .el-drawer__header {
margin-bottom: 0;
padding-bottom: 14px;
border-bottom: 1px solid var(--op-border);
}
.kingdee-drawer-body {
padding: 0 4px 24px;
}
.kingdee-drawer-summary {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 16px;
align-items: center;
margin-bottom: 18px;
padding: 14px 18px;
border-radius: var(--op-radius);
background: linear-gradient(135deg, #f8fafc 0%, #eef2f7 100%);
border: 1px solid var(--op-border);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.kingdee-drawer-summary__label {
display: block;
font-size: 11px;
color: var(--op-ink-muted);
margin-bottom: 4px;
letter-spacing: 0.04em;
}
.kingdee-drawer-summary__value {
font-size: 14px;
font-weight: 600;
color: var(--op-ink);
word-break: break-all;
}
.kingdee-drawer-summary__flags {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 148px;
}
.kingdee-drawer-flag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
border: 1px solid transparent;
}
.kingdee-drawer-flag--done {
color: #1e7e4a;
background: var(--op-success-soft);
border-color: rgba(30, 126, 74, 0.25);
}
.kingdee-drawer-flag--idle {
color: #94a3b8;
background: #f8fafc;
border-color: #e2e8f0;
}
.kingdee-order-tabs >>> .el-tabs__header {
margin-bottom: 14px;
}
.kingdee-order-tabs >>> .el-tabs__item {
font-weight: 600;
color: var(--op-ink-muted);
}
.kingdee-order-tabs >>> .el-tabs__item.is-active {
color: var(--op-steel);
}
.kingdee-tab-label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.kingdee-tab-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #d97706;
box-shadow: 0 0 0 2px rgba(217, 119, 6, 0.25);
animation: kingdeeDotPulse 1.8s ease-in-out infinite;
}
@keyframes kingdeeDotPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.65; transform: scale(0.85); }
}
.kingdee-order-table {
width: 100%;
border-radius: 8px;
overflow: hidden;
}
.kingdee-order-table >>> .el-table__header th {
background: linear-gradient(180deg, #f5f7fa 0%, #eef2f6 100%) !important;
color: var(--op-ink);
font-weight: 600;
}
@media (max-width: 960px) {
.kingdee-drawer-summary {
grid-template-columns: 1fr;
}
}
.drawer-section-title {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
font-size: 14px;
font-weight: 600;
border-radius: 20px;
}
.drawer-section-title--block {
display: flex;
margin-bottom: 16px;
}
.drawer-section-title--green {
color: #1e7e4a;
background: var(--op-success-soft);
border: 1px solid rgba(30, 126, 74, 0.2);
}
.drawer-section-title--blue {
color: var(--op-steel);
background: #e8f0fa;
border: 1px solid rgba(44, 82, 130, 0.2);
}
.drawer-divider {
margin: 24px 0 16px;
}
.drawer-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.drawer-toolbar--wrap {
margin-bottom: 10px;
}
.drawer-action-block {
margin-bottom: 20px;
}
.draw-progress {
margin: 12px 0;
}
.draw-log-card {
background: #1a2942;
border: none;
border-radius: 8px;
min-height: 120px;
max-height: 220px;
overflow-y: auto;
}
.draw-log-card >>> .el-card__body {
padding: 14px 16px;
}
.draw-log-content {
font-family: "Consolas", "Monaco", "Courier New", monospace;
font-size: 12px;
line-height: 1.6;
color: #a8d4a0;
white-space: pre-line;
}
/* 图纸类型下拉选项样式 */
.drawing-option {
padding: 8px 0;
line-height: 1.4;
width: 100%;
}
.drawing-header {
display: flex;
align-items: center;
margin-bottom: 6px;
}
.drawing-header i {
color: #409eff;
margin-right: 8px;
font-size: 14px;
}
.drawing-type {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.drawing-path {
display: flex;
align-items: flex-start;
color: #909399;
font-size: 12px;
line-height: 1.3;
margin-top: 6px;
width: 100%;
padding-left: 22px;
/* 与上面的图标对齐 */
}
.drawing-path i {
color: #c0c4cc;
margin-right: 8px;
margin-top: 1px;
font-size: 12px;
flex-shrink: 0;
}
.path-text {
flex: 1;
word-break: break-all;
white-space: normal;
line-height: 1.4;
color: #909399;
font-size: 12px;
display: block;
overflow: visible;
}
/* 全局下拉框宽度控制 */
:global(.drawing-select-dropdown) {
min-width: 600px !important;
max-width: 800px !important;
}
:global(.drawing-select-dropdown .el-select-dropdown__item) {
padding: 12px 16px !important;
min-height: 80px !important;
}
:global(.drawing-select-dropdown .el-select-dropdown__item .drawing-option) {
width: 100% !important;
}
:global(.drawing-select-dropdown .el-select-dropdown__item .drawing-path) {
display: flex !important;
align-items: flex-start !important;
margin-top: 6px !important;
padding-left: 22px !important;
}
:global(.drawing-select-dropdown .el-select-dropdown__item .path-text) {
flex: 1 !important;
word-break: break-all !important;
white-space: normal !important;
line-height: 1.4 !important;
color: #909399 !important;
font-size: 12px !important;
display: block !important;
overflow: visible !important;
}
:global(.drawing-select-dropdown .el-select-dropdown__item.selected .drawing-path) {
color: rgba(255, 255, 255, 0.8) !important;
}
:global(.drawing-select-dropdown .el-select-dropdown__item.selected .path-text) {
color: rgba(255, 255, 255, 0.8) !important;
}
.chain-suggestion-item {
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
.chain-suggestion-item:last-child {
border-bottom: none;
}
.chain-name {
font-size: 14px;
color: var(--op-steel, #2c5282);
margin-bottom: 4px;
}
.chain-info {
display: flex;
flex-wrap: wrap;
gap: 16px;
color: #909399;
font-size: 12px;
margin-top: 2px;
line-height: 1.3;
word-break: break-all;
}
.chain-info span {
margin-right: 8px;
}
.el-select {
width: 100%;
margin-bottom: 20px;
}
.project-name-column {
color: #2c7a7b;
font-weight: 500;
}
/* MRP运算结果复查表格样式 */
.mrp-result-card >>> .el-card__header {
background: linear-gradient(135deg, #f8f9fa 0%, #eef2f7 100%);
border-bottom: 2px solid var(--op-accent, #c9a227);
}
.mrp-result-card .el-table {
margin-top: 10px;
}
.mrp-result-card .el-table th {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%) !important;
color: #606266;
font-weight: 600;
}
.mrp-result-card .el-table--striped .el-table__body tr.el-table__row--striped td {
background-color: #fafafa;
}
.mrp-result-card .el-table__body tr:hover>td {
background-color: #f0f9ff !important;
}
/* 数值列的颜色样式 */
.mrp-result-card .el-table .cell {
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 13px;
}
/* 分页样式 */
.mrp-result-card .el-pagination {
margin-top: 20px;
text-align: right;
}
.mrp-result-card .el-pagination .el-pager li {
background: #fff;
border: 1px solid #d9d9d9;
color: #606266;
}
.mrp-result-card .el-pagination .el-pager li.active {
background: #409eff;
color: #fff;
border-color: #409eff;
}
/* 负库存行样式 */
.mrp-result-card .el-table .negative-stock-row {
background-color: #fef0f0 !important;
}
.mrp-result-card .el-table .negative-stock-row:hover>td {
background-color: #fde2e2 !important;
}
/* 零库存行样式 */
.mrp-result-card .el-table .zero-stock-row {
background-color: #fdf6ec !important;
}
.mrp-result-card .el-table .zero-stock-row:hover>td {
background-color: #faecd8 !important;
}
/* MRP运算状态指示器样式 */
.mrp-status-indicator {
display: flex;
align-items: center;
padding: 8px 12px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 1px solid #bae6fd;
border-radius: 6px;
color: #0369a1;
font-size: 13px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.mrp-status-indicator i {
color: #0ea5e9;
font-size: 14px;
}
/* MRP核算按钮样式 */
.mrp-calculate-btn {
min-width: 100px;
height: 36px;
font-weight: 500;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
transition: all 0.3s ease;
}
.mrp-calculate-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(64, 158, 255, 0.3);
}
.mrp-calculate-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* MRP导出按钮样式 */
.mrp-export-btn {
min-width: 90px;
height: 32px;
font-weight: 500;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(103, 194, 58, 0.2);
transition: all 0.3s ease;
}
.mrp-export-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(103, 194, 58, 0.3);
}
.mrp-export-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* MRP搜索输入框样式 */
.mrp-search-input {
transition: all 0.3s ease;
}
.mrp-search-input:hover .el-input__inner {
border-color: #409eff;
}
.mrp-search-input .el-input__inner:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
</style>