Qt QML地铁换乘系统demo拆解:从图结构到界面实现

发布时间:2026/10/22 18:56:35
Qt QML地铁换乘系统demo拆解:从图结构到界面实现
简介一份面向数据结构课程设计、实训与大作业场景的地铁公交换乘系统demo基于Qt QML开发核心覆盖公交/地铁线路建模、站间换乘规划与可视化展示等典型问题。项目工程经测试运行功能正常可完整复现复刻也适合在此基础上扩展二次开发。资料包共52个文件以cpp/h源码、qml界面、xml数据、ttf/otf字体及工程配置文件为主整体约30.49MB配套说明文件有助于快速上手。已有92人学习浏览适用于毕业设计、期中/期末大作业、工程实训、大创及学科竞赛等项目立项与借鉴。除源码工程外还包含设计报告可参考的完整项目脉络以及QML界面与数据分离的清晰目录结构若需开发工具或学习资料可联系作者获取支持。项目仅供开源学习与技术交流请勿用于商业用途。1. 课程设计选 Qt QML 做地铁公交换乘系统先搞懂这份 demo 值不值得拆又到数据结构课程设计季选题永远是经典三件套通讯录、图书管理、地铁换乘。前两个大家闭着眼都能写第三个看着简单真要掏出图结构、最短路径、换乘方案生成时一堆人卡在算法会写但界面不会做上。这份基于 Qt QML 的地铁公交换乘系统 demo把 C 核心算法和 QML 界面的协作方式完整摊开了适合正做课设、毕设或者实训项目的学生。它的价值不在于成品多炫而在于你拿到的是一个能跑、能改、能扩展的骨架站点数据在 XML 里维护换乘算法在 C 层实现界面用 QML 独立渲染。本文直接按工程结构拆给你看同时把复现过程中最容易踩的坑一并交代省得你像我当初那样对着空白窗口怀疑人生。2. 拆解工程骨架QML 界面与 C 核心的职责切分拿到压缩包先别急着 Qt Creator 里一把梭先把文件清单过一遍。这个 demo 的工程组织和大部分课设项目不一样它不是纯 Widgets 程序而是 QML 界面 C 业务逻辑的组合文件分三类工程配置、C 源文件、QML 界面文件。2.1 pro 工程配置与资源文件先读懂这几个文件的协作关系TransferHelper_demo.pro是整个工程的入口它决定了 qmake 怎么组织这次构建。我先说结论最常见的翻车方式是用 Qt Creator 打开后直接点运行然后报一堆找不到 qml.qrc的错因为工程里 QML 文件是走资源系统加载的不是靠相对路径直读。QT quick qml xml CONFIG c11 SOURCES \ main.cpp \ dstation.cpp \ dline.cpp \ dtransinfo.cpp \ dtransmap.cpp \ dtransferplan.cpp \ dtransferplanlist.cpp \ dchkdata.cpp \ dpathinfo.cpp HEADERS \ dstation.h \ dline.h \ dtransinfo.h \ dtransmap.h \ dtransferplan.h \ dtransferplanlist.h \ dchkdata.h \ dpathinfo.h \ dstationname.h RESOURCES qml.qrc这份配置里最关键的一行是QT quick qml xml。quick 和 qml 是 QML 界面的两个模块xml 则给 C 层解析地铁站点数据用的。很多课设同学只写了widgets, 导致QQmlApplicationEngine直接编译失败。CONFIG c11指定了 C 标准老版本 Qt 默认没开c11如果代码里用了 lambda 或 auto 就会报错。再看qml.qrc资源文件RCC qresource prefix/ filemain.qml/file fileSelectView.qml/file fileStasel.qml/file fileRoadsView.qml/file fileqtquickcontrols2.conf/file fileSourceHanSansHWSC-Regular.otf/file /qresource /RCC注意这里字体文件直接注册进 qrc而不是放系统里等 Qt 自己找。我一般会把字体资源单独放一个fonts子目录但这 demo 直接平铺在根目录问题不大关键是你要理解qrc路径和相对路径的差别。QML 里加载字体时写qrc:/SourceHanSansHWSC-Regular.otf才能命中写相对路径很容易因为工作目录不对而变成黑方块。2.2 main.cpp 启动流程类型注册、数据装配、界面加载main.cpp 是整个程序的桥头堡。QML 界面里调用的那些DStation、DLine类型全是在 main.cpp 里注册进去的。#include QGuiApplication #include QQmlApplicationEngine #include QQmlContext #include dtransmap.h #include dtransferplanlist.h int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); app.setOrganizationName(TransitDemo); app.setApplicationName(TransferHelper); // 加载中文字体避免 QML 界面变成方块 QFont font(SourceHanSansHWSC); font.setPixelSize(14); app.setFont(font); QQmlApplicationEngine engine; // 核心地图数据负责站点、线路、换乘关系解析 DTransMap transMap; transMap.loadData(:/data/transdb.xml); // 换乘方案列表算法层生成的方案都放在这里 DTransferPlanList planList; planList.setMap(transMap); // 注册给 QML 使用 engine.rootContext()-setContextProperty(transMap, transMap); engine.rootContext()-setContextProperty(planList, planList); // 注册自定义 QML 类型允许 QML 里直接构造站点信息 qmlRegisterTypeDStation(Transit.Core, 1, 0, DStation); qmlRegisterTypeDTransferPlan(Transit.Core, 1, 0, DTransferPlan); engine.load(QUrl(QStringLiteral(qrc:/main.qml))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }参数说明setContextProperty把 C 对象以上下文属性形式暴露给 QML这样 QML 里直接写transMap.stationCount就能访问数据模型。qmlRegisterType则是注册完整类型允许 QML 里DStation {}这样直接创建对象。这两者的区别很重要——前者是单例模式后者是类型模式。这段代码是标准做法。我拆过不少课设项目很多学生把数据读取全部塞进 main.cpp几百行堆进去能跑但没法维护。这个 demo 把数据装载抽成了loadData(:/data/transdb.xml)从资源路径装载而非绝对路径这样部署到别的机器上不会因为路径不同而崩。2.3 数据层建模DStation 与 DLine 背后的图结构思路站点和线路是换乘系统的两大实体。dstation.h负责站点信息dline.h负责线路信息dtransmap.h负责把这两者整合成完整的线路-站点关联图。// dstation.h 核心结构 class DStation { public: QString name; // 站点显示名 int lineId; // 所属线路编号 int stationSeq; // 在线路中的序号 double lon, lat; // 经纬度或相对坐标 QVectorint transferLines; // 可换乘的其他线路编号 };// dline.h 核心结构 class DLine { public: int lineId; // 线路编号 QString name; // 线路名如 1号线 QVectorint stationSeqList; // 按顺序排列的站点序列 };为什么这样建模地铁换乘本质上是一种特殊的图。最常见的建模方式是站点为节点、相邻站为边但这个 demo 选择了线路为节点、公共站点为换乘边的建模方式。也就是说DTransMap里存的是线路之间的换乘矩阵而不是站点间的地铁图。这样做的好处是换乘次数路径上的边数最少换乘问题直接退化成最短路径问题算法实现和演示效果更直观。DTransMap提供的基本操作是stationsOfLine(lineId)和findTransferStation(lineA, lineB)。前者遍历返回线路上的站点名后者是关键——它遍历两个线路的站点集合找到交集这个交集站点就是换乘站。3. 核心换乘逻辑与算法实现从 BFS 到换乘方案生成数据结构课设的灵魂在算法界面只是表达。换乘系统的算法核心在多少个文件里体现dtransmap负责提供图数据dtransferplan负责单条换乘方案dtransferplanlist负责承载多条方案dchkdata负责数据合法性校验。这和教科书上图 路径 路径集合的三层结构完全对得上。3.1 邻接表构建把 XML 数据落成可计算的线路图先看数据怎么来。工程里带了好几个 XML 文件transdb.xml是总的换乘数据库stadb.xml是站点库db0.xml和db1.xml是分线路数据。XML 的优点是扩展性好加一条线路不需要改代码改数据就行缺点是解析容易出错且运行时无类型安全。!-- transdb.xml 简化示例 -- transdb line id1 name一号线 station id101 nameA站 transfer2/ station id102 nameB站 transfer/ station id103 nameC站 transfer3/ /line line id2 name二号线 station id201 nameD站 transfer1/ /line /transdbDTransMap::loadData的解析逻辑是 C 层一个有意思的模块。它用 QXmlStreamReader 而非 DOM 解析原因很实际QXmlStreamReader 是流式解析内存占用低而且接口更符合从头扫到尾的流程。DOM 需要对整棵树建模型这个 demo 的数据量不大所以无所谓但当你在课设答辩现场改数据文件时流式解析的调试体验会好很多因为错误定位更直接。解析完成后的构造逻辑我把代码结构写出来你自己对应去看 dtransmap.cppvoid DTransMap::loadData(const QString xmlPath) { QFile file(xmlPath); if (!file.open(QIODevice::ReadOnly)) { qWarning() Cannot open xmlPath; return; } QXmlStreamReader reader(file); int currentLineId -1; while (!reader.atEnd()) { reader.readNext(); if (reader.isStartElement()) { if (reader.name() line) { currentLineId reader.attributes().value(id).toInt(); DLine line; line.lineId currentLineId; line.name reader.attributes().value(name).toString(); m_lines.append(line); } else if (reader.name() station) { DStation st; st.name reader.attributes().value(name).toString(); st.lineId currentLineId; st.stationSeq reader.attributes().value(id).toInt(); m_stations.append(st); // 记录线路内部站点序列 m_lineStations[currentLineId].append(st.name); } } } buildTransferMatrix(); }这段代码里要注意m_lineStations是QHashint, QStringList它记录的是线路 → 站点名列表是后续算换乘的基础。buildTransferMatrix()是额外一步遍历所有站点如果某个站点的transfer属性非空就记录一条换乘关系。这一步在数据加载完就要做免得每次查换乘都全表扫描。3.2 换乘算法选型为什么最少换乘可以退化成最短路数据结构课里讲过的最短路径算法有 Dijkstra、Floyd、BFS换乘系统里到底用哪个先说结论最少换乘次数的场景用 BFS带权值时间、票价等的场景用 Dijkstra。这个 demo 的默认设计是换乘次数优先所以它的核心算法是 BFS 在线路图上的变体。为什么不是站点图上跑 BFS因为站点图太大了——某城市动辄几百个站点站间最短路径可能跨 20 站但换乘只有 2 次。如果以站点为节点做 BFS找到的是最少站数而不是最少换乘两者不一样。而这个 demo 显然想让答辩展示最少换乘这个亮点所以线路图建模是更聪明的策略。QListDTransferPlan DTransferPlanList::search(int startLine, int endLine) { QQueueint queue; QHashint, int prevLine; // 记录前驱线路 QHashint, int transferStation; // 记录从哪站换乘 QSetint visited; QListDTransferPlan results; queue.enqueue(startLine); visited.insert(startLine); prevLine[startLine] -1; while (!queue.isEmpty()) { int curLine queue.dequeue(); if (curLine endLine) { // 回溯构造方案 DTransferPlan plan; int trace curLine; while (trace ! -1) { plan.appendLineFront(trace); trace prevLine[trace]; } results.append(plan); break; // BFS 第一次到达即最优 } // 遍历与当前线路有换乘站的所有线路 const QSetint reachable m_map-transferLinesOf(curLine); for (int nextLine : reachable) { if (!visited.contains(nextLine)) { visited.insert(nextLine); prevLine[nextLine] curLine; transferStation[nextLine] m_map-findTransferStation(curLine, nextLine); queue.enqueue(nextLine); } } } return results; }逻辑说明transferLinesOf(curLine)返回与当前线路有公共换乘站的所有线路编号。找到目标线路后通过prevLine回溯出完整线路序列。transferStation记录了换乘站的站点名用于最终界面展示。参数说明这里prevLine和transferStation都是哈希表key 是线路编号value 分别是前驱线路和换乘站名空间复杂度 O(线路数)。visited防止环路死循环没有它两条互相换乘的线路会让 BFS 永远跑不完。这里有个细节容易被课设答辩老师追问为什么 BFS 找到的第一个方案就是最优换乘因为 BFS 按层扩展第一次到达目标时经过的边数一定是最少的。而这里边的含义是线路之间的换乘边所以直达0 次换乘跨一跳1 次换乘。3.3 方案生成与排序多方案输出与用户偏好不少课设要求给出所有可能的换乘方案并排序demo 里的dtransferplanlist不只是返回第一个结果。我做过多方案版本一般会给searchAll方法限制最大换乘次数为 3然后按总耗时或总站数排序。QListDTransferPlan DTransferPlanList::searchAll(int startLine, int endLine, int maxTransfers) { QListDTransferPlan allPlans; // 使用 DFS 深度限制避免环路 QListint path; QSetint visited; dfs(startLine, endLine, maxTransfers, path, visited, allPlans); // 按总站数排序站数少者排前 std::sort(allPlans.begin(), allPlans.end(), [](const DTransferPlan a, const DTransferPlan b) { return a.totalStops() b.totalStops(); }); return allPlans; }这是我改过的一个版本它的关键参数是maxTransfers。设置成 3 是为了防止方案爆炸——在复杂线网里允许 5 次换乘会生成十几种绕路方案没实际意义。排序用totalStops()做依据体现换乘少且路程短的朴素偏好。实际运行中你会发现 BFS 和 DFS 的差别BFS 保证第一个方案换乘最少但想拿次优第三优就得逐层限制DFS 加深度限制能一口气拿到所有方案但复杂度可能指数增长。课设场景建议用 DFS剪枝因为数据量小几十条线路且能直接应对列出所有方案这种题目要求。3.4 数据校验为什么缺一个闭合线路系统就崩dchkdata.cpp这个文件名容易被忽略但它承担了课设里异常处理的得分点。它的核心职责有两个检查 XML 数据里是否存在孤立线路没有任何换乘站的其他线路检查站点编号是否重复。bool DChkData::validate(const DTransMap map, QString errorMsg) { // 1. 检查每条线路是否至少有一个换乘站终点线除外 for (const DLine line : map.lines()) { int transferCount 0; for (const QString stationName : line.stationNames()) { if (map.stationByName(stationName).transferLines.size() 1) transferCount; } if (transferCount 0) { errorMsg QString(线路 %1 没有换乘站系统无法跨线).arg(line.name); return false; } } // 2. 检查换乘关系是否对称A能换BB也必须能换A for (const auto pair : map.transferPairs()) { if (!map.canTransfer(pair.second, pair.first)) { errorMsg 换乘关系不对称请检查 transdb.xml; return false; } } return true; }这段代码最容易被抄错的地方是换乘关系不对称。有些同学在 XML 里写了A站 transfer2但漏了在线路 2 上回填transfer1导致搜索时线路 1 能到线路 2反向搜索却不认识路。所以dchkdata这个模块别删答辩前跑一遍能救你一命。4. 界面交互与数据绑定SelectView、Stasel、RoadsView 怎么把数据画出来QML 界面是这个 demo 比较出彩的部分它能用很少的代码实现流畅的交互。但 QML 的调试体验比 Widgets 差得多排错成本高所以理解数据如何流到界面比抄界面代码更重要。4.1 SelectView 与 Stasel 站点选择QML 怎么拿 C 数据SelectView.qml是用户交互的起点通常负责让用户选择起点站和终点站。Stasel.qml从文件名推断是 station select 的缩写大概率是一个站点列表弹层让用户在列表里挑选站点名。界面拿 C 数据的方式在 QML 里是模型/视图模式。在 main.cpp 里setContextProperty注入的对象可以直接作为 QML 里的全局变量使用// SelectView.qml 简化逻辑 import QtQuick 2.15 import QtQuick.Controls 2.15 Column { property string startStation: property string endStation: ComboBox { id: startCombo width: parent.width model: transMap.allStationNames() // C 方法直接返回 QStringList onActivated: { startStation currentText planList.setStartStation(currentText) } } Button { text: 查询方案 enabled: startStation.length 0 endStation.length 0 onClicked: planList.search() } }transMap.allStationNames()是注册到上下文属性里的 C 对象方法。QML 里调用 C 方法靠的是 Qt 的元对象系统——只要在类声明里加了Q_INVOKABLE或者方法是public slotQML 就能直接调。演示时注意如果某个 C 方法在 QML 里调用无反应常见的原因是方法签名带了QString引用参数QML 传参类型对不上。解决办法是改成Q_INVOKABLE QStringList allStationNames()这样简单的返回值类型。4.2 RoadsView 线路图渲染换乘方案的可视化逻辑RoadsView.qml是结果展示视图把换乘方案以横向路线图的方式画出来。它的做法通常是一个ListView每个delegate是一条线路段段内数据显示站点名和换乘站。// RoadsView.qml 渲染一个换乘方案 ListView { model: planList.currentPlan.stationList() // QStringList delegate: Row { spacing: 8 Text { text: modelData; font.family: appFont } Rectangle { width: 6; height: 6; radius: 3 color: index 0 ? green : (modelData.includes(换乘) ? orange : gray) } } }这里的聪明用法是把换乘站信息放进字符串本身例如A站 (换乘2号线)QML 里直接展示不用再做复杂的对象模型。缺点是不方便做二次计算但对于课设展示足够。我拆的时候注意到这个 demo 的RoadsView用了Canvas元素来画线路图而不是纯 ListView。Canvas 的好处是支持画线、画圆、画箭头能做出接近真实地铁图的交互效果坑在于 Canvas 的坐标系默认是设备像素高 DPI 屏上会显示模糊。解决办法是在onPaint里绑定width和height的devicePixelRatio缩放。4.3 字体方案与跨平台显示花在字体的时间比写代码还多这个工程把一堆字体SourceHanSansHWSC、Arial、ARIALN 等直接放进资源文件不是没道理的。QML 默认字体在中文环境下的渲染经常翻车——Windows 下可能显示宋体Linux 下可能直接变方块macOS 下粗细不统一。demo 先用app.setFont在 C 层设置全局字体再从 qrc 加载字体文件这是给后续做显示保底的。// main.qml 里的字体加载 FontLoader { id: chineseFontLoader source: qrc:/SourceHanSansHWSC-Regular.otf onStatusChanged: if (status FontLoader.Ready) console.log(字体加载成功) }注意FontLoader的source用的是 qrc 路径不是本地路径。你如果改成本地相对路径跑Windows 上可能没问题Ubuntu 上就崩因为起点工作目录不同。复制到别的电脑执行时qrc 路径是唯一稳定的方案——它不依赖当前工作目录这也是资源文件存在的意义。5. 避坑与常见问题排查换乘系统 demo 复现时的五个典型坑这个 demo 我复现过两次第一次在 UI 层翻车第二次在算法层翻车。下面按现象 → 原因 → 解决的方式列出最常见的五个问题覆盖从编译到运行的完整链路。5.1 现象点击运行后 QML 窗口一片空白这个坑出现的频率极高。.pro工程加载后直接 run弹出窗口但里面什么都没有控制台往往有QML module not found之类的提示。原因qml.qrc中的资源路径没配对。比如工程里main.qml实际路径带子目录但engine.load()里写了qrc:/main.qml大小写不一致也会失败。另一个原因是 Qt Creator 打开工程时没有正确识别RESOURCES qml.qrc导致资源根本没有编译进二进制。解决先确认qrc文件里的文件路径是相对qrc文件所在目录的。然后在main.cpp里给engine.load的 QUrl 加打印用qqWarning() engine.url()看实际路径。最稳的做法是QUrl(qrc:/main.qml)和qrc里prefix/ 根目录直接放main.qml配对。5.2 现象站点中文名全部显示为方块这个坑坑了大量 Linux 用户Windows 上反而不常见。QT 在 Linux 上默认找不到合适的中文字体于是 fallback 失败画出一排方块。原因QGuiApplication初始化的字体只设置了 font family但系统中根本没有这个 family 对应的字体文件。demo 把字体文件放进 qrc 只算第一步第二步没在main.cpp里QFontDatabase::addApplicationFont注册字体只管放不生效。解决在 C 层加载 qrc 字体并用QFontDatabase注册int fontId QFontDatabase::addApplicationFont(:/SourceHanSansHWSC-Regular.otf); QString family QFontDatabase::applicationFontFamilies(fontId).at(0); app.setFont(QFont(family));这个顺序不能反先 add 字体再取 family再 setFont。我见过不少同学直接写app.setFont(QFont(SourceHanSansHWSC))但没调用 addApplicationFontQt 找不到这个字体白搭。如果你发现字体文件在 qrc 里但加载失败检查 otf 文件是否完整——压缩包里的字体文件被某些杀毒软件拦截后会出现 0 字节文件导致解析失败。5.3 现象换乘方案始终为空算法不返回结果界面、字体都正常选起点终点后点查询结果列表空荡荡。原因99% 是换乘关系不对称。XML 里线路 1 的站点标了transfer2但线路 2 对应站点没有反向标注。BFS 在扩展transferLinesOf时只能找到单向关系目标线路永远不在可达集合里。另外也可能是findTransferStation按站点名精确匹配但 XML 里一个写人民广场一个写人民广场 多了空格匹配失败。解决这个排查要看dchkdata.cpp的校验输出或者自己在 main.cpp 里打印线路之间的可达矩阵。我第一次跑通时用了最笨但最有效的方法写一个嵌套循环打印所有换乘关系对用肉眼看一眼数据对称性。之后凡是扩展了线路数据我都会先跑一遍校验逻辑不要直接跑 GUI。5.4 现象改了 XML 数据文件界面毫无变化这是 Qt 工程最常见的认知误区之一。尤其当你有多个 XML 文件stadb.xml、transdb.xml、db0.xml、db1.xml时不搞清楚哪个是真正运行时加载的改一晚上都没用。原因Qt Creator 默认开启 shadow build生成的可执行文件在 build 目录里XML 文件如果走的是QFile(transdb.xml)这种方式它会去当前工作目录找文件。直接运行时工作目录是 build 目录你改的是源码目录下的 XML永远读不到。如果 XML 文件被加入 qrc则改动后需要重新编译资源只保存文件不重新构建二进制里的资源不会更新。解决确认你的加载代码走的是哪种方式。如果代码是QFile(:/data/transdb.xml)走 qrc 资源改数据后必须重新编译整个工程如果是相对路径就把 XML 文件复制到 build 目录下。我后来习惯统一用 qrc因为 shadow build 模式下源码路径不稳定qrc 至少保证行为一致。5.5 现象不同电脑上界面显示效果不一样同一份工程在自己电脑上字体、间距都正常拷到另一个人电脑上布局错乱按钮文字被截断。原因Qt 版本不一致导致的 QML 渲染差异以及系统字体 fallback 结果不同。Text元素没设置font.pixelSize时不同平台上默认字体大小不一样布局就崩。解决在所有 Text 元素或者全局设置里显式指定font.pixelSize不要依赖系统默认。字体资源文件随工程走确保解析后使用同一 family。另外qtquickcontrols2.conf里可以强制选择Material风格能减少平台默认样式差异。[Controls] StyleMaterial [Universal] ThemeLight Accent#2196F3这个配置文件在工程里是合理的它会统一按钮、输入框的默认样式让你在答辩演示时不用反复调整控件尺寸。缺点是 Material 风格在低配机器上有点吃性能但课设场景无所谓。6. 把 demo 改造成可答辩的完整项目数据扩展、参数调整与效果验证你拿到手的是个 demo但答辩时不能直接说我下了个 demo。它能不能变成你的课设关键看你能否在源码基础上做三个动作扩展数据量、调整算法偏好、验证完整流程。6.1 扩展数据往 transdb.xml 里加一条新线路复制工程里 db0.xml 或 db1.xml 的线路结构在transdb.xml中新增一个line段落。注意四个原则线路编号全局唯一站点 ID 不与其他线路重复换乘关系必须双向标注新线路至少包含一个与既有线路的公共站点。按这个原则加入两条新线路后用第 5 章的校验代码跑一遍确认不会报孤立线路。6.2 调整排序偏好把最少换乘改成最少耗时核心类dtransferplan里加一个double totalTime属性在构建换乘方案时累加每段线路的站间时间。dtransferplanlist的排序 lambda 从totalStops改成totalTime。你可以在dtransinfo.h里定义参数表比如每站 2 分钟、换乘步行 5 分钟从而让方案排序更贴近实际。这样一改答辩时就能回答如果加入权重怎么处理的追问比原 demo 多一个可谈的亮点。6.3 验证与演示一页 checklist演示前按这个清单走一遍A 站到 B 站直达有结果A 站到 C 站跨线换乘 1 次正确不存在路径的两站返回空提示而非崩溃连续查询不同方案时界面不残留旧数据关闭窗口后进程完全退出无后台残留。我一般还会录入一个错误 XML 来演示程序能弹错误提示而不是闪退——这个异常处理的表现往往是拿高分的关键。我从那以后再做这类课设都强制先跑数据校验、再看算法结果、最后碰界面顺序反过来全是坑。这份资源包把界面、算法、数据三层的职责分得很清楚你复刻时保持这个分层习惯后续扩展才不会把自己绕晕。希望帮到你。本文还有配套的精品资源点击获取