From 3471290659516cf21db3211a9053daff5f283e03 Mon Sep 17 00:00:00 2001
From: zhuguifei <312353457@qq.com>
Date: 星期五, 20 三月 2026 15:50:18 +0800
Subject: [PATCH] feat: 基础数据仪器管理、判定依据、判定依据明细

---
 ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue |  282 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 282 insertions(+), 0 deletions(-)

diff --git a/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue
new file mode 100755
index 0000000..4021450
--- /dev/null
+++ b/ruoyi-plus-soybean/src/views/analy/store-silk/modules/store-silk-sankey.vue
@@ -0,0 +1,282 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import { useEcharts } from '@/hooks/common/echarts';
+
+defineOptions({
+  name: 'StoreSilkSankey'
+});
+
+interface StoreSilkDetailVo {
+  fsNum?: string;
+  siloNum?: string;
+  equNo?: string;
+  output?: number | null;
+}
+
+interface Props {
+  rollerRecordList?: StoreSilkDetailVo[];
+  packerRecordList?: StoreSilkDetailVo[];
+  rows?: any[];
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  rollerRecordList: () => [],
+  packerRecordList: () => [],
+  rows: () => []
+});
+
+type SankeyType = 'roller' | 'packer';
+
+const sankeyType = ref<SankeyType>('roller');
+
+function normalizeNumber(val: unknown) {
+  const v = Number(val);
+  if (!Number.isFinite(v)) return null;
+  return v;
+}
+
+function calcRollerBox(val: unknown) {
+  if (val === null || val === undefined) return null;
+  const v = Number(val) / 50;
+  if (!Number.isFinite(v)) return null;
+  return v;
+}
+
+function calcPackerBox(val: unknown) {
+  if (val === null || val === undefined) return null;
+  const v = Number(val) / 10 / 250;
+  if (!Number.isFinite(v)) return null;
+  return v;
+}
+
+function formatTime(val: unknown) {
+  if (!val) return '';
+  const s = String(val);
+  return s.length > 19 ? s.slice(0, 19) : s;
+}
+
+function buildChartData(list: StoreSilkDetailVo[], type: SankeyType) {
+  const nodeSet = new Set<string>();
+  const linkMap = new Map<string, { source: string; target: string; value: number }>();
+  const nodeInValue = new Map<string, number>();
+  const nodeOutValue = new Map<string, number>();
+  const nodeExtra = new Map<string, any>();
+  const rows = Array.isArray(props.rows) ? props.rows : [];
+
+  function addLink(source: string, target: string, value: number) {
+    if (value <= 0) return;
+    nodeSet.add(source);
+    nodeSet.add(target);
+    nodeInValue.set(target, (nodeInValue.get(target) ?? 0) + value);
+    nodeOutValue.set(source, (nodeOutValue.get(source) ?? 0) + value);
+    const key = `${source}__->__${target}`;
+    const existed = linkMap.get(key);
+    if (existed) {
+      existed.value += value;
+      return;
+    }
+    linkMap.set(key, { source, target, value });
+  }
+
+  for (const item of list) {
+    const val = normalizeNumber(item.output);
+    if (val === null) continue;
+    if (val <= 0) continue;
+
+    const fsBase = `鍠備笣鏈� ${item.fsNum || '-'}`;
+    const siloBase = `鍌ㄤ笣鏌� ${item.siloNum || '-'}`;
+    const equ = item.equNo ? `${item.equNo}#${type === 'roller' ? '鍗锋帴鏈�' : '鍖呰鏈�'}` : type === 'roller' ? '鍗锋帴鏈�' : '鍖呰鏈�';
+    const target = equ;
+    const fs = fsBase;
+    const silo = siloBase;
+
+    addLink(fs, silo, val);
+    addLink(silo, target, val);
+
+    const parentRow = rows.find((r: any) => {
+      const dList = type === 'roller' ? r?.rollerDetailList : r?.packerDetailList;
+      if (!Array.isArray(dList)) return false;
+      return dList.some((d: any) => d?.fsNum === item.fsNum && d?.siloNum === item.siloNum && d?.equNo === item.equNo);
+    });
+
+    if (parentRow) {
+      const materialname = String(parentRow.materialname ?? '');
+      const batchcode = String(parentRow.batchcode ?? parentRow.batchCode ?? '');
+      const jobinput = Number(parentRow.jobinput);
+      const distimebegin = formatTime(parentRow.distimebegin);
+      const distimeend = formatTime(parentRow.distimeend);
+
+      nodeExtra.set(fs, {
+        kind: 'fs',
+        displayName: fsBase,
+        meta: { materialname, batchcode, jobinput }
+      });
+      nodeExtra.set(silo, {
+        kind: 'silo',
+        displayName: siloBase,
+        meta: { distimebegin, distimeend }
+      });
+    }
+  }
+
+  const nodes = Array.from(nodeSet).map(name => {
+    const inV = nodeInValue.get(name) ?? 0;
+    const outV = nodeOutValue.get(name) ?? 0;
+    const value = inV > 0 ? inV : outV;
+    return { name, value, ...(nodeExtra.get(name) ?? {}) };
+  });
+  const links = Array.from(linkMap.values());
+
+  return { nodes, links };
+}
+
+const activeList = computed(() => {
+  return sankeyType.value === 'roller' ? props.rollerRecordList : props.packerRecordList;
+});
+
+watch(
+  () => [props.rollerRecordList, props.packerRecordList],
+  () => {
+    const rollerLen = Array.isArray(props.rollerRecordList) ? props.rollerRecordList.length : 0;
+    const packerLen = Array.isArray(props.packerRecordList) ? props.packerRecordList.length : 0;
+
+    if (sankeyType.value === 'roller' && rollerLen === 0 && packerLen > 0) {
+      sankeyType.value = 'packer';
+      return;
+    }
+
+    if (sankeyType.value === 'packer' && packerLen === 0 && rollerLen > 0) {
+      sankeyType.value = 'roller';
+    }
+  },
+  { deep: true, immediate: true }
+);
+
+const hasData = computed(() => {
+  return Boolean(activeList.value && activeList.value.length > 0);
+});
+
+const { domRef, updateOptions } = useEcharts(() => {
+  const { nodes, links } = buildChartData(activeList.value, sankeyType.value);
+
+  return {
+    tooltip: {
+      trigger: 'item'
+    },
+    series: [
+      {
+        type: 'sankey',
+        data: nodes,
+        links,
+        nodeAlign: 'justify',
+        layoutIterations: 32,
+        emphasis: {
+          focus: 'adjacency'
+        },
+        lineStyle: {
+          color: 'source',
+          curveness: 0.5,
+          opacity: 0.35
+        },
+        label: {
+          position: 'right',
+          formatter: (params: any) => {
+            const name = String(params?.data?.name ?? '');
+            const value = params?.data?.value;
+            const kind = params?.data?.kind;
+            const displayName = String(params?.data?.displayName ?? name);
+            const meta = params?.data?.meta;
+
+            if (kind === 'fs') {
+              const materialLine = meta?.materialname ? `鐗屽彿锛�${meta.materialname}` : '';
+              const batchLine = meta?.batchcode ? `鎵规锛�${meta.batchcode}` : '';
+              const jobLine =
+                Number.isFinite(Number(meta?.jobinput)) ? `鎶曟枡閲嶉噺锛�${Number(meta.jobinput).toFixed(2)}kg` : '';
+              const lines: string[] = [displayName];
+              lines.push('');
+              if (materialLine) lines.push(materialLine);
+              if (batchLine) lines.push(batchLine);
+              if (jobLine) {
+                lines.push('');
+                lines.push(jobLine);
+              }
+              return lines.join('\n');
+            }
+
+            if (kind === 'silo') {
+              const beginLine = meta?.distimebegin ? `寮�濮嬶細${meta.distimebegin}` : '';
+              const endLine = meta?.distimeend ? `缁撴潫锛�${meta.distimeend}` : '';
+              const valLine =
+                typeof value === 'number' && Number.isFinite(value) ? `浜ч噺锛�${value.toFixed(2)}绠盽 : '';
+              const lines: string[] = [displayName];
+              lines.push('');
+              if (beginLine) lines.push(beginLine);
+              if (endLine) lines.push(endLine);
+              if (valLine) {
+                lines.push('');
+                lines.push(valLine);
+              }
+              return lines.join('\n');
+            }
+            const showValue =
+              name.startsWith('鍌ㄤ笣鏌� ') ||
+              name.endsWith('#鍗锋帴鏈�') ||
+              name.endsWith('#鍖呰鏈�') ||
+              name === '鍗锋帴鏈�' ||
+              name === '鍖呰鏈�';
+
+            if (showValue && typeof value === 'number' && Number.isFinite(value)) {
+              return `${displayName}\n${value.toFixed(2)}绠盽;
+            }
+            return displayName;
+        }
+      }
+      },
+    ]
+  };
+});
+
+watch(
+  () => [props.rollerRecordList, props.packerRecordList, sankeyType.value],
+  () => {
+    updateOptions(opts => {
+      const { nodes, links } = buildChartData(activeList.value, sankeyType.value);
+      opts.series[0].data = nodes as any;
+      (opts.series[0] as any).links = links;
+      return opts;
+    });
+  },
+  { deep: true, immediate: true }
+);
+</script>
+
+<template>
+  <div class="h-full flex-col-stretch">
+    <div class="pb-8px">
+      <NSpace :size="8">
+        <NButton
+          size="small"
+          ghost
+          :type="sankeyType === 'roller' ? 'primary' : 'default'"
+          @click="sankeyType = 'roller'"
+        >
+          鍗锋帴
+        </NButton>
+        <NButton
+          size="small"
+          ghost
+          :type="sankeyType === 'packer' ? 'primary' : 'default'"
+          @click="sankeyType = 'packer'"
+        >
+          鍖呰
+        </NButton>
+      </NSpace>
+    </div>
+    <div v-if="!hasData" class="flex-center flex-1 text-gray-400">
+      鏆傛棤{{ sankeyType === 'roller' ? '鍗锋帴' : '鍖呰' }}妗戝熀鍥炬暟鎹�
+    </div>
+    <div v-else ref="domRef" class="flex-1 min-h-360px overflow-hidden"></div>
+  </div>
+</template>
+
+<style scoped></style>

--
Gitblit v1.9.3