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>
posted @ 2026-04-29 09:12  douzishuo  阅读(17)  评论(0)    收藏  举报