Skip to content

[Bug] Sankey Error #21032

Open
Open
@Euraxluo

Description

@Euraxluo

Version

5.6.0

Link to Minimal Reproduction

Steps to Reproduce

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>库存分配可视化 - ECharts版本</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.6.0/echarts.min.js"></script>
    <style>
        body { 
            font-family: 'Microsoft YaHei', Arial, sans-serif; 
            margin: 20px; 
            background-color: #f5f5f5;
        }
        .container { 
            max-width: 1400px; 
            margin: 0 auto; 
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            padding: 20px;
        }
        .input-area { 
            margin: 20px 0; 
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
        }
        textarea { 
            width: 100%; 
            height: 200px; 
            margin: 10px 0; 
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 10px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
        }
        .button { 
            padding: 12px 24px; 
            background: linear-gradient(45deg, #4CAF50, #45a049); 
            color: white; 
            border: none; 
            cursor: pointer; 
            border-radius: 6px;
            font-size: 16px;
            transition: all 0.3s ease;
        }
        .button:hover { 
            background: linear-gradient(45deg, #45a049, #4CAF50); 
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        }
        #visualization { 
            margin-top: 20px; 
            height: 600px;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            background: white;
        }
        .metrics { 
            display: flex; 
            flex-wrap: wrap; 
            gap: 16px; 
            margin-bottom: 20px; 
        }
        .metric { 
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); 
            padding: 15px 25px; 
            border-radius: 10px; 
            min-width: 180px; 
            font-size: 1.1em; 
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            transition: transform 0.3s ease;
        }
        .metric:hover {
            transform: translateY(-3px);
        }
        .metric.success { 
            background: linear-gradient(135deg, #d4edda 0%, #a8d8a8 100%); 
        }
        .metric.warning { 
            background: linear-gradient(135deg, #fff3cd 0%, #ffd93d 100%); 
        }
        .metric.danger { 
            background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%); 
        }
        table { 
            width: 100%; 
            border-collapse: collapse; 
            margin: 10px 0; 
            box-shadow: 0 2px 4px rgba(0,0,0,0.05);
            border-radius: 8px;
            overflow: hidden;
        }
        th, td { 
            border: 1px solid #ddd; 
            padding: 12px; 
            text-align: left; 
        }
        th { 
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 
            color: white;
            font-weight: bold;
        }
        tr:nth-child(even) { 
            background-color: #f8f9fa; 
        }
        tr:hover {
            background-color: #e3f2fd;
        }
        h1 {
            color: #2c3e50;
            text-align: center;
            margin-bottom: 30px;
            font-size: 2.5em;
        }
        h3 {
            color: #34495e;
            border-left: 4px solid #3498db;
            padding-left: 15px;
            margin-top: 30px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📦 库存分配可视化系统</h1>
        <div class="input-area">
            <h3>🔧 输入分配结果JSON:</h3>
            <textarea id="jsonInput" placeholder="请粘贴AllocationResponse的JSON数据..."></textarea>
            <button class="button" onclick="visualize()">🚀 生成可视化</button>
            <div style="margin-top: 15px; background: #f0f8ff; padding: 15px; border-radius: 8px; border: 1px solid #d1e6ff;">
                <h3 style="margin-top: 0;">📊 Sankey图布局调整</h3>
                <div style="display: flex; flex-wrap: wrap; gap: 15px;">
                    <div style="flex: 1; min-width: 200px;">
                        <label for="nodeAlign">节点对齐方式:</label>
                        <select id="nodeAlign" style="width: 100%; padding: 8px; margin-top: 5px; border-radius: 4px; border: 1px solid #ccc;">
                            <option value="left">左对齐</option>
                            <option value="right" selected>右对齐</option>
                            <option value="justify">两端对齐</option>
                        </select>
                    </div>
                    <div style="flex: 1; min-width: 200px;">
                        <label for="layoutOrientation">布局方向:</label>
                        <select id="layoutOrientation" style="width: 100%; padding: 8px; margin-top: 5px; border-radius: 4px; border: 1px solid #ccc;">
                            <option value="horizontal" selected>水平</option>
                            <option value="vertical">垂直</option>
                        </select>
                    </div>
                    <div style="flex: 1; min-width: 200px;">
                        <label for="iterations">布局迭代次数 (32-128):</label>
                        <input type="number" id="iterations" value="64" min="32" max="128" step="8" style="width: 100%; padding: 8px; margin-top: 5px; border-radius: 4px; border: 1px solid #ccc;">
                    </div>
                </div>
                <div style="margin-top: 10px; display: flex; flex-wrap: wrap; gap: 15px;">
                    <div style="flex: 1; min-width: 200px;">
                        <label for="waveFilter">波次筛选 (多选):</label>
                        <select id="waveFilter" multiple style="width: 100%; padding: 8px; margin-top: 5px; border-radius: 4px; border: 1px solid #ccc; height: 100px;">
                        </select>
                    </div>
                    <div style="flex: 1; min-width: 200px;">
                        <label for="locationFilter">库位筛选 (多选):</label>
                        <select id="locationFilter" multiple style="width: 100%; padding: 8px; margin-top: 5px; border-radius: 4px; border: 1px solid #ccc; height: 100px;">
                        </select>
                    </div>
                </div>
                <div style="display: flex; justify-content: center; margin-top: 10px; gap: 15px;">
                    <button class="button" style="background: linear-gradient(45deg, #3498db, #2980b9);" onclick="updateLayout()">更新布局</button>
                    <button class="button" style="background: linear-gradient(45deg, #9b59b6, #8e44ad);" onclick="applyFilters()">应用筛选</button>
                    <button class="button" style="background: linear-gradient(45deg, #e74c3c, #c0392b);" onclick="resetFilters()">重置筛选</button>
                </div>
            </div>
        </div>
        <div id="metrics"></div>
        <div id="visualization"></div>
        <div id="tables"></div>
    </div>

    <script>
        // 全局变量
        let myChart = null;
        let originalSankeyData = null; // 存储原始数据
        
        // 示例数据
        const sampleData = {
            "emptyLocations": 1,
            "multiUseLocations": 0,
            "skuLevelData": {
                "allPackages": [{"packageId": 1, "requestNum": 10, "skuId": 1001}],
                "capacityByPriority": {1001: {0: 8}},
                "locationsByPriority": {1001: {0: 1}},
                "packageWaveMap": {"1": "WAVE_001"},
                "skuLocationsByPriority": {
                    1001: {
                        0: [{"availableQty": 8, "locationId": 1, "priority": 0, "skuId": 1001}]
                    }
                },
                "skuTotalDemand": {1001: 10},
                "totalCapacity": {1001: 8},
                "totalLocations": {1001: 1},
                "waveDemand": {"WAVE_001": 10},
                "waveLocations": {"WAVE_001": [1]},
                "wavePackages": {"WAVE_001": [1]}
            },
            "totalAllocated": 8,
            "waveInfoRespList": [{
                "packageInfoList": [{
                    "locationInfoList": [
                        {"locationId": 1, "packageId": 1, "allocatedQty": 8}
                    ],
                    "packageId": 1,
                    "status": "1"
                }],
                "totalAllocated": 8,
                "waveCode": "WAVE_001"
            }]
        };

        // 页面加载完成时加载示例数据
        window.onload = function() {
            document.getElementById('jsonInput').value = JSON.stringify(sampleData, null, 2);
            visualize();
        };

        // 主入口函数
        function visualize() {
            const jsonData = document.getElementById('jsonInput').value;
            if (!jsonData) {
                alert('请输入JSON数据');
                return;
            }
            try {
                // 修复 JSON 中的数字键问题
                const fixedJson = jsonData.replace(/([\{,]\s*)(\d+)(\s*:)/g, '$1"$2"$3');
                const data = JSON.parse(fixedJson);
                
                // 填充过滤选择器
                populateFilters(data);
                
                // 1. 统计卡片
                renderMetrics(data);
                // 2. ECharts Sankey图
                renderEChartsSankey(data);
                // 3. 表格
                renderTables(data);
            } catch (e) {
                alert('JSON格式错误或数据处理失败:' + e.message);
                console.error('JSON解析错误:', e);
                console.error('原始JSON:', jsonData);
            }
        }

        // 填充筛选选择器
        function populateFilters(data) {
            const waveFilter = document.getElementById('waveFilter');
            const locationFilter = document.getElementById('locationFilter');
            
            // 清空现有选项
            waveFilter.innerHTML = '';
            locationFilter.innerHTML = '';
            
            const skuData = data.skuLevelData || {};
            const waves = Object.keys(skuData.waveDemand || {});
            const locationSet = new Set();
            
            // 收集所有的库位ID
            for (const waveInfo of (data.waveInfoRespList || [])) {
                for (const pkg of (waveInfo.packageInfoList || [])) {
                    for (const loc of (pkg.locationInfoList || [])) {
                        locationSet.add(loc.locationId);
                    }
                }
            }
            
            // 按照数字大小排序的函数
            const sortNumeric = (a, b) => {
                // 提取数字部分
                const numA = parseInt(a.replace(/\D/g, '')) || 0;
                const numB = parseInt(b.replace(/\D/g, '')) || 0;
                return numA - numB;
            };
            
            // 添加波次选项
            waves.sort(sortNumeric).forEach(wave => {
                const option = document.createElement('option');
                option.value = wave;
                option.textContent = wave;
                waveFilter.appendChild(option);
            });
            
            // 添加库位选项
            Array.from(locationSet).sort((a, b) => a - b).forEach(locId => {
                const option = document.createElement('option');
                option.value = locId;
                option.textContent = `库位-${locId}`;
                locationFilter.appendChild(option);
            });
        }

        // 应用筛选
        function applyFilters() {
            const chartDom = document.getElementById('visualization');
            if (!myChart || !originalSankeyData) {
                return;
            }
            
            // 获取选中的波次和库位
            const waveFilter = document.getElementById('waveFilter');
            const locationFilter = document.getElementById('locationFilter');
            
            const selectedWaves = Array.from(waveFilter.selectedOptions).map(opt => opt.value);
            const selectedLocations = Array.from(locationFilter.selectedOptions).map(opt => parseInt(opt.value));
            
            // 如果没有选择任何筛选条件,则保留所有数据
            if (selectedWaves.length === 0 && selectedLocations.length === 0) {
                resetFilters();
                return;
            }
            
            // 深拷贝原始数据
            const { nodes, links } = JSON.parse(JSON.stringify(originalSankeyData));
            
            // 筛选链接
            const filteredLinks = links.filter(link => {
                // 波次筛选
                if (selectedWaves.length > 0) {
                    const isWaveSource = selectedWaves.some(wave => link.source.includes(`波次-${wave}`));
                    if (isWaveSource) {
                        return true;
                    }
                }
                
                // 库位筛选
                if (selectedLocations.length > 0) {
                    const isLocSource = selectedLocations.some(loc => link.source.includes(`库位-${loc}`));
                    const isLocTarget = selectedLocations.some(loc => link.target.includes(`库位-${loc}`));
                    if (isLocSource || isLocTarget) {
                        return true;
                    }
                }
                
                return selectedWaves.length === 0 && selectedLocations.length === 0;
            });
            
            // 获取过滤后的链接中涉及的所有节点名称
            const remainingNodeNames = new Set();
            filteredLinks.forEach(link => {
                remainingNodeNames.add(link.source);
                remainingNodeNames.add(link.target);
            });
            
            // 筛选节点
            const filteredNodes = nodes.filter(node => remainingNodeNames.has(node.name));
            
            // 更新图表
            myChart.setOption({
                series: [{
                    data: filteredNodes,
                    links: filteredLinks
                }]
            });
        }
        
        // 重置筛选
        function resetFilters() {
            // 清除选中状态
            document.getElementById('waveFilter').selectedIndex = -1;
            document.getElementById('locationFilter').selectedIndex = -1;
            
            // 如果有原始数据和图表,恢复完整数据
            if (originalSankeyData && myChart) {
                myChart.setOption({
                    series: [{
                        data: originalSankeyData.nodes,
                        links: originalSankeyData.links
                    }]
                });
            }
        }

        // 统计卡片渲染
        function renderMetrics(data) {
            const totalAllocated = data.totalAllocated || 0;
            const emptyLocations = data.emptyLocations || 0;
            const multiUseLocations = data.multiUseLocations || 0;
            
            const skuData = data.skuLevelData || {};
            const totalDemand = Object.values(skuData.skuTotalDemand || {}).reduce((a, b) => a + b, 0);
            const totalCapacity = Object.values(skuData.totalCapacity || {}).reduce((a, b) => a + b, 0);
            const totalLocations = Object.values(skuData.totalLocations || {}).reduce((a, b) => a + b, 0);
            const totalPackages = (skuData.allPackages || []).length;
            const totalWaves = Object.keys(skuData.waveDemand || {}).length;
            
            const allocationRate = totalDemand > 0 ? (totalAllocated / totalDemand * 100).toFixed(2) : 0;
            const capacityUtilization = totalCapacity > 0 ? (totalAllocated / totalCapacity * 100).toFixed(2) : 0;
            
            let html = `
                <div class="metrics">
                    <div class="metric success"><strong>📊 分配总量:</strong> ${totalAllocated}</div>
                    <div class="metric success"><strong>🎯 总需求:</strong> ${totalDemand}</div>
                    <div class="metric success"><strong>📈 分配率:</strong> ${allocationRate}%</div>
                    <div class="metric"><strong>📦 总容量:</strong> ${totalCapacity}</div>
                    <div class="metric"><strong>⚡ 容量利用率:</strong> ${capacityUtilization}%</div>
                    <div class="metric"><strong>🏢 总库位数:</strong> ${totalLocations}</div>
                    <div class="metric warning"><strong>🗂️ 清空库位数:</strong> ${emptyLocations}</div>
                    <div class="metric warning"><strong>🔄 多波次库位数:</strong> ${multiUseLocations}</div>
                    <div class="metric"><strong>📋 总包裹数:</strong> ${totalPackages}</div>
                    <div class="metric"><strong>🌊 总波次数:</strong> ${totalWaves}</div>
                </div>
            `;
            document.getElementById('metrics').innerHTML = html;
        }

        // ECharts Sankey图渲染
        function renderEChartsSankey(data) {
            const chartDom = document.getElementById('visualization');
            
            if (myChart) {
                myChart.dispose();
            }
            myChart = echarts.init(chartDom);

            // 构建节点和链接数据
            const { nodes, links } = buildSankeyData(data);
            
            // 保存原始数据
            originalSankeyData = { nodes: JSON.parse(JSON.stringify(nodes)), links: JSON.parse(JSON.stringify(links)) };
            
            // 获取用户布局参数
            const nodeAlign = document.getElementById('nodeAlign').value;
            const layoutOrientation = document.getElementById('layoutOrientation').value;
            const iterations = parseInt(document.getElementById('iterations').value) || 64;
            
            const option = {
                title: {
                    text: '库存分配流向图',
                    left: 'center',
                    textStyle: {
                        fontSize: 20,
                        fontWeight: 'bold',
                        color: '#2c3e50'
                    }
                },
                tooltip: {
                    trigger: 'item',
                    triggerOn: 'mousemove',
                    backgroundColor: 'rgba(50, 50, 50, 0.9)',
                    textStyle: {
                        color: '#fff'
                    },
                    formatter: function(params) {
                        if (params.dataType === 'node') {
                            return params.data.tooltip || params.data.name;
                        } else if (params.dataType === 'edge') {
                            return `${params.data.source}${params.data.target}<br/>分配量: ${params.data.value}`;
                        }
                    }
                },
                animation: true,
                animationDuration: 1000,
                animationEasing: 'cubicInOut',
                series: [{
                    type: 'sankey',
                    data: nodes,
                    links: links,
                    emphasis: {
                        focus: 'adjacency'
                    },
                    orient: layoutOrientation,
                    levels: [
                        {
                            depth: 0,
                            itemStyle: {
                                color: '#ff7f7f'
                            },
                            lineStyle: {
                                color: 'source',
                                opacity: 0.6
                            }
                        },
                        {
                            depth: 1,
                            itemStyle: {
                                color: '#87ceeb'
                            },
                            lineStyle: {
                                color: 'source',
                                opacity: 0.6
                            }
                        },
                        {
                            depth: 2,
                            itemStyle: {
                                color: '#98fb98'
                            },
                            lineStyle: {
                                color: 'source',
                                opacity: 0.6
                            }
                        }
                    ],
                    lineStyle: {
                        curveness: 0.5,
                        opacity: 0.7,
                        color: 'gradient'
                    },
                    itemStyle: {
                        borderWidth: 1,
                        borderColor: '#aaa',
                        borderType: 'solid'
                    },
                    label: {
                        fontSize: 12,
                        fontWeight: 'bold'
                    },
                    nodeAlign: nodeAlign,
                    nodeGap: 30,
                    nodeWidth: 25,
                    layoutIterations: iterations
                }]
            };

            myChart.setOption(option);
            
            // 响应式调整
            window.addEventListener('resize', function() {
                if (myChart) {
                    myChart.resize();
                }
            });
        }

        // 构建Sankey数据
        function buildSankeyData(data) {
            const nodes = [];
            const links = [];
            const nodeMap = new Map();
            
            const skuData = data.skuLevelData || {};
            const locationPriorityMap = new Map();
            const locationAvailableQtyMap = new Map();
            const waveDemandMap = new Map();
            const packageRequestMap = new Map();
            const packageWaveMap = new Map();
            
            // 预处理数据
            for (const [wave, demand] of Object.entries(skuData.waveDemand || {})) {
                waveDemandMap.set(wave, demand);
            }
            
            for (const pkg of (skuData.allPackages || [])) {
                packageRequestMap.set(pkg.packageId, pkg.requestNum);
            }
            
            for (const [pkgId, wave] of Object.entries(skuData.packageWaveMap || {})) {
                packageWaveMap.set(parseInt(pkgId), wave);
            }
            
            for (const [skuId, priorityMap] of Object.entries(skuData.skuLocationsByPriority || {})) {
                for (const [priority, locations] of Object.entries(priorityMap)) {
                    for (const loc of locations) {
                        locationPriorityMap.set(loc.locationId, parseInt(priority));
                        locationAvailableQtyMap.set(loc.locationId, loc.availableQty);
                    }
                }
            }

            // 添加节点的辅助函数
            function addNode(name, tooltip) {
                if (!nodeMap.has(name)) {
                    nodeMap.set(name, nodes.length);
                    nodes.push({ name: name, tooltip: tooltip });
                }
                return nodeMap.get(name);
            }

            // 构建节点和链接
            const waveLocLinks = new Map();
            const locPkgLinks = new Map();

            (data.waveInfoRespList || []).forEach(wave => {
                const waveLabel = `波次-${wave.waveCode}`;
                const waveDemand = waveDemandMap.get(wave.waveCode) || 0;
                const waveTooltip = `波次: ${wave.waveCode}<br/>需求量: ${waveDemand}`;
                addNode(waveLabel, waveTooltip);
                
                (wave.packageInfoList || []).forEach(pkg => {
                    (pkg.locationInfoList || []).forEach(loc => {
                        const locationLabel = `库位-${loc.locationId}`;
                        const priority = locationPriorityMap.get(loc.locationId) || -1;
                        const availableQty = locationAvailableQtyMap.get(loc.locationId) || 0;
                        const locationTooltip = `库位: ${loc.locationId}<br/>优先级: ${priority}<br/>可用量: ${availableQty}`;
                        addNode(locationLabel, locationTooltip);
                        
                        const packageLabel = `包裹-${loc.packageId}`;
                        const requestNum = packageRequestMap.get(loc.packageId) || 0;
                        const packageWave = packageWaveMap.get(loc.packageId);
                        const packageTooltip = `包裹: ${loc.packageId}<br/>需求量: ${requestNum}<br/>所属波次: ${packageWave}`;
                        addNode(packageLabel, packageTooltip);
                        
                        const qty = loc.allocatedQty || 0;
                        
                        // 波次到库位的链接
                        const waveIdx = nodeMap.get(waveLabel);
                        const locationIdx = nodeMap.get(locationLabel);
                        const waveLocKey = `${waveIdx}_${locationIdx}`;
                        waveLocLinks.set(waveLocKey, (waveLocLinks.get(waveLocKey) || 0) + qty);
                        
                        // 库位到包裹的链接
                        const packageIdx = nodeMap.get(packageLabel);
                        const locPkgKey = `${locationIdx}_${packageIdx}`;
                        locPkgLinks.set(locPkgKey, (locPkgLinks.get(locPkgKey) || 0) + qty);
                    });
                });
            });

            // 添加波次到库位的链接
            for (const [key, qty] of waveLocLinks.entries()) {
                const [src, tgt] = key.split('_').map(Number);
                links.push({
                    source: nodes[src].name,
                    target: nodes[tgt].name,
                    value: qty
                });
            }
            
            // 添加库位到包裹的链接
            for (const [key, qty] of locPkgLinks.entries()) {
                const [src, tgt] = key.split('_').map(Number);
                links.push({
                    source: nodes[src].name,
                    target: nodes[tgt].name,
                    value: qty
                });
            }

            // 优化节点顺序,减少边交叉
            sortSankey(nodes, links);

            return { nodes, links };
        }

        // 优化Sankey图节点顺序,减少边交叉
        function sortSankey(nodes, links) {
            // 按照节点类型和连接情况排序
            const nodeTypes = new Map();
            const nodeConnections = new Map();
            
            // 计算每个节点的连接数和类型
            nodes.forEach((node, index) => {
                const name = node.name;
                if (name.startsWith('波次-')) {
                    nodeTypes.set(name, 0); // 波次类型
                } else if (name.startsWith('库位-')) {
                    nodeTypes.set(name, 1); // 库位类型
                } else if (name.startsWith('包裹-')) {
                    nodeTypes.set(name, 2); // 包裹类型
                }
                
                nodeConnections.set(name, 0);
            });
            
            // 计算连接数
            links.forEach(link => {
                nodeConnections.set(link.source, (nodeConnections.get(link.source) || 0) + link.value);
                nodeConnections.set(link.target, (nodeConnections.get(link.target) || 0) + link.value);
            });
            
            // 按类型和连接数排序节点
            nodes.sort((a, b) => {
                const typeA = nodeTypes.get(a.name);
                const typeB = nodeTypes.get(b.name);
                
                // 首先按类型排序
                if (typeA !== typeB) {
                    return typeA - typeB;
                }
                
                // 同类型按连接数降序排序
                const connA = nodeConnections.get(a.name) || 0;
                const connB = nodeConnections.get(b.name) || 0;
                return connB - connA;
            });
        }

        // 表格渲染
        function renderTables(data) {
            const skuData = data.skuLevelData || {};
            let html = '';
            
            const locationPriorityMap = new Map();
            const locationAvailableQtyMap = new Map();
            
            for (const [skuId, priorityMap] of Object.entries(skuData.skuLocationsByPriority || {})) {
                for (const [priority, locations] of Object.entries(priorityMap)) {
                    for (const loc of locations) {
                        locationPriorityMap.set(loc.locationId, parseInt(priority));
                        locationAvailableQtyMap.set(loc.locationId, loc.availableQty);
                    }
                }
            }
            
            // 1. SKU级别统计表格
            html += `<h3>📈 SKU级别统计</h3>
                <table>
                    <tr>
                        <th>SKU ID</th>
                        <th>总需求</th>
                        <th>总容量</th>
                        <th>总库位数</th>
                        <th>分配率</th>
                        <th>优先级0库位数</th>
                        <th>优先级1库位数</th>
                    </tr>`;
            
            for (const [skuId, demand] of Object.entries(skuData.skuTotalDemand || {})) {
                const capacity = skuData.totalCapacity?.[skuId] || 0;
                const locations = skuData.totalLocations?.[skuId] || 0;
                const allocationRate = demand > 0 ? (capacity / demand * 100).toFixed(2) : 0;
                const priority0Locations = skuData.locationsByPriority?.[skuId]?.[0] || 0;
                const priority1Locations = skuData.locationsByPriority?.[skuId]?.[1] || 0;
                
                html += `<tr>
                    <td>${skuId}</td>
                    <td>${demand}</td>
                    <td>${capacity}</td>
                    <td>${locations}</td>
                    <td>${allocationRate}%</td>
                    <td>${priority0Locations}</td>
                    <td>${priority1Locations}</td>
                </tr>`;
            }
            html += `</table>`;
            
            // 2. 波次统计表格
            html += `<h3>🌊 波次统计</h3>
                <table>
                    <tr>
                        <th>波次</th>
                        <th>需求数量</th>
                        <th>包裹数量</th>
                        <th>使用库位数</th>
                    </tr>`;
            
            for (const [wave, demand] of Object.entries(skuData.waveDemand || {})) {
                const packages = skuData.wavePackages?.[wave]?.length || 0;
                const locations = skuData.waveLocations?.[wave]?.length || 0;
                
                html += `<tr>
                    <td>${wave}</td>
                    <td>${demand}</td>
                    <td>${packages}</td>
                    <td>${locations}</td>
                </tr>`;
            }
            html += `</table>`;
            
            // 3. 库位分配统计表格
            html += `<h3>🏢 库位分配统计</h3>
                <table>
                    <tr>
                        <th>库位ID</th>
                        <th>优先级</th>
                        <th>可用容量</th>
                        <th>分配总量</th>
                        <th>被使用次数</th>
                        <th>涉及包裹数</th>
                        <th>涉及波次数</th>
                        <th>是否多波次</th>
                    </tr>`;
            
            const locationStats = {};
            (data.waveInfoRespList || []).forEach(wave => {
                (wave.packageInfoList || []).forEach(pkg => {
                    (pkg.locationInfoList || []).forEach(loc => {
                        const locId = loc.locationId;
                        if (!locationStats[locId]) {
                            locationStats[locId] = {
                                total: 0,
                                count: 0,
                                packages: new Set(),
                                waves: new Set(),
                                priority: locationPriorityMap.get(locId) || -1,
                                availableQty: locationAvailableQtyMap.get(locId) || 0
                            };
                        }
                        locationStats[locId].total += (loc.allocatedQty || 0);
                        locationStats[locId].count += 1;
                        locationStats[locId].packages.add(loc.packageId);
                        locationStats[locId].waves.add(wave.waveCode);
                    });
                });
            });
            
            for (const locId in locationStats) {
                const stat = locationStats[locId];
                const isMultiWave = stat.waves.size > 1 ? '✅ 是' : '❌ 否';
                html += `<tr>
                    <td>${locId}</td>
                    <td>${stat.priority}</td>
                    <td>${stat.availableQty}</td>
                    <td>${stat.total}</td>
                    <td>${stat.count}</td>
                    <td>${stat.packages.size}</td>
                    <td>${stat.waves.size}</td>
                    <td>${isMultiWave}</td>
                </tr>`;
            }
            html += `</table>`;
            
            document.getElementById('tables').innerHTML = html;
        }

        function updateLayout() {
            const nodeAlign = document.getElementById('nodeAlign').value;
            const layoutOrientation = document.getElementById('layoutOrientation').value;
            const iterations = document.getElementById('iterations').value;

            const chartDom = document.getElementById('visualization');
            const chart = echarts.getInstanceByDom(chartDom);

            if (chart) {
                chart.setOption({
                    series: [{
                        type: 'sankey',
                        data: chart.getOption().series[0].data,
                        links: chart.getOption().series[0].links,
                        emphasis: {
                            focus: 'adjacency'
                        },
                        orient: layoutOrientation,
                        levels: [
                            {
                                depth: 0,
                                itemStyle: {
                                    color: '#ff7f7f'
                                },
                                lineStyle: {
                                    color: 'source',
                                    opacity: 0.6
                                }
                            },
                            {
                                depth: 1,
                                itemStyle: {
                                    color: '#87ceeb'
                                },
                                lineStyle: {
                                    color: 'source',
                                    opacity: 0.6
                                }
                            },
                            {
                                depth: 2,
                                itemStyle: {
                                    color: '#98fb98'
                                },
                                lineStyle: {
                                    color: 'source',
                                    opacity: 0.6
                                }
                            }
                        ],
                        lineStyle: {
                            curveness: 0.5,
                            opacity: 0.7,
                            color: 'gradient'
                        },
                        itemStyle: {
                            borderWidth: 1,
                            borderColor: '#aaa',
                            borderType: 'solid'
                        },
                        label: {
                            fontSize: 12,
                            fontWeight: 'bold'
                        },
                        nodeAlign: nodeAlign,
                        nodeGap: 30,
                        nodeWidth: 25,
                        layoutIterations: iterations
                    }]
                });
            }
        }
    </script>
</body>
</html>
{"emptyLocations":0,"multiUseLocations":1,"skuLevelData":{"allPackages":[{"packageId":1,"requestNum":4,"skuId":816324},{"packageId":2,"requestNum":4,"skuId":816324},{"packageId":3,"requestNum":4,"skuId":816324},{"packageId":4,"requestNum":4,"skuId":816324},{"packageId":5,"requestNum":4,"skuId":816324},{"packageId":57,"requestNum":4,"skuId":816324},{"packageId":58,"requestNum":4,"skuId":816324},{"packageId":59,"requestNum":4,"skuId":816324},{"packageId":60,"requestNum":4,"skuId":816324},{"packageId":61,"requestNum":4,"skuId":816324},{"packageId":113,"requestNum":4,"skuId":816324},{"packageId":114,"requestNum":4,"skuId":816324},{"packageId":115,"requestNum":4,"skuId":816324},{"packageId":116,"requestNum":4,"skuId":816324},{"packageId":117,"requestNum":4,"skuId":816324},{"packageId":118,"requestNum":4,"skuId":816324},{"packageId":119,"requestNum":4,"skuId":816324},{"packageId":120,"requestNum":4,"skuId":816324},{"packageId":121,"requestNum":4,"skuId":816324},{"packageId":122,"requestNum":4,"skuId":816324}],"capacityByPriority":{816324:{0:104,1:224}},"locationsByPriority":{816324:{0:1,1:2}},"packageWaveMap":{1:"WAVE_001",2:"WAVE_001",3:"WAVE_001",4:"WAVE_001",5:"WAVE_001",113:"WAVE_003",114:"WAVE_003",115:"WAVE_003",116:"WAVE_003",117:"WAVE_003",118:"WAVE_003",119:"WAVE_003",120:"WAVE_003",57:"WAVE_002",121:"WAVE_003",58:"WAVE_002",122:"WAVE_003",59:"WAVE_002",60:"WAVE_002",61:"WAVE_002"},"skuLocationsByPriority":{816324:{0:[{"availableQty":104,"locationId":1,"priority":0,"skuId":816324}],1:[{"availableQty":112,"locationId":2,"priority":1,"skuId":816324},{"availableQty":112,"locationId":3,"priority":1,"skuId":816324}]}},"skuTotalDemand":{816324:80},"totalCapacity":{816324:328},"totalLocations":{816324:3},"waveDemand":{"WAVE_002":20,"WAVE_003":40,"WAVE_001":20},"waveLocations":{"WAVE_002":[1,2,3],"WAVE_003":[1,2,3],"WAVE_001":[1,2,3]},"wavePackages":{"WAVE_002":[57,58,59,60,61],"WAVE_003":[113,114,115,116,117,118,119,120,121,122],"WAVE_001":[1,2,3,4,5]}},"totalAllocated":80,"waveInfoRespList":[{"packageInfoList":[{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":1,"skuId":816324}],"packageId":1,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":2,"skuId":816324}],"packageId":2,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":3,"skuId":816324}],"packageId":3,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":4,"skuId":816324}],"packageId":4,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":5,"skuId":816324}],"packageId":5,"status":"1"}],"totalAllocated":20,"waveCode":"WAVE_001"},{"packageInfoList":[{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":57,"skuId":816324}],"packageId":57,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":58,"skuId":816324}],"packageId":58,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":59,"skuId":816324}],"packageId":59,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":60,"skuId":816324}],"packageId":60,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":61,"skuId":816324}],"packageId":61,"status":"1"}],"totalAllocated":20,"waveCode":"WAVE_002"},{"packageInfoList":[{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":113,"skuId":816324}],"packageId":113,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":114,"skuId":816324}],"packageId":114,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":115,"skuId":816324}],"packageId":115,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":116,"skuId":816324}],"packageId":116,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":117,"skuId":816324}],"packageId":117,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":118,"skuId":816324}],"packageId":118,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":119,"skuId":816324}],"packageId":119,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":120,"skuId":816324}],"packageId":120,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":121,"skuId":816324}],"packageId":121,"status":"1"},{"locationInfoList":[{"allocatedQty":4,"locationId":1,"packageId":122,"skuId":816324}],"packageId":122,"status":"1"}],"totalAllocated":40,"waveCode":"WAVE_003"}]}

Current Behavior

Image

Expected Behavior

Image

Environment

- OS: OSX
- Browser: Chrome
- Framework: CDN use with htm&js

Any additional comments?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugenThis issue is in EnglishpendingWe are not sure about whether this is a bug/new feature.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions