首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >长安荔枝滞销?不,是你没装「荔枝使」专属外挂!

长安荔枝滞销?不,是你没装「荔枝使」专属外挂!

原创
作者头像
Jimaks
发布2025-06-23 16:38:03
发布2025-06-23 16:38:03
39720
代码可运行
举报
文章被收录于专栏:技术杂烩技术杂烩
运行总次数:0
代码可运行

背景:

最近追《长安的荔枝》追得上头,看着李善德抱着账本算路线算到秃头,恨不得隔空扔给他一台笔记本电脑 —— 您瞧,当年要是有咱这「荔枝运输智能规划系统」,哪儿还需要在岭南烈日下暴走?直接坐在长安城胡商的酒肆里,喝着葡萄酿就把路线优化了!

想当年,圣人一句「要吃岭南鲜荔枝」,可把李善德坑惨了:马不停蹄赶路,算错一步荔枝就变「荔干」,还要防着沿途驿站使绊子。但咱现代人不一样啊!咱有「荔枝使」专属可视化工具:选起点「岭南(深圳)」,终点「长安(西安)」,调个「时间优先」权重拉满,再把可能被节度使截胡的路段设为「阻断」—— 滴,算法跑完,最优路线直接在地图上闪金光,比李善德手绘的羊皮卷靠谱十倍!

您看这城市数据里的「深圳→广州→韶关→长沙→武汉→洛阳→西安」路线,放古代得靠驿站换马接力,现在用 Dijkstra 算法一算,总时间、总费用一目了然。要是李善德有这工具,怕是能在长安城开连锁荔枝铺,顺便给杨贵妃搞个「荔枝订阅制」,妥妥的大唐商业鬼才!

工具设想

设计一个城市路线可视化工具,可以帮助快速查找城市之间的最优路线,并以可视化的方式呈现。可以选择起点和终点城市,设置优化目标(时间优先或费用优先)和权重,还能添加路线阻断信息。工具会实时计算并展示最优路线、总运输时间和费用,同时提供时间分析和费用分布的图表。

咱这工具可不是闹着玩的,核心就俩字:「智能」。就像李善德得研究每段路的马速、驿站距离,咱的算法直接把「时间」和「费用」当砝码:

Dijkstra 算法:相当于给每个城市节点装了「荔枝保鲜计时器」,优先选耗时最短的路线,比李善德拿算盘算三天三夜还准;

可视化地图:把古代驿道换成 SVG 线条,高亮的最优路线用橙色标出来,像不像给荔枝铺了条「黄金传送带」?阻断的路段直接标红,比遇到劫匪还醒目;

权重调节:想省点运费?把「费用优先」勾上,算法自动找性价比高的路,比跟驿站老板砍价还管用。

最绝的是图表分析:时间分布柱状图一看,就知道长沙到武汉那段路最费时间;费用饼图一画,驿站马料钱占大头 —— 放古代,这数据能让户部尚书都惊掉官帽:「原来运荔枝的钱都花在这儿了!」

工具最终效果:

技术架构

前端框架与库

  • HTML/CSS:构建页面结构和样式,使用 Tailwind CSS 进行快速布局和样式设计。
  • JavaScript:实现交互逻辑和算法计算,使用 Chart.js 库生成时间分析和费用分布的图表。
  • Font Awesome:提供图标支持,增强界面的视觉效果。

算法实现

  • Dijkstra 算法:用于查找最短路径,根据用户设置的时间和费用权重计算最优路线。

代码实现细节

1. 城市数据定义

代码语言:javascript
代码运行次数:0
运行
复制
// 城市坐标数据
const cityCoordinates = {
    '深圳': { x: 100, y: 400 },
    '广州': { x: 200, y: 350 },
    '东莞': { x: 150, y: 370 },
    '惠州': { x: 200, y: 420 },
    '韶关': { x: 300, y: 300 },
    '长沙': { x: 400, y: 320 },
    '武汉': { x: 500, y: 350 },
    '郑州': { x: 550, y: 250 },
    '洛阳': { x: 600, y: 200 },
    '西安': { x: 700, y: 150 }
};

// 城市图数据(时间和费用)
const cityGraph = {
    '深圳': { '广州': { time: 1.5, cost: 200 }, '东莞': { time: 1.0, cost: 150 } },
    '广州': { '深圳': { time: 1.5, cost: 200 }, '韶关': { time: 2.5, cost: 300 }, '长沙': { time: 5.5, cost: 500 } },
    '东莞': { '深圳': { time: 1.0, cost: 150 }, '惠州': { time: 1.2, cost: 180 } },
    '惠州': { '东莞': { time: 1.2, cost: 180 }, '武汉': { time: 8.0, cost: 800 } },
    '韶关': { '广州': { time: 2.5, cost: 300 }, '长沙': { time: 4.0, cost: 450 } },
    '长沙': { '韶关': { time: 4.0, cost: 450 }, '武汉': { time: 3.0, cost: 350 }, '郑州': { time: 8.0, cost: 700 } },
    '武汉': { '惠州': { time: 8.0, cost: 800 }, '长沙': { time: 3.0, cost: 350 }, '郑州': { time: 4.5, cost: 500 }, '西安': { time: 10.0, cost: 900 } },
    '郑州': { '长沙': { time: 8.0, cost: 700 }, '武汉': { time: 4.5, cost: 500 }, '洛阳': { time: 2.0, cost: 250 } },
    '洛阳': { '郑州': { time: 2.0, cost: 250 }, '西安': { time: 5.0, cost: 600 } },
    '西安': { '武汉': { time: 10.0, cost: 900 }, '洛阳': { time: 5.0, cost: 600 } }
};

2. 地图初始化

代码语言:javascript
代码运行次数:0
运行
复制
function initMap() {
    const svg = document.getElementById('city-map');
    
    // 清空地图
    while (svg.lastChild) {
        if (svg.lastChild.tagName === 'defs') {
            break;
        }
        svg.removeChild(svg.lastChild);
    }
    
    // 添加图例
    const legend = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    legend.setAttribute('transform', 'translate(20, 20)');
    svg.appendChild(legend);
    
    // 绘制所有城市节点和路线
    Object.entries(cityCoordinates).forEach(([city, { x, y }]) => {
        // 创建城市节点
        const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        node.setAttribute('cx', x);
        node.setAttribute('cy', y);
        node.setAttribute('r', '12');
        node.setAttribute('fill', '#165DFF');
        node.setAttribute('class', 'node');
        node.setAttribute('data-city', city);
        svg.appendChild(node);
        
        // 创建城市标签
        const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        label.setAttribute('x', x);
        label.setAttribute('y', y);
        label.setAttribute('text-anchor', 'middle');
        label.setAttribute('dominant-baseline', 'middle');
        label.setAttribute('fill', 'white');
        label.setAttribute('class', 'node-label');
        label.textContent = city;
        svg.appendChild(label);
    });
    
    Object.entries(cityGraph).forEach(([fromCity, neighbors]) => {
        const fromCoord = cityCoordinates[fromCity];
        
        Object.entries(neighbors).forEach(([toCity, { time, cost }]) => {
            if (fromCity < toCity) {
                const toCoord = cityCoordinates[toCity];
                
                // 计算路线中点
                const midX = (fromCoord.x + toCoord.x) / 2;
                const midY = (fromCoord.y + toCoord.y) / 2;
                
                // 创建路线
                const edge = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                edge.setAttribute('x1', fromCoord.x);
                edge.setAttribute('y1', fromCoord.y);
                edge.setAttribute('x2', toCoord.x);
                edge.setAttribute('y2', toCoord.y);
                edge.setAttribute('stroke', '#165DFF');
                edge.setAttribute('stroke-width', '2');
                edge.setAttribute('marker-end', 'url(#arrowhead)');
                edge.setAttribute('class', 'edge');
                edge.setAttribute('data-from', fromCity);
                edge.setAttribute('data-to', toCity);
                svg.appendChild(edge);
                
                // 添加距离标签
                const distanceLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                distanceLabel.setAttribute('x', midX);
                distanceLabel.setAttribute('y', midY);
                distanceLabel.setAttribute('text-anchor', 'middle');
                distanceLabel.setAttribute('dominant-baseline', 'middle');
                distanceLabel.setAttribute('class', 'distance-label');
                distanceLabel.textContent = `${time}h, ¥${cost}`;
                svg.appendChild(distanceLabel);
            }
        });
    });
}

3. Dijkstra 算法实现

代码语言:javascript
代码运行次数:0
运行
复制
function findShortestPath(start, end, blockedEdges = [], timeWeight = 70) {
    const distances = {};
    const previousNodes = {};
    const priorityQueue = [];
    
    // 初始化距离和前一个节点
    Object.keys(cityGraph).forEach(city => {
        distances[city] = Infinity;
        previousNodes[city] = null;
    });
    
    distances[start] = 0;
    priorityQueue.push({ distance: 0, city: start, time: 0, cost: 0, path: [start] });
    
    while (priorityQueue.length > 0) {
        // 找到距离最小的节点
        priorityQueue.sort((a, b) => a.distance - b.distance);
        const { city, time, cost, path } = priorityQueue.shift();
        
        // 如果到达终点,返回路径
        if (city === end) {
            return { path, totalTime: time, totalCost: cost };
        }
        
        // 跳过已经处理过的节点
        if (city !== start && distances[city] < time * (timeWeight/100) + cost * (1 - timeWeight/100)) {
            continue;
        }
        
        // 处理所有邻居
        for (const [neighbor, { time: edgeTime, cost: edgeCost }] of Object.entries(cityGraph[city])) {
            // 检查路线是否被阻断
            const isBlocked = blockedEdges.some(edge => 
                (edge[0] === city && edge[1] === neighbor) || 
                (edge[0] === neighbor && edge[1] === city)
            );
            
            if (isBlocked) {
                continue;
            }
            
            // 计算新的加权距离
            const newDistance = time * (timeWeight/100) + cost * (1 - timeWeight/100) + 
                               edgeTime * (timeWeight/100) + edgeCost * (1 - timeWeight/100);
            
            // 如果新路径更短,则更新
            if (newDistance < distances[neighbor]) {
                distances[neighbor] = newDistance;
                const newTime = time + edgeTime;
                const newCost = cost + edgeCost;
                const newPath = [...path, neighbor];
                
                priorityQueue.push({ 
                    distance: newDistance, 
                    city: neighbor, 
                    time: newTime, 
                    cost: newCost, 
                    path: newPath 
                });
            }
        }
    }
    
    // 如果没有找到路径
    return { path: [], totalTime: Infinity, totalCost: Infinity };
}

4. 路线高亮与信息更新

代码语言:javascript
代码运行次数:0
运行
复制
// 高亮显示最优路线
function highlightRoute(path) {
    // 移除之前的高亮
    document.querySelectorAll('.route-edge').forEach(el => {
        el.classList.remove('route-edge');
        el.setAttribute('stroke', '#165DFF');
        el.setAttribute('stroke-width', '2');
        el.setAttribute('marker-end', 'url(#arrowhead)');
    });
    
    // 如果没有路径,直接返回
    if (path.length <= 1) {
        return;
    }
    
    // 高亮新路径
    for (let i = 0; i < path.length - 1; i++) {
        const fromCity = path[i];
        const toCity = path[i + 1];
        
        // 找到对应的边并高亮
        const edge = document.querySelector(`.edge[data-from="${fromCity}"][data-to="${toCity}"]`) ||
                     document.querySelector(`.edge[data-from="${toCity}"][data-to="${fromCity}"]`);
        
        if (edge) {
            edge.classList.add('route-edge');
            edge.setAttribute('stroke', '#F59E0B');
            edge.setAttribute('stroke-width', '4');
            edge.setAttribute('marker-end', 'url(#arrowhead-route)');
        }
    }
}

// 更新路径信息
function updatePathInfo(path, totalTime, totalCost) {
    const pathInfo = document.getElementById('path-info');
    const timeInfo = document.getElementById('time-info');
    const costInfo = document.getElementById('cost-info');
    
    if (path.length === 0) {
        pathInfo.textContent = '没有找到可行路径';
        timeInfo.textContent = '';
        costInfo.textContent = '';
        return;
    }
    
    pathInfo.textContent = `最优路径: ${path.join(' → ')}`;
    timeInfo.textContent = `总运输时间: ${totalTime.toFixed(1)} 小时`;
    costInfo.textContent = `总运输费用: ¥${totalCost.toFixed(0)}`;
    
    // 更新图表
    updateCharts(path, totalTime, totalCost);
}

交互功能

  • 控制面板:用户可以选择起点和终点城市、优化目标、设置时间和费用权重,简直是「懒人福音」:选城市不用翻地图,下拉框一点就行;阻断路线?点两下鼠标,比写加急文书还快。
  • 地图操作:支持缩放和重置视图,方便用户查看地图。
  • 结果展示:实时显示最优路线、总运输时间和费用,并通过柱状图和饼图展示时间分析和费用分布。当最优路线算出来,地图上的线条「唰」地变成橙色,旁边跳出「总时间 XX 小时,总费用 ¥XXX」—— 想象一下李善德要是能看到这行字,估计会把算盘往地上一摔:「早有这玩意儿,我何苦在岭南晒成黑炭!」

从「一骑红尘」到「一键红尘」

看完《长安的荔枝》最大的感慨就是:古代人运荔枝靠命硬,现代人运荔枝靠聪明。咱这工具虽不能真把荔枝穿越时空送到长安,但从技术逻辑上,可是圆了李善德的「荔枝保鲜梦」:

  • 用算法代替脚力,省去翻山越岭的苦;
  • 用数据代替猜测,再也不怕算错驿站间距;
  • 用可视化代替账本,连杨贵妃都能看懂「这荔枝为啥这么贵」。

要是把这工具打包送给剧中的李善德,说不定剧情会变成这样:他喝着茶在屏幕上点几点,最优路线出来了;随手调调权重,运输成本降了;最后生成图表递给圣人 —— 得,不仅不用被贬,说不定还能升职做「大唐物流总监」,顺便给荔枝申请个「非物质文化遗产冷链运输」称号!

所以说啊,技术才是最强「荔枝使」—— 毕竟,能让荔枝从岭南到长安保持新鲜的,除了杨贵妃的美貌,还有咱程序员的智慧呀~

附完整代码:

代码语言:html
复制
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>城市路线可视化工具</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
    
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#165DFF',
                        secondary: '#36D399',
                        accent: '#F59E0B',
                        dark: '#1E293B',
                        light: '#F8FAFC'
                    },
                    fontFamily: {
                        inter: ['Inter', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .node {
                @apply transition-all duration-300 cursor-pointer;
            }
            .node:hover {
                @apply scale-110;
            }
            .edge {
                @apply transition-all duration-300;
            }
            .route-edge {
                @apply stroke-accent stroke-4;
            }
            .node-label {
                @apply font-semibold text-dark pointer-events-none;
            }
            .distance-label {
                @apply text-xs text-gray-500 pointer-events-none;
            }
            .control-panel {
                @apply bg-white/90 backdrop-blur-sm rounded-xl shadow-lg p-4 transition-all duration-300;
            }
            .btn {
                @apply px-4 py-2 rounded-lg font-medium transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2;
            }
            .btn-primary {
                @apply bg-primary text-white hover:bg-primary/90 focus:ring-primary/50;
            }
            .btn-secondary {
                @apply bg-secondary text-white hover:bg-secondary/90 focus:ring-secondary/50;
            }
            .btn-accent {
                @apply bg-accent text-white hover:bg-accent/90 focus:ring-accent/50;
            }
            .btn-outline {
                @apply border border-gray-300 hover:bg-gray-100 focus:ring-gray-200;
            }
            .input-field {
                @apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-200;
            }
            .select-field {
                @apply w-full px-3 py-2 border border-gray-300 rounded-lg appearance-none bg-white focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-200;
            }
            .form-group {
                @apply mb-4;
            }
            .result-card {
                @apply bg-white rounded-xl shadow-md p-4 transition-all duration-300 hover:shadow-lg;
            }
            .result-title {
                @apply font-bold text-lg mb-2;
            }
            .result-content {
                @apply text-gray-700;
            }
            .blocked-route {
                @apply stroke-red-500 stroke-dashed;
            }
        }
    </style>
</head>
<body class="font-inter bg-gradient-to-br from-light to-gray-100 min-h-screen">
    <header class="bg-primary text-white shadow-md">
        <div class="container mx-auto px-4 py-6">
            <h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold flex items-center">
                <i class="fa fa-map-marker mr-3"></i>城市路线可视化工具
                <span class="ml-3 text-sm font-normal bg-white/20 px-2 py-1 rounded">Beta</span>
            </h1>
            <p class="text-white/80 mt-2">使用Dijkstra算法查找最优路线并可视化展示</p>
        </div>
    </header>

    <main class="container mx-auto px-4 py-8">
        <div class="flex flex-col lg:flex-row gap-8">
            <!-- 控制面板 -->
            <div class="lg:w-1/4">
                <div class="control-panel sticky top-4">
                    <h2 class="text-xl font-bold mb-4 flex items-center">
                        <i class="fa fa-sliders mr-2"></i>路径控制
                    </h2>
                    
                    <div class="form-group">
                        <label for="start-city" class="block text-sm font-medium text-gray-700 mb-1">起点城市</label>
                        <select id="start-city" class="select-field">
                            <option value="深圳">深圳</option>
                            <option value="广州">广州</option>
                            <option value="东莞">东莞</option>
                            <option value="惠州">惠州</option>
                            <option value="韶关">韶关</option>
                            <option value="长沙">长沙</option>
                            <option value="武汉">武汉</option>
                            <option value="郑州">郑州</option>
                            <option value="洛阳">洛阳</option>
                            <option value="西安">西安</option>
                        </select>
                    </div>
                    
                    <div class="form-group">
                        <label for="end-city" class="block text-sm font-medium text-gray-700 mb-1">终点城市</label>
                        <select id="end-city" class="select-field">
                            <option value="深圳">深圳</option>
                            <option value="广州">广州</option>
                            <option value="东莞">东莞</option>
                            <option value="惠州">惠州</option>
                            <option value="韶关">韶关</option>
                            <option value="长沙">长沙</option>
                            <option value="武汉">武汉</option>
                            <option value="郑州">郑州</option>
                            <option value="洛阳">洛阳</option>
                            <option value="西安">西安</option>
                        </select>
                    </div>
                    
                    <div class="form-group">
                        <label class="block text-sm font-medium text-gray-700 mb-1">优化目标</label>
                        <div class="flex items-center space-x-4">
                            <label class="inline-flex items-center">
                                <input type="radio" name="optimization" value="time" class="form-radio text-primary h-4 w-4" checked>
                                <span class="ml-2">时间优先</span>
                            </label>
                            <label class="inline-flex items-center">
                                <input type="radio" name="optimization" value="cost" class="form-radio text-primary h-4 w-4">
                                <span class="ml-2">费用优先</span>
                            </label>
                        </div>
                    </div>
                    
                    <div class="form-group">
                        <label class="block text-sm font-medium text-gray-700 mb-1">权重设置</label>
                        <div class="flex items-center">
                            <span class="text-xs text-gray-500">时间</span>
                            <input type="range" id="time-weight" min="0" max="100" value="70" class="w-full mx-2 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary">
                            <span class="text-xs text-gray-500">费用</span>
                        </div>
                        <div class="flex justify-between text-xs text-gray-500">
                            <span>0%</span>
                            <span id="weight-value">70% : 30%</span>
                            <span>100%</span>
                        </div>
                    </div>
                    
                    <div class="form-group">
                        <label class="block text-sm font-medium text-gray-700 mb-1">路线阻断</label>
                        <div id="blocked-routes" class="space-y-2 max-h-40 overflow-y-auto p-2 border border-gray-200 rounded-lg">
                            <!-- 动态生成的阻断路线选项 -->
                        </div>
                        <button id="add-blocked-route" class="btn btn-outline text-sm w-full mt-2 flex items-center justify-center">
                            <i class="fa fa-plus mr-1"></i> 添加阻断路线
                        </button>
                    </div>
                    
                    <button id="find-route" class="btn btn-primary w-full flex items-center justify-center">
                        <i class="fa fa-search mr-2"></i> 查找最优路线
                    </button>
                </div>
                
                <div class="result-card mt-6">
                    <h3 class="result-title flex items-center">
                        <i class="fa fa-info-circle mr-2"></i> 路径信息
                    </h3>
                    <div class="result-content">
                        <p id="path-info">请选择起点和终点城市,然后点击"查找最优路线"</p>
                        <p id="time-info" class="text-sm text-gray-500 mt-1"></p>
                        <p id="cost-info" class="text-sm text-gray-500"></p>
                    </div>
                </div>
            </div>
            
            <!-- 地图可视化区域 -->
            <div class="lg:w-3/4">
                <div class="bg-white rounded-xl shadow-lg p-4 h-[600px] relative overflow-hidden">
                    <div class="absolute top-4 right-4 z-10 flex space-x-2">
                        <button id="zoom-in" class="btn btn-outline p-2">
                            <i class="fa fa-search-plus"></i>
                        </button>
                        <button id="zoom-out" class="btn btn-outline p-2">
                            <i class="fa fa-search-minus"></i>
                        </button>
                        <button id="reset-view" class="btn btn-outline p-2">
                            <i class="fa fa-refresh"></i>
                        </button>
                    </div>
                    
                    <h2 class="text-xl font-bold mb-4 flex items-center">
                        <i class="fa fa-map mr-2"></i>城市路线图
                    </h2>
                    
                    <div class="relative h-[calc(100%-2rem)]">
                        <svg id="city-map" viewBox="0 0 800 500" class="w-full h-full cursor-move">
                            <!-- 城市节点和路线将通过JavaScript动态生成 -->
                            <defs>
                                <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
                                    <polygon points="0 0, 10 3.5, 0 7" fill="#165DFF"/>
                                </marker>
                                <marker id="arrowhead-route" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
                                    <polygon points="0 0, 10 3.5, 0 7" fill="#F59E0B"/>
                                </marker>
                                <marker id="arrowhead-blocked" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
                                    <polygon points="0 0, 10 3.5, 0 7" fill="#EF4444"/>
                                </marker>
                            </defs>
                            
                            <!-- 图例 -->
                            <g transform="translate(20, 20)">
                                <text x="0" y="0" class="text-sm font-medium">图例:</text>
                                
                                <line x1="0" y1="20" x2="30" y2="20" stroke="#165DFF" stroke-width="2" marker-end="url(#arrowhead)"></line>
                                <text x="40" y="24" class="text-xs">常规路线</text>
                                
                                <line x1="0" y1="40" x2="30" y2="40" stroke="#F59E0B" stroke-width="4" marker-end="url(#arrowhead-route)"></line>
                                <text x="40" y="44" class="text-xs">最优路线</text>
                                
                                <line x1="0" y1="60" x2="30" y2="60" stroke="#EF4444" stroke-width="2" stroke-dasharray="5,3" marker-end="url(#arrowhead-blocked)"></line>
                                <text x="40" y="64" class="text-xs">阻断路线</text>
                            </g>
                        </svg>
                    </div>
                </div>
                
                <div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
                    <div class="bg-white rounded-xl shadow-lg p-4">
                        <h3 class="text-lg font-bold mb-3 flex items-center">
                            <i class="fa fa-bar-chart mr-2"></i> 时间分析
                        </h3>
                        <canvas id="time-chart" height="200"></canvas>
                    </div>
                    
                    <div class="bg-white rounded-xl shadow-lg p-4">
                        <h3 class="text-lg font-bold mb-3 flex items-center">
                            <i class="fa fa-pie-chart mr-2"></i> 费用分布
                        </h3>
                        <canvas id="cost-chart" height="200"></canvas>
                    </div>
                </div>
            </div>
        </div>
    </main>

    <footer class="bg-dark text-white mt-12">
        <div class="container mx-auto px-4 py-6">
            <div class="flex flex-col md:flex-row justify-between items-center">
                <div class="mb-4 md:mb-0">
                    <h3 class="text-lg font-bold">城市路线可视化工具</h3>
                    <p class="text-gray-400 text-sm mt-1">使用Dijkstra算法查找最优路线</p>
                </div>
                <div class="flex space-x-4">
                    <a href="#" class="text-gray-400 hover:text-white transition-colors">
                        <i class="fa fa-github text-xl"></i>
                    </a>
                    <a href="#" class="text-gray-400 hover:text-white transition-colors">
                        <i class="fa fa-twitter text-xl"></i>
                    </a>
                    <a href="#" class="text-gray-400 hover:text-white transition-colors">
                        <i class="fa fa-linkedin text-xl"></i>
                    </a>
                </div>
            </div>
            <div class="border-t border-gray-700 mt-6 pt-6 text-center text-gray-400 text-sm">
                &copy; 2025 城市路线可视化工具 | 由豆包编程助手提供技术支持
            </div>
        </div>
    </footer>

    <script>
        // 城市坐标数据
        const cityCoordinates = {
            '深圳': { x: 100, y: 400 },
            '广州': { x: 200, y: 350 },
            '东莞': { x: 150, y: 370 },
            '惠州': { x: 200, y: 420 },
            '韶关': { x: 300, y: 300 },
            '长沙': { x: 400, y: 320 },
            '武汉': { x: 500, y: 350 },
            '郑州': { x: 550, y: 250 },
            '洛阳': { x: 600, y: 200 },
            '西安': { x: 700, y: 150 }
        };
        
        // 城市图数据(时间和费用)
        const cityGraph = {
            '深圳': { '广州': { time: 1.5, cost: 200 }, '东莞': { time: 1.0, cost: 150 } },
            '广州': { '深圳': { time: 1.5, cost: 200 }, '韶关': { time: 2.5, cost: 300 }, '长沙': { time: 5.5, cost: 500 } },
            '东莞': { '深圳': { time: 1.0, cost: 150 }, '惠州': { time: 1.2, cost: 180 } },
            '惠州': { '东莞': { time: 1.2, cost: 180 }, '武汉': { time: 8.0, cost: 800 } },
            '韶关': { '广州': { time: 2.5, cost: 300 }, '长沙': { time: 4.0, cost: 450 } },
            '长沙': { '韶关': { time: 4.0, cost: 450 }, '武汉': { time: 3.0, cost: 350 }, '郑州': { time: 8.0, cost: 700 } },
            '武汉': { '惠州': { time: 8.0, cost: 800 }, '长沙': { time: 3.0, cost: 350 }, '郑州': { time: 4.5, cost: 500 }, '西安': { time: 10.0, cost: 900 } },
            '郑州': { '长沙': { time: 8.0, cost: 700 }, '武汉': { time: 4.5, cost: 500 }, '洛阳': { time: 2.0, cost: 250 } },
            '洛阳': { '郑州': { time: 2.0, cost: 250 }, '西安': { time: 5.0, cost: 600 } },
            '西安': { '武汉': { time: 10.0, cost: 900 }, '洛阳': { time: 5.0, cost: 600 } }
        };
        
        // 初始化地图
        function initMap() {
            const svg = document.getElementById('city-map');
            
            // 清空地图
            while (svg.lastChild) {
                if (svg.lastChild.tagName === 'defs') {
                    break;
                }
                svg.removeChild(svg.lastChild);
            }
            
            // 添加图例
            const legend = document.createElementNS('http://www.w3.org/2000/svg', 'g');
            legend.setAttribute('transform', 'translate(20, 20)');
            svg.appendChild(legend);
            
            // 添加图例文本
            const legendText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
            legendText.setAttribute('x', '0');
            legendText.setAttribute('y', '0');
            legendText.setAttribute('class', 'text-sm font-medium');
            legendText.textContent = '图例:';
            legend.appendChild(legendText);
            
            // 添加图例项 - 常规路线
            const normalLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
            normalLine.setAttribute('x1', '0');
            normalLine.setAttribute('y1', '20');
            normalLine.setAttribute('x2', '30');
            normalLine.setAttribute('y2', '20');
            normalLine.setAttribute('stroke', '#165DFF');
            normalLine.setAttribute('stroke-width', '2');
            normalLine.setAttribute('marker-end', 'url(#arrowhead)');
            legend.appendChild(normalLine);
            
            const normalLineText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
            normalLineText.setAttribute('x', '40');
            normalLineText.setAttribute('y', '24');
            normalLineText.setAttribute('class', 'text-xs');
            normalLineText.textContent = '常规路线';
            legend.appendChild(normalLineText);
            
            // 添加图例项 - 最优路线
            const routeLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
            routeLine.setAttribute('x1', '0');
            routeLine.setAttribute('y1', '40');
            routeLine.setAttribute('x2', '30');
            routeLine.setAttribute('y2', '40');
            routeLine.setAttribute('stroke', '#F59E0B');
            routeLine.setAttribute('stroke-width', '4');
            routeLine.setAttribute('marker-end', 'url(#arrowhead-route)');
            legend.appendChild(routeLine);
            
            const routeLineText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
            routeLineText.setAttribute('x', '40');
            routeLineText.setAttribute('y', '44');
            routeLineText.setAttribute('class', 'text-xs');
            routeLineText.textContent = '最优路线';
            legend.appendChild(routeLineText);
            
            // 添加图例项 - 阻断路线
            const blockedLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
            blockedLine.setAttribute('x1', '0');
            blockedLine.setAttribute('y1', '60');
            blockedLine.setAttribute('x2', '30');
            blockedLine.setAttribute('y2', '60');
            blockedLine.setAttribute('stroke', '#EF4444');
            blockedLine.setAttribute('stroke-width', '2');
            blockedLine.setAttribute('stroke-dasharray', '5,3');
            blockedLine.setAttribute('marker-end', 'url(#arrowhead-blocked)');
            legend.appendChild(blockedLine);
            
            const blockedLineText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
            blockedLineText.setAttribute('x', '40');
            blockedLineText.setAttribute('y', '64');
            blockedLineText.setAttribute('class', 'text-xs');
            blockedLineText.textContent = '阻断路线';
            legend.appendChild(blockedLineText);
            
            // 绘制所有城市节点
            Object.entries(cityCoordinates).forEach(([city, { x, y }]) => {
                const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                node.setAttribute('cx', x);
                node.setAttribute('cy', y);
                node.setAttribute('r', '12');
                node.setAttribute('fill', '#165DFF');
                node.setAttribute('class', 'node');
                node.setAttribute('data-city', city);
                svg.appendChild(node);
                
                const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                label.setAttribute('x', x);
                label.setAttribute('y', y);
                label.setAttribute('text-anchor', 'middle');
                label.setAttribute('dominant-baseline', 'middle');
                label.setAttribute('fill', 'white');
                label.setAttribute('class', 'node-label');
                label.textContent = city;
                svg.appendChild(label);
            });
            
            // 绘制所有路线
            Object.entries(cityGraph).forEach(([fromCity, neighbors]) => {
                const fromCoord = cityCoordinates[fromCity];
                
                Object.entries(neighbors).forEach(([toCity, { time, cost }]) => {
                    // 只绘制一次路线(避免重复)
                    if (fromCity < toCity) {
                        const toCoord = cityCoordinates[toCity];
                        
                        // 计算路线的中点
                        const midX = (fromCoord.x + toCoord.x) / 2;
                        const midY = (fromCoord.y + toCoord.y) / 2;
                        
                        // 创建路线
                        const edge = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                        edge.setAttribute('x1', fromCoord.x);
                        edge.setAttribute('y1', fromCoord.y);
                        edge.setAttribute('x2', toCoord.x);
                        edge.setAttribute('y2', toCoord.y);
                        edge.setAttribute('stroke', '#165DFF');
                        edge.setAttribute('stroke-width', '2');
                        edge.setAttribute('marker-end', 'url(#arrowhead)');
                        edge.setAttribute('class', 'edge');
                        edge.setAttribute('data-from', fromCity);
                        edge.setAttribute('data-to', toCity);
                        svg.appendChild(edge);
                        
                        // 添加距离标签
                        const distanceLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                        distanceLabel.setAttribute('x', midX);
                        distanceLabel.setAttribute('y', midY);
                        distanceLabel.setAttribute('text-anchor', 'middle');
                        distanceLabel.setAttribute('dominant-baseline', 'middle');
                        distanceLabel.setAttribute('class', 'distance-label');
                        distanceLabel.textContent = `${time}h, ¥${cost}`;
                        svg.appendChild(distanceLabel);
                    }
                });
            });
        }
        
        // 使用Dijkstra算法查找最短路径
        function findShortestPath(start, end, blockedEdges = [], timeWeight = 70) {
            const distances = {};
            const previousNodes = {};
            const priorityQueue = [];
            
            // 初始化距离和前一个节点
            Object.keys(cityGraph).forEach(city => {
                distances[city] = Infinity;
                previousNodes[city] = null;
            });
            
            distances[start] = 0;
            priorityQueue.push({ distance: 0, city: start, time: 0, cost: 0, path: [start] });
            
            while (priorityQueue.length > 0) {
                // 找到距离最小的节点
                priorityQueue.sort((a, b) => a.distance - b.distance);
                const { city, time, cost, path } = priorityQueue.shift();
                
                // 如果到达终点,返回路径
                if (city === end) {
                    return { path, totalTime: time, totalCost: cost };
                }
                
                // 跳过已经处理过的节点
                if (city !== start && distances[city] < time * (timeWeight/100) + cost * (1 - timeWeight/100)) {
                    continue;
                }
                
                // 处理所有邻居
                for (const [neighbor, { time: edgeTime, cost: edgeCost }] of Object.entries(cityGraph[city])) {
                    // 检查路线是否被阻断
                    const isBlocked = blockedEdges.some(edge => 
                        (edge[0] === city && edge[1] === neighbor) || 
                        (edge[0] === neighbor && edge[1] === city)
                    );
                    
                    if (isBlocked) {
                        continue;
                    }
                    
                    // 计算新的加权距离
                    const newDistance = time * (timeWeight/100) + cost * (1 - timeWeight/100) + 
                                       edgeTime * (timeWeight/100) + edgeCost * (1 - timeWeight/100);
                    
                    // 如果新路径更短,则更新
                    if (newDistance < distances[neighbor]) {
                        distances[neighbor] = newDistance;
                        const newTime = time + edgeTime;
                        const newCost = cost + edgeCost;
                        const newPath = [...path, neighbor];
                        
                        priorityQueue.push({ 
                            distance: newDistance, 
                            city: neighbor, 
                            time: newTime, 
                            cost: newCost, 
                            path: newPath 
                        });
                    }
                }
            }
            
            // 如果没有找到路径
            return { path: [], totalTime: Infinity, totalCost: Infinity };
        }
        
        // 高亮显示最优路线
        function highlightRoute(path) {
            // 移除之前的高亮
            document.querySelectorAll('.route-edge').forEach(el => {
                el.classList.remove('route-edge');
                el.setAttribute('stroke', '#165DFF');
                el.setAttribute('stroke-width', '2');
                el.setAttribute('marker-end', 'url(#arrowhead)');
            });
            
            // 如果没有路径,直接返回
            if (path.length <= 1) {
                return;
            }
            
            // 高亮新路径
            for (let i = 0; i < path.length - 1; i++) {
                const fromCity = path[i];
                const toCity = path[i + 1];
                
                // 找到对应的边并高亮
                const edge = document.querySelector(`.edge[data-from="${fromCity}"][data-to="${toCity}"]`) ||
                             document.querySelector(`.edge[data-from="${toCity}"][data-to="${fromCity}"]`);
                
                if (edge) {
                    edge.classList.add('route-edge');
                    edge.setAttribute('stroke', '#F59E0B');
                    edge.setAttribute('stroke-width', '4');
                    edge.setAttribute('marker-end', 'url(#arrowhead-route)');
                }
            }
        }
        
        // 高亮显示阻断路线
        function highlightBlockedRoutes(blockedEdges) {
            // 移除之前的阻断高亮
            document.querySelectorAll('.blocked-route').forEach(el => {
                el.classList.remove('blocked-route');
                el.setAttribute('stroke', '#165DFF');
                el.setAttribute('stroke-dasharray', '');
                el.setAttribute('marker-end', 'url(#arrowhead)');
            });
            
            // 高亮新的阻断路线
            blockedEdges.forEach(([city1, city2]) => {
                const edge = document.querySelector(`.edge[data-from="${city1}"][data-to="${city2}"]`) ||
                             document.querySelector(`.edge[data-from="${city2}"][data-to="${city1}"]`);
                
                if (edge) {
                    edge.classList.add('blocked-route');
                    edge.setAttribute('stroke', '#EF4444');
                    edge.setAttribute('stroke-dasharray', '5,3');
                    edge.setAttribute('marker-end', 'url(#arrowhead-blocked)');
                }
            });
        }
        
        // 更新路径信息
        function updatePathInfo(path, totalTime, totalCost) {
            const pathInfo = document.getElementById('path-info');
            const timeInfo = document.getElementById('time-info');
            const costInfo = document.getElementById('cost-info');
            
            if (path.length === 0) {
                pathInfo.textContent = '没有找到可行路径';
                timeInfo.textContent = '';
                costInfo.textContent = '';
                return;
            }
            
            pathInfo.textContent = `最优路径: ${path.join(' → ')}`;
            timeInfo.textContent = `总运输时间: ${totalTime.toFixed(1)} 小时`;
            costInfo.textContent = `总运输费用: ¥${totalCost.toFixed(0)}`;
            
            // 更新图表
            updateCharts(path, totalTime, totalCost);
        }
        
        // 更新图表
        function updateCharts(path, totalTime, totalCost) {
            // 如果没有路径,清空图表
            if (path.length <= 1) {
                document.getElementById('time-chart').getContext('2d').clearRect(0, 0, 400, 200);
                document.getElementById('cost-chart').getContext('2d').clearRect(0, 0, 400, 200);
                return;
            }
            
            // 准备时间数据
            const timeLabels = [];
            const timeData = [];
            let cumulativeTime = 0;
            
            for (let i = 0; i < path.length - 1; i++) {
                const fromCity = path[i];
                const toCity = path[i + 1];
                const time = cityGraph[fromCity][toCity].time;
                
                timeLabels.push(`${fromCity} → ${toCity}`);
                timeData.push(time);
                cumulativeTime += time;
            }
            
            // 更新时间图表
            const timeCtx = document.getElementById('time-chart').getContext('2d');
            new Chart(timeCtx, {
                type: 'bar',
                data: {
                    labels: timeLabels,
                    datasets: [{
                        label: '运输时间 (小时)',
                        data: timeData,
                        backgroundColor: '#165DFF',
                        borderColor: '#165DFF',
                        borderWidth: 1
                    }]
                },
                options: {
                    responsive: true,
                    scales: {
                        y: {
                            beginAtZero: true,
                            title: {
                                display: true,
                                text: '小时'
                            }
                        }
                    }
                }
            });
            
            // 准备费用数据
            const costLabels = [];
            const costData = [];
            let cumulativeCost = 0;
            
            for (let i = 0; i < path.length - 1; i++) {
                const fromCity = path[i];
                const toCity = path[i + 1];
                const cost = cityGraph[fromCity][toCity].cost;
                
                costLabels.push(`${fromCity} → ${toCity}`);
                costData.push(cost);
                cumulativeCost += cost;
            }
            
            // 更新费用图表
            const costCtx = document.getElementById('cost-chart').getContext('2d');
            new Chart(costCtx, {
                type: 'pie',
                data: {
                    labels: costLabels,
                    datasets: [{
                        label: '运输费用 (元)',
                        data: costData,
                        backgroundColor: [
                            '#165DFF',
                            '#36D399',
                            '#F59E0B',
                            '#EF4444',
                            '#8B5CF6',
                            '#EC4899',
                            '#06B6D4',
                            '#10B981'
                        ],
                        borderWidth: 1
                    }]
                },
                options: {
                    responsive: true,
                    plugins: {
                        legend: {
                            position: 'right'
                        }
                    }
                }
            });
        }
        
        // 生成所有可能的路线选项
        function generateRouteOptions() {
            const routeOptions = [];
            const cities = Object.keys(cityGraph);
            
            for (let i = 0; i < cities.length; i++) {
                for (let j = i + 1; j < cities.length; j++) {
                    const city1 = cities[i];
                    const city2 = cities[j];
                    
                    // 检查这两个城市之间是否有直接连接
                    if (cityGraph[city1].hasOwnProperty(city2) || cityGraph[city2].hasOwnProperty(city1)) {
                        routeOptions.push([city1, city2]);
                    }
                }
            }
            
            return routeOptions;
        }
        
        // 初始化阻断路线选择器
        function initBlockedRoutesSelector() {
            const blockedRoutesContainer = document.getElementById('blocked-routes');
            const routeOptions = generateRouteOptions();
            
            // 清空容器
            blockedRoutesContainer.innerHTML = '';
            
            // 为每个路线选项创建一个复选框
            routeOptions.forEach(([city1, city2]) => {
                const routeId = `block-${city1}-${city2}`;
                
                const div = document.createElement('div');
                div.className = 'flex items-center';
                
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.id = routeId;
                checkbox.name = 'blocked-routes';
                checkbox.value = `${city1},${city2}`;
                checkbox.className = 'h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded';
                
                const label = document.createElement('label');
                label.htmlFor = routeId;
                label.className = 'ml-2 block text-sm text-gray-700';
                label.textContent = `${city1} ↔ ${city2}`;
                
                div.appendChild(checkbox);
                div.appendChild(label);
                blockedRoutesContainer.appendChild(div);
            });
        }
        
        // 初始化事件监听器
        function initEventListeners() {
            // 查找最优路线按钮
            document.getElementById('find-route').addEventListener('click', () => {
                const startCity = document.getElementById('start-city').value;
                const endCity = document.getElementById('end-city').value;
                
                // 获取选中的阻断路线
                const blockedRoutes = Array.from(
                    document.querySelectorAll('input[name="blocked-routes"]:checked')
                ).map(el => el.value.split(','));
                
                // 获取优化目标和权重
                const optimization = document.querySelector('input[name="optimization"]:checked').value;
                const timeWeight = optimization === 'time' ? 90 : 
                                   optimization === 'cost' ? 10 : 
                                   document.getElementById('time-weight').value;
                
                // 查找最短路径
                const { path, totalTime, totalCost } = findShortestPath(startCity, endCity, blockedRoutes, timeWeight);
                
                // 高亮显示路径
                highlightRoute(path);
                
                // 更新路径信息
                updatePathInfo(path, totalTime, totalCost);
                
                // 高亮显示阻断路线
                highlightBlockedRoutes(blockedRoutes);
            });
            
            // 时间权重滑块
            const timeWeightSlider = document.getElementById('time-weight');
            const weightValue = document.getElementById('weight-value');
            
            timeWeightSlider.addEventListener('input', () => {
                const timePercent = parseInt(timeWeightSlider.value);
                const costPercent = 100 - timePercent;
                weightValue.textContent = `${timePercent}% : ${costPercent}%`;
            });
            
            // 地图缩放控制
            const cityMap = document.getElementById('city-map');
            let scale = 1;
            let translateX = 0;
            let translateY = 0;
            
            document.getElementById('zoom-in').addEventListener('click', () => {
                scale = Math.min(scale * 1.2, 3);
                cityMap.setAttribute('transform', `scale(${scale}) translate(${translateX/scale}, ${translateY/scale})`);
            });
            
            document.getElementById('zoom-out').addEventListener('click', () => {
                scale = Math.max(scale / 1.2, 0.5);
                cityMap.setAttribute('transform', `scale(${scale}) translate(${translateX/scale}, ${translateY/scale})`);
            });
            
            document.getElementById('reset-view').addEventListener('click', () => {
                scale = 1;
                translateX = 0;
                translateY = 0;
                cityMap.setAttribute('transform', '');
            });
            
            // 地图拖拽功能
            let isDragging = false;
            let startX, startY;
            
            cityMap.addEventListener('mousedown', (e) => {
                if (e.target === cityMap) {
                    isDragging = true;
                    startX = e.clientX - translateX;
                    startY = e.clientY - translateY;
                }
            });
            
            document.addEventListener('mousemove', (e) => {
                if (!isDragging) return;
                e.preventDefault();
                translateX = e.clientX - startX;
                translateY = e.clientY - startY;
                cityMap.setAttribute('transform', `translate(${translateX}, ${translateY}) scale(${scale})`);
            });
            
            document.addEventListener('mouseup', () => {
                isDragging = false;
            });
        }
        
        // 初始化页面
        function init() {
            initMap();
            initBlockedRoutesSelector();
            initEventListeners();
            
            // 初始设置终点为西安
            document.getElementById('end-city').value = '西安';
        }
        
        // 页面加载完成后初始化
        window.addEventListener('DOMContentLoaded', init);
    </script>
</body>
</html>    

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景:
  • 工具设想
  • 技术架构
    • 前端框架与库
    • 算法实现
  • 代码实现细节
    • 1. 城市数据定义
    • 2. 地图初始化
    • 3. Dijkstra 算法实现
    • 4. 路线高亮与信息更新
  • 交互功能
  • 从「一骑红尘」到「一键红尘」
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档