From 42f6f83c3af64338335bc26199ae8972d1a8c08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= Date: Sat, 31 Jan 2026 14:21:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=89=A9=E6=96=99?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=9C=8B=E6=9D=BF=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?=E5=A4=9A=E9=A1=B9=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增物料管理看板功能,包含统计卡片和图表展示 优化物料选择器组件,支持分页和搜索功能 重构物料详情展示组件,支持动态加载数据 添加多个ECharts图表组件用于数据可视化 完善出入库和采购单相关功能,增加在途数量显示 修复若干界面显示问题和交互逻辑 --- gear-ui3/package.json | 1 + gear-ui3/pnpm-lock.yaml | 23 +- gear-ui3/src/components/RawSelector/index.vue | 107 ++++++ gear-ui3/src/components/Renderer/Raw.vue | 39 ++- .../components/StickyDragContainer/index.vue | 149 +++++++++ gear-ui3/src/views/mat/components/bom.vue | 17 +- .../mat/components/charts/FactoryBarChart.vue | 135 ++++++++ .../components/charts/InOutCompareChart.vue | 112 +++++++ .../mat/components/charts/StockTrendChart.vue | 103 ++++++ gear-ui3/src/views/mat/components/in.vue | 17 +- gear-ui3/src/views/mat/components/price.vue | 197 +++++++++++ gear-ui3/src/views/mat/dashboard/index.vue | 313 +++++++++++++++++- gear-ui3/src/views/mat/in/index.vue | 5 +- gear-ui3/src/views/mat/out/index.vue | 5 +- gear-ui3/src/views/mat/product/index.vue | 107 +++--- gear-ui3/src/views/mat/purchase/index.vue | 40 ++- gear-ui3/src/views/mat/raw/index.vue | 23 +- 17 files changed, 1278 insertions(+), 115 deletions(-) create mode 100644 gear-ui3/src/components/StickyDragContainer/index.vue create mode 100644 gear-ui3/src/views/mat/components/charts/FactoryBarChart.vue create mode 100644 gear-ui3/src/views/mat/components/charts/InOutCompareChart.vue create mode 100644 gear-ui3/src/views/mat/components/charts/StockTrendChart.vue create mode 100644 gear-ui3/src/views/mat/components/price.vue diff --git a/gear-ui3/package.json b/gear-ui3/package.json index cf06ebf..f7a0fec 100644 --- a/gear-ui3/package.json +++ b/gear-ui3/package.json @@ -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", diff --git a/gear-ui3/pnpm-lock.yaml b/gear-ui3/pnpm-lock.yaml index 2eff8b3..6d7f9b8 100644 --- a/gear-ui3/pnpm-lock.yaml +++ b/gear-ui3/pnpm-lock.yaml @@ -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 diff --git a/gear-ui3/src/components/RawSelector/index.vue b/gear-ui3/src/components/RawSelector/index.vue index e69de29..379f063 100644 --- a/gear-ui3/src/components/RawSelector/index.vue +++ b/gear-ui3/src/components/RawSelector/index.vue @@ -0,0 +1,107 @@ + + + \ No newline at end of file diff --git a/gear-ui3/src/components/Renderer/Raw.vue b/gear-ui3/src/components/Renderer/Raw.vue index bd8ca2e..fb7484d 100644 --- a/gear-ui3/src/components/Renderer/Raw.vue +++ b/gear-ui3/src/components/Renderer/Raw.vue @@ -1,25 +1,48 @@ \ No newline at end of file diff --git a/gear-ui3/src/components/StickyDragContainer/index.vue b/gear-ui3/src/components/StickyDragContainer/index.vue new file mode 100644 index 0000000..3f45b88 --- /dev/null +++ b/gear-ui3/src/components/StickyDragContainer/index.vue @@ -0,0 +1,149 @@ + + + + + + \ No newline at end of file diff --git a/gear-ui3/src/views/mat/components/bom.vue b/gear-ui3/src/views/mat/components/bom.vue index b8bc9de..4b7399d 100644 --- a/gear-ui3/src/views/mat/components/bom.vue +++ b/gear-ui3/src/views/mat/components/bom.vue @@ -20,7 +20,11 @@ - + + + @@ -42,7 +46,7 @@ --> - + @@ -67,7 +71,8 @@ + + \ No newline at end of file diff --git a/gear-ui3/src/views/mat/components/charts/InOutCompareChart.vue b/gear-ui3/src/views/mat/components/charts/InOutCompareChart.vue new file mode 100644 index 0000000..c885c3c --- /dev/null +++ b/gear-ui3/src/views/mat/components/charts/InOutCompareChart.vue @@ -0,0 +1,112 @@ + + + + + \ No newline at end of file diff --git a/gear-ui3/src/views/mat/components/charts/StockTrendChart.vue b/gear-ui3/src/views/mat/components/charts/StockTrendChart.vue new file mode 100644 index 0000000..fe6fa80 --- /dev/null +++ b/gear-ui3/src/views/mat/components/charts/StockTrendChart.vue @@ -0,0 +1,103 @@ + + + + + \ No newline at end of file diff --git a/gear-ui3/src/views/mat/components/in.vue b/gear-ui3/src/views/mat/components/in.vue index cda6797..858c1a2 100644 --- a/gear-ui3/src/views/mat/components/in.vue +++ b/gear-ui3/src/views/mat/components/in.vue @@ -13,19 +13,6 @@ - - @@ -283,4 +270,8 @@ watch(() => props.purchaseId, (newVal, oldVal) => { getList(); } }, { immediate: true }) + +defineExpose({ + getList +}) diff --git a/gear-ui3/src/views/mat/components/price.vue b/gear-ui3/src/views/mat/components/price.vue new file mode 100644 index 0000000..785cfb1 --- /dev/null +++ b/gear-ui3/src/views/mat/components/price.vue @@ -0,0 +1,197 @@ + + + + + \ No newline at end of file diff --git a/gear-ui3/src/views/mat/dashboard/index.vue b/gear-ui3/src/views/mat/dashboard/index.vue index c998ae1..961c871 100644 --- a/gear-ui3/src/views/mat/dashboard/index.vue +++ b/gear-ui3/src/views/mat/dashboard/index.vue @@ -1,17 +1,312 @@ - \ No newline at end of file +// 响应式数据:接口返回列表 +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() +}) + + + \ No newline at end of file diff --git a/gear-ui3/src/views/mat/in/index.vue b/gear-ui3/src/views/mat/in/index.vue index 479bf2e..63a63f1 100644 --- a/gear-ui3/src/views/mat/in/index.vue +++ b/gear-ui3/src/views/mat/in/index.vue @@ -2,7 +2,7 @@
- + @@ -73,7 +73,7 @@ --> - + @@ -109,6 +109,7 @@ diff --git a/gear-ui3/src/views/mat/purchase/index.vue b/gear-ui3/src/views/mat/purchase/index.vue index f1ae406..f33a2c5 100644 --- a/gear-ui3/src/views/mat/purchase/index.vue +++ b/gear-ui3/src/views/mat/purchase/index.vue @@ -8,7 +8,7 @@ - + @@ -51,7 +51,9 @@ - + + + @@ -92,7 +94,9 @@ - + + + @@ -135,7 +139,7 @@ --> - + @@ -161,7 +165,7 @@
- +
@@ -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(); diff --git a/gear-ui3/src/views/mat/raw/index.vue b/gear-ui3/src/views/mat/raw/index.vue index baed83e..bc907aa 100644 --- a/gear-ui3/src/views/mat/raw/index.vue +++ b/gear-ui3/src/views/mat/raw/index.vue @@ -41,7 +41,7 @@ - + @@ -50,6 +50,7 @@ + + + +
@@ -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;