feat: 新增物料管理看板功能及多项优化

新增物料管理看板功能,包含统计卡片和图表展示
优化物料选择器组件,支持分页和搜索功能
重构物料详情展示组件,支持动态加载数据
添加多个ECharts图表组件用于数据可视化
完善出入库和采购单相关功能,增加在途数量显示
修复若干界面显示问题和交互逻辑
This commit is contained in:
砂糖
2026-01-31 14:21:49 +08:00
parent 48e75676c5
commit 42f6f83c3a
17 changed files with 1278 additions and 115 deletions

View File

@@ -22,6 +22,7 @@
"@vueuse/core": "13.3.0",
"axios": "1.9.0",
"clipboard": "2.0.11",
"dayjs": "^1.11.19",
"echarts": "5.6.0",
"element-plus": "2.9.9",
"file-saver": "2.0.5",

View File

@@ -26,6 +26,9 @@ importers:
clipboard:
specifier: 2.0.11
version: 2.0.11
dayjs:
specifier: ^1.11.19
version: 1.11.19
echarts:
specifier: 5.6.0
version: 5.6.0
@@ -71,6 +74,9 @@ importers:
vue-router:
specifier: 4.5.1
version: 4.5.1(vue@3.5.16)
vue3-treeselect:
specifier: ^0.1.10
version: 0.1.10(vue@3.5.16)
vuedraggable:
specifier: 4.1.0
version: 4.1.0(vue@3.5.16)
@@ -911,8 +917,8 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
@@ -2463,6 +2469,11 @@ packages:
peerDependencies:
vue: ^3.2.0
vue3-treeselect@0.1.10:
resolution: {integrity: sha512-QawdAbzmlZ7T3uBdSU4FRnrnmcV0Q9Jrph5hUBQJcXmM9OZ8lULQo7O7YbKxkOyuDX9Yx2rGjs6L5FKcL1FeXA==}
peerDependencies:
vue: ^3.0.0
vue@3.5.16:
resolution: {integrity: sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==}
peerDependencies:
@@ -3245,7 +3256,7 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
dayjs@1.11.13: {}
dayjs@1.11.19: {}
debug@2.6.9:
dependencies:
@@ -3361,7 +3372,7 @@ snapshots:
'@types/lodash-es': 4.17.12
'@vueuse/core': 9.13.0(vue@3.5.16)
async-validator: 4.2.5
dayjs: 1.11.13
dayjs: 1.11.19
escape-html: 1.0.3
lodash: 4.17.21
lodash-es: 4.17.21
@@ -4972,6 +4983,10 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.16
vue3-treeselect@0.1.10(vue@3.5.16):
dependencies:
vue: 3.5.16
vue@3.5.16:
dependencies:
'@vue/compiler-dom': 3.5.16

View File

@@ -0,0 +1,107 @@
<template>
<div @click="open = true" style="cursor: pointer;">
<!-- 触发区插槽默认展示选择按钮/已选物料信息 -->
<slot name="trigger">
<el-button v-if="!materialId" type="primary" size="small">选择配料</el-button>
<el-tag v-else type="info" closable @close="handleClear">
{{ currentMaterial.materialName }}({{ currentMaterial.spec }})[{{ currentMaterial.model }}] - {{
currentMaterial.factory }}库存{{ currentMaterial.currentStock }}{{ currentMaterial.unit }}
</el-tag>
</slot>
</div>
<!-- 选择弹窗表格+分页+底部按钮 -->
<el-dialog title="选择配料" v-model="open" width="800px" destroy-on-close>
<el-table :data="list" style="width: 100%" border stripe @row-click="handleRowSelect" highlight-current-row
row-key="materialId" :current-row-key="materialId">
<el-table-column prop="materialName" label="配料名称" min-width="120" align="center" />
<el-table-column prop="spec" label="配料规格" min-width="100" align="center" />
<el-table-column prop="model" label="配料型号" min-width="100" align="center" />
<el-table-column prop="factory" label="生产厂家" min-width="120" align="center" />
<el-table-column prop="currentStock" label="现存库存" min-width="80" align="center" />
<el-table-column prop="unit" label="计量单位" width="80" align="center" />
</el-table>
<!-- 分页组件 -->
<div style="margin: 15px 0; text-align: right; width: 100%">
<el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total"
:page-sizes="[5, 10, 20, 50]" layout="prev, pager, next, jumper, ->, total, sizes"
@size-change="fetchMaterialList" @current-change="fetchMaterialList" small />
</div>
<template #footer>
<el-button @click="open = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup name="RawSelector">
import { ref, computed, watch, onMounted } from 'vue';
import { listMaterial, getMaterial } from '@/api/mat/material';
import { ElMessage } from 'element-plus';
// 双向绑定物料ID
const materialId = defineModel({
type: String,
default: ""
})
// 计算当前选中的物料信息
const currentMaterial = computed(() => {
if (!materialId.value) return {};
return list.value.find(item => item.materialId === materialId.value) || {};
});
// 定义事件
const emit = defineEmits(['change']);
// 基础响应式数据
const list = ref([]); // 物料列表
const open = ref(false); // 弹窗显隐
// 分页响应式数据(核心新增)
const pageNum = ref(1); // 当前页码
const pageSize = ref(10); // 每页条数
const total = ref(0); // 总数据条数
// 组件挂载加载列表
onMounted(async () => {
await fetchMaterialList();
});
// 加载物料列表(适配分页参数)
async function fetchMaterialList() {
try {
const res = await listMaterial({ pageNum: pageNum.value, pageSize: pageSize.value });
list.value = res.rows;
total.value = res.total; // 赋值总条数供分页使用
// 分页查询后检查是否存在当前选中的物料ID
const isExist = res.rows.some(item => item.materialId === materialId.value);
if (!isExist) {
console.log('获取物料详情:', materialId.value);
if (!materialId.value) return;
const res = await getMaterial(materialId.value);
list.value.push(res.data);
}
} catch (err) {
console.error('加载物料列表失败:', err);
ElMessage.error('配料列表加载失败,请刷新重试');
}
}
// 表格行选择物料
function handleRowSelect(row) {
if (row.materialId === materialId.value) return;
materialId.value = row.materialId;
emit('change', row);
open.value = false;
}
// 清空已选物料
function handleClear() {
materialId.value = '';
emit('change', {});
ElMessage.info('已清空配料选择');
}
</script>

View File

@@ -1,25 +1,48 @@
<template>
<el-popover width="400">
<template #reference>
<el-button type="primary" size="mini" link>{{data.materialName}}</el-button>
<el-button type="primary" size="mini" link>{{formattedData.materialName}}</el-button>
</template>
<el-descriptions :column="2">
<el-descriptions-item label="配料名称">{{ data.materialName }}</el-descriptions-item>
<el-descriptions-item label="配料规格">{{ data.spec }}</el-descriptions-item>
<el-descriptions-item label="配料型号">{{ data.model }}</el-descriptions-item>
<el-descriptions-item label="厂家">{{ data.factory }}</el-descriptions-item>
<el-descriptions-item label="现存库存">{{ data.currentStock }}</el-descriptions-item>
<el-descriptions-item label="计量单位">{{ data.unit }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ data.remark }}</el-descriptions-item>
<el-descriptions-item label="配料名称">{{ formattedData.materialName }}</el-descriptions-item>
<el-descriptions-item label="配料规格">{{ formattedData.spec }}</el-descriptions-item>
<el-descriptions-item label="配料型号">{{ formattedData.model }}</el-descriptions-item>
<el-descriptions-item label="厂家">{{ formattedData.factory }}</el-descriptions-item>
<el-descriptions-item label="现存库存">{{ formattedData.currentStock }}</el-descriptions-item>
<el-descriptions-item label="计量单位">{{ formattedData.unit }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ formattedData.remark }}</el-descriptions-item>
</el-descriptions>
</el-popover>
</template>
<script setup>
import { getMaterial } from '@/api/mat/material'
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
materialId: {
type: String,
default: ''
}
})
const content = ref({});
watch(() => props.materialId, (newVal, oldVal) => {
if (newVal) {
getMaterial(newVal).then(response => {
content.value = response.data;
});
}
}, { immediate: true });
const formattedData = computed(() => {
if (props.materialId) {
return content.value;
}
return props.data;
})
</script>

View File

@@ -0,0 +1,149 @@
<!-- @/components/StickyDragContainer/index.vue -->
<template>
<div class="sticky-drag-wrapper" :style="wrapperStyle">
<!-- 拖拽条 -->
<div class="drag-bar" ref="dragBarRef" @mousedown="handleDragStart"></div>
<!-- 高度可调节+内部滚动容器 -->
<div
class="content-scroll-container"
ref="scrollContainerRef"
:style="{ height: `${containerHeight}px` }"
>
<!-- 默认插槽接收外部传入的任意内容 -->
<slot></slot>
</div>
</div>
</template>
<script setup name="StickyDragContainer">
import { ref, onMounted, onUnmounted, defineProps, watch, nextTick } from 'vue'
// 新增接收父容器Ref必传用于获取父容器布局信息
const props = defineProps({
parentRef: { // 父容器的Ref对象外部传入
type: Object,
required: true
},
initialHeight: { // 容器初始高度px
type: Number,
default: 300
},
minHeight: { // 容器最小高度px
type: Number,
default: 200
},
zIndex: { // 容器层级
type: Number,
default: 99
}
})
// 响应式变量
const dragBarRef = ref(null)
const scrollContainerRef = ref(null)
const startY = ref(0)
const startHeight = ref(props.initialHeight)
const containerHeight = ref(props.initialHeight)
// 新增组件外层样式动态绑定宽度、左侧偏移、z-index等
const wrapperStyle = ref({
zIndex: props.zIndex,
width: '0px',
left: '0px'
})
// 核心方法:更新组件布局(宽度/左侧偏移),与父容器保持一致
const updateContainerLayout = () => {
nextTick(() => {
// 校验父容器Ref是否有效
console.log(props.parentRef)
if (!props.parentRef) {
console.warn('StickyDragContainer传入的parentRef无效请确保绑定了正确的DOM Ref')
return
}
// 获取父容器的实际布局信息(相对于视口的位置、宽度)
const parentRect = props.parentRef.getBoundingClientRect()
// 动态设置组件宽度(与父容器完全一致)、左侧偏移(与父容器左对齐)
wrapperStyle.value = {
zIndex: props.zIndex,
width: `${parentRect.width}px`, // 继承父容器宽度
left: `${parentRect.left}px` // 与父容器左对齐
}
})
}
// 拖拽相关方法(保持不变)
const handleDragStart = (e) => {
e.preventDefault()
startY.value = e.clientY
startHeight.value = scrollContainerRef.value?.getBoundingClientRect().height || containerHeight.value
document.addEventListener('mousemove', handleDragMove)
document.addEventListener('mouseup', handleDragEnd)
}
const handleDragMove = (e) => {
e.preventDefault()
const diffY = startY.value - e.clientY
const newHeight = Math.max(props.minHeight, startHeight.value + diffY)
containerHeight.value = newHeight
}
const handleDragEnd = () => {
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
}
// 生命周期:初始化+监听窗口缩放
onMounted(() => {
containerHeight.value = props.initialHeight
updateContainerLayout() // 初始化时适配父容器布局
// 新增:监听窗口缩放,父容器宽度变化时同步更新组件布局
window.addEventListener('resize', updateContainerLayout)
})
onUnmounted(() => {
// 解绑所有事件,防止内存泄漏
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
window.removeEventListener('resize', updateContainerLayout) // 解绑窗口监听
})
// 监听props变化动态更新
watch([() => props.initialHeight, () => props.zIndex], () => {
containerHeight.value = props.initialHeight
wrapperStyle.value.zIndex = props.zIndex
}, { immediate: true })
// 新增如果父容器Ref变化重新适配布局
watch(() => props.parentRef, () => {
updateContainerLayout()
}, { deep: true, immediate: true })
</script>
<style scoped>
/* 吸底外层容器移除left/right:0改为动态绑定保留fixed+bottom:0核心吸底 */
.sticky-drag-wrapper {
position: fixed;
bottom: 0; /* 仅保留吸底,宽度/左侧偏移由JS动态设置 */
background: #ffffff;
border-top: 1px solid #e6e6e6;
box-sizing: border-box;
transition: all 0.1s ease; /* 宽度/高度变化平滑过渡 */
}
/* 拖拽条样式(保持不变) */
.drag-bar {
height: 6px;
background-color: #e6e6e6;
cursor: n-resize;
transition: background-color 0.2s ease;
}
.drag-bar:hover {
background-color: #409eff;
}
/* 内容滚动容器样式(保持不变) */
.content-scroll-container {
width: 100%; /* 继承外层wrapper的宽度即父容器宽度 */
overflow: auto;
box-sizing: border-box;
transition: height 0.1s ease;
}
</style>

View File

@@ -20,7 +20,11 @@
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="关联ID 主键" align="center" prop="relationId" v-if="true"/> -->
<!-- <el-table-column label="产品ID 关联t_product.id" align="center" prop="productId" /> -->
<el-table-column label="配料" align="center" prop="materialId" />
<el-table-column label="配料" align="center" prop="materialId">
<template #default="scope">
<raw :data="scope.row" :materialId="scope.row.materialId" />
</template>
</el-table-column>
<el-table-column label="所需数量" align="center" prop="materialNum" />
<!-- <el-table-column label="配料排序 用于前端展示顺序" align="center" prop="sort" /> -->
<el-table-column label="备注" align="center" prop="remark" />
@@ -42,7 +46,7 @@
<el-input v-model="form.productId" placeholder="请输入产品ID 关联t_product.id" />
</el-form-item> -->
<el-form-item label="配料" prop="materialId">
<el-input v-model="form.materialId" placeholder="请输入配料" />
<raw-selector ref="rawSelector" v-model="form.materialId" placeholder="请选择配料" />
</el-form-item>
<el-form-item label="所需数量" prop="materialNum">
<el-input v-model="form.materialNum" placeholder="请输入所需数量" />
@@ -67,7 +71,8 @@
<script setup name="ProductMaterialRelation">
import { listProductMaterialRelation, getProductMaterialRelation, delProductMaterialRelation, addProductMaterialRelation, updateProductMaterialRelation } from "@/api/mat/productMaterialRelation";
import RawSelector from '@/components/RawSelector/index.vue'
import Raw from '@/components/Renderer/Raw.vue'
const { proxy } = getCurrentInstance();
const productMaterialRelationList = ref([]);
@@ -193,7 +198,7 @@ function submitForm() {
buttonLoading.value = true;
if (form.value.relationId != null) {
updateProductMaterialRelation(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
proxy.$modal.msgSuccess("修改成功,刷新后生效");
open.value = false;
getList();
}).finally(() => {
@@ -201,7 +206,7 @@ function submitForm() {
});
} else {
addProductMaterialRelation(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
proxy.$modal.msgSuccess("新增成功,刷新后生效");
open.value = false;
getList();
}).finally(() => {
@@ -221,7 +226,7 @@ function handleDelete(row) {
}).then(() => {
loading.value = true;
getList();
proxy.$modal.msgSuccess("删除成功");
proxy.$modal.msgSuccess("删除成功,刷新后生效");
}).catch(() => {
}).finally(() => {
loading.value = false;

View File

@@ -0,0 +1,135 @@
<template>
<div ref="chartRef" class="echarts-container"></div>
</template>
<script setup>
import { ref, onMounted, onUpdated, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
// 接收父组件传入的厂家数据
const props = defineProps({
factoryData: {
type: Array,
default: () => []
}
})
const chartRef = ref(null) // ECharts容器ref
let myChart = null // ECharts实例
// 初始化/更新ECharts
const initEcharts = () => {
// 容器不存在则返回
if (!chartRef.value) return
// 初始化实例(单例模式)
myChart = echarts.init(chartRef.value)
// 设置配置项
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
legend: {
data: ['材料个数', '库存总数'],
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.factoryData.map(item => item.factory),
axisLabel: {
rotate: 30, // 标签旋转,防止重叠
fontSize: 12
}
},
// 双Y轴适配材料个数小数值和库存总数大数值
yAxis: [
{
type: 'value',
name: '材料个数',
min: 0,
axisLabel: { formatter: '{value} 种' }
},
{
type: 'value',
name: '库存总数',
min: 0,
axisLabel: { formatter: '{value} 件' }
}
],
series: [
{
name: '材料个数',
type: 'bar',
yAxisIndex: 0, // 对应第一个Y轴
data: props.factoryData.map(item => item.materialCount),
itemStyle: { color: '#409EFF' }
},
{
name: '库存总数',
type: 'bar',
yAxisIndex: 1, // 对应第二个Y轴
data: props.factoryData.map(item => item.stockTotal),
itemStyle: { color: '#67C23A' }
}
],
// 空数据提示
noDataLoadingOption: {
text: '暂无厂家数据',
textStyle: { fontSize: 14 }
}
}
// 设置配置项并渲染
myChart.setOption(option, true)
// 自适应窗口大小
window.addEventListener('resize', resizeEcharts)
}
// 图表自适应
const resizeEcharts = () => {
if (myChart) myChart.resize()
}
// 销毁ECharts实例
const destroyEcharts = () => {
if (myChart) {
myChart.dispose()
myChart = null
window.removeEventListener('resize', resizeEcharts)
}
}
// 组件挂载时初始化
onMounted(() => {
initEcharts()
})
// 组件更新时props变化重绘
onUpdated(() => {
destroyEcharts()
initEcharts()
})
// 组件卸载时销毁实例
onUnmounted(() => {
destroyEcharts()
})
// 深度监听数据源变化,重绘图表
watch(() => props.factoryData, () => {
destroyEcharts()
initEcharts()
}, { deep: true })
</script>
<style scoped>
.echarts-container {
width: 100%;
height: 280px; /* 固定图表高度,可根据需求调整 */
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<div ref="chartRef" class="echarts-container"></div>
</template>
<script setup>
import { ref, onMounted, onUpdated, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
// 接收父组件传入的出入库对比数据
const props = defineProps({
inOutData: {
type: Array,
default: () => []
}
})
const chartRef = ref(null)
let myChart = null
const initEcharts = () => {
if (!chartRef.value) return
myChart = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
formatter: '日期:{b}<br/>{a}{c} 件'
},
legend: {
data: ['入库数量', '出库数量'],
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.inOutData.map(item => item.time),
axisLabel: {
rotate: 45,
fontSize: 12
}
},
yAxis: {
type: 'value',
name: '操作数量(件)',
min: 0
},
series: [
{
name: '入库数量',
type: 'line',
data: props.inOutData.map(item => item.inNum),
smooth: true,
lineStyle: { width: 2, color: '#67C23A' },
itemStyle: { color: '#67C23A' },
symbol: 'circle',
symbolSize: 6
},
{
name: '出库数量',
type: 'line',
data: props.inOutData.map(item => item.outNum),
smooth: true,
lineStyle: { width: 2, color: '#E6A23C' },
itemStyle: { color: '#E6A23C' },
symbol: 'triangle',
symbolSize: 6
}
],
noDataLoadingOption: {
text: '暂无出入库数据',
textStyle: { fontSize: 14 }
}
}
myChart.setOption(option, true)
window.addEventListener('resize', resizeEcharts)
}
const resizeEcharts = () => {
if (myChart) myChart.resize()
}
const destroyEcharts = () => {
if (myChart) {
myChart.dispose()
myChart = null
window.removeEventListener('resize', resizeEcharts)
}
}
onMounted(() => initEcharts())
onUpdated(() => {
destroyEcharts()
initEcharts()
})
onUnmounted(() => destroyEcharts())
watch(() => props.inOutData, () => {
destroyEcharts()
initEcharts()
}, { deep: true })
</script>
<style scoped>
.echarts-container {
width: 100%;
height: 280px;
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<div ref="chartRef" class="echarts-container"></div>
</template>
<script setup>
import { ref, onMounted, onUpdated, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
// 接收父组件传入的库存趋势数据
const props = defineProps({
trendData: {
type: Array,
default: () => []
}
})
const chartRef = ref(null)
let myChart = null
const initEcharts = () => {
if (!chartRef.value) return
myChart = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
formatter: '日期:{b}<br/>库存总量:{c} 件'
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.trendData.map(item => item.time),
axisLabel: {
rotate: 45,
fontSize: 12
}
},
yAxis: {
type: 'value',
name: '库存数量(件)',
min: 0
},
series: [
{
name: '库存总量',
type: 'line',
data: props.trendData.map(item => item.stock.toFixed(2)),
smooth: true, // 平滑折线
lineStyle: { width: 2 },
itemStyle: { color: '#F56C6C' },
areaStyle: {
// 面积渐变背景
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(245,108,108,0.3)' },
{ offset: 1, color: 'rgba(245,108,108,0)' }
])
}
}
],
noDataLoadingOption: {
text: '暂无库存趋势数据',
textStyle: { fontSize: 14 }
}
}
myChart.setOption(option, true)
window.addEventListener('resize', resizeEcharts)
}
const resizeEcharts = () => {
if (myChart) myChart.resize()
}
const destroyEcharts = () => {
if (myChart) {
myChart.dispose()
myChart = null
window.removeEventListener('resize', resizeEcharts)
}
}
onMounted(() => initEcharts())
onUpdated(() => {
destroyEcharts()
initEcharts()
})
onUnmounted(() => destroyEcharts())
watch(() => props.trendData, () => {
destroyEcharts()
initEcharts()
}, { deep: true })
</script>
<style scoped>
.echarts-container {
width: 100%;
height: 280px;
}
</style>

View File

@@ -13,19 +13,6 @@
</el-form-item>
</el-form>
<!-- <el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> -->
<el-table v-loading="loading" :data="purchaseInDetailList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="入库明细ID 主键" align="center" prop="detailId" v-if="false" />
@@ -283,4 +270,8 @@ watch(() => props.purchaseId, (newVal, oldVal) => {
getList();
}
}, { immediate: true })
defineExpose({
getList
})
</script>

View File

@@ -0,0 +1,197 @@
<template>
<div v-loading="loading">
<div v-if="!materialId" style="height: 400px; display: flex; align-items: center; justify-content: center;">
<el-empty description="请选择配料"></el-empty>
</div>
<!-- 图表容器必须设置宽高否则ECharts无法渲染 -->
<div v-else-if="priceHistoryList.length > 0" ref="chartRef" class="price-chart-container"></div>
<div v-else style="height: 400px; display: flex; align-items: center; justify-content: center;">
<el-empty description="暂无价格历史记录"></el-empty>
</div>
</div>
</template>
<script setup name="Price">
import * as echarts from 'echarts';
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { listMatPriceHistory } from "@/api/mat/matPriceHistory";
// 接收父组件传参物料ID
const props = defineProps({
materialId: {
type: String,
default: ''
}
})
// 价格历史列表
const priceHistoryList = ref([]);
// ECharts实例引用核心用于实例管理和自适应
const chartRef = ref(null);
// 存储ECharts实例方便后续销毁和更新
let myChart = null;
// 窗口大小监听标识(用于销毁监听)
let resizeObserver = null;
const loading = ref(false);
const getListChart = () => {
loading.value = true;
listMatPriceHistory({
materialId: props.materialId,
pageSize: 1000,
pageNum: 1
}).then(response => {
// 倒序显示,最新数据在顶部
priceHistoryList.value = (response.rows || []).reverse();
// 关键等待Vue异步更新DOM确保图表容器已渲染再初始化图表
nextTick(() => {
initECharts();
loading.value = false;
});
});
};
// 初始化ECharts图表
const initECharts = () => {
// 容器不存在,直接返回
if (!chartRef.value) return;
// 先销毁旧实例,避免叠加
destroyChart();
// 初始化新实例
myChart = echarts.init(chartRef.value);
// 解析图表数据
const chartData = parseChartData();
// 设置图表配置项
const option = {
// 标题:显示物料名称+规格,居中
title: {
text: `历史平均价格`,
left: 'center',
textStyle: { fontSize: 16 }
},
// 提示框鼠标悬浮显示价格格式化保留2位小数
tooltip: {
trigger: 'axis',
formatter: '价格:<b>{c} 元</b>',
axisPointer: { type: 'line', lineStyle: { type: 'dashed' } }
},
// 图例:单系列可隐藏,多系列需配置
legend: { data: ['单价'], left: 'left' },
// 网格:防止坐标轴标签超出容器
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
// X轴类目轴使用historyId作为标识
xAxis: {
// 隐藏x轴
show: false,
type: 'category',
data: chartData.xData,
// X轴标签旋转防止重叠
axisLabel: { rotate: 30 },
// 隐藏X轴刻度线
axisTick: { show: false }
},
// Y轴数值轴价格保留2位小数单位元
yAxis: {
type: 'value',
name: '单价(元)',
min: 0,
// 格式化Y轴标签保留2位小数
axisLabel: { formatter: '{value} ' },
// 保留2位小数的刻度
splitNumber: 5,
precision: 2
},
// 系列:折线图,核心价格趋势
series: [
{
name: '单价',
type: 'line',
data: chartData.yData,
// 折线平滑
smooth: true,
// 标记点:显示每个价格节点
symbol: 'circle',
symbolSize: 6,
// 标记点悬浮放大
emphasis: { symbolSize: 8 },
// 区域填充:增加视觉效果
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(66, 165, 245, 0.3)' },
{ offset: 1, color: 'rgba(66, 165, 245, 0)' }
])
},
// 折线颜色
lineStyle: { color: '#42a5f5', width: 2 }
}
]
};
// 设置配置项并渲染
myChart.setOption(option);
// 绑定窗口大小自适应事件(仅绑定一次)
if (!resizeObserver) {
resizeObserver = () => { myChart && myChart.resize(); };
window.addEventListener('resize', resizeObserver);
}
};
// 解析接口数据为ECharts所需格式
const parseChartData = () => {
const list = priceHistoryList.value || [];
// 物料名称和规格取第一条即可同物料ID下一致
const materialName = list[0]?.materialName || '物料';
const spec = list[0]?.spec || '';
// X轴数据historyId唯一标识Y轴数据价格转数值
const xData = list.map(item => item.historyId);
const yData = list.map(item => Number(item.price) || 0);
return { materialName, spec, xData, yData };
};
// 销毁ECharts实例
const destroyChart = () => {
if (myChart && echarts.getInstanceByDom(chartRef.value)) {
myChart.dispose();
myChart = null;
}
};
// 监听物料ID变化重新请求数据
watch(() => props.materialId, (newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
getListChart();
} else if (!newVal) {
// 清空物料ID时重置列表和销毁图表
priceHistoryList.value = [];
destroyChart();
}
}, { immediate: true }); // 立即执行初始加载若有materialId则直接请求
// 初始化若初始有物料ID直接请求数据
onMounted(() => {
if (props.materialId) {
getListChart();
}
});
// 销毁:移除监听、销毁图表实例,防止内存泄漏
onUnmounted(() => {
// destroyChart();
if (resizeObserver) {
window.removeEventListener('resize', resizeObserver);
}
});
defineExpose({
getListChart
})
</script>
<style scoped>
/* 图表容器样式设置固定高度宽度100%自适应父容器 */
.price-chart-container {
width: 100%;
height: 400px;
min-width: 300px;
}
</style>

View File

@@ -1,17 +1,312 @@
<template>
<div class="stock-dashboard-container" style="padding: 20px;">
<!-- 加载状态 -->
<el-loading v-loading="loading" text="数据加载中...">
<!-- 第一行4个指标卡 -->
<el-row :gutter="20" mb="20">
<el-col :span="6" v-for="item in statCards" :key="item.key">
<el-card shadow="hover" class="stat-card">
<el-statistic
:title="item.title"
:value="item.value"
:precision="item.precision || 2"
:suffix="item.suffix"
/>
</el-card>
</el-col>
</el-row>
<!-- 第二行3个图表组件 -->
<el-row :gutter="20" mb="20" style="margin-top: 10px;">
<!-- 厂家汇总柱状图 -->
<el-col :span="8">
<el-card shadow="hover" title="按厂家汇总材料数/库存数">
<FactoryBarChart :factory-data="factoryBarData" />
</el-card>
</el-col>
<!-- 库存总量时间趋势折线图 -->
<el-col :span="8">
<el-card shadow="hover" title="库存总量随时间变化趋势">
<StockTrendChart :trend-data="stockTrendData" />
</el-card>
</el-col>
<!-- 出入库数量对比折线图 -->
<el-col :span="8">
<el-card shadow="hover" title="出入库数量按时间对比">
<InOutCompareChart :in-out-data="inOutCompareData" />
</el-card>
</el-col>
</el-row>
<!-- 第三行材料原始数据表格带分页 -->
<el-row style="margin-top: 10px;">
<el-col :span="24">
<el-card shadow="hover" title="材料原始数据">
<el-table
:data="paginationMaterialList"
border
stripe
style="width: 100%; margin-bottom: 20px;"
empty-text="暂无材料数据"
>
<el-table-column prop="materialName" label="材料名称" align="center" />
<el-table-column prop="spec" label="规格" align="center" />
<el-table-column prop="model" label="型号" align="center" />
<el-table-column prop="factory" label="生产厂家" align="center" />
<el-table-column prop="unit" label="单位" align="center" />
<el-table-column
prop="currentStock"
label="当前库存"
align="center"
/>
<el-table-column prop="inTransitNum" label="在途数" align="center" />
<!-- <el-table-column prop="planNum" label="计划数" align="center" /> -->
</el-table>
<!-- 分页组件 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 50, 100]"
@size-change="getMaterialList"
@current-change="getMaterialList"
style="text-align: right;"
/>
</el-card>
</el-col>
</el-row>
</el-loading>
</div>
</template>
<script>
// 库存看板
import * as echarts from 'echarts';
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import * as echarts from 'echarts'
import dayjs from 'dayjs'
// 导入接口
import { listPurchaseInDetail } from "@/api/mat/purchaseInDetail";
import { listMaterialOut } from "@/api/mat/materialOut";
import { listPurchase } from "@/api/mat/purchase";
import { listMaterial } from "@/api/mat/material";
// 导入封装的图表组件
import FactoryBarChart from '../components/charts/FactoryBarChart.vue'
import StockTrendChart from '../components/charts/StockTrendChart.vue'
import InOutCompareChart from '../components/charts/InOutCompareChart.vue'
const purchaseList = ref([]);
const materialList = ref([]);
const purchaseInDetailList = ref([]);
const materialOutList = ref([]);
</script>
// 响应式数据:接口返回列表
const materialList = ref([]) // 材料列表
const purchaseInDetailList = ref([]) // 入库列表
const materialOutList = ref([]) // 出库列表
// 加载状态
const loading = ref(false)
// 分页参数(若依接口标准分页)
const queryParams = reactive({
pageNum: 1,
pageSize: 10
})
const total = ref(0) // 材料总条数
// 格式化数值保留2位小数处理字符串转数字
const formatNumber = (row, column) => {
return Number(row[column.prop] || 0).toFixed(2)
}
// 分页后的材料列表(计算属性)
const paginationMaterialList = computed(() => {
const start = (queryParams.pageNum - 1) * queryParams.pageSize
const end = start + queryParams.pageSize
return materialList.value.slice(start, end)
})
// 指标卡数据:库存总量、材料类型数、库存操作数、库存变化量
const statCards = ref([
{ title: '库存总量', value: 0, suffix: '件', precision: 2, key: 'totalStock' },
{ title: '材料类型数量', value: 0, key: 'materialTypeCount', precision: 0 },
{ title: '库存操作数', value: 0, key: 'stockOperateCount', precision: 0 },
{ title: '库存变化量', value: 0, suffix: '件', precision: 2, key: 'stockChange' }
])
// 图表数据源(供子组件使用)
const factoryBarData = ref([]) // 厂家汇总柱状图数据
const stockTrendData = ref([]) // 库存趋势折线图数据
const inOutCompareData = ref([]) // 出入库对比折线图数据
// 1. 获取材料列表(带分页)
const getMaterialList = async () => {
try {
const res = await listMaterial(queryParams) // 传入分页参数
materialList.value = res.rows || []
total.value = res.total || 0 // 若依接口返回total总条数
} catch (err) {
console.error('获取材料列表失败:', err)
materialList.value = []
}
}
// 2. 获取入库+出库列表(无需分页,取全部数据做统计)
const getInOutList = async () => {
try {
// 并行请求,提高效率
const [inRes, outRes] = await Promise.all([
listPurchaseInDetail(),
listMaterialOut()
])
purchaseInDetailList.value = inRes.rows || []
materialOutList.value = outRes.rows || []
} catch (err) {
console.error('获取出入库列表失败:', err)
purchaseInDetailList.value = []
materialOutList.value = []
}
}
// 3. 计算指标卡数值
const calcStatCards = () => {
// 库存总量:所有材料当前库存求和
const totalStock = materialList.value.reduce((sum, item) => {
return sum + Number(item.currentStock || 0)
}, 0)
// 材料类型数量去重按materialId
const materialTypeCount = new Set(materialList.value.map(item => item.materialId)).size
// 库存操作数:入库次数+出库次数
const stockOperateCount = purchaseInDetailList.value.length + materialOutList.value.length
// 库存变化量:总入库数 - 总出库数
const totalInNum = purchaseInDetailList.value.reduce((sum, item) => sum + Number(item.inNum || 0), 0)
const totalOutNum = materialOutList.value.reduce((sum, item) => sum + Number(item.outNum || 0), 0)
const stockChange = totalInNum - totalOutNum
// 更新指标卡
statCards.value = [
{ title: '库存总量', value: totalStock, suffix: '件', precision: 2, key: 'totalStock' },
{ title: '材料类型数量', value: materialTypeCount, key: 'materialTypeCount', precision: 0 },
{ title: '库存操作数', value: stockOperateCount, key: 'stockOperateCount', precision: 0 },
{ title: '库存变化量', value: stockChange, suffix: '件', precision: 2, key: 'stockChange' }
]
}
// 4. 转换厂家汇总柱状图数据:按厂家分组,统计材料数+库存数
const transformFactoryBarData = () => {
const factoryMap = new Map()
// 按厂家分组
materialList.value.forEach(item => {
const factory = item.factory || '未知厂家'
if (!factoryMap.has(factory)) {
factoryMap.set(factory, {
materialCount: 0, // 材料个数(去重后)
stockTotal: 0, // 库存总数
materialIds: new Set() // 去重材料ID
})
}
const factoryItem = factoryMap.get(factory)
factoryItem.materialIds.add(item.materialId)
factoryItem.materialCount = factoryItem.materialIds.size
factoryItem.stockTotal += Number(item.currentStock || 0)
})
// 转换为数组供图表使用
factoryBarData.value = Array.from(factoryMap).map(([factory, data]) => ({
factory,
materialCount: data.materialCount,
stockTotal: data.stockTotal.toFixed(2)
}))
}
// 5. 转换库存趋势+出入库对比图表数据(按时间分组)
const transformTimeTrendData = () => {
// 整合所有时间操作记录:入库+出库,统一格式
const allOperate = [
...purchaseInDetailList.value.map(item => ({
time: dayjs(item.inTime).format('YYYY-MM-DD'),
type: 'in',
num: Number(item.inNum || 0)
})),
...materialOutList.value.map(item => ({
time: dayjs(item.outTime).format('YYYY-MM-DD'),
type: 'out',
num: Number(item.outNum || 0)
}))
]
if (allOperate.length === 0) {
stockTrendData.value = []
inOutCompareData.value = []
return
}
// 按时间排序,获取所有唯一时间(升序)
const timeSet = new Set(allOperate.map(item => item.time))
const timeList = Array.from(timeSet).sort((a, b) => dayjs(a) - dayjs(b))
// 初始化时间分组数据:统计每日入库/出库数
const timeDataMap = {}
timeList.forEach(time => {
timeDataMap[time] = { in: 0, out: 0 }
})
allOperate.forEach(item => {
timeDataMap[item.time][item.type] += item.num
})
// 转换出入库对比数据
inOutCompareData.value = timeList.map(time => ({
time,
inNum: timeDataMap[time].in,
outNum: timeDataMap[time].out
}))
// 转换库存趋势数据:累计库存 = 初始库存 + 累计入库 - 累计出库
// 初始库存:当前总库存 - (总入库 - 总出库),保证趋势图最后一个点匹配当前库存
const totalStockNow = materialList.value.reduce((sum, item) => sum + Number(item.currentStock || 0), 0)
const totalInAll = purchaseInDetailList.value.reduce((sum, item) => sum + Number(item.inNum || 0), 0)
const totalOutAll = materialOutList.value.reduce((sum, item) => sum + Number(item.outNum || 0), 0)
const initStock = totalStockNow - (totalInAll - totalOutAll)
let cumulativeIn = 0 // 累计入库
let cumulativeOut = 0 // 累计出库
stockTrendData.value = timeList.map(time => {
cumulativeIn += timeDataMap[time].in
cumulativeOut += timeDataMap[time].out
const currentStock = initStock + cumulativeIn - cumulativeOut
return {
time,
stock: currentStock < 0 ? 0 : currentStock // 库存不能为负
}
})
}
// 核心:加载所有数据并处理转换
const loadAllData = async () => {
loading.value = true
try {
// 并行请求所有数据
await Promise.all([
getMaterialList(),
getInOutList()
])
// 数据转换:指标卡 + 所有图表
calcStatCards()
transformFactoryBarData()
transformTimeTrendData()
} catch (err) {
console.error('数据加载失败:', err)
} finally {
loading.value = false
}
}
// 页面挂载时加载数据
onMounted(() => {
loadAllData()
})
</script>
<style scoped>
.stat-card {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
/* 图表容器高度由子组件控制,父组件仅做布局 */
.el-card {
height: 100%;
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="配料" prop="materialId">
<el-input v-model="queryParams.materialId" placeholder="请输入配料" clearable @keyup.enter="handleQuery" />
<raw-selector ref="rawSelector" v-model="queryParams.materialId" placeholder="请选择配料" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="操作人" prop="operator">
<el-input v-model="queryParams.operator" placeholder="请输入入库操作人" clearable @keyup.enter="handleQuery" />
@@ -73,7 +73,7 @@
<el-input v-model="form.purchaseId" placeholder="请输入采购单ID 关联t_purchase.id" />
</el-form-item> -->
<el-form-item label="配料" prop="materialId">
<el-input v-model="form.materialId" placeholder="请输入配料" />
<raw-selector ref="rawSelector" v-model="form.materialId" placeholder="请选择配料" />
</el-form-item>
<el-form-item label="入库数量" prop="inNum">
<el-input v-model="form.inNum" placeholder="请输入入库数量" />
@@ -109,6 +109,7 @@
<script setup name="PurchaseInDetail">
import { listPurchaseInDetail, getPurchaseInDetail, delPurchaseInDetail, addPurchaseInDetail, updatePurchaseInDetail } from "@/api/mat/purchaseInDetail";
import useUserStore from '@/store/modules/user'
import RawSelector from '@/components/RawSelector/index.vue'
import Raw from '@/components/Renderer/Raw.vue'
const userStore = useUserStore()

View File

@@ -5,7 +5,7 @@
<el-input v-model="queryParams.outNo" placeholder="请输入出库单号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="配料" prop="materialId">
<el-input v-model="queryParams.materialId" placeholder="请输入配料" clearable @keyup.enter="handleQuery" />
<raw-selector ref="rawSelector" v-model="queryParams.materialId" placeholder="请选择配料" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="操作人" prop="operator">
<el-input v-model="queryParams.operator" placeholder="请输入出库操作人" clearable @keyup.enter="handleQuery" />
@@ -75,7 +75,7 @@
<el-input v-model="form.outNo" placeholder="请输入出库单号" />
</el-form-item>
<el-form-item label="配料" prop="materialId">
<el-input v-model="form.materialId" placeholder="请输入配料" />
<raw-selector ref="rawSelector" v-model="form.materialId" placeholder="请选择配料" />
</el-form-item>
<el-form-item label="出库数量" prop="outNum">
<el-input v-model="form.outNum" placeholder="请输入出库数量" />
@@ -109,6 +109,7 @@
import { listMaterialOut, getMaterialOut, delMaterialOut, addMaterialOut, updateMaterialOut } from "@/api/mat/materialOut";
import useUserStore from '@/store/modules/user'
import { computed } from "vue";
import RawSelector from '@/components/RawSelector/index.vue'
import Raw from '@/components/Renderer/Raw.vue'
const userStore = useUserStore()

View File

@@ -1,29 +1,14 @@
<template>
<div class="app-container">
<div class="app-container" ref="appContainer">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="queryParams.productName"
placeholder="请输入产品名称"
clearable
@keyup.enter="handleQuery"
/>
<el-input v-model="queryParams.productName" placeholder="请输入产品名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="产品规格" prop="spec">
<el-input
v-model="queryParams.spec"
placeholder="请输入产品规格"
clearable
@keyup.enter="handleQuery"
/>
<el-input v-model="queryParams.spec" placeholder="请输入产品规格" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="产品型号" prop="model">
<el-input
v-model="queryParams.model"
placeholder="请输入产品型号"
clearable
@keyup.enter="handleQuery"
/>
<el-input v-model="queryParams.model" placeholder="请输入产品型号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -33,66 +18,40 @@
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>新增</el-button>
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
>导出</el-button>
<el-button type="warning" plain icon="Download" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="productList" @selection-change="handleSelectionChange">
<el-table v-loading="loading" :data="productList" @selection-change="handleSelectionChange"
@row-click="handleRowClick">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="产品ID 主键" align="center" prop="productId" v-if="false"/>
<el-table-column label="产品ID 主键" align="center" prop="productId" v-if="false" />
<el-table-column label="产品名称" align="center" prop="productName" />
<el-table-column label="产品规格" align="center" prop="spec" />
<el-table-column label="产品型号" align="center" prop="model" />
<el-table-column label="产品单价" align="center" prop="unitPrice" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Plus" @click="handleBom(scope.row)">配方</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
<template #default="scope">
<el-button link type="primary" icon="Plus" @click="handleBom(scope.row)">配方</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 添加或修改产品基础信息对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
@@ -121,15 +80,35 @@
</template>
</el-dialog>
<el-dialog title="产品配方" v-model="bomOpen" width="500px" append-to-body>
<el-dialog title="产品配方" v-model="bomOpen" width="800px" append-to-body>
<bom :productId="currentProductId" @close="bomOpen = false" />
</el-dialog>
<!-- 将这个表格改为始终吸底且外层容器可以拖拽调节高度 -->
<!-- <StickyDragContainer v-if="currentProduct.productId" :parent-ref="appContainer"> -->
<div v-if="currentProduct.productId">
<h3>产品配料</h3>
<!-- 插槽内容原有配料表格无需任何修改 -->
<el-table :data="currentProduct.materials">
<el-table-column label="配料名称" align="center" prop="materialName" />
<el-table-column label="配料规格" align="center" prop="spec" />
<el-table-column label="配料型号" align="center" prop="model" />
<el-table-column label="厂家" align="center" prop="factory" />
<el-table-column label="计量单位" align="center" prop="unit" />
<el-table-column label="现存库存" align="center" prop="currentStock" />
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
</div>
<el-empty v-else description="选择产品查看配料信息" />
<!-- </StickyDragContainer> -->
</div>
</template>
<script setup name="Product">
import { listProduct, getProduct, delProduct, addProduct, updateProduct } from "@/api/mat/product";
import bom from "@/views/mat/components/bom.vue";
import StickyDragContainer from "@/components/StickyDragContainer/index.vue";
const bomOpen = ref(false);
@@ -146,6 +125,8 @@ const multiple = ref(true);
const total = ref(0);
const title = ref("");
const currentProductId = ref(null);
const currentProduct = ref({});
const appContainer = ref(null);
const formatterTime = (time) => {
return proxy.parseTime(time, '{y}-{m}-{d}')
@@ -268,7 +249,7 @@ function submitForm() {
/** 删除按钮操作 */
function handleDelete(row) {
const _productIds = row.productId || ids.value;
proxy.$modal.confirm('是否确认删除产品基础信息编号为"' + _productIds + '"的数据项?').then(function() {
proxy.$modal.confirm('是否确认删除产品基础信息编号为"' + _productIds + '"的数据项?').then(function () {
loading.value = true;
return delProduct(_productIds);
}).then(() => {
@@ -293,5 +274,9 @@ function handleExport() {
}, `product_${new Date().getTime()}.xlsx`)
}
function handleRowClick(row) {
currentProduct.value = row;
}
getList();
</script>

View File

@@ -8,7 +8,7 @@
<el-input v-model="queryParams.factory" placeholder="请输入供应商" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="配料" prop="materialId">
<el-input v-model="queryParams.materialId" placeholder="请输入配料" clearable @keyup.enter="handleQuery" />
<raw-selector ref="rawSelector" v-model="queryParams.materialId" placeholder="请选择配料" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="采购状态" prop="status">
<el-select style="width: 120px" v-model="queryParams.status" placeholder="请选择采购状态" clearable>
@@ -51,7 +51,9 @@
<raw :data="scope.row" />
</template>
</el-table-column>
<el-table-column label="采购数" align="center" prop="planNum" />
<el-table-column label="采购数" align="center" prop="planNum" />
<el-table-column label="已收数量" align="center" prop="receivedNum" />
<el-table-column label="在途数量" align="center" prop="inTransitNum" />
<el-table-column label="采购单价" align="center" prop="purchasePrice" />
<el-table-column label="截止日期" align="center" prop="deadline" width="180">
<template #default="scope">
@@ -73,8 +75,8 @@
@click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" v-if="scope.row.status == 1 || scope.row.status == 3"
@click="handleDelete(scope.row)">删除</el-button>
<el-button link type="primary" icon="Delete" v-if="scope.row.status == 1 || scope.row.status == 3"
@click="handleZero(scope.row)">归零</el-button>
<el-button link type="primary" icon="Check" v-if="scope.row.status == 1 || scope.row.status == 3"
@click="handleZero(scope.row)">完成</el-button>
</template>
</el-table-column>
</el-table>
@@ -92,7 +94,9 @@
<el-input v-model="form.factory" placeholder="请输入供应商" />
</el-form-item>
<el-form-item label="配料" prop="materialId">
<el-input v-model="form.materialId" placeholder="请输入配料ID 关联material.id" />
<raw-selector v-model="form.materialId" />
<!-- <el-input v-model="form.materialId" placeholder="请输入配料ID 关联material.id" /> -->
<!-- <el-input v-model="form.materialId" placeholder="请输入配料ID 关联material.id" /> -->
</el-form-item>
<el-form-item label="采购数量" prop="planNum" v-if="!form.purchaseId">
<el-input v-model="form.planNum" placeholder="请输入采购数量" />
@@ -135,7 +139,7 @@
<el-input v-model="inForm.materialId" placeholder="请输入配料" />
</el-form-item> -->
<el-form-item label="入库数量" prop="inNum">
<el-input v-model="inForm.inNum" placeholder="请输入入库数量" />
<el-input-number style="width: 100%" v-model="inForm.inNum" :placeholder="'请输入入库数量,最大数量为' + maxInNum" :max="maxInNum" :controls="false" />
</el-form-item>
<el-form-item label="入库单价" prop="inPrice">
<el-input v-model="inForm.inPrice" placeholder="请输入入库单价" />
@@ -161,7 +165,7 @@
</el-dialog>
<div style="border: 1px solid #aaa; margin-top: 10px;">
<PurchaseInDetail v-if="currentPurchaseId" :purchaseId="currentPurchaseId" />
<PurchaseInDetail ref="inDetailRef" v-if="currentPurchaseId" :purchaseId="currentPurchaseId" />
<el-empty v-else description="选择采购单查看入库记录详情" />
</div>
@@ -172,6 +176,7 @@
import { listPurchase, getPurchase, delPurchase, addPurchase, updatePurchase } from "@/api/mat/purchase";
import { listPurchaseInDetail, addPurchaseInDetail } from "@/api/mat/purchaseInDetail";
import PurchaseInDetail from "@/views/mat/components/in.vue"
import RawSelector from '@/components/RawSelector/index.vue'; // 引入组件
import Raw from "@/components/Renderer/Raw.vue"
import useUserStore from '@/store/modules/user'
import { computed } from "vue";
@@ -362,10 +367,13 @@ function handleExport() {
const inOpen = ref(false);
const inForm = ref({});
const maxInNum = ref(0);
const inDetailRef = ref(null);
function handleIn(row) {
inOpen.value = true;
const inTime = proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
maxInNum.value = row.inTransitNum;
inForm.value = {
detailId: null,
@@ -388,6 +396,7 @@ function submitFormIn() {
proxy.$modal.msgSuccess("新增成功");
inOpen.value = false;
getList();
inDetailRef.value?.getList();
}).finally(() => {
buttonLoading.value = false;
loading.value = false;
@@ -400,5 +409,22 @@ function cancelIn() {
inOpen.value = false;
}
function handleZero(row) {
proxy.$modal.confirm('是否将采购单编号为"' + row.purchaseId + '"的计划数量设为与已收数量相同,且状态设为已完成?').then(function () {
loading.value = true;
return updatePurchase({
purchaseId: row.purchaseId,
planNum: row.receivedNum,
status: 2,
});
}).then(() => {
loading.value = true;
getList();
proxy.$modal.msgSuccess("发货计划已归档");
}).finally(() => {
loading.value = false;
});
}
getList();
</script>

View File

@@ -41,7 +41,7 @@
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="materialList" @selection-change="handleSelectionChange">
<el-table v-loading="loading" :data="materialList" @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="配料ID 主键" align="center" prop="materialId" v-if="true" /> -->
<el-table-column label="配料名称" align="center" prop="materialName" />
@@ -50,6 +50,7 @@
<el-table-column label="厂家" align="center" prop="factory" />
<el-table-column label="计量单位" align="center" prop="unit" />
<el-table-column label="现存库存" align="center" prop="currentStock" />
<el-table-column label="在途数量" align="center" prop="inTransitNum" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
@@ -100,7 +101,7 @@
<el-dialog title="入库" v-model="inOpen" width="500px" append-to-body>
<el-form ref="materialInRef" :model="inForm" label-width="80px">
<el-form-item label="配料" prop="materialId">
<el-input v-model="inForm.materialId" placeholder="请输入配料" />
<raw-selector ref="rawSelector" v-model="inForm.materialId" placeholder="请选择配料" />
</el-form-item>
<el-form-item label="入库数量" prop="inNum">
<el-input v-model="inForm.inNum" placeholder="请输入入库数量" />
@@ -134,7 +135,7 @@
<el-input v-model="outForm.outNo" placeholder="请输入出库单号" />
</el-form-item>
<el-form-item label="配料" prop="materialId">
<el-input v-model="outForm.materialId" placeholder="请输入配料" />
<raw-selector ref="rawSelector" v-model="outForm.materialId" placeholder="请选择配料" />
</el-form-item>
<el-form-item label="出库数量" prop="outNum">
<el-input v-model="outForm.outNum" placeholder="请输入出库数量" />
@@ -161,6 +162,9 @@
</div>
</template>
</el-dialog>
<Price :materialId="currentMaterialId" ref="priceRef" />
</div>
</template>
@@ -168,6 +172,8 @@
import { listMaterial, getMaterial, delMaterial, addMaterial, updateMaterial } from "@/api/mat/material";
import { addPurchaseInDetail } from "@/api/mat/purchaseInDetail";
import { addMaterialOut } from "@/api/mat/materialOut";
import Price from "@/views/mat/components/price.vue";
import RawSelector from "@/components/RawSelector/index.vue";
import useUserStore from '@/store/modules/user'
@@ -191,6 +197,14 @@ const outForm = ref({});
const inOpen = ref(false);
const outOpen = ref(false);
const currentMaterialId = ref(null);
function handleRowClick(row) {
currentMaterialId.value = row.materialId;
}
const priceHistoryList = ref([]);
const nickName = computed(() => userStore.nickName)
@@ -367,6 +381,8 @@ function cancelIn() {
inOpen.value = false;
}
const priceRef = ref(null);
function submitFormIn() {
loading.value = true;
buttonLoading.value = true;
@@ -374,6 +390,7 @@ function submitFormIn() {
proxy.$modal.msgSuccess("新增成功");
inOpen.value = false;
getList();
priceRef.value.getListChart();
}).finally(() => {
buttonLoading.value = false;
loading.value = false;