Files
xgy-oa/klp-ui/src/views/crm/coil/index.vue
砂糖 7930991eb8 feat(员工信息): 新增查看功能并优化附件展示
refactor(异常管理): 重构异常记录表格布局和保存逻辑

feat(库存管理): 新增CRM卷材库存视图和分组筛选功能
2026-04-08 17:40:30 +08:00

686 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="app-container stock-layout">
<!-- 左侧树结构 -->
<div class="stock-tree-col">
<el-card shadow="never" class="stock-tree-card">
<div slot="header" class="stock-tree-title">
<div style="display: flex; align-items: center; justify-content: space-between;">
<span>
仓库结构
</span>
<el-select v-model="warehouseType" placeholder="请选择仓库类别" style="width: 50%;">
<el-option label="真实库区" value="real" />
<el-option label="逻辑库位" value="virtual" />
</el-select>
</div>
</div>
<WarehouseTree @node-click="handleTreeSelect" :warehouseType="warehouseType" :showEmpty="true" />
</el-card>
</div>
<!-- 右侧内容 -->
<div class="stock-main-col">
<!-- <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<MaterialSelect :itemType.sync="queryParams.itemType" :itemId.sync="queryParams.itemId" @change="getList" />
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-form-item>
</el-form> -->
<!-- 上方增加分组信息会影响下面的表格显示, 可以添加多级分组例如按照itemType按照itemName, 按照材质material, 按照规格specification, 按照厂家manufacturer -->
<div class="group-controls">
<el-form :model="groupForm" size="small" :inline="true">
<el-form-item label="分组维度">
<el-select v-model="groupForm.groupingCriteria" multiple placeholder="请选择分组维度" style="width: 300px;">
<el-option label="物料类型" value="itemType" />
<el-option label="物料名称" value="itemName" />
<el-option label="材质" value="material" />
<el-option label="规格" value="specification" />
<el-option label="宽度" value="width" />
<el-option label="厚度" value="thickness" />
<el-option label="厂家" value="manufacturer" />
<el-option label="镀层质量" value="zincLayer" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">应用分组</el-button>
<el-button @click="resetGroup">重置分组</el-button>
</el-form-item>
</el-form>
</div>
<vxe-toolbar :refresh="{query: reload}" export custom>
<template v-slot:buttons>
<!-- 统计数据{{ total }} -->
<span style="margin-left: 10px; font-weight: bold;">统计数据{{ total }}总重量{{ totalWeight.toFixed(2) }}</span>
<!-- <vxe-input v-model.lazy="filterName" type="search" placeholder="全表搜索" style="width: 200px; margin-left: 10px;"></vxe-input> -->
<!-- <vxe-button @click="$refs.xTree.setAllTreeExpand(true)">展开所有</vxe-button>
<vxe-button @click="$refs.xTree.clearTreeExpand()">关闭所有</vxe-button> -->
<vxe-button @click="clearFilterEvent">清除所有筛选</vxe-button>
</template>
</vxe-toolbar>
<div style="height: calc(100vh - 260px);">
<vxe-table ref="xTree" :loading="loading" :data="list" size="mini" :tree-config="effectiveTreeConfig"
max-height="100%" @row-click="handleTableRowClick" :export-config="{}" :scroll-y="{ enabled: true }">
<vxe-table-column field="itemType" title="物料类型" align="center" :formatter="formatterItemType" tree-node sortable :filters="[{label: '产品', value: 'product'}, {label: '原料', value: 'raw_material'}]" :filter-method="filterItemTypeMethod">
<template v-slot="{ row }">
<span v-if="row.itemType">{{ formatterItemType(row.itemType) }}</span>
<span v-else>{{ row.itemType || row.itemName || row.material || row.specification || row.manufacturer || row.zincLayer ||
row.width ? ('宽度' + row.width) : undefined ||
row.thickness ? ('厚度' + row.thickness) : undefined
}}</span>
</template>
</vxe-table-column>
<vxe-table-column sortable field="itemName" title="物料名称" align="center" :filters="itemNameOptions" :filter-method="filterItemNameMethod">
</vxe-table-column>
<vxe-table-column sortable field="material" title="材质" align="center" :filters="materialOptions" :filter-method="filterMaterialMethod">
</vxe-table-column>
<vxe-table-column sortable field="specification" title="规格" align="center" :filters="specificationOptions" :filter-method="filterSpecificationMethod">
</vxe-table-column>
<vxe-table-column sortable field="thickness" title="厚度" align="center" :filters="thicknessOptions" :filter-method="filterThicknessMethod">
</vxe-table-column>
<vxe-table-column sortable field="width" title="宽度" align="center" :filters="widthOptions" :filter-method="filterWidthMethod">
</vxe-table-column>
<vxe-table-column sortable field="manufacturer" title="厂家" align="center" :filters="manufacturerOptions" :filter-method="filterManufacturerMethod">
</vxe-table-column>
<vxe-table-column sortable field="zincLayer" title="镀层质量" align="center" :filters="zincLayerOptions" :filter-method="filterZincLayerMethod">
</vxe-table-column>
<!-- <vxe-table-column sortable field="totalQuantity" title="库存数量" align="center">
</vxe-table-column> -->
<vxe-table-column sortable field="netWeight" title="重量" align="center">
</vxe-table-column>
<vxe-table-column sortable field="quantity" title="数量" align="center">
</vxe-table-column>
<!-- <vxe-table-column field="remark" title="备注" align="center" /> -->
</vxe-table>
</div>
</div>
</div>
</template>
<script>
import { listMaterialCoil } from "@/api/wms/coil";
import WarehouseSelect from "@/components/WarehouseSelect";
import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo";
import ProductInfo from "@/components/KLPService/Renderer/ProductInfo";
import WarehouseTree from "@/components/KLPService/WarehouseTree/index.vue";
import MaterialSelect from "@/components/KLPService/MaterialSelect";
export default {
name: "Stock",
dicts: ['stock_item_type'],
components: {
WarehouseSelect,
RawMaterialInfo,
ProductInfo,
WarehouseTree,
MaterialSelect,
},
data() {
return {
// 库存分析对话框显示状态
stockBoxVisible: false,
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
totalQuantity: 0,
totalWeight: 0,
// 宽度和厚度汇总
widths: [],
thicknesses: [],
// 库存:原材料/产品与库区/库位的存放关系表格数据
stockList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 99,
status: 0,
dateType: 1,
warehouseId: undefined,
},
warehouseType: 'virtual',
// 表单参数
form: {},
// 分组相关数据
groupForm: {
groupingCriteria: []
},
// vxe-table树配置
tableTreeConfig: {
children: 'children',
accordion: true,
expandAll: false,
line: true,
},
// 搜索关键字
filterName: '',
// 筛选选项数组
itemNameOptions: [],
materialOptions: [],
specificationOptions: [],
manufacturerOptions: [],
zincLayerOptions: [],
thicknessOptions: [],
widthOptions: []
};
},
computed: {
list() {
const filterName = this.filterName.trim().toLowerCase();
if (!filterName) {
return this.stockList;
}
const searchProps = ['itemType', 'itemName', 'material', 'specification', 'manufacturer', 'totalQuantity', 'remark'];
const filterData = (data) => {
return data.filter(item => {
if (item.children) {
item.children = filterData(item.children);
return item.children.length > 0 || searchProps.some(key => {
return String(item[key] || '').toLowerCase().includes(filterName);
});
}
return searchProps.some(key => {
return String(item[key] || '').toLowerCase().includes(filterName);
});
});
};
return filterData([...this.stockList]);
},
// 控制是否启用树形数据配置
effectiveTreeConfig() {
// 当没有分组条件时,不启用树形数据
return this.groupForm.groupingCriteria && this.groupForm.groupingCriteria.length > 0 ? this.tableTreeConfig : undefined;
}
},
created() {
this.getList();
},
methods: {
/** 查询库存列表 */
getList() {
this.loading = true;
if (this.warehouseType === 'real') {
listMaterialCoil(this.queryParams).then(res => {
let list = res.rows
// 处理数据,添加宽度、厚度和数量字段
list = list.map(item => {
const parts = item.specification ? item.specification.split('*') : [];
return {
...item,
thickness: parts[0] || '',
width: parts[1] || '',
quantity: 1 // 数量字段设为1
};
});
this.stockList = this.groupData(list, this.groupForm.groupingCriteria);
this.extractOptions(list);
// 计算总数量和总重量
this.calculateTotals(list);
this.loading = false;
})
} else {
listMaterialCoil(this.queryParams).then(res => {
let list = res.rows
// 处理数据,添加宽度、厚度和数量字段
list = list.map(item => {
const parts = item.specification ? item.specification.split('*') : [];
return {
...item,
thickness: parts[0] || '',
width: parts[1] || '',
quantity: 1 // 数量字段设为1
};
});
this.stockList = this.groupData(list, this.groupForm.groupingCriteria);
this.extractOptions(list);
// 计算总数量和总重量
this.calculateTotals(list);
this.loading = false;
})
}
},
/** 提取筛选选项值 */
extractOptions(data) {
// 提取唯一值
const itemNames = new Set();
const materials = new Set();
const specifications = new Set();
const manufacturers = new Set();
const zincLayers = new Set();
const thicknesses = new Set();
const widths = new Set();
data.forEach(item => {
if (item.itemName) itemNames.add(item.itemName);
if (item.material) materials.add(item.material);
if (item.specification) specifications.add(item.specification);
if (item.manufacturer) manufacturers.add(item.manufacturer);
if (item.zincLayer) zincLayers.add(item.zincLayer);
if (item.thickness) thicknesses.add(item.thickness);
if (item.width) widths.add(item.width);
});
// 转换为数组并排序
this.itemNameOptions = Array.from(itemNames).sort();
this.materialOptions = Array.from(materials).sort();
this.specificationOptions = Array.from(specifications).sort();
this.manufacturerOptions = Array.from(manufacturers).sort();
this.zincLayerOptions = Array.from(zincLayers).sort();
this.thicknessOptions = Array.from(thicknesses).sort();
this.widthOptions = Array.from(widths).sort();
this.updateManufacturerFilterEvent();
this.updateZincLayerFilterEvent();
this.updateSpecificationFilterEvent();
this.updateMaterialFilterEvent();
this.updateNameFilterEvent();
this.updateThicknessFilterEvent();
this.updateWidthFilterEvent();
console.log(this.itemNameOptions, itemNames, data);
},
formatterItemType(itemType) {
return itemType === 'raw_material' ? '原材料' : '产品';
},
/** 格式化厚度 */
formatterThickness(row) {
if (!row.specification) return '';
const parts = row.specification.split('*');
return parts[0] || '';
},
/** 格式化宽度 */
formatterWidth(row) {
if (!row.specification) return '';
const parts = row.specification.split('*');
return parts[1] || '';
},
/** 计算总数量和总重量 */
calculateTotals(data) {
// 计算总数量(求和)
this.total = data.length;
// 计算总重量(求和并保留两位小数)
this.totalWeight = parseFloat(data.reduce((sum, item) => {
return sum + (parseFloat(item.netWeight) || 0);
}, 0).toFixed(2));
// 汇总宽度和厚度(提取唯一值)
this.widths = [...new Set(data.map(item => item.width).filter(w => w))].sort();
this.thicknesses = [...new Set(data.map(item => item.thickness).filter(t => t))].sort();
},
// 树节点点击
handleTreeSelect(node) {
if (this.warehouseType === 'real') {
this.queryParams.actualWarehouseId = node.actualWarehouseId;
this.queryParams.warehouseId = '';
} else {
this.queryParams.warehouseId = node.warehouseId;
this.queryParams.actualWarehouseId = '';
}
this.queryParams.pageNum = 1;
this.getList();
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
/** 导出按钮操作 */
handleExport() {
this.download('wms/stock/export', {
...this.queryParams
}, `stock_${new Date().getTime()}.xlsx`)
},
/** 重置分组 */
resetGroup() {
this.groupForm.groupingCriteria = [];
this.getList();
},
/** 分组数据处理 */
groupData(data, criteria) {
if (!criteria || criteria.length === 0) {
return data;
}
const groupBy = (arr, key) => {
return arr.reduce((groups, item) => {
const value = item[key] || '未知';
if (!groups[value]) {
groups[value] = [];
}
groups[value].push(item);
return groups;
}, {});
};
const buildTree = (items, level) => {
if (level >= criteria.length) {
return items;
}
const currentCriteria = criteria[level];
const grouped = groupBy(items, currentCriteria);
return Object.keys(grouped).map(key => {
const children = buildTree(grouped[key], level + 1);
const totalQuantity = children.reduce((sum, child) => {
return sum + (parseInt(child.quantity) || 0);
}, 0);
const totalWeight = parseFloat(children.reduce((sum, child) => {
return sum + (parseFloat(child.netWeight) || 0);
}, 0).toFixed(2));
return {
[currentCriteria]: key,
quantity: totalQuantity,
netWeight: totalWeight,
children,
hasChildren: children.length > 0
};
});
};
return buildTree(data, 0);
},
/** 物料类型筛选 */
filterItemTypeMethod({ cellValue, option }) {
return cellValue === option.value;
},
/** 物料名称筛选 */
filterItemNameMethod({ cellValue, option }) {
return option.value ? cellValue === option.value : true;
},
/** 材质筛选 */
filterMaterialMethod({ cellValue, option }) {
return option.value ? cellValue === option.value : true;
},
/** 规格筛选 */
filterSpecificationMethod({ cellValue, option }) {
return option.value ? cellValue === option.value : true;
},
/** 厂家筛选 */
filterManufacturerMethod({ cellValue, option }) {
return option.value ? cellValue === option.value : true;
},
/** 镀层质量筛选 */
filterZincLayerMethod({ cellValue, option }) {
return option.value ? cellValue === option.value : true;
},
/** 厚度筛选 */
filterThicknessMethod({ cellValue, option }) {
return option.value ? cellValue === option.value : true;
},
/** 宽度筛选 */
filterWidthMethod({ cellValue, option }) {
return option.value ? cellValue === option.value : true;
},
/** 库存数量筛选 */
filterTotalQuantityMethod({ cellValue, option }) {
const value = Number(cellValue || 0);
const filterValue = Number(option.value || 0);
switch (option.operator || option.value) {
case 'gt':
return value > filterValue;
case 'gte':
return value >= filterValue;
case 'lt':
return value < filterValue;
case 'lte':
return value <= filterValue;
case 'eq':
return value === filterValue;
default:
return true;
}
},
/** 更改物料名称筛选条件 */
updateNameFilterEvent() {
const xTable = this.$refs.xTree;
const column = xTable.getColumnByField('itemName');
// 使用从数据中汇总的实际选项
const options = this.itemNameOptions.map(item => ({
label: item,
value: item
}));
// 修改筛选列表
xTable.setFilter(column, options);
// 修改条件之后,需要手动调用 updateData 处理表格数据
xTable.updateData();
},
/** 更改材质筛选条件 */
updateMaterialFilterEvent() {
const xTable = this.$refs.xTree;
const column = xTable.getColumnByField('material');
// 使用从数据中汇总的实际选项
const options = this.materialOptions.map(item => ({
label: item,
value: item
}));
// 修改筛选列表
xTable.setFilter(column, options);
// 修改条件之后,需要手动调用 updateData 处理表格数据
xTable.updateData();
},
/** 更改规格筛选条件 */
updateSpecificationFilterEvent() {
const xTable = this.$refs.xTree;
const column = xTable.getColumnByField('specification');
// 使用从数据中汇总的实际选项
const options = this.specificationOptions.map(item => ({
label: item,
value: item
}));
// 修改筛选列表
xTable.setFilter(column, options);
// 修改条件之后,需要手动调用 updateData 处理表格数据
xTable.updateData();
},
/** 更改厂家筛选条件 */
updateManufacturerFilterEvent() {
const xTable = this.$refs.xTree;
const column = xTable.getColumnByField('manufacturer');
// 使用从数据中汇总的实际选项
const options = this.manufacturerOptions.map(item => ({
label: item,
value: item
}));
// 修改筛选列表
xTable.setFilter(column, options);
// 修改条件之后,需要手动调用 updateData 处理表格数据
xTable.updateData();
},
/** 更改镀层质量筛选条件 */
updateZincLayerFilterEvent() {
const xTable = this.$refs.xTree;
const column = xTable.getColumnByField('zincLayer');
// 使用从数据中汇总的实际选项
const options = this.zincLayerOptions.map(item => ({
label: item,
value: item
}));
// 修改筛选列表
xTable.setFilter(column, options);
// 修改条件之后,需要手动调用 updateData 处理表格数据
xTable.updateData();
},
/** 更改厚度筛选条件 */
updateThicknessFilterEvent() {
const xTable = this.$refs.xTree;
const column = xTable.getColumnByField('thickness');
// 使用从数据中汇总的实际选项
const options = this.thicknessOptions.map(item => ({
label: item,
value: item
}));
// 修改筛选列表
xTable.setFilter(column, options);
// 修改条件之后,需要手动调用 updateData 处理表格数据
xTable.updateData();
},
/** 更改宽度筛选条件 */
updateWidthFilterEvent() {
const xTable = this.$refs.xTree;
const column = xTable.getColumnByField('width');
// 使用从数据中汇总的实际选项
const options = this.widthOptions.map(item => ({
label: item,
value: item
}));
// 修改筛选列表
xTable.setFilter(column, options);
// 修改条件之后,需要手动调用 updateData 处理表格数据
xTable.updateData();
},
/** 清除筛选条件 */
clearFilterEvent() {
const xTable = this.$refs.xTree;
xTable.clearFilter();
xTable.updateData();
},
/** 刷新数据 */
reload() {
this.getList();
}
}
};
</script>
<style scoped>
.stock-layout {
height: 100%;
display: flex;
flex-direction: row;
}
.stock-tree-col {
width: 260px;
max-width: 300px;
background: #fff;
border-right: 1px solid #f0f0f0;
height: 100%;
padding-right: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.stock-tree-card {
height: 100%;
border: none;
box-shadow: none;
}
.stock-tree-title {
font-weight: bold;
font-size: 16px;
padding: 8px 0;
}
.stock-tree {
min-height: 500px;
max-height: 80vh;
overflow-y: auto;
}
.stock-main-col {
flex: 1;
padding-left: 24px;
min-width: 0;
display: flex;
flex-direction: column;
}
.group-controls {
margin-bottom: 16px;
padding: 12px;
background-color: #f9f9f9;
border-radius: 4px;
border: 1px solid #eaeaea;
}
.group-controls .el-form {
margin-bottom: 0;
}
/* 分组行样式 */
.el-table__row.group-row {
background-color: #f5f7fa;
}
/* 叶子节点样式 */
.el-table__row.leaf-row {
background-color: #ffffff;
}
/* 筛选输入框样式 */
.my-input {
width: 100%;
padding: 4px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 12px;
outline: none;
transition: border-color 0.3s;
}
.my-input:focus {
border-color: #409eff;
}
/* 筛选下拉选择框样式 */
.my-select {
width: 100%;
padding: 4px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 12px;
outline: none;
transition: border-color 0.3s;
background-color: #fff;
}
.my-select:focus {
border-color: #409eff;
}
/* 库存数量筛选样式 */
.quantity-filter {
display: flex;
flex-direction: column;
gap: 4px;
}
.quantity-filter .my-select {
width: 100%;
}
.quantity-filter .my-input {
width: 100%;
}
</style>