<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="min-h-360px flex-1 overflow-hidden"></div>
|
</div>
|
</template>
|
|
<style scoped></style>
|