前端
This commit is contained in:
539
web_ui/templates/bom_compare.html
Normal file
539
web_ui/templates/bom_compare.html
Normal file
@@ -0,0 +1,539 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BOM 成本期间对比分析表</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #ebeef5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header h2 { margin: 0; color: #303133; }
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 20px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.left-sidebar {
|
||||
width: 250px;
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.parent-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-top: 15px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.parent-item {
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.parent-item:hover { background-color: #f0f9eb; }
|
||||
.parent-item.active {
|
||||
background-color: #ecf5ff;
|
||||
border-left: 4px solid #409EFF;
|
||||
}
|
||||
.parent-code { font-size: 12px; color: #909399; margin-bottom: 4px; }
|
||||
.parent-name { font-size: 14px; color: #303133; font-weight: 500; }
|
||||
|
||||
.right-content {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.dot-normal, .dot-missing { display: none; }
|
||||
.dot-fallback { background-color: #F56C6C; } /* 红色:沿用历史价 */
|
||||
|
||||
.price-up { color: #F56C6C; }
|
||||
.price-down { color: #67C23A; }
|
||||
|
||||
/* 紧凑型表格样式 */
|
||||
.el-table--small td, .el-table--small th, .el-table--mini td, .el-table--mini th {
|
||||
padding: 0px !important;
|
||||
}
|
||||
.el-table .cell {
|
||||
line-height: 1.1 !important;
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
font-size: 15px !important; /* 字体放大 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 修复 flex 布局导致 el-table-column 的 align 属性失效的问题 */
|
||||
.el-table td.is-center .cell {
|
||||
justify-content: center;
|
||||
}
|
||||
.el-table td.is-right .cell {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 修复表头由于 display: flex 导致的错位问题 */
|
||||
.el-table th .cell {
|
||||
display: block; /* 恢复表头原有的显示方式 */
|
||||
}
|
||||
|
||||
/* 针对左侧树形结构特殊处理,让前面的箭头和内容对齐 */
|
||||
.el-table__expand-icon {
|
||||
height: 16px !important;
|
||||
line-height: 16px !important;
|
||||
margin-right: 5px; /* 让箭头与文字间距正常 */
|
||||
}
|
||||
|
||||
/* 表格斑马纹样式:隔行换色(白色和非常淡的蓝色) */
|
||||
.el-table__row--striped td {
|
||||
background-color: #F4F9FF !important;
|
||||
}
|
||||
/* 鼠标悬停时的颜色覆盖(稍微加深一点点) */
|
||||
.el-table__body tr:hover > td {
|
||||
background-color: #ecf5ff !important;
|
||||
}
|
||||
|
||||
/* 过滤隐藏行的样式 */
|
||||
.filtered-out {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="header">
|
||||
<h2>📊 BOM 成本期间对比分析表</h2>
|
||||
<div>
|
||||
<el-button type="text" @click="goToBomTree">返回雷达图</el-button>
|
||||
<el-button type="text" @click="goToReceipts">返回收货明细</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<!-- 左侧 1/4:父件选择列表 -->
|
||||
<div class="left-sidebar">
|
||||
<el-input
|
||||
placeholder="搜索成品/父件"
|
||||
v-model="searchKeyword"
|
||||
size="small"
|
||||
clearable
|
||||
@keyup.enter.native="searchParents"
|
||||
@clear="searchParents">
|
||||
<el-button slot="append" icon="el-icon-search" @click="searchParents"></el-button>
|
||||
</el-input>
|
||||
|
||||
<div class="parent-list" v-loading="loadingParents">
|
||||
<div v-if="parents.length === 0" style="text-align: center; color: #909399; padding: 20px;">
|
||||
没有找到成品数据
|
||||
</div>
|
||||
<div
|
||||
v-for="item in parents"
|
||||
:key="item.parent_material_code"
|
||||
class="parent-item"
|
||||
:class="{ active: currentParentCode === item.parent_material_code }"
|
||||
@click="loadCompareData(item.parent_material_code)">
|
||||
<div class="parent-code" v-text="item.parent_material_code"></div>
|
||||
<div class="parent-name" v-text="item.parent_material_name"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 3/4:树形表格展示区 -->
|
||||
<div class="right-content">
|
||||
<!-- 顶部期间选择器 -->
|
||||
<div class="toolbar" v-if="currentParentCode">
|
||||
<!-- 第一行:期间选择 -->
|
||||
<el-row :gutter="20" type="flex" align="middle" style="flex-wrap: wrap; margin-bottom: 10px;">
|
||||
<el-col :span="11">
|
||||
<span style="font-size: 14px; font-weight: bold; margin-right: 10px;">期间 A (基准期)</span>
|
||||
<el-date-picker
|
||||
v-model="periodA_start"
|
||||
type="date"
|
||||
size="small"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="开始日期"
|
||||
style="width: 140px;">
|
||||
</el-date-picker>
|
||||
<span style="margin: 0 5px; color: #909399;">至</span>
|
||||
<el-date-picker
|
||||
v-model="periodA_end"
|
||||
type="date"
|
||||
size="small"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="结束日期"
|
||||
style="width: 140px;">
|
||||
</el-date-picker>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="11">
|
||||
<span style="font-size: 14px; font-weight: bold; margin-right: 10px;">期间 B (对比期)</span>
|
||||
<el-date-picker
|
||||
v-model="periodB_start"
|
||||
type="date"
|
||||
size="small"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="开始日期"
|
||||
style="width: 140px;">
|
||||
</el-date-picker>
|
||||
<span style="margin: 0 5px; color: #909399;">至</span>
|
||||
<el-date-picker
|
||||
v-model="periodB_end"
|
||||
type="date"
|
||||
size="small"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="结束日期"
|
||||
style="width: 140px;">
|
||||
</el-date-picker>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="fetchTreeData" :loading="loadingData">执行对比</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第二行:子件搜索与图例 -->
|
||||
<el-row :gutter="20" type="flex" align="middle" justify="space-between">
|
||||
<el-col :span="12">
|
||||
<el-input
|
||||
placeholder="输入物料名称搜索子件"
|
||||
v-model="filterName"
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
@input="filterNode">
|
||||
</el-input>
|
||||
<el-input
|
||||
placeholder="输入物料代码搜索子件"
|
||||
v-model="filterCode"
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 200px;"
|
||||
@input="filterNode">
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right; font-size: 12px; color: #909399;">
|
||||
图例说明:
|
||||
<span class="status-dot dot-fallback" style="display:inline-block"></span> 期间内无数据,已回溯取历史最近价
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 核心树形表格 -->
|
||||
<div class="table-container" v-if="currentParentCode">
|
||||
<el-table
|
||||
v-loading="loadingData"
|
||||
:data="tableData"
|
||||
size="mini"
|
||||
stripe
|
||||
style="width: 100%; height: 100%;"
|
||||
row-key="id"
|
||||
border
|
||||
default-expand-all
|
||||
height="100%"
|
||||
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
|
||||
|
||||
<el-table-column prop="materialName" label="BOM 结构 (物料代码 - 物料名称)" min-width="350" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div :class="{'filtered-out': !scope.row.visible}" style="display: flex; align-items: center;">
|
||||
<span style="font-family: monospace; font-size: 16px; color: #409EFF; margin-right: 10px; flex-shrink: 0;" v-text="scope.row.materialCode"></span>
|
||||
<strong style="font-size: 16px;" v-text="scope.row.materialName"></strong>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="BOM" align="center" min-width="80">
|
||||
<template slot-scope="scope">
|
||||
<span v-text="scope.row.usageQty + (scope.row.unitName || '')"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="铸件" align="center" min-width="80">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.castingWeight" v-text="scope.row.castingWeight + 'KG'"></span>
|
||||
<span v-else style="color: #C0C4CC;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="历史最新基准价 (¥)" align="center" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.totalLatest > 0" v-text="scope.row.totalLatest.toFixed(2)"></span>
|
||||
<span v-else style="color: #C0C4CC;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="期间 A 最新价 (¥)" align="center" min-width="140">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip effect="dark" :content="getStatusText(scope.row.showAStatus)" placement="top" :disabled="scope.row.showAStatus !== 'fallback'">
|
||||
<span style="display: flex; justify-content: center; align-items: center;">
|
||||
<span class="status-dot" :class="'dot-' + scope.row.showAStatus"></span>
|
||||
<span v-if="scope.row.totalPeriodA > 0" v-text="scope.row.totalPeriodA.toFixed(2)"></span>
|
||||
<span v-else style="color: #C0C4CC; margin-left: 2px;">-</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="期间 B 最新价 (¥)" align="center" min-width="140">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip effect="dark" :content="getStatusText(scope.row.showBStatus)" placement="top" :disabled="scope.row.showBStatus !== 'fallback'">
|
||||
<span style="display: flex; justify-content: center; align-items: center;">
|
||||
<span class="status-dot" :class="'dot-' + scope.row.showBStatus"></span>
|
||||
<span v-if="scope.row.totalPeriodB > 0" v-text="scope.row.totalPeriodB.toFixed(2)"></span>
|
||||
<span v-else style="color: #C0C4CC; margin-left: 2px;">-</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="差异对比 (B - A)" align="center" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.totalPeriodA > 0 && scope.row.totalPeriodB > 0">
|
||||
<span v-if="scope.row.totalPeriodB > scope.row.totalPeriodA" class="price-up">
|
||||
<i class="el-icon-top"></i> <span v-text="(scope.row.totalPeriodB - scope.row.totalPeriodA).toFixed(2)"></span>
|
||||
</span>
|
||||
<span v-else-if="scope.row.totalPeriodB < scope.row.totalPeriodA" class="price-down">
|
||||
<i class="el-icon-bottom"></i> <span v-text="(scope.row.totalPeriodA - scope.row.totalPeriodB).toFixed(2)"></span>
|
||||
</span>
|
||||
<span v-else style="color: #909399;">持平</span>
|
||||
</span>
|
||||
<span v-else style="color: #C0C4CC;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-else style="flex: 1; display: flex; justify-content: center; align-items: center; color: #909399;">
|
||||
<i class="el-icon-mouse" style="font-size: 24px; margin-right: 10px;"></i> 请在左侧选择一个成品父件进行分析
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: function() {
|
||||
return {
|
||||
searchKeyword: '',
|
||||
filterName: '',
|
||||
filterCode: '',
|
||||
parents: [],
|
||||
loadingParents: false,
|
||||
loadingData: false,
|
||||
currentParentCode: null,
|
||||
tableData: [],
|
||||
originalTableData: [],
|
||||
|
||||
// 默认设定期间,方便测试
|
||||
periodA_start: '2023-01-01',
|
||||
periodA_end: '2023-12-31',
|
||||
periodB_start: '2024-01-01',
|
||||
periodB_end: '2024-12-31'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.searchParents();
|
||||
},
|
||||
methods: {
|
||||
goToReceipts() { window.location.href = '/'; },
|
||||
goToBomTree() { window.location.href = '/bom'; },
|
||||
|
||||
getStatusText(status) {
|
||||
if (status === 'fallback') return '期间内无数据,已取历史最近价';
|
||||
return '';
|
||||
},
|
||||
|
||||
searchParents() {
|
||||
this.loadingParents = true;
|
||||
axios.get(`/api/bom_parents?keyword=${this.searchKeyword}`)
|
||||
.then(response => {
|
||||
this.parents = response.data;
|
||||
this.loadingParents = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error('加载父件列表失败');
|
||||
this.loadingParents = false;
|
||||
});
|
||||
},
|
||||
|
||||
loadCompareData(code) {
|
||||
this.currentParentCode = code;
|
||||
this.fetchTreeData();
|
||||
},
|
||||
|
||||
fetchTreeData() {
|
||||
if (!this.currentParentCode) return;
|
||||
|
||||
if (!this.periodA_start || !this.periodA_end || !this.periodB_start || !this.periodB_end) {
|
||||
this.$message.warning('请完整选择期间A和期间B的时间范围');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingData = true;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
start_a: this.periodA_start,
|
||||
end_a: this.periodA_end,
|
||||
start_b: this.periodB_start,
|
||||
end_b: this.periodB_end
|
||||
});
|
||||
|
||||
axios.get(`/api/bom_tree_compare/${this.currentParentCode}?${params.toString()}`)
|
||||
.then(response => {
|
||||
// 初始化所有节点的可见性
|
||||
const initVisible = (nodes) => {
|
||||
nodes.forEach(node => {
|
||||
node.visible = true;
|
||||
if (node.children && node.children.length > 0) {
|
||||
initVisible(node.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
initVisible(response.data);
|
||||
|
||||
this.originalTableData = JSON.parse(JSON.stringify(response.data));
|
||||
this.tableData = response.data;
|
||||
this.loadingData = false;
|
||||
|
||||
// 如果有残留的搜索条件,加载完自动应用过滤
|
||||
if (this.filterName || this.filterCode) {
|
||||
this.filterNode();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error('执行成本对比计算失败');
|
||||
this.loadingData = false;
|
||||
});
|
||||
},
|
||||
|
||||
filterNode() {
|
||||
if (!this.filterName && !this.filterCode) {
|
||||
// 恢复全部可见
|
||||
this.tableData = JSON.parse(JSON.stringify(this.originalTableData));
|
||||
return;
|
||||
}
|
||||
|
||||
const nameKeyword = this.filterName.toLowerCase();
|
||||
const codeKeyword = this.filterCode.toLowerCase();
|
||||
|
||||
// 深度克隆一份原始数据进行过滤
|
||||
let newData = JSON.parse(JSON.stringify(this.originalTableData));
|
||||
|
||||
const checkNode = (node) => {
|
||||
let isMatch = true;
|
||||
|
||||
if (nameKeyword && !node.materialName.toLowerCase().includes(nameKeyword)) {
|
||||
isMatch = false;
|
||||
}
|
||||
|
||||
if (codeKeyword && !node.materialCode.toLowerCase().includes(codeKeyword)) {
|
||||
isMatch = false;
|
||||
}
|
||||
|
||||
node.visible = isMatch;
|
||||
|
||||
let hasVisibleChild = false;
|
||||
if (node.children && node.children.length > 0) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
if (checkNode(node.children[i])) {
|
||||
hasVisibleChild = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果子节点有匹配的,父节点也必须可见,否则树形结构会断裂
|
||||
if (hasVisibleChild) {
|
||||
node.visible = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return node.visible;
|
||||
};
|
||||
|
||||
newData.forEach(rootNode => checkNode(rootNode));
|
||||
|
||||
// Element UI 树形表格不支持直接隐藏节点,需要从数据结构中过滤掉
|
||||
const filterInvisibleNodes = (nodes) => {
|
||||
return nodes.filter(node => {
|
||||
if (!node.visible) return false;
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children = filterInvisibleNodes(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
this.tableData = filterInvisibleNodes(newData);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
366
web_ui/templates/bom_tree.html
Normal file
366
web_ui/templates/bom_tree.html
Normal file
@@ -0,0 +1,366 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BOM 成本动态雷达图 - 数据展示看板</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<!-- 引入 ECharts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
height: 100vh; /* 严格锁定整个页面的高度为屏幕高度 */
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* 禁止整个网页出现最外层大滚动条 */
|
||||
}
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; /* 关键:让 Vue 根节点也撑满 100vh */
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #ebeef5;
|
||||
flex-shrink: 0; /* 防止头部被压缩 */
|
||||
}
|
||||
.header h2 { margin: 0; color: #303133; }
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex: 1; /* 占据剩下的所有垂直空间 */
|
||||
gap: 20px;
|
||||
min-height: 0; /* Flex 布局核心:防止子元素把父容器撑爆 */
|
||||
}
|
||||
|
||||
.left-sidebar {
|
||||
width: 300px;
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; /* 左侧菜单撑满容器 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.parent-list {
|
||||
flex: 1; /* 占据左侧剩下的空间 */
|
||||
overflow-y: auto; /* 关键:让左侧列表自己出现滚动条 */
|
||||
margin-top: 15px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.right-content {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
height: 100%; /* 右侧内容区严格撑满,不再随着网页滚动 */
|
||||
overflow: hidden; /* 防止 ECharts 画布撑爆右侧容器 */
|
||||
}
|
||||
|
||||
#echarts-container {
|
||||
width: 100%;
|
||||
height: 100%; /* 完全填满 right-content */
|
||||
position: absolute; /* 让 ECharts 完全接管这块区域,解决高度坍塌 */
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* 悬浮信息卡片(必须要保留,不然卡片样式全乱) */
|
||||
.info-card {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.parent-item {
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.parent-item:hover { background-color: #f0f9eb; }
|
||||
.parent-item.active {
|
||||
background-color: #ecf5ff;
|
||||
border-left: 4px solid #409EFF;
|
||||
}
|
||||
.parent-code { font-size: 12px; color: #909399; margin-bottom: 4px; }
|
||||
.parent-name { font-size: 14px; color: #303133; font-weight: 500; }
|
||||
|
||||
.price-highlight {
|
||||
font-size: 24px;
|
||||
color: #F56C6C;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="header">
|
||||
<h2>🕸️ ERP 动态 BOM 成本核算雷达图</h2>
|
||||
<div>
|
||||
<el-button type="primary" @click="goToCompare" plain>切换至 BOM 期间成本对比</el-button>
|
||||
<el-button type="text" @click="goToReceipts">返回收货明细</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<!-- 左侧:父件选择列表 -->
|
||||
<div class="left-sidebar">
|
||||
<el-input
|
||||
placeholder="搜索成品/父件"
|
||||
v-model="searchKeyword"
|
||||
class="input-with-select"
|
||||
clearable
|
||||
@keyup.enter.native="searchParents"
|
||||
@clear="searchParents">
|
||||
<el-button slot="append" icon="el-icon-search" @click="searchParents"></el-button>
|
||||
</el-input>
|
||||
|
||||
<div class="parent-list" v-loading="loadingParents">
|
||||
<div v-if="parents.length === 0" style="text-align: center; color: #909399; padding: 20px;">
|
||||
没有找到成品数据
|
||||
</div>
|
||||
<div
|
||||
v-for="item in parents"
|
||||
:key="item.parent_material_code"
|
||||
class="parent-item"
|
||||
:class="{ active: currentParentCode === item.parent_material_code }"
|
||||
@click="loadBomTree(item.parent_material_code, item.parent_material_name)">
|
||||
<div class="parent-code" v-text="item.parent_material_code"></div>
|
||||
<div class="parent-name" v-text="item.parent_material_name"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:ECharts 树图展示区 -->
|
||||
<div class="right-content" v-loading="loadingTree">
|
||||
<div id="echarts-container"></div>
|
||||
|
||||
<!-- 悬浮的最新价格信息卡片 -->
|
||||
<el-card class="info-card" v-show="currentNode">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold;" v-text="currentNode ? currentNode.materialName : ''"></span>
|
||||
</div>
|
||||
<div v-if="currentNode">
|
||||
<div style="font-size: 13px; color: #606266; margin-bottom: 15px;">
|
||||
代码: <span v-text="currentNode.materialCode"></span><br>
|
||||
层级: <el-tag size="mini" type="info">第 <span v-text="currentNode.bomLevel"></span> 层</el-tag><br>
|
||||
用量: <span v-text="currentNode.usageQty"></span>
|
||||
</div>
|
||||
|
||||
<div v-loading="loadingPrice">
|
||||
<!-- 展示组件/成品的汇总成本 -->
|
||||
<div v-if="currentNode.children && currentNode.children.length > 0">
|
||||
<div style="color: #909399; font-size: 12px;">该组件累加总成本 (其下所有子件之和)</div>
|
||||
<div class="price-highlight" style="color: #E6A23C;" v-pre>¥ </div><span class="price-highlight" style="color: #E6A23C;" v-text="currentNode.totalCost.toFixed(2)"></span>
|
||||
</div>
|
||||
|
||||
<!-- 展示单个物料的最新采购价 -->
|
||||
<div v-if="!currentNode.children || currentNode.children.length === 0 || currentNode.ownPrice > 0" style="margin-top: 15px;">
|
||||
<div style="color: #909399; font-size: 12px;">自身最新收货单价</div>
|
||||
<div class="price-highlight" v-pre>¥ </div><span class="price-highlight" v-text="currentNode.ownPrice.toFixed(2)"></span>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div style="font-size: 12px; color: #606266; line-height: 1.8;">
|
||||
<div><strong>采购单号:</strong> <span v-text="currentNode.poCode"></span></div>
|
||||
<div><strong>供应商:</strong> <span v-text="currentNode.supplierName"></span></div>
|
||||
<div><strong>收货时间:</strong> <span v-text="currentNode.receiptTime"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: function() {
|
||||
return {
|
||||
searchKeyword: '',
|
||||
parents: [],
|
||||
loadingParents: false,
|
||||
loadingTree: false,
|
||||
loadingPrice: false,
|
||||
currentParentCode: null,
|
||||
myChart: null,
|
||||
currentNode: null,
|
||||
latestPrice: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.searchParents();
|
||||
// 初始化 ECharts
|
||||
this.myChart = echarts.init(document.getElementById('echarts-container'));
|
||||
|
||||
// 监听 ECharts 节点的点击事件
|
||||
this.myChart.on('click', (params) => {
|
||||
if (params.componentType === 'series' && params.data) {
|
||||
this.handleNodeClick(params.data);
|
||||
}
|
||||
});
|
||||
|
||||
// 窗口大小改变时重绘图表
|
||||
window.addEventListener('resize', () => {
|
||||
this.myChart.resize();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
goToReceipts() {
|
||||
window.location.href = '/';
|
||||
},
|
||||
goToCompare() {
|
||||
window.location.href = '/compare';
|
||||
},
|
||||
searchParents() {
|
||||
this.loadingParents = true;
|
||||
axios.get(`/api/bom_parents?keyword=${this.searchKeyword}`)
|
||||
.then(response => {
|
||||
this.parents = response.data;
|
||||
this.loadingParents = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error('加载父件列表失败');
|
||||
this.loadingParents = false;
|
||||
});
|
||||
},
|
||||
loadBomTree(code, name) {
|
||||
this.currentParentCode = code;
|
||||
this.currentNode = null; // 切换树时清空卡片
|
||||
this.loadingTree = true;
|
||||
|
||||
axios.get(`/api/bom_tree/${code}`)
|
||||
.then(response => {
|
||||
const treeData = response.data;
|
||||
this.renderECharts(treeData);
|
||||
this.loadingTree = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error('加载 BOM 树结构失败');
|
||||
this.loadingTree = false;
|
||||
});
|
||||
},
|
||||
handleNodeClick(nodeData) {
|
||||
// 当点击圆圈节点时触发
|
||||
this.currentNode = nodeData;
|
||||
// 我们已经在后端计算好了所有价格信息并附带在 nodeData 中,
|
||||
// 所以不再需要单独发起网络请求了!实现真正的秒级响应。
|
||||
this.loadingPrice = false;
|
||||
},
|
||||
renderECharts(treeData) {
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
triggerOn: 'mousemove',
|
||||
// 让提示框紧紧跟随鼠标,避免遮挡目标
|
||||
position: 'right',
|
||||
formatter: function(params) {
|
||||
// 鼠标悬浮时,除了名称也显示它的成本
|
||||
return params.data.name.replace('\n', '<br/>') + '<br/>用量: ' + params.data.usageQty + '<br/>总成本: ¥ ' + params.data.totalCost.toFixed(2);
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'tree',
|
||||
data: [treeData],
|
||||
// 留出足够的内边距,方便缩放拖拽
|
||||
top: '15%',
|
||||
left: '10%',
|
||||
bottom: '15%',
|
||||
right: '10%',
|
||||
// 恢复小圆圈,放弃把 10 位数长编码强塞进圆圈的执念,否则必定重叠
|
||||
symbolSize: 22,
|
||||
// 恢复为正圆
|
||||
symbol: 'circle',
|
||||
|
||||
// 【核心修改 1:改变布局方向为从上到下】
|
||||
orient: 'TB', // Top to Bottom
|
||||
|
||||
// 【核心修改 2:美化连线为优雅的贝塞尔曲线】
|
||||
edgeShape: 'curve',
|
||||
|
||||
// 【核心修改 3:真正的自由画布】
|
||||
roam: true,
|
||||
|
||||
// 【核心修改 4:解决极端拥挤问题】
|
||||
// 如果某个父件下面挂了上百个零件,原来的间距还是不够用
|
||||
nodePadding: 200, // 暴力拉开!兄弟节点之间的横向距离强制设定为 200px
|
||||
layerPadding: 180, // 父子层级间距
|
||||
|
||||
// 默认只展开第一层(直接点开的话只看到顶级成品和它的一级子件)
|
||||
// 这样用户可以自己一层一层慢慢点开,绝不会一上来就满屏爆炸
|
||||
initialTreeDepth: 1,
|
||||
|
||||
// 我们把物料名称放到圆圈的外面(比如正下方),
|
||||
// 物料编码隐藏,只在点击后的右上角卡片或悬浮 tooltip 中显示
|
||||
label: {
|
||||
position: 'bottom', // 统一放在圆圈下方
|
||||
distance: 10, // 距离圆圈 10px
|
||||
verticalAlign: 'middle',
|
||||
align: 'center',
|
||||
// 只返回物料名称
|
||||
formatter: function(params) {
|
||||
return params.data.materialName;
|
||||
},
|
||||
color: '#606266', // 名称用深灰色
|
||||
fontSize: 12,
|
||||
backgroundColor: 'transparent', // 彻底变透明
|
||||
padding: [4, 6],
|
||||
// 如果名称太长,用省略号截断(最大宽度 100px)
|
||||
width: 100,
|
||||
overflow: 'truncate'
|
||||
},
|
||||
// 关键修改:只让圆圈本体响应事件,让长长的标签变透明
|
||||
// 避免鼠标还没碰到圆圈,就被旁边的长名字抢了焦点
|
||||
triggerEvent: false,
|
||||
// 如果同级子件太多,为了防止下面的牌子打架,我们让它们错位或倾斜一点点
|
||||
leaves: {
|
||||
label: {
|
||||
position: 'bottom',
|
||||
distance: 10,
|
||||
rotate: -25 // 叶子节点倾斜 25 度,完美错开
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'descendant'
|
||||
},
|
||||
expandAndCollapse: true,
|
||||
animationDuration: 550,
|
||||
animationDurationUpdate: 750
|
||||
}
|
||||
]
|
||||
};
|
||||
this.myChart.setOption(option);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
220
web_ui/templates/home.html
Normal file
220
web_ui/templates/home.html
Normal file
@@ -0,0 +1,220 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ERP 成本与数据看板 - 主控台</title>
|
||||
|
||||
<!-- 引入 ElementUI 样式 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
|
||||
<!-- 引入 Vue.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||
<!-- 引入 ElementUI 组件库 -->
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
<!-- 引入 axios -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f0f2f5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 80%;
|
||||
max-width: 900px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: #303133;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: #909399;
|
||||
font-size: 16px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.nav-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 30px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
|
||||
background-color: #fafafa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.08);
|
||||
border-color: #409EFF;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.nav-card i {
|
||||
font-size: 48px;
|
||||
color: #409EFF;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.nav-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.nav-card p {
|
||||
margin: 0;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 特定卡片的颜色定制 */
|
||||
.card-receipt:hover { border-color: #67C23A; background-color: #f0f9eb; }
|
||||
.card-receipt i { color: #67C23A; }
|
||||
|
||||
.card-bom-tree:hover { border-color: #409EFF; background-color: #ecf5ff; }
|
||||
.card-bom-tree i { color: #409EFF; }
|
||||
|
||||
.card-bom-compare:hover { border-color: #E6A23C; background-color: #fdf6ec; }
|
||||
.card-bom-compare i { color: #E6A23C; }
|
||||
|
||||
.action-group {
|
||||
margin-top: 40px;
|
||||
padding-top: 30px;
|
||||
border-top: 1px dashed #ebeef5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="dashboard-container" id="app">
|
||||
<div class="header-title">ERP 数据分析与成本管控台</div>
|
||||
<div class="header-subtitle">请选择您需要进入的业务模块</div>
|
||||
|
||||
<div class="nav-grid">
|
||||
<!-- 卡片 1: 收货明细表 -->
|
||||
<div class="nav-card card-receipt" onclick="window.location.href='/receipts'">
|
||||
<i class="el-icon-document"></i>
|
||||
<h3>收货明细报表</h3>
|
||||
<p>查看、搜索所有历史收货记录及详细价格数据。</p>
|
||||
</div>
|
||||
|
||||
<!-- 卡片 2: BOM 雷达图 -->
|
||||
<div class="nav-card card-bom-tree" onclick="window.location.href='/bom'">
|
||||
<i class="el-icon-share"></i>
|
||||
<h3>BOM 成本雷达图</h3>
|
||||
<p>以关系图谱形式可视化展示产品的层级结构与汇总成本。</p>
|
||||
</div>
|
||||
|
||||
<!-- 卡片 3: 期间成本对比 -->
|
||||
<div class="nav-card card-bom-compare" onclick="window.location.href='/compare'">
|
||||
<i class="el-icon-data-line"></i>
|
||||
<h3>期间成本对比分析表</h3>
|
||||
<p>跨时间段核算 BOM 最新价差异,支持虚拟件过滤与历史价回溯。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-group">
|
||||
<el-button
|
||||
type="success"
|
||||
:icon="syncing ? '' : 'el-icon-refresh'"
|
||||
:loading="syncing"
|
||||
@click="syncReceipts"
|
||||
round>
|
||||
<span v-text="syncing ? '正在后台同步增量数据,请稍候...' : '读取最新收货明细报表'"></span>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="syncingBom ? '' : 'el-icon-refresh-right'"
|
||||
:loading="syncingBom"
|
||||
@click="syncBom"
|
||||
round>
|
||||
<span v-text="syncingBom ? '正在后台抓取 BOM 树,耗时较长,请稍候...' : '读取最新 BOM 表'"></span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
syncing: false,
|
||||
syncingBom: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
syncReceipts() {
|
||||
this.syncing = true;
|
||||
|
||||
axios.post('/api/sync_receipts')
|
||||
.then(res => {
|
||||
if (res.data.success) {
|
||||
this.$message.success('明细同步成功!' + res.data.message);
|
||||
} else {
|
||||
this.$message.error('同步失败:' + res.data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error('请求发生异常,请检查后端日志。');
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.syncing = false;
|
||||
});
|
||||
},
|
||||
syncBom() {
|
||||
this.syncingBom = true;
|
||||
|
||||
axios.post('/api/sync_bom')
|
||||
.then(res => {
|
||||
if (res.data.success) {
|
||||
this.$message.success('BOM 同步成功!' + res.data.message);
|
||||
} else {
|
||||
this.$message.error('同步失败:' + res.data.message);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error('请求发生异常,这可能需要较长时间,请检查控制台日志。');
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.syncingBom = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
186
web_ui/templates/index.html
Normal file
186
web_ui/templates/index.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>收货明细报表 - 数据展示看板</title>
|
||||
|
||||
<!-- 引入 ElementUI (Vue的经典UI库) 样式 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
||||
|
||||
<!-- 引入 Vue.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||
<!-- 引入 ElementUI 组件库 -->
|
||||
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
||||
<!-- 引入 axios 用于发送 HTTP 请求 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #ebeef5;
|
||||
}
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
}
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="header">
|
||||
<h2>📦 ERP 数据展示看板 - 收货明细报表</h2>
|
||||
<el-button type="primary" @click="goToBomTree" icon="el-icon-data-analysis">切换至 BOM 成本雷达图</el-button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<!-- 搜索工具栏 -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
|
||||
<el-form-item label="供应商名称">
|
||||
<el-input v-model="searchForm.supplier_name" placeholder="支持模糊搜索" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="物料名称">
|
||||
<el-input v-model="searchForm.material_name" placeholder="支持模糊搜索" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="采购订单号">
|
||||
<el-input v-model="searchForm.po_code" placeholder="支持模糊搜索" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" icon="el-icon-search">查询</el-button>
|
||||
<el-button @click="resetSearch" icon="el-icon-refresh">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%">
|
||||
|
||||
<el-table-column prop="receipt_time" label="收货时间" width="160" sortable></el-table-column>
|
||||
<el-table-column prop="purchase_order_code" label="采购订单号" width="130"></el-table-column>
|
||||
<el-table-column prop="row_no" label="行号" width="60" align="center"></el-table-column>
|
||||
<el-table-column prop="material_code" label="物料代码" width="130"></el-table-column>
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="150" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="material_specification" label="规格" min-width="150" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="warehouse_name" label="收货仓库" width="120"></el-table-column>
|
||||
<el-table-column prop="supplier_name" label="供应商名称" min-width="200" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="receive_price" label="收货单价" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span style="color: #F56C6C; font-weight: bold;" v-pre>¥ </span><span style="color: #F56C6C; font-weight: bold;" v-text="scope.row.receive_price.toFixed(2)"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[20, 50, 100, 500]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="totalRows">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: function() {
|
||||
return {
|
||||
tableData: [],
|
||||
loading: false,
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
totalRows: 0,
|
||||
searchForm: {
|
||||
supplier_name: '',
|
||||
material_name: '',
|
||||
po_code: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 页面加载完成后立即拉取第一页数据
|
||||
this.fetchData();
|
||||
},
|
||||
methods: {
|
||||
goToBomTree() {
|
||||
window.location.href = '/bom';
|
||||
},
|
||||
fetchData() {
|
||||
this.loading = true;
|
||||
// 构造带有搜索参数的请求 URL
|
||||
const params = new URLSearchParams({
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
supplier_name: this.searchForm.supplier_name,
|
||||
material_name: this.searchForm.material_name,
|
||||
po_code: this.searchForm.po_code
|
||||
});
|
||||
|
||||
axios.get(`/api/receipts?${params.toString()}`)
|
||||
.then(response => {
|
||||
this.tableData = response.data.rows;
|
||||
this.totalRows = response.data.total;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("数据加载失败:", error);
|
||||
this.$message.error('抱歉,获取数据失败了!');
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchData();
|
||||
},
|
||||
resetSearch() {
|
||||
this.searchForm = {
|
||||
supplier_name: '',
|
||||
material_name: '',
|
||||
po_code: ''
|
||||
};
|
||||
this.currentPage = 1;
|
||||
this.fetchData();
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.fetchData();
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val;
|
||||
this.fetchData();
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user