4.27
今日
1.完成北京地铁导航项目手机app开发
应用最短路算法,以及uni-app打包成apk文件安装至安卓系统手机
2.程序主页面vue学习
<template>
<view class="app-container">
<!-- 顶部导航栏 -->
<view class="app-header">
<view class="logo-icon">🚇</view>
<view class="header-text">
<text class="header-title">地铁查询系统</text>
<text class="subtitle">北京全路网 · 精准导航</text>
</view>
<view class="back-btn" v-if="currentPage !== 'home'" @click="goHome">← 返回</view>
</view>
<!-- 主容器 -->
<view class="main-container">
<!-- 首页 -->
<view v-if="currentPage === 'home'" class="page">
<view class="home-badges">
<text
v-for="(line, name) in metroLines"
:key="name"
class="line-badge"
:style="{ background: line.color }"
>{{ name }}</text>
</view>
<view class="card" @click="currentPage = 'line-query'">
<view class="card-icon icon-blue">🔍</view>
<view class="card-title">线路查询</view>
<view class="card-desc">查询某条线路的所有站名</view>
<view class="card-arrow">›</view>
</view>
<view class="card" @click="currentPage = 'station-query'">
<view class="card-icon icon-green">📍</view>
<view class="card-title">站点查询</view>
<view class="card-desc">查询途经该站的所有线路</view>
<view class="card-arrow">›</view>
</view>
<view class="card" @click="currentPage = 'route-query'">
<view class="card-icon icon-orange">🗺️</view>
<view class="card-title">起点-终点查询</view>
<view class="card-desc">最短路径,含换乘信息</view>
<view class="card-arrow">›</view>
</view>
<view class="card" @click="currentPage = 'traverse'">
<view class="card-icon icon-purple">🔄</view>
<view class="card-title">遍历所有车站</view>
<view class="card-desc">从起始站出发经过全部车站</view>
<view class="card-arrow">›</view>
</view>
<view class="card" @click="currentPage = 'min-transfer'">
<view class="card-icon icon-teal">⚡</view>
<view class="card-title">最少换乘查询</view>
<view class="card-desc">换乘次数最少的路线</view>
<view class="card-arrow">›</view>
</view>
<view class="card" @click="currentPage = 'manage'">
<view class="card-icon icon-red">📋</view>
<view class="card-title">线路管理</view>
<view class="card-desc">添加、删除自定义地铁线路</view>
<view class="card-arrow">›</view>
</view>
</view>
<!-- 线路查询页 -->
<view v-if="currentPage === 'line-query'" class="page">
<view class="page-title">🔍 线路查询</view>
<view class="form-group">
<view class="form-label">选择线路</view>
<input class="form-input" v-model="lineInput" placeholder="例如:10号线" />
</view>
<button class="btn btn-primary" @click="queryLine">查询</button>
<view v-if="lineResultHtml" v-html="lineResultHtml" class="result-container"></view>
</view>
<!-- 站点查询页 -->
<view v-if="currentPage === 'station-query'" class="page">
<view class="page-title">📍 站点查询</view>
<view class="form-group">
<view class="form-label">选择站点</view>
<input class="form-input" v-model="stationInput" placeholder="例如:知春路" />
</view>
<button class="btn btn-primary" @click="queryStation">查询</button>
<!-- 多个匹配站点时显示列表 -->
<view v-if="stationMatches.length > 1" class="result-container">
<view class="no-result">🔍 找到多个匹配站点:</view>
<view
v-for="s in stationMatches"
:key="s"
class="station-tag"
style="margin:4px;"
@click="selectMatchedStation(s)"
>{{ s }}</view>
</view>
<view v-else-if="stationResultHtml" v-html="stationResultHtml" class="result-container"></view>
</view>
<!-- 起点-终点查询页 -->
<view v-if="currentPage === 'route-query'" class="page">
<view class="page-title">🗺️ 最短路径</view>
<view class="form-group">
<view class="form-label">起点站</view>
<input class="form-input" v-model="startInput" placeholder="例如:知春路" />
</view>
<view class="form-group">
<view class="form-label">终点站</view>
<input class="form-input" v-model="endInput" placeholder="例如:中关村" />
</view>
<button class="btn btn-primary" @click="queryRoute">查询最短路径</button>
<view v-if="routeResultHtml" v-html="routeResultHtml" class="result-container"></view>
</view>
<!-- 遍历所有车站页 -->
<view v-if="currentPage === 'traverse'" class="page">
<view class="page-title">🔄 遍历所有车站</view>
<view class="form-group">
<view class="form-label">起始站</view>
<input class="form-input" v-model="traverseStartInput" placeholder="例如:知春路" />
</view>
<button class="btn btn-primary" @click="queryTraverse">开始遍历</button>
<view v-if="traverseResultHtml" v-html="traverseResultHtml" class="result-container"></view>
</view>
<!-- 最少换乘查询页 -->
<view v-if="currentPage === 'min-transfer'" class="page">
<view class="page-title">⚡ 最少换乘</view>
<view class="form-group">
<view class="form-label">起点站</view>
<input class="form-input" v-model="mtStartInput" placeholder="例如:西直门" />
</view>
<view class="form-group">
<view class="form-label">终点站</view>
<input class="form-input" v-model="mtEndInput" placeholder="例如:国贸" />
</view>
<button class="btn btn-primary" @click="queryMinTransfer">查询最少换乘</button>
<view v-if="minTransferResultHtml" v-html="minTransferResultHtml" class="result-container"></view>
</view>
<!-- 线路管理页 -->
<view v-if="currentPage === 'manage'" class="page">
<view class="page-title">📋 线路管理</view>
<view class="line-list-container">
<view v-for="(line, name) in metroLines" :key="name" class="line-list-item">
<view>
<view class="name">
<view class="color-dot" :style="{ background: line.color }"></view>
{{ name }}{{ line.isLoop ? '🔄' : '' }}
</view>
<view class="stations">
{{ line.stations.length }} 站:{{ line.stations.slice(0,3).join('、') }}{{ line.stations.length>3?'…':'' }}
</view>
</view>
<button class="btn btn-danger" @click="deleteLine(name)">删除</button>
</view>
</view>
<view class="add-line-panel">
<view class="form-group">
<view class="form-label">线路名(如“5号线”)</view>
<input class="form-input" v-model="newLineName" placeholder="5号线" />
</view>
<view class="form-group">
<view class="form-label">站点列表(英文逗号分隔)</view>
<input class="form-input" v-model="newLineStations" placeholder="宋家庄,刘家窑,蒲黄榆..." />
</view>
<view class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="newLineIsLoop" /> 环线
</label>
</view>
<button class="btn btn-primary" @click="addLine">添加线路</button>
</view>
</view>
</view>
</view>
</template>
<script>
// ====================== 线路数据 ======================
const DEFAULT_LINES = {
"1号线": { name:"1号线", color:"#c23a30", isLoop:false, stations:["福寿岭","苹果园","古城","八角游乐园","八宝山","玉泉路","五棵松","万寿路","公主坟","军事博物馆","木樨地","南礼士路","复兴门","西单","天安门西","天安门东","王府井","东单","建国门","永安里","国贸","大望路","四惠","四惠东"] },
"2号线": { name:"2号线", color:"#004b87", isLoop:true, stations:["西直门","积水潭","鼓楼大街","安定门","雍和宫","东直门","东四十条","朝阳门","建国门","北京站","崇文门","前门","和平门","宣武门","长椿街","复兴门","阜成门","车公庄"] },
"4号线": { name:"4号线", color:"#008c95", isLoop:false, stations:["安河桥北","北宫门","西苑","圆明园","北京大学东门","中关村","海淀黄庄","人民大学","魏公村","国家图书馆","动物园","西直门","新街口","平安里","西四","灵境胡同","西单","宣武门","菜市口","陶然亭","北京南站","马家堡","角门西","公益西桥"] },
"5号线": { name:"5号线", color:"#aa0061", isLoop:false, stations:["宋家庄","刘家窑","蒲黄榆","天坛东门","磁器口","崇文门","东单","灯市口","东四","张自忠路","北新桥","雍和宫","和平里北街","和平西桥","惠新西街南口","大屯路东","北苑路北","立水桥南","立水桥","天通苑南","天通苑","天通苑北"] },
"6号线": { name:"6号线", color:"#d49c0d", isLoop:false, stations:["金安桥","苹果园","杨庄","西黄村","廖公庄","田村","海淀五路居","慈寿寺","花园桥","白石桥南","车公庄西","车公庄","平安里","北海北","南锣鼓巷","东四","朝阳门","东大桥","呼家楼","金台路","十里堡","青年路","褡裢坡","黄渠","常营","草房","物资学院路","通州北关","北运河西","郝家府","东夏园","潞城"] },
"7号线": { name:"7号线", color:"#f4c300", isLoop:false, stations:["北京西站","湾子","达官营","广安门内","菜市口","虎坊桥","珠市口","桥湾","磁器口","广渠门内","广渠门外","双井","九龙山","大郊亭","百子湾","化工","南楼梓庄","欢乐谷景区","垡头","双合","焦化厂","黄厂","郎辛庄","黑庄户","万盛西","万盛东","群芳","高楼金","花庄","环球度假区"] },
"8号线": { name:"8号线", color:"#009b77", isLoop:false, stations:["朱辛庄","育知路","平西府","回龙观东大街","霍营","育新","西小口","永泰庄","林萃桥","森林公园南门","奥林匹克公园","奥体中心","北土城","安华桥","安德里北街","鼓楼大街","什刹海","南锣鼓巷","中国美术馆","金鱼胡同","王府井","前门","珠市口","天桥","永定门外","木樨园","海户屯","大红门南","和义","东高地","火箭万源","五福堂","德茂","瀛海"] },
"9号线": { name:"9号线", color:"#7fbb42", isLoop:false, stations:["国家图书馆","白石桥南","白堆子","军事博物馆","北京西站","六里桥东","六里桥","七里庄","丰台东大街","丰台南路","科怡路","丰台科技园","郭公庄"] },
"10号线":{ name:"10号线", color:"#0092bc", isLoop:true, stations:["巴沟","车道沟","长春桥","火器营","西钓鱼台","公主坟","莲花桥","六里桥","西局","泥洼","丰台站","首经贸","纪家庙","草桥","角门西","角门东","大红门","石榴庄","宋家庄","成寿寺","分钟寺","十里河","潘家园","劲松","双井","国贸","金台夕照","呼家楼","团结湖","农业展览馆","亮马桥","三元桥","太阳宫","芍药居","惠新西街南口","安贞门","北土城","健德门","牡丹园","西土城","知春路","知春里","海淀黄庄","苏州街"] },
"13号线":{ name:"13号线", color:"#d4a800", isLoop:false, stations:["西直门","大钟寺","知春路","五道口","上地","西二旗","龙泽","回龙观","霍营","立水桥","北苑","望京西","芍药居","光熙门","柳芳","东直门"] },
"14号线":{ name:"14号线", color:"#ca9a64", isLoop:false, stations:["张郭庄","园博园","大瓦窑","郭庄子","大井","七里庄","西局","东管头","丽泽商务区","菜户营","西铁营","景风门","北京南站","永定门外","景泰","蒲黄榆","方庄","十里河","北工大西门","平乐园","九龙山","大望路","金台路","朝阳公园","枣营","东风北桥","将台","望京南","阜通","望京","东湖渠","来广营","善各庄"] },
"15号线":{ name:"15号线", color:"#653279", isLoop:false, stations:["清华东路西口","六道口","北沙滩","奥林匹克公园","安立路","大屯路东","关庄","望京西","望京","望京东","崔各庄","马泉营","孙河","国展","花梨坎","后沙峪","南法信","石门","顺义","俸伯"] },
"16号线":{ name:"16号线", color:"#6cc24a", isLoop:false, stations:["北安河","温阳路","稻香湖路","屯佃","永丰","永丰南","西北旺","马连洼","农大南路","西苑","万泉河桥","苏州街","苏州桥","万寿寺","国家图书馆","甘家口","玉渊潭东门","木樨地","达官营","红莲南路","丽泽商务区","东管头南","丰台站","丰台南路","富丰桥","看丹","榆树庄","洪泰庄","宛平城"] },
"八通线":{ name:"八通线", color:"#c23a30", isLoop:false, stations:["四惠","四惠东","高碑店","传媒大学","双桥","管庄","八里桥","通州北苑","果园","九棵树","梨园","临河里","土桥","花庄","环球度假区"] },
"大兴线":{ name:"大兴线", color:"#008c95", isLoop:false, stations:["公益西桥","新宫","西红门","高米店北","高米店南","枣园","清源路","黄村西大街","黄村火车站","义和庄","生物医药基地","天宫院"] },
"房山线":{ name:"房山线", color:"#ed6e00", isLoop:false, stations:["东管头南","首经贸","花乡东桥","白盆窑","郭公庄","大葆台","稻田","长阳","篱笆房","广阳城","良乡大学城北","良乡大学城","良乡大学城西","良乡南关","苏庄","阎村东"] },
"昌平线":{ name:"昌平线", color:"#d986ba", isLoop:false, stations:["昌平西山口","十三陵景区","昌平","昌平东关","北邵洼","南邵","沙河高教园","沙河","巩华城","朱辛庄","生命科学园","西二旗","清河站","上清桥","学知园","六道口","学院桥","西土城","蓟门桥"] },
"亦庄线":{ name:"亦庄线", color:"#e60039", isLoop:false, stations:["宋家庄","肖村","小红门","旧宫","亦庄桥","亦庄文化园","万源街","荣京东街","荣昌东街","同济南路","经海路","次渠南","次渠","亦庄火车站"] },
"首都机场线":{ name:"首都机场线", color:"#a0a0a0", isLoop:false, stations:["北新桥","东直门","三元桥","3号航站楼","2号航站楼"] },
"大兴机场线":{ name:"大兴机场线", color:"#004a9f", isLoop:false, stations:["草桥","大兴新城","大兴机场"] }
};
export default {
data() {
return {
currentPage: 'home',
lineInput: '10号线',
stationInput: '知春路',
startInput: '知春路',
endInput: '中关村',
traverseStartInput: '知春路',
mtStartInput: '西直门',
mtEndInput: '国贸',
newLineName: '',
newLineStations: '',
newLineIsLoop: false,
lineResultHtml: '',
stationResultHtml: '',
routeResultHtml: '',
traverseResultHtml: '',
minTransferResultHtml: '',
stationMatches: [], // 用于存储站点查询时的多个匹配结果
metroLines: {},
allStationsNames: [],
lineNames: [],
stationLinesMap: {},
adjacencyList: {},
shortestPathFinder: null,
minTransferFinder: null,
traverseFinder: null
};
},
created() {
this.loadData();
this.rebuildDataStructures();
this.initAlgorithms();
},
methods: {
loadData() {
try {
const saved = uni.getStorageSync('metro_custom_lines');
if (saved) {
this.metroLines = JSON.parse(saved);
} else {
this.metroLines = JSON.parse(JSON.stringify(DEFAULT_LINES));
}
} catch (e) {
this.metroLines = JSON.parse(JSON.stringify(DEFAULT_LINES));
}
},
saveData() {
uni.setStorageSync('metro_custom_lines', JSON.stringify(this.metroLines));
this.rebuildDataStructures();
this.initAlgorithms();
},
deleteLine(lineName) {
uni.showModal({
title: '提示',
content: `确定删除线路“${lineName}”吗?`,
success: (res) => {
if (res.confirm) {
const newLines = { ...this.metroLines };
delete newLines[lineName];
this.metroLines = newLines;
this.saveData();
}
}
});
},
addLine() {
const name = this.newLineName.trim();
const stationsStr = this.newLineStations.trim();
if (!name || !stationsStr) {
uni.showToast({ title: '请填写线路名和站点列表', icon: 'none' });
return;
}
const stations = stationsStr.split(',').map(s => s.trim()).filter(s => s);
if (stations.length < 2) {
uni.showToast({ title: '至少需要两个站点', icon: 'none' });
return;
}
const color = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
const newLines = { ...this.metroLines, [name]: { name, color, isLoop: !!this.newLineIsLoop, stations } };
this.metroLines = newLines;
this.saveData();
this.newLineName = '';
this.newLineStations = '';
this.newLineIsLoop = false;
uni.showToast({ title: `线路“${name}”添加成功!`, icon: 'success' });
},
rebuildDataStructures() {
const stationLinesMap = {};
const allStations = new Set();
for (let [ln, ld] of Object.entries(this.metroLines)) {
ld.stations.forEach(s => {
if (!stationLinesMap[s]) stationLinesMap[s] = [];
if (!stationLinesMap[s].includes(ln)) stationLinesMap[s].push(ln);
allStations.add(s);
});
}
this.stationLinesMap = stationLinesMap;
this.allStationsNames = Array.from(allStations);
this.lineNames = Object.keys(this.metroLines);
const adj = {};
for (let [ln, ld] of Object.entries(this.metroLines)) {
const sts = ld.stations;
for (let i = 0; i < sts.length; i++) {
const s = sts[i];
if (!adj[s]) adj[s] = [];
if (i > 0) adj[s].push({ neighbor: sts[i-1], line: ln });
else if (ld.isLoop && sts.length > 1) adj[s].push({ neighbor: sts[sts.length-1], line: ln });
if (i < sts.length-1) adj[s].push({ neighbor: sts[i+1], line: ln });
else if (ld.isLoop && sts.length > 1) adj[s].push({ neighbor: sts[0], line: ln });
}
}
for (let s in adj) {
const seen = new Set();
const unique = [];
adj[s].forEach(item => {
const sig = item.neighbor + '|' + item.line;
if (!seen.has(sig)) { seen.add(sig); unique.push(item); }
});
adj[s] = unique;
}
this.adjacencyList = adj;
},
initAlgorithms() {
this.shortestPathFinder = new ShortestPath(this.adjacencyList);
this.minTransferFinder = new MinTransfer(this.adjacencyList, this.stationLinesMap);
this.traverseFinder = new TraverseStations(this.adjacencyList, this.allStationsNames, this.shortestPathFinder);
},
goHome() {
this.currentPage = 'home';
},
getLineStyle(lineName) {
const color = this.metroLines[lineName]?.color || '#7c3aed';
return `background:${color};color:#fff;`;
},
matchStation(input) {
if (this.allStationsNames.includes(input)) return input;
const matches = this.allStationsNames.filter(s => s.includes(input));
if (matches.length === 1) return matches[0];
return matches.length > 1 ? matches : null; // 返回数组以便展示
},
// 站点查询:当有多个匹配时点击其中一个
selectMatchedStation(station) {
this.stationInput = station;
this.stationMatches = [];
this.queryStation();
},
queryLine() {
if (!this.lineInput) {
this.lineResultHtml = '<div class="no-result">⚠️ 请选择线路</div>';
return;
}
let lineName = this.lineInput;
if (!this.metroLines[lineName]) {
const match = Object.keys(this.metroLines).find(l => l.includes(lineName));
if (match) lineName = match;
else {
this.lineResultHtml = '<div class="no-result">❌ 未找到线路</div>';
return;
}
}
const ld = this.metroLines[lineName];
const stationsHtml = ld.stations.map(s => `<span class="station-tag">${s}</span>`).join(' ');
this.lineResultHtml = `<div class="result-title"><span class="line-badge" style="${this.getLineStyle(lineName)}">${lineName}</span>${ld.isLoop?' 🔄环线':''}</div><div class="result-stat">📊 共 <b>${ld.stations.length}</b> 站</div><div style="line-height:2.2;margin-top:8px;">${stationsHtml}</div>`;
},
queryStation() {
this.stationResultHtml = '';
this.stationMatches = [];
if (!this.stationInput) {
this.stationResultHtml = '<div class="no-result">⚠️ 请选择站点</div>';
return;
}
const lines = this.stationLinesMap[this.stationInput];
if (lines) {
const isTransfer = lines.length > 1;
this.stationResultHtml = `<div class="result-title">📍 ${this.stationInput}</div><div class="result-stat">🚇 途经线路:<b>${lines.length}</b> 条</div><div>${lines.map(l=>`<span class="line-badge" style="${this.getLineStyle(l)}">${l}</span>`).join(' ')}</div>${isTransfer?'<div class="transfer-alert">🔄 换乘站</div>':''}`;
return;
}
// 未直接匹配,查找模糊匹配
const matched = this.allStationsNames.filter(s => s.includes(this.stationInput));
if (matched.length === 1) {
this.stationInput = matched[0];
this.queryStation(); // 递归调用
} else if (matched.length > 1) {
// 多个匹配,显示点击列表
this.stationMatches = matched;
} else {
this.stationResultHtml = '<div class="no-result">❌ 未找到站点</div>';
}
},
queryRoute() {
if (!this.startInput || !this.endInput) {
this.routeResultHtml = '<div class="no-result">⚠️ 请输入起点和终点</div>';
return;
}
let start = this.matchStation(this.startInput);
let end = this.matchStation(this.endInput);
// 如果是数组(多个匹配),取第一个?不,用户应明确选择,这里简单兼容
if (Array.isArray(start)) start = start[0];
if (Array.isArray(end)) end = end[0];
if (!start || !end || !this.allStationsNames.includes(start) || !this.allStationsNames.includes(end)) {
this.routeResultHtml = '<div class="no-result">❌ 站点名称不准确,请从下拉列表选择</div>';
return;
}
const res = this.shortestPathFinder.findShortestPath(start, end);
if (!res) {
this.routeResultHtml = '<div class="no-result">❌ 无法到达</div>';
return;
}
this.routeResultHtml = this.renderPathResult(start, end, res.distance, res.path, '最短路径');
},
queryTraverse() {
if (!this.traverseStartInput) {
this.traverseResultHtml = '<div class="no-result">⚠️ 请选择起始站</div>';
return;
}
let start = this.matchStation(this.traverseStartInput);
if (Array.isArray(start)) start = start[0];
if (!start || !this.allStationsNames.includes(start)) {
this.traverseResultHtml = '<div class="no-result">❌ 站点不存在</div>';
return;
}
this.traverseResultHtml = '<div style="text-align:center;padding:20px;">⏳ 正在计算遍历路线...</div>';
setTimeout(() => {
const res = this.traverseFinder.traverseAll(start);
let pathHtml = '';
res.path.forEach((step, i) => {
const tag = step.line ? ` <span class="line-tag" style="${this.getLineStyle(step.line)}">${step.line}</span>` : '';
pathHtml += `<div style="padding:4px 8px;border-bottom:1px solid #f3f4f6;">${i+1}. <b>${step.station}</b>${tag}</div>`;
});
this.traverseResultHtml = `<div class="result-title">🔄 遍历所有车站</div><div class="result-stat">🚩 起始:<b>${start}</b></div><div class="result-stat" style="background:#e6f4ea;color:#1e8e3e;">📊 经历总站数:<b>${res.totalStops}</b></div><div style="margin-top:12px;max-height:400px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:10px;padding:8px;">${pathHtml}</div>`;
}, 100);
},
queryMinTransfer() {
if (!this.mtStartInput || !this.mtEndInput) {
this.minTransferResultHtml = '<div class="no-result">⚠️ 请输入起点和终点</div>';
return;
}
let start = this.matchStation(this.mtStartInput);
let end = this.matchStation(this.mtEndInput);
if (Array.isArray(start)) start = start[0];
if (Array.isArray(end)) end = end[0];
if (!start || !end || !this.allStationsNames.includes(start) || !this.allStationsNames.includes(end)) {
this.minTransferResultHtml = '<div class="no-result">❌ 站点名称不准确</div>';
return;
}
const res = this.minTransferFinder.findMinTransferPath(start, end);
if (!res) {
this.minTransferResultHtml = '<div class="no-result">❌ 无法到达</div>';
return;
}
let pathHtml = '', currentLine = null;
res.path.forEach((step, i) => {
const isTransfer = (i>0 && step.line !== currentLine && step.line && currentLine);
const isFirst = (i===0);
if (isTransfer) pathHtml += `<div class="transfer-alert">🔄 在 <b>${res.path[i-1].station}</b> 换乘 <span class="line-badge" style="${this.getLineStyle(step.line)}">${step.line}</span></div>`;
pathHtml += `<div class="path-item ${isTransfer?'transfer':''}"><span class="station-name">${step.station}</span>${(isFirst||isTransfer)&&step.line?` <span class="line-tag" style="${this.getLineStyle(step.line)}">${step.line}</span>`:''}</div>`;
if (step.line) currentLine = step.line;
});
this.minTransferResultHtml = `<div class="result-title">⚡ ${start} → ${end}</div><div class="result-stat" style="background:#fef3e0;color:#e8710a;">🔄 换乘 <b>${res.transfers}</b> 次</div><div class="result-stat">📏 经过 <b>${res.stations}</b> 站</div>${pathHtml}`;
},
renderPathResult(start, end, distance, path, title) {
let html = '', currentLine = null;
path.forEach((step, i) => {
const isTransfer = (i>0 && step.line !== currentLine && step.line && currentLine);
const isFirst = (i===0);
if (isTransfer) html += `<div class="transfer-alert">🔄 在 <b>${path[i-1].station}</b> 换乘 <span class="line-badge" style="${this.getLineStyle(step.line)}">${step.line}</span></div>`;
html += `<div class="path-item ${isTransfer?'transfer':''}"><span class="station-name">${step.station}</span>${(isFirst||isTransfer)&&step.line?` <span class="line-tag" style="${this.getLineStyle(step.line)}">${step.line}</span>`:''}</div>`;
if (step.line) currentLine = step.line;
});
return `<div class="result-title">🗺️ ${start} → ${end}</div><div class="result-stat">📏 ${title}:<b>${distance}</b> 站</div>${html}`;
}
}
};
// ====================== 算法类 ======================
class ShortestPath {
constructor(adjList) { this.adjList = adjList; }
findShortestPath(start, end) {
if (start === end) return { distance:0, path:[{station:start, line:null}] };
const queue = [{ station:start, path:[{station:start, line:null}] }];
const visited = new Set([start]);
while (queue.length) {
const cur = queue.shift();
for (let {neighbor, line} of (this.adjList[cur.station] || [])) {
if (visited.has(neighbor)) continue;
visited.add(neighbor);
const newPath = [...cur.path, {station:neighbor, line}];
if (neighbor === end) return { distance: newPath.length-1, path: newPath };
queue.push({ station:neighbor, path: newPath });
}
}
return null;
}
}
class MinTransfer {
constructor(adjList, stLineMap) { this.adjList = adjList; this.stLineMap = stLineMap; }
findMinTransferPath(start, end) {
if (start === end) return { transfers:0, stations:0, path:[{station:start, line:null}] };
const startLines = this.stLineMap[start] || [];
if (!startLines.length) return null;
const best = new Map();
const queue = [];
for (let line of startLines) {
const key = start + '|' + line;
best.set(key, { transfers:0, stationCount:0 });
queue.push({ station:start, line, transfers:0, stationCount:0, path:[{station:start, line}] });
}
let maxIter = 50000;
while (queue.length > 0 && maxIter-- > 0) {
let minIdx = 0;
for (let i = 1; i < queue.length; i++) {
const a = queue[i], b = queue[minIdx];
if (a.transfers < b.transfers || (a.transfers === b.transfers && a.stationCount < b.stationCount)) minIdx = i;
}
const cur = queue.splice(minIdx, 1)[0];
if (cur.station === end) return { transfers:cur.transfers, stations:cur.stationCount, path:cur.path };
for (let {neighbor, line} of (this.adjList[cur.station] || [])) {
const same = (line === cur.line);
const newTransfers = same ? cur.transfers : cur.transfers+1;
const newStationCount = cur.stationCount+1;
const key = neighbor + '|' + line;
const prev = best.get(key);
if (prev) {
if (newTransfers > prev.transfers) continue;
if (newTransfers === prev.transfers && newStationCount >= prev.stationCount) continue;
prev.transfers = newTransfers;
prev.stationCount = newStationCount;
} else {
best.set(key, { transfers: newTransfers, stationCount: newStationCount });
}
queue.push({ station:neighbor, line, transfers:newTransfers, stationCount:newStationCount, path:[...cur.path, {station:neighbor, line}] });
}
}
return null;
}
}
class TraverseStations {
constructor(adjList, allSts, spFinder) {
this.adjList = adjList;
this.allSts = allSts;
// spFinder 不再需要,我们将使用自己的 BFS
}
// 从起点 station 运行一次 BFS,返回到达所有站点最短路径的信息
bfsFrom(start) {
const dist = new Map(); // 站点 -> 最短距离
const prev = new Map(); // 站点 -> 前驱站点
const lineTo = new Map(); // 站点 -> 从 prev 到该站点所乘坐的线路
const queue = [start];
dist.set(start, 0);
prev.set(start, null);
lineTo.set(start, null);
while (queue.length > 0) {
const current = queue.shift();
const currentDist = dist.get(current);
const neighbors = this.adjList[current] || [];
for (let { neighbor, line } of neighbors) {
if (!dist.has(neighbor)) {
dist.set(neighbor, currentDist + 1);
prev.set(neighbor, current);
lineTo.set(neighbor, line);
queue.push(neighbor);
}
}
}
return { dist, prev, lineTo };
}
traverseAll(start) {
const unvisited = new Set(this.allSts);
unvisited.delete(start);
const fullPath = [{ station: start, line: null }];
let current = start;
// 安全限制:最多执行 1000 次 BFS(通常远小于此值)
let maxIter = 1000;
while (unvisited.size > 0 && maxIter-- > 0) {
// 1. 从当前站运行一次 BFS
const { dist, prev, lineTo } = this.bfsFrom(current);
// 2. 在未访问站点中找到距离最近的
let bestTarget = null;
let bestDist = Infinity;
for (let station of unvisited) {
const d = dist.get(station);
if (d !== undefined && d < bestDist) {
bestDist = d;
bestTarget = station;
}
}
// 如果没有任何未访问站点可从当前站到达(图不连通),终止
if (!bestTarget) break;
// 3. 回溯路径:从 bestTarget 回到 current,得到完整路径段
const segment = [];
let node = bestTarget;
while (node !== current) {
const prevNode = prev.get(node);
const line = lineTo.get(node);
if (!prevNode) break; // 无前驱,理论上不应发生
segment.unshift({ station: node, line: line });
node = prevNode;
}
// 4. 将路径段添加到总路径中,并顺路标记所有经过的站点为已访问
for (let step of segment) {
fullPath.push(step);
unvisited.delete(step.station);
}
// 更新当前位置为最佳目标的站点
current = bestTarget;
}
return {
totalStops: fullPath.length,
path: fullPath
};
}
}
</script>
<style>
/* ========== 全局样式(供 v-html 使用) ========== */
.result-container {
background: #ffffff;
border-radius: 16px;
padding: 20px;
margin-top: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
border: 1px solid #e5e7eb;
animation: fadeInUp 0.35s ease;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.result-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 10px;
color: #1a73e8;
}
.result-stat {
display: inline-flex;
align-items: center;
gap: 4px;
background: #e8f0fe;
color: #1a73e8;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
margin: 4px 4px 4px 0;
}
.path-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 0;
position: relative;
}
.path-item::before {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #1a73e8;
flex-shrink: 0;
margin-top: 5px;
box-shadow: 0 0 0 4px rgba(26,115,232,0.2);
}
.path-item.transfer::before {
background: #e8710a;
width: 12px;
height: 12px;
margin-top: 4px;
}
.station-name {
font-weight: 600;
font-size: 15px;
}
.line-tag {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
color: #fff;
margin-left: 6px;
}
.line-badge {
display: inline-block;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 700;
color: #fff;
margin: 3px 4px;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.station-tag {
display: inline-block;
padding: 5px 12px;
background: #f3f4f6;
border-radius: 16px;
font-size: 13px;
margin: 2px 3px;
border: 1px solid #e5e7eb;
cursor: pointer;
}
.station-tag:active {
background: #e5e7eb;
}
.transfer-alert {
background: #fff8e6;
border-left: 4px solid #e8710a;
padding: 12px 16px;
border-radius: 8px;
margin: 8px 0;
font-weight: 600;
font-size: 14px;
color: #b8600a;
}
.no-result {
text-align: center;
color: #9ca3af;
padding: 30px;
font-size: 14px;
}
</style>
<style scoped>
/* ========== 组件样式 ========== */
.app-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: #f5f6f8;
color: #1f2937;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
padding-bottom: env(safe-area-inset-bottom);
}
.app-header {
background: linear-gradient(135deg, #1a3c5e 0%, #1a5276 40%, #1a6e8e 100%);
color: #fff;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 4px 20px rgba(26,60,94,0.3);
position: sticky;
top: 0;
z-index: 100;
}
.logo-icon {
width: 40px;
height: 40px;
background: rgba(255,255,255,0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.header-title {
font-size: 20px;
font-weight: 700;
}
.subtitle {
font-size: 12px;
opacity: 0.85;
}
.main-container {
max-width: 500px;
margin: 0 auto;
padding: 16px 16px 100px;
}
.card {
background: #ffffff;
border-radius: 16px;
padding: 20px;
margin-bottom: 14px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
border: 1px solid #e5e7eb;
position: relative;
}
.card:active {
transform: scale(0.985);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 12px;
}
.card-title {
font-size: 17px;
font-weight: 700;
margin-bottom: 4px;
}
.card-desc {
font-size: 13px;
color: #6b7280;
}
.card-arrow {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
color: #c0c7cf;
font-size: 18px;
}
.icon-blue { background: #e8f0fe; color: #1a73e8; }
.icon-green { background: #e6f4ea; color: #1e8e3e; }
.icon-orange { background: #fef3e0; color: #e8710a; }
.icon-purple { background: #f0e6f6; color: #7c3aed; }
.icon-teal { background: #e0f7f6; color: #0d9488; }
.icon-red { background: #fce4e4; color: #c62828; }
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 13px 16px;
border: 2px solid #e5e7eb;
border-radius: 10px;
font-size: 15px;
background: #fafbfc;
outline: none;
}
.form-input:focus {
border-color: #1a73e8;
background: #fff;
box-shadow: 0 0 0 4px rgba(26,115,232,0.1);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 13px 28px;
border: none;
border-radius: 25px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
width: 100%;
}
.btn-primary {
background: linear-gradient(135deg, #1a73e8, #1557b0);
color: #fff;
box-shadow: 0 4px 14px rgba(26,115,232,0.35);
}
.btn-primary:active {
transform: scale(0.96);
}
.btn-danger {
background: #c62828;
color: #fff;
width: auto;
padding: 8px 16px;
font-size: 13px;
border-radius: 20px;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255,255,255,0.15);
color: #fff;
border: 1px solid rgba(255,255,255,0.3);
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
margin-left: auto;
}
.back-btn:active {
background: rgba(255,255,255,0.25);
}
.page {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.page-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.home-badges {
text-align: center;
padding: 8px 0 16px;
}
.line-list-container {
margin-bottom: 20px;
}
.line-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: #f9fafb;
border-radius: 12px;
margin-bottom: 8px;
}
.name {
font-weight: 600;
display: flex;
align-items: center;
}
.color-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 6px;
}
.stations {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.add-line-panel {
background: #f9fafb;
border-radius: 16px;
padding: 16px;
margin-top: 20px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
}
input[type="checkbox"] {
transform: scale(1.3);
}
</style>

浙公网安备 33010602011771号