BOM发料对比
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>绩效核查与 BOM 比对</title>
|
||||
<!-- 引入 ElementUI 样式 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
@@ -13,62 +12,104 @@
|
||||
<!-- 引入 axios -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; background-color: #f0f2f5; }
|
||||
.box-card { margin-bottom: 20px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-header h2 { margin: 0; color: #303133; }
|
||||
.action-row { margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; background-color: #f8f9fa; padding: 15px; border-radius: 4px; border-left: 5px solid #409EFF;}
|
||||
.pagination-container { margin-top: 20px; text-align: right; }
|
||||
.filter-row { margin-bottom: 15px; display: flex; gap: 15px;}
|
||||
body { margin: 0; padding: 0; background-color: #f0f2f5; font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; }
|
||||
.header { background-color: #fff; padding: 15px 30px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.header h2 { margin: 0; color: #303133; font-size: 20px; }
|
||||
.main-container { padding: 0 30px; margin-bottom: 30px; }
|
||||
.card-panel { background-color: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); padding: 20px; margin-bottom: 20px; }
|
||||
.filter-row { display: flex; gap: 15px; margin-bottom: 15px; align-items: center; }
|
||||
.pagination-container { margin-top: 15px; display: flex; justify-content: flex-end; }
|
||||
.el-table--small td, .el-table--small th { padding: 4px 0; }
|
||||
.el-table__expand-icon { height: 16px !important; line-height: 16px !important; margin-right: 5px; }
|
||||
.el-tag { font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<el-card class="box-card">
|
||||
<div class="page-header">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<h2 style="margin-right: 20px;"><i class="el-icon-data-analysis" style="margin-right: 10px; color: #409EFF;"></i>绩效核查与 BOM 比对</h2>
|
||||
<el-tag type="success" effect="dark" style="font-size: 14px; padding: 0 15px; height: 32px; line-height: 30px;" v-if="dateRange.start !== '-'">
|
||||
<i class="el-icon-date"></i> <span v-text="'当前核对月份数据:' + dateRange.start + ' 至 ' + dateRange.end"></span>
|
||||
</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
|
||||
</div>
|
||||
<div class="header">
|
||||
<h2>🎯 绩效核查与 BOM 比对</h2>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<el-button type="primary" :icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-refresh'" @click="syncAbnormalReport" :disabled="isSystemBusy" size="small">
|
||||
<span v-text="isSystemBusy ? '抓取中...' : '同步BOM发料台账'"></span>
|
||||
</el-button>
|
||||
<el-button type="warning" :icon="isSystemBusy ? 'el-icon-loading' : 'el-icon-refresh'" @click="syncIssueReceipts" :disabled="isSystemBusy" size="small">
|
||||
<span v-text="isSystemBusy ? '抓取中...' : '同步发料单明细'"></span>
|
||||
</el-button>
|
||||
<el-button type="info" plain icon="el-icon-back" @click="goBack" size="small">返回主控台</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
<div class="action-row">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="font-size: 14px; color: #606266; margin-right: 15px;">
|
||||
<i class="el-icon-info"></i> 执行自动对账前,请先点击"提取并匹配工单"进行数据清洗。
|
||||
</span>
|
||||
<el-date-picker
|
||||
v-model="dateRangeSelect"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
size="small"
|
||||
style="width: 260px; margin-right: 15px;"
|
||||
@change="handleDateChange">
|
||||
</el-date-picker>
|
||||
<div class="main-container">
|
||||
<div class="card-panel" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<span style="font-size: 14px; color: #606266; margin-right: 15px;"><i class="el-icon-date"></i> 对账月份:</span>
|
||||
<el-date-picker v-model="dateRangeSelect" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" size="small" style="width: 260px; margin-right: 15px;" @change="handleDateChange"></el-date-picker>
|
||||
</div>
|
||||
<el-button type="primary" icon="el-icon-magic-stick" @click="triggerMatch" :loading="matching">提取并匹配工单</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<el-tabs v-model="activeTab" type="border-card" @tab-click="handleTabClick">
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
|
||||
<!-- Tab 1: 无工单明细视图 -->
|
||||
<!-- Tab 1: 工单发料明细 -->
|
||||
<el-tab-pane label="工单发料明细" name="official">
|
||||
<span slot="label"><i class="el-icon-document"></i> 工单发料明细</span>
|
||||
<div class="filter-row">
|
||||
<el-input v-model="officialSearch" placeholder="搜索工单号/物料名称/代码" style="width: 300px" clearable prefix-icon="el-icon-search"></el-input>
|
||||
<el-radio-group v-model="officialStatusFilter" size="small" style="margin-left: 15px;">
|
||||
<el-radio-button label="">全部 (<span v-text="officialStatusCounts['全部']"></span>)</el-radio-button>
|
||||
<el-radio-button label="发料正常">发料正常 (<span v-text="officialStatusCounts['发料正常']"></span>)</el-radio-button>
|
||||
<el-radio-button label="超领发料">超领发料 (<span v-text="officialStatusCounts['超领发料']"></span>)</el-radio-button>
|
||||
<el-radio-button label="少领发料">少领发料 (<span v-text="officialStatusCounts['少领发料']"></span>)</el-radio-button>
|
||||
<el-radio-button label="未发料">未发料 (<span v-text="officialStatusCounts['未发料']"></span>)</el-radio-button>
|
||||
<el-radio-button label="BOM外发料">BOM外发料 (<span v-text="officialStatusCounts['BOM外发料']"></span>)</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="success" icon="el-icon-download" @click="exportOfficialData" size="small" style="margin-left: auto;">导出 Excel</el-button>
|
||||
</div>
|
||||
<el-table :data="pagedOfficialData" v-loading="loadingOfficial" row-key="id" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||
<el-table-column prop="sfc" label="工单号 (SFC)" width="150" sortable></el-table-column>
|
||||
<el-table-column prop="order_date" label="工单时间" width="160" sortable></el-table-column>
|
||||
<el-table-column prop="workshop" label="生产车间" width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="material_code" label="物料代码" width="120"></el-table-column>
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="180" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="物料项数" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.id && scope.row.id.startsWith('sfc_')" size="mini" type="info" effect="plain" v-text="getMaterialCount(scope.row) + ' 项'"></el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发料对比" align="center">
|
||||
<el-table-column prop="bom_qty" label="BOM 应发量" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.bom_qty !== ''" v-text="Number(scope.row.bom_qty).toFixed(4)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actual_qty" label="实际发料量" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.actual_qty !== ''" style="font-weight: bold;" v-text="Number(scope.row.actual_qty).toFixed(4)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="diff_qty" label="差异数量 (实-应)" width="130" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.diff_qty !== ''" :style="{color: scope.row.diff_qty > 0 ? '#F56C6C' : (scope.row.diff_qty < 0 ? '#E6A23C' : '#67C23A'), fontWeight: 'bold'}" v-text="(scope.row.diff_qty > 0 ? '+' : '') + Number(scope.row.diff_qty).toFixed(4)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120" align="center" sortable>
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status && scope.row.status !== '-'" :type="getStatusType(scope.row.status)" effect="dark" size="small" v-text="scope.row.status"></el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-container">
|
||||
<el-pagination @size-change="handleOfficialSizeChange" @current-change="handleOfficialCurrentChange" :current-page="officialPage" :page-sizes="[20, 50, 100, 500]" :page-size="officialPageSize" layout="total, sizes, prev, pager, next, jumper" :total="filteredOfficialData.length"></el-pagination>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: 无工单发料明细 -->
|
||||
<el-tab-pane label="无工单发料明细" name="unmatched">
|
||||
<span slot="label"><i class="el-icon-warning-outline"></i> 无工单发料明细 <el-badge v-if="filteredUnmatchedData.length > 0" :value="filteredUnmatchedData.length" class="mark" type="warning" /></span>
|
||||
|
||||
<span slot="label"><i class="el-icon-warning-outline"></i> 无工单发料明细 <el-badge v-if="filteredUnmatchedData.length > 0" :value="filteredUnmatchedData.length" class="mark" type="warning"></el-badge></span>
|
||||
<div class="filter-row">
|
||||
<el-input v-model="unmatchedSearch" placeholder="搜索物料名称/代码/领料人" style="width: 300px" clearable prefix-icon="el-icon-search"></el-input>
|
||||
</div>
|
||||
|
||||
<el-table :data="pagedUnmatchedData" v-loading="loadingUnmatched" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="execution_time" label="执行时间" width="160" sortable></el-table-column>
|
||||
@@ -91,43 +132,37 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 前端分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
@size-change="val => { unmatchedPageSize = val; unmatchedPage = 1; }"
|
||||
@current-change="val => { unmatchedPage = val; }"
|
||||
:current-page="unmatchedPage"
|
||||
:page-sizes="[20, 50, 100, 500]"
|
||||
:page-size="unmatchedPageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="filteredUnmatchedData.length">
|
||||
</el-pagination>
|
||||
<el-pagination @size-change="handleUnmatchedSizeChange" @current-change="handleUnmatchedCurrentChange" :current-page="unmatchedPage" :page-sizes="[20, 50, 100, 500]" :page-size="unmatchedPageSize" layout="total, sizes, prev, pager, next, jumper" :total="filteredUnmatchedData.length"></el-pagination>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: BOM发料差异视图 -->
|
||||
<el-tab-pane label="BOM 发料对账" name="reconciliation">
|
||||
<span slot="label"><i class="el-icon-finished"></i> BOM 发料对账</span>
|
||||
|
||||
<!-- Tab 3: 从备注中提取的工单 -->
|
||||
<el-tab-pane label="从备注中提取的工单" name="inferred">
|
||||
<span slot="label"><i class="el-icon-magic-stick"></i> 从备注中提取的工单</span>
|
||||
<div class="filter-row">
|
||||
<el-input v-model="reconSearch" placeholder="搜索工单号/物料名称/代码" style="width: 300px" clearable prefix-icon="el-icon-search"></el-input>
|
||||
<el-select v-model="reconStatusFilter" placeholder="状态筛选" clearable style="width: 150px">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="发料正常" value="发料正常"></el-option>
|
||||
<el-option label="超领发料" value="超领发料"></el-option>
|
||||
<el-option label="少领发料" value="少领发料"></el-option>
|
||||
<el-option label="未发料" value="未发料"></el-option>
|
||||
<el-option label="BOM外发料" value="BOM外发料"></el-option>
|
||||
</el-select>
|
||||
<el-button type="success" icon="el-icon-download" @click="exportReconData" size="small" style="margin-left: auto;">导出 Excel</el-button>
|
||||
<el-input v-model="inferredSearch" placeholder="搜索工单号/物料名称/代码" style="width: 300px" clearable prefix-icon="el-icon-search"></el-input>
|
||||
<el-radio-group v-model="inferredStatusFilter" size="small" style="margin-left: 15px;">
|
||||
<el-radio-button label="">全部 (<span v-text="inferredStatusCounts['全部']"></span>)</el-radio-button>
|
||||
<el-radio-button label="发料正常">发料正常 (<span v-text="inferredStatusCounts['发料正常']"></span>)</el-radio-button>
|
||||
<el-radio-button label="超领发料">超领发料 (<span v-text="inferredStatusCounts['超领发料']"></span>)</el-radio-button>
|
||||
<el-radio-button label="少领发料">少领发料 (<span v-text="inferredStatusCounts['少领发料']"></span>)</el-radio-button>
|
||||
<el-radio-button label="未发料">未发料 (<span v-text="inferredStatusCounts['未发料']"></span>)</el-radio-button>
|
||||
<el-radio-button label="BOM外发料">BOM外发料 (<span v-text="inferredStatusCounts['BOM外发料']"></span>)</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="success" icon="el-icon-download" @click="exportInferredData" size="small" style="margin-left: auto;">导出 Excel</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="pagedReconData" v-loading="loadingRecon" row-key="id" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||
<el-table-column prop="sfc" label="工单号 (SFC)" width="150" sortable></el-table-column>
|
||||
<el-table :data="pagedInferredData" v-loading="loadingInferred" row-key="id" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" border style="width: 100%" stripe size="small" :header-cell-style="{background:'#f5f7fa',color:'#606266'}">
|
||||
<el-table-column prop="sfc" label="提取的工单号" width="150" sortable></el-table-column>
|
||||
<el-table-column prop="order_date" label="工单时间" width="160" sortable></el-table-column>
|
||||
<el-table-column prop="workshop" label="生产车间" width="120" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="material_code" label="物料代码" width="120"></el-table-column>
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="180" show-overflow-tooltip></el-table-column>
|
||||
|
||||
<el-table-column label="物料项数" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.id && scope.row.id.startsWith('sfc_')" size="mini" type="info" effect="plain" v-text="getMaterialCount(scope.row) + ' 项'"></el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发料对比" align="center">
|
||||
<el-table-column prop="bom_qty" label="BOM 应发量" width="110" align="right">
|
||||
<template slot-scope="scope">
|
||||
@@ -141,38 +176,23 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="diff_qty" label="差异数量 (实-应)" width="130" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.diff_qty !== ''" :style="{
|
||||
color: scope.row.diff_qty > 0 ? '#F56C6C' : (scope.row.diff_qty < 0 ? '#E6A23C' : '#67C23A'),
|
||||
fontWeight: 'bold'
|
||||
}" v-text="(scope.row.diff_qty > 0 ? '+' : '') + Number(scope.row.diff_qty).toFixed(4)">
|
||||
</span>
|
||||
<span v-if="scope.row.diff_qty !== ''" :style="{color: scope.row.diff_qty > 0 ? '#F56C6C' : (scope.row.diff_qty < 0 ? '#E6A23C' : '#67C23A'), fontWeight: 'bold'}" v-text="(scope.row.diff_qty > 0 ? '+' : '') + Number(scope.row.diff_qty).toFixed(4)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="状态" width="120" align="center" sortable>
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status && scope.row.status !== '-'" :type="getStatusType(scope.row.status)" effect="dark" size="small" v-text="scope.row.status"></el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 前端分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
@size-change="val => { reconPageSize = val; reconPage = 1; }"
|
||||
@current-change="val => { reconPage = val; }"
|
||||
:current-page="reconPage"
|
||||
:page-sizes="[20, 50, 100, 500]"
|
||||
:page-size="reconPageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="filteredReconData.length">
|
||||
</el-pagination>
|
||||
<el-pagination @size-change="handleInferredSizeChange" @current-change="handleInferredCurrentChange" :current-page="inferredPage" :page-sizes="[20, 50, 100, 500]" :page-size="inferredPageSize" layout="total, sizes, prev, pager, next, jumper" :total="filteredInferredData.length"></el-pagination>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -180,34 +200,78 @@
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
isSystemBusy: false,
|
||||
activeTab: 'official',
|
||||
matching: false,
|
||||
dateRange: { start: '-', end: '-' },
|
||||
dateRangeSelect: [],
|
||||
|
||||
// Unmatched Tab Data
|
||||
loadingOfficial: false,
|
||||
officialData: [],
|
||||
officialSearch: '',
|
||||
officialStatusFilter: '',
|
||||
officialPage: 1,
|
||||
officialPageSize: 50,
|
||||
|
||||
loadingUnmatched: false,
|
||||
unmatchedData: [],
|
||||
unmatchedSearch: '',
|
||||
unmatchedPage: 1,
|
||||
unmatchedPageSize: 50,
|
||||
|
||||
// Reconciliation Tab Data
|
||||
loadingRecon: false,
|
||||
reconData: [],
|
||||
reconSearch: '',
|
||||
reconStatusFilter: '',
|
||||
reconPage: 1,
|
||||
reconPageSize: 50
|
||||
}
|
||||
loadingInferred: false,
|
||||
inferredData: [],
|
||||
inferredSearch: '',
|
||||
inferredStatusFilter: '',
|
||||
inferredPage: 1,
|
||||
inferredPageSize: 50,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 前端过滤和分页计算属性
|
||||
officialStatusCounts() {
|
||||
const counts = { '全部': 0, '发料正常': 0, '超领发料': 0, '少领发料': 0, '未发料': 0, 'BOM外发料': 0 };
|
||||
if (!this.officialData) return counts;
|
||||
|
||||
const checkStatus = (node, status) => {
|
||||
if (node.status === status) return true;
|
||||
if (node.children && node.children.length > 0) {
|
||||
return node.children.some(child => checkStatus(child, status));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
counts['全部'] = this.officialData.length;
|
||||
['发料正常', '超领发料', '少领发料', '未发料', 'BOM外发料'].forEach(status => {
|
||||
counts[status] = this.officialData.filter(row => checkStatus(row, status)).length;
|
||||
});
|
||||
|
||||
return counts;
|
||||
},
|
||||
inferredStatusCounts() {
|
||||
const counts = { '全部': 0, '发料正常': 0, '超领发料': 0, '少领发料': 0, '未发料': 0, 'BOM外发料': 0 };
|
||||
if (!this.inferredData) return counts;
|
||||
|
||||
const checkStatus = (node, status) => {
|
||||
if (node.status === status) return true;
|
||||
if (node.children && node.children.length > 0) {
|
||||
return node.children.some(child => checkStatus(child, status));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
counts['全部'] = this.inferredData.length;
|
||||
['发料正常', '超领发料', '少领发料', '未发料', 'BOM外发料'].forEach(status => {
|
||||
counts[status] = this.inferredData.filter(row => checkStatus(row, status)).length;
|
||||
});
|
||||
|
||||
return counts;
|
||||
},
|
||||
filteredUnmatchedData() {
|
||||
let data = this.unmatchedData;
|
||||
let data = this.unmatchedData || [];
|
||||
if (this.unmatchedSearch) {
|
||||
const keyword = this.unmatchedSearch.toLowerCase();
|
||||
data = data.filter(item =>
|
||||
(item.issue_receipt_no && item.issue_receipt_no.toLowerCase().includes(keyword)) ||
|
||||
(item.material_name && item.material_name.toLowerCase().includes(keyword)) ||
|
||||
(item.material_code && item.material_code.toLowerCase().includes(keyword)) ||
|
||||
(item.executor_user_name && item.executor_user_name.toLowerCase().includes(keyword))
|
||||
@@ -220,15 +284,11 @@
|
||||
const end = start + this.unmatchedPageSize;
|
||||
return this.filteredUnmatchedData.slice(start, end);
|
||||
},
|
||||
|
||||
filteredReconData() {
|
||||
let data = this.reconData;
|
||||
|
||||
if (this.reconSearch || this.reconStatusFilter) {
|
||||
const keyword = this.reconSearch ? this.reconSearch.toLowerCase() : '';
|
||||
const statusFilter = this.reconStatusFilter;
|
||||
|
||||
// 辅助函数:递归检查节点及其子节点是否匹配条件
|
||||
filteredOfficialData() {
|
||||
let result = this.officialData || [];
|
||||
if (this.officialSearch || this.officialStatusFilter) {
|
||||
const keyword = this.officialSearch ? this.officialSearch.toLowerCase() : '';
|
||||
const statusFilter = this.officialStatusFilter;
|
||||
const checkNode = (node) => {
|
||||
let match = true;
|
||||
if (keyword) {
|
||||
@@ -238,129 +298,187 @@
|
||||
(node.material_name && node.material_name.toLowerCase().includes(keyword))
|
||||
);
|
||||
}
|
||||
if (statusFilter) {
|
||||
match = match && (node.status === statusFilter);
|
||||
}
|
||||
if (statusFilter) { match = match && (node.status === statusFilter); }
|
||||
if (match) return true;
|
||||
|
||||
// 如果当前节点不匹配,检查是否有子节点匹配
|
||||
if (node.children && node.children.length > 0) {
|
||||
return node.children.some(child => checkNode(child));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
data = data.filter(row => checkNode(row));
|
||||
result = result.filter(row => checkNode(row));
|
||||
}
|
||||
return data;
|
||||
return result;
|
||||
},
|
||||
pagedReconData() {
|
||||
const start = (this.reconPage - 1) * this.reconPageSize;
|
||||
const end = start + this.reconPageSize;
|
||||
return this.filteredReconData.slice(start, end);
|
||||
pagedOfficialData() {
|
||||
const start = (this.officialPage - 1) * this.officialPageSize;
|
||||
const end = start + this.officialPageSize;
|
||||
return this.filteredOfficialData.slice(start, end);
|
||||
},
|
||||
filteredInferredData() {
|
||||
let result = this.inferredData || [];
|
||||
if (this.inferredSearch || this.inferredStatusFilter) {
|
||||
const keyword = this.inferredSearch ? this.inferredSearch.toLowerCase() : '';
|
||||
const statusFilter = this.inferredStatusFilter;
|
||||
const checkNode = (node) => {
|
||||
let match = true;
|
||||
if (keyword) {
|
||||
match = match && (
|
||||
(node.sfc && node.sfc.toLowerCase().includes(keyword)) ||
|
||||
(node.material_code && node.material_code.toLowerCase().includes(keyword)) ||
|
||||
(node.material_name && node.material_name.toLowerCase().includes(keyword))
|
||||
);
|
||||
}
|
||||
if (statusFilter) { match = match && (node.status === statusFilter); }
|
||||
if (match) return true;
|
||||
if (node.children && node.children.length > 0) {
|
||||
return node.children.some(child => checkNode(child));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
result = result.filter(row => checkNode(row));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
pagedInferredData() {
|
||||
const start = (this.inferredPage - 1) * this.inferredPageSize;
|
||||
const end = start + this.inferredPageSize;
|
||||
return this.filteredInferredData.slice(start, end);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dateRangeSelect(newVal) {
|
||||
if (newVal && newVal.length === 2) {
|
||||
this.loadUnmatchedData();
|
||||
this.loadOfficialData();
|
||||
this.loadInferredData();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 默认初始化为当月的第一天和最后一天
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth() + 1;
|
||||
const pad = (n) => n.toString().padStart(2, '0');
|
||||
const lastDayDate = new Date(y, m, 0);
|
||||
const firstDay = `${y}-${pad(m)}-01`;
|
||||
const lastDay = `${y}-${pad(m)}-${pad(lastDayDate.getDate())}`;
|
||||
this.dateRangeSelect = [firstDay, lastDay];
|
||||
|
||||
// 初始化加载数据
|
||||
this.loadSummary();
|
||||
this.loadUnmatchedData();
|
||||
this.loadReconData();
|
||||
this.loadOfficialData();
|
||||
this.loadInferredData();
|
||||
this.timer = setInterval(this.checkTaskStatus, 2000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
},
|
||||
methods: {
|
||||
handleDateChange(val) {
|
||||
if (val && val.length === 2) {
|
||||
this.dateRange.start = val[0];
|
||||
this.dateRange.end = val[1];
|
||||
this.loadUnmatchedData();
|
||||
this.loadReconData();
|
||||
} else {
|
||||
this.loadSummary();
|
||||
}
|
||||
},
|
||||
loadSummary() {
|
||||
axios.get('/api/analysis/summary')
|
||||
.then(res => {
|
||||
this.dateRange = res.data;
|
||||
if (this.dateRange.start !== '-') {
|
||||
this.dateRangeSelect = [this.dateRange.start, this.dateRange.end];
|
||||
}
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
},
|
||||
goBack() {
|
||||
window.location.href = '/';
|
||||
},
|
||||
handleTabClick(tab) {
|
||||
// if (tab.name === 'unmatched') this.loadUnmatchedData();
|
||||
// if (tab.name === 'reconciliation') this.loadReconData();
|
||||
},
|
||||
getStatusType(status) {
|
||||
const map = {
|
||||
'发料正常': 'success',
|
||||
'超领发料': 'danger',
|
||||
'少领发料': 'warning',
|
||||
'未发料': 'info',
|
||||
'BOM外发料': 'danger'
|
||||
};
|
||||
return map[status] || 'info';
|
||||
},
|
||||
triggerMatch() {
|
||||
this.$confirm('此操作将基于最新抓取的发料单据和 BOM 表数据进行自动清洗匹配,确认执行?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
syncAbnormalReport() {
|
||||
this.$confirm('确定要抓取最新的 BOM 发料异常核对报表吗?该操作会在后台自动执行。', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => {
|
||||
this.matching = true;
|
||||
if (window.globalLogApp) {
|
||||
window.globalLogApp.logDialogVisible = true;
|
||||
}
|
||||
axios.post('/api/analysis/match_work_orders')
|
||||
.then(res => {
|
||||
if (res.data.success) {
|
||||
this.$message.success(res.data.message);
|
||||
// 开启轮询等待后台任务完成,完成后再刷新数据
|
||||
this.waitForMatchTask();
|
||||
} else {
|
||||
this.$message.error('执行失败: ' + res.data.message);
|
||||
this.matching = false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.status === 409) {
|
||||
this.$message.warning(err.response.data.message);
|
||||
} else {
|
||||
this.$message.error('请求出错,请检查后台日志');
|
||||
}
|
||||
this.matching = false;
|
||||
});
|
||||
this.isSystemBusy = true;
|
||||
axios.post('/api/sync_abnormal_report').then(res => {
|
||||
if (res.data.success) {
|
||||
this.$message.success('已触发!' + res.data.message);
|
||||
setTimeout(this.checkTaskStatus, 1000);
|
||||
} else {
|
||||
this.$message.error('触发失败:' + res.data.message);
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$message.error('请求失败');
|
||||
this.isSystemBusy = false;
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
waitForMatchTask() {
|
||||
// 轮询检查任务状态,一旦结束就刷新列表
|
||||
let checkInterval = setInterval(() => {
|
||||
axios.get('/api/task_status')
|
||||
.then(res => {
|
||||
if (!res.data.is_busy) {
|
||||
clearInterval(checkInterval);
|
||||
this.matching = false;
|
||||
this.$message.success('清洗匹配完成!');
|
||||
this.loadSummary();
|
||||
this.loadUnmatchedData();
|
||||
this.loadReconData();
|
||||
}
|
||||
})
|
||||
.catch(err => {});
|
||||
}, 1000);
|
||||
syncIssueReceipts() {
|
||||
this.$confirm('确定要抓取最新的 发料单明细 吗?该操作会在后台自动执行。', '提示', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
|
||||
}).then(() => {
|
||||
this.isSystemBusy = true;
|
||||
axios.post('/api/sync_issue_receipts').then(res => {
|
||||
if (res.data.success) {
|
||||
this.$message.success('已触发!' + res.data.message);
|
||||
setTimeout(this.checkTaskStatus, 1000);
|
||||
} else {
|
||||
this.$message.error('触发失败:' + res.data.message);
|
||||
}
|
||||
}).catch(err => {
|
||||
this.$message.error('请求失败');
|
||||
this.isSystemBusy = false;
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
checkTaskStatus() {
|
||||
axios.get('/api/task_status').then(res => {
|
||||
if (res.data.is_busy) {
|
||||
this.isSystemBusy = true;
|
||||
setTimeout(this.checkTaskStatus, 3000);
|
||||
} else {
|
||||
if (this.isSystemBusy) {
|
||||
this.$message.success('数据抓取任务已完成!正在重新加载数据...');
|
||||
this.loadSummary();
|
||||
this.loadUnmatchedData();
|
||||
this.loadOfficialData();
|
||||
this.loadInferredData();
|
||||
}
|
||||
this.isSystemBusy = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
getMaterialCount(row) {
|
||||
if (!row.id || !row.id.startsWith('sfc_')) return '';
|
||||
let count = 0;
|
||||
const traverse = (nodes) => {
|
||||
if (!nodes) return;
|
||||
nodes.forEach(n => {
|
||||
count++;
|
||||
traverse(n.children);
|
||||
});
|
||||
};
|
||||
traverse(row.children);
|
||||
return count;
|
||||
},
|
||||
goBack() { window.location.href = '/'; },
|
||||
handleDateChange(val) {
|
||||
if (!val) this.dateRangeSelect = [this.dateRange.start, this.dateRange.end];
|
||||
},
|
||||
handleOfficialSizeChange(val) { this.officialPageSize = val; this.officialPage = 1; },
|
||||
handleOfficialCurrentChange(val) { this.officialPage = val; },
|
||||
handleUnmatchedSizeChange(val) { this.unmatchedPageSize = val; this.unmatchedPage = 1; },
|
||||
handleUnmatchedCurrentChange(val) { this.unmatchedPage = val; },
|
||||
handleInferredSizeChange(val) { this.inferredPageSize = val; this.inferredPage = 1; },
|
||||
handleInferredCurrentChange(val) { this.inferredPage = val; },
|
||||
getStatusType(status) {
|
||||
if (status === '发料正常') return 'success';
|
||||
if (status === '超领发料') return 'danger';
|
||||
if (status === '少领发料') return 'warning';
|
||||
if (status === '未发料') return 'info';
|
||||
if (status === 'BOM外发料') return 'primary';
|
||||
return 'info';
|
||||
},
|
||||
loadSummary() {
|
||||
axios.get('/api/analysis/summary').then(res => {
|
||||
this.dateRange = res.data;
|
||||
if (!this.dateRangeSelect || this.dateRangeSelect.length === 0) {
|
||||
this.dateRangeSelect = [this.dateRange.start, this.dateRange.end];
|
||||
}
|
||||
}).catch(err => console.error(err));
|
||||
},
|
||||
checkTaskStatus() {
|
||||
axios.get('/api/task_status').then(res => {
|
||||
if (res.data.is_busy && res.data.task_name.includes('自动清洗')) {
|
||||
this.matching = true;
|
||||
} else {
|
||||
if (this.matching) {
|
||||
this.matching = false;
|
||||
this.loadUnmatchedData();
|
||||
this.loadOfficialData();
|
||||
this.loadInferredData();
|
||||
}
|
||||
}
|
||||
}).catch(err => console.error('获取任务状态失败', err));
|
||||
},
|
||||
triggerMatch() {
|
||||
this.matching = true;
|
||||
axios.post('/api/analysis/match_work_orders')
|
||||
.then(res => this.$message.success(res.data.message))
|
||||
.catch(err => {
|
||||
this.matching = false;
|
||||
this.$message.error(err.response?.data?.message || '触发失败');
|
||||
});
|
||||
},
|
||||
loadUnmatchedData() {
|
||||
this.loadingUnmatched = true;
|
||||
@@ -368,69 +486,74 @@
|
||||
if (this.dateRangeSelect && this.dateRangeSelect.length === 2) {
|
||||
url += `?start=${this.dateRangeSelect[0]}&end=${this.dateRangeSelect[1]}`;
|
||||
}
|
||||
axios.get(url)
|
||||
.then(res => {
|
||||
this.unmatchedData = res.data.rows;
|
||||
this.unmatchedPage = 1;
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error('加载无工单明细失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingUnmatched = false;
|
||||
});
|
||||
axios.get(url).then(res => {
|
||||
this.unmatchedData = res.data.rows || [];
|
||||
this.unmatchedPage = 1;
|
||||
}).catch(err => console.error(err)).finally(() => { this.loadingUnmatched = false; });
|
||||
},
|
||||
loadReconData() {
|
||||
this.loadingRecon = true;
|
||||
let url = '/api/analysis/bom_reconciliation';
|
||||
loadOfficialData() {
|
||||
this.loadingOfficial = true;
|
||||
let url = '/api/analysis/bom_reconciliation?match_type=official';
|
||||
if (this.dateRangeSelect && this.dateRangeSelect.length === 2) {
|
||||
url += `?start=${this.dateRangeSelect[0]}&end=${this.dateRangeSelect[1]}`;
|
||||
url += `&start=${this.dateRangeSelect[0]}&end=${this.dateRangeSelect[1]}`;
|
||||
}
|
||||
axios.get(url)
|
||||
.then(res => {
|
||||
this.reconData = res.data.rows;
|
||||
this.reconPage = 1;
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error('加载 BOM 比对数据失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingRecon = false;
|
||||
});
|
||||
axios.get(url).then(res => {
|
||||
this.officialData = res.data.rows || [];
|
||||
this.officialPage = 1;
|
||||
}).catch(err => console.error(err)).finally(() => { this.loadingOfficial = false; });
|
||||
},
|
||||
exportReconData() {
|
||||
// 简单的前端 CSV 导出(支持树形结构展开)
|
||||
loadInferredData() {
|
||||
this.loadingInferred = true;
|
||||
let url = '/api/analysis/bom_reconciliation?match_type=inferred';
|
||||
if (this.dateRangeSelect && this.dateRangeSelect.length === 2) {
|
||||
url += `&start=${this.dateRangeSelect[0]}&end=${this.dateRangeSelect[1]}`;
|
||||
}
|
||||
axios.get(url).then(res => {
|
||||
this.inferredData = res.data.rows || [];
|
||||
this.inferredPage = 1;
|
||||
}).catch(err => console.error(err)).finally(() => { this.loadingInferred = false; });
|
||||
},
|
||||
exportOfficialData() {
|
||||
const headers = ['层级', '工单号(SFC)', '物料代码', '物料名称', 'BOM应发量', '实际发料量', '差异数量', '状态'];
|
||||
let csvContent = "data:text/csv;charset=utf-8,\uFEFF" + headers.join(",") + "\n";
|
||||
|
||||
const processRow = (node, level = 0) => {
|
||||
const indent = " ".repeat(level);
|
||||
const rowData = [
|
||||
level,
|
||||
node.sfc || '',
|
||||
node.material_code || '',
|
||||
`"${indent}${node.material_name || ''}"`,
|
||||
node.bom_qty !== '' ? node.bom_qty : '',
|
||||
node.actual_qty !== '' ? node.actual_qty : '',
|
||||
node.diff_qty !== '' ? node.diff_qty : '',
|
||||
node.status || ''
|
||||
level, node.sfc || '', node.material_code || '', `"${indent}${node.material_name || ''}"`,
|
||||
node.bom_qty !== '' ? node.bom_qty : '', node.actual_qty !== '' ? node.actual_qty : '',
|
||||
node.diff_qty !== '' ? node.diff_qty : '', node.status || ''
|
||||
];
|
||||
csvContent += rowData.join(",") + "\n";
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(child => processRow(child, level + 1));
|
||||
}
|
||||
};
|
||||
|
||||
this.filteredReconData.forEach(row => processRow(row, 0));
|
||||
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
this.filteredOfficialData.forEach(row => processRow(row, 0));
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", "BOM发料对账单.csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
link.setAttribute("href", encodeURI(csvContent));
|
||||
link.setAttribute("download", "工单发料对账单.csv");
|
||||
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
||||
},
|
||||
exportInferredData() {
|
||||
const headers = ['层级', '工单号(SFC)', '物料代码', '物料名称', 'BOM应发量', '实际发料量', '差异数量', '状态'];
|
||||
let csvContent = "data:text/csv;charset=utf-8,\uFEFF" + headers.join(",") + "\n";
|
||||
const processRow = (node, level = 0) => {
|
||||
const indent = " ".repeat(level);
|
||||
const rowData = [
|
||||
level, node.sfc || '', node.material_code || '', `"${indent}${node.material_name || ''}"`,
|
||||
node.bom_qty !== '' ? node.bom_qty : '', node.actual_qty !== '' ? node.actual_qty : '',
|
||||
node.diff_qty !== '' ? node.diff_qty : '', node.status || ''
|
||||
];
|
||||
csvContent += rowData.join(",") + "\n";
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(child => processRow(child, level + 1));
|
||||
}
|
||||
};
|
||||
this.filteredInferredData.forEach(row => processRow(row, 0));
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodeURI(csvContent));
|
||||
link.setAttribute("download", "备注提取工单对账单.csv");
|
||||
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user