6168 lines
207 KiB
Vue
6168 lines
207 KiB
Vue
<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>
|