HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十六):【响应式布局】折叠屏与平板完美适配------一套代码,多端呈现
摘要 :《灵犀厨房》从第1篇到现在,一直以手机竖屏为基准设计。但随着折叠屏手机的普及、平板用户的增长,单一布局已无法满足多设备场景。本篇利用现有的
Breakpoint.ets断点系统(SM/MD/LG)和 ArkUI 的响应式能力,让首页瀑布流和菜谱详情页在折叠屏和平板上自动切换为更高效的多列/双栏布局------无需为每个设备单独写页面,只需在现有布局中添加断点分支。
一、引言:当手机布局撑满平板屏幕
一个有趣的实验:把《灵犀厨房》安装到平板上,看看首页的推荐瀑布流会怎样。
结果令人失望:两列卡片被拉伸到平板宽度,每张卡片宽得离谱,封面图从精致的菜谱照片变成了模糊的拉伸图。而屏幕两侧留有大片空白------既难看,又浪费。
这不是 Bug,但比 Bug 更致命。它告诉用户:"这个 App 不是为你的设备设计的。"
| 设备 | 原布局表现 | 问题 |
|---|---|---|
| 手机竖屏(< 600vp) | 2 列瀑布流 | ✅ 设计基准 |
| 折叠屏展开(600-840vp) | 2 列瀑布流 + 大量空白 | ❌ 卡片过宽,屏幕利用率低 |
| 平板横屏(≥ 840vp) | 2 列瀑布流 + 海量空白 | ❌ 严重浪费空间,观感差 |
| 平板上的菜谱详情 | Swiper 全屏滑动 | ❌ 步骤文字被拉成一行 40 个字 |
🎯 本篇目标:用最少的新增代码(约 90 行),让《灵犀厨房》在三种设备尺寸上都呈现最佳布局------手机 2 列、折叠屏 3 列、平板 4 列;菜谱详情在平板上自动切换为左列表右详情的双栏模式。
二、核心原理:断点系统的设计哲学
2.1 为什么是"断点"而非"自适应"?
两种响应式策略:
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 自适应(流体) | 用百分比/fr 做连续缩放 |
任何尺寸都"能看" | 极端尺寸下布局失控 |
| 断点(自适应 + 开关) | 分段决策:SM/MD/LG 各有独立布局 | 每个区间体验最优 | 需要预先定义断点阈值 |
《灵犀厨房》选择后者------因为在 300vp 的折叠屏外屏和 1200vp 的平板横屏之间,不存在一个"连续缩放"能同时兼顾的布局。断点不是限制,而是承认设备的差异性,并为之量身定制。
2.2 断点系统的三层阈值
#mermaid-svg-LsSdOaNlrDo3GQNN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LsSdOaNlrDo3GQNN .error-icon{fill:#552222;}#mermaid-svg-LsSdOaNlrDo3GQNN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LsSdOaNlrDo3GQNN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LsSdOaNlrDo3GQNN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LsSdOaNlrDo3GQNN .marker.cross{stroke:#333333;}#mermaid-svg-LsSdOaNlrDo3GQNN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LsSdOaNlrDo3GQNN p{margin:0;}#mermaid-svg-LsSdOaNlrDo3GQNN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LsSdOaNlrDo3GQNN .cluster-label text{fill:#333;}#mermaid-svg-LsSdOaNlrDo3GQNN .cluster-label span{color:#333;}#mermaid-svg-LsSdOaNlrDo3GQNN .cluster-label span p{background-color:transparent;}#mermaid-svg-LsSdOaNlrDo3GQNN .label text,#mermaid-svg-LsSdOaNlrDo3GQNN span{fill:#333;color:#333;}#mermaid-svg-LsSdOaNlrDo3GQNN .node rect,#mermaid-svg-LsSdOaNlrDo3GQNN .node circle,#mermaid-svg-LsSdOaNlrDo3GQNN .node ellipse,#mermaid-svg-LsSdOaNlrDo3GQNN .node polygon,#mermaid-svg-LsSdOaNlrDo3GQNN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LsSdOaNlrDo3GQNN .rough-node .label text,#mermaid-svg-LsSdOaNlrDo3GQNN .node .label text,#mermaid-svg-LsSdOaNlrDo3GQNN .image-shape .label,#mermaid-svg-LsSdOaNlrDo3GQNN .icon-shape .label{text-anchor:middle;}#mermaid-svg-LsSdOaNlrDo3GQNN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LsSdOaNlrDo3GQNN .rough-node .label,#mermaid-svg-LsSdOaNlrDo3GQNN .node .label,#mermaid-svg-LsSdOaNlrDo3GQNN .image-shape .label,#mermaid-svg-LsSdOaNlrDo3GQNN .icon-shape .label{text-align:center;}#mermaid-svg-LsSdOaNlrDo3GQNN .node.clickable{cursor:pointer;}#mermaid-svg-LsSdOaNlrDo3GQNN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LsSdOaNlrDo3GQNN .arrowheadPath{fill:#333333;}#mermaid-svg-LsSdOaNlrDo3GQNN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LsSdOaNlrDo3GQNN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LsSdOaNlrDo3GQNN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LsSdOaNlrDo3GQNN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LsSdOaNlrDo3GQNN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LsSdOaNlrDo3GQNN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LsSdOaNlrDo3GQNN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LsSdOaNlrDo3GQNN .cluster text{fill:#333;}#mermaid-svg-LsSdOaNlrDo3GQNN .cluster span{color:#333;}#mermaid-svg-LsSdOaNlrDo3GQNN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LsSdOaNlrDo3GQNN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LsSdOaNlrDo3GQNN rect.text{fill:none;stroke-width:0;}#mermaid-svg-LsSdOaNlrDo3GQNN .icon-shape,#mermaid-svg-LsSdOaNlrDo3GQNN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LsSdOaNlrDo3GQNN .icon-shape p,#mermaid-svg-LsSdOaNlrDo3GQNN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LsSdOaNlrDo3GQNN .icon-shape .label rect,#mermaid-svg-LsSdOaNlrDo3GQNN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LsSdOaNlrDo3GQNN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LsSdOaNlrDo3GQNN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LsSdOaNlrDo3GQNN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LG ≥ 840vp
平板横屏 / 大折叠屏
4 列瀑布流
菜谱详情 → 双栏
MD 600-840vp
折叠屏展开 / 小平板
3 列瀑布流
SM < 600vp
手机竖屏
2 列瀑布流
图一解读:三个断点对应三种典型的设备使用场景。SM 是设计基准------所有页面在此断点下做主要开发。MD 是第一个响应式升级------3 列让折叠屏的用户感到"这个 App 适配了我的展开模式"。LG 是信息密度最高的场景------4 列和双栏布局充分利用大屏空间,让 App 看起来像原生平板应用。
2.3 已有的断点基础设施
在第 3 篇中,我们建立了轻量断点系统 Breakpoint.ets。Index.ets 的 aboutToAppear 已经监听了 windowSizeChange 事件,动态更新 currentBreakpoint。本篇不新增任何断点基础设施,只利用已有能力。
三、实战一:首页瀑布流动态列数
3.1 从硬编码到动态决策
原来 Grid 的列数是写死的:
typescript
.columnsTemplate('1fr 1fr') // 永远2列,平板也2列------浪费屏幕
改为动态方法:
typescript
.columnsTemplate(this.getGridColumns())
3.2 列数决策方法
typescript
private getGridColumns(): string {
if (this.currentBreakpoint === Breakpoint.LG) return '1fr 1fr 1fr 1fr'; // 平板:4列
if (this.currentBreakpoint === Breakpoint.MD) return '1fr 1fr 1fr'; // 折叠屏:3列
return '1fr 1fr'; // 手机:2列
}
3.3 响应链路
用户旋转/展开设备
→ windowSizeChange 事件触发
→ currentBreakpoint 更新
→ ArkUI 检测到 @Local 变化
→ build() 重新渲染
→ getGridColumns() 返回新列数
→ Grid 自动重新布局 ✅
整个过程无需任何手动刷新调用------ArkUI 的 @Local 响应式系统会自动处理。这是 HarmonyOS 声明式 UI 的核心优势:你只描述"数据如何决定布局",框架负责"何时重绘"。
四、实战二:菜谱详情页 LG 双栏布局
4.1 布局选择的关键决策:MD 为什么不双栏?
一个常见问题是:"折叠屏展开有 680vp,为什么不做双栏?"
答案是信息密度 vs 可用性的权衡。680vp 减去安全区域后,有效内容宽度约 600vp。如果拆成双栏,左侧列表只剩约 180vp------中文步骤标题需要 10-15 字(约 150-200vp),刚好塞满甚至截断。而 Swiper 沉浸式在这种宽度下体验更好------用户焦点集中,不会被过小的侧边栏分散注意力。
LG(≥840vp)才触发双栏的阈值,是因为此时左侧 35%(约 250vp)足够容纳中文步骤标题,右侧 65%(约 450vp)有充足的呼吸空间。
4.2 build() 分流逻辑
typescript
build() {
Stack() {
// LG 平板 → 双栏布局
if (this.flowState === FlowState.LOCAL && this.currentBreakpoint === Breakpoint.LG) {
this.buildLGLayout()
}
// SM/MD 手机 → Swiper 布局
if ((this.flowState === FlowState.LOCAL || ...) && this.currentBreakpoint !== Breakpoint.LG) {
this.buildPhoneLayout()
}
// ... 流转后的平板/智慧屏布局不变 ...
}
}
4.3 双栏布局结构
┌──────────────────────────────────────────────┐
│ 🍳 番茄牛腩煲 │
├─────────────────┬────────────────────────────┤
│ 左侧 35% │ 右侧 65% │
│ │ │
│ ● 第1步 焯水 │ 第 1 步 │
│ ○ 第2步 炒香 │ │
│ ○ 第3步 炖煮 │ 焯水:牛腩切块冷水入锅... │
│ ○ 第4步 调味 │ │
│ │ │
│ 🥬 食材列表 │ [← 上一步] 1/4 [下一步 →] │
└─────────────────┴────────────────────────────┘

左侧 List 可点击切换当前步骤,右侧实时更新步骤详情。与流转后的 buildTabletLayout 不同------LG 布局不包含分布式流转的设备标识,是纯本机的平板适配。
#mermaid-svg-FsdVDUezotKBzYpL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FsdVDUezotKBzYpL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FsdVDUezotKBzYpL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FsdVDUezotKBzYpL .error-icon{fill:#552222;}#mermaid-svg-FsdVDUezotKBzYpL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FsdVDUezotKBzYpL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FsdVDUezotKBzYpL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FsdVDUezotKBzYpL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FsdVDUezotKBzYpL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FsdVDUezotKBzYpL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FsdVDUezotKBzYpL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FsdVDUezotKBzYpL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FsdVDUezotKBzYpL .marker.cross{stroke:#333333;}#mermaid-svg-FsdVDUezotKBzYpL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FsdVDUezotKBzYpL p{margin:0;}#mermaid-svg-FsdVDUezotKBzYpL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FsdVDUezotKBzYpL .cluster-label text{fill:#333;}#mermaid-svg-FsdVDUezotKBzYpL .cluster-label span{color:#333;}#mermaid-svg-FsdVDUezotKBzYpL .cluster-label span p{background-color:transparent;}#mermaid-svg-FsdVDUezotKBzYpL .label text,#mermaid-svg-FsdVDUezotKBzYpL span{fill:#333;color:#333;}#mermaid-svg-FsdVDUezotKBzYpL .node rect,#mermaid-svg-FsdVDUezotKBzYpL .node circle,#mermaid-svg-FsdVDUezotKBzYpL .node ellipse,#mermaid-svg-FsdVDUezotKBzYpL .node polygon,#mermaid-svg-FsdVDUezotKBzYpL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FsdVDUezotKBzYpL .rough-node .label text,#mermaid-svg-FsdVDUezotKBzYpL .node .label text,#mermaid-svg-FsdVDUezotKBzYpL .image-shape .label,#mermaid-svg-FsdVDUezotKBzYpL .icon-shape .label{text-anchor:middle;}#mermaid-svg-FsdVDUezotKBzYpL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FsdVDUezotKBzYpL .rough-node .label,#mermaid-svg-FsdVDUezotKBzYpL .node .label,#mermaid-svg-FsdVDUezotKBzYpL .image-shape .label,#mermaid-svg-FsdVDUezotKBzYpL .icon-shape .label{text-align:center;}#mermaid-svg-FsdVDUezotKBzYpL .node.clickable{cursor:pointer;}#mermaid-svg-FsdVDUezotKBzYpL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FsdVDUezotKBzYpL .arrowheadPath{fill:#333333;}#mermaid-svg-FsdVDUezotKBzYpL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FsdVDUezotKBzYpL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FsdVDUezotKBzYpL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FsdVDUezotKBzYpL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FsdVDUezotKBzYpL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FsdVDUezotKBzYpL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FsdVDUezotKBzYpL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FsdVDUezotKBzYpL .cluster text{fill:#333;}#mermaid-svg-FsdVDUezotKBzYpL .cluster span{color:#333;}#mermaid-svg-FsdVDUezotKBzYpL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FsdVDUezotKBzYpL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FsdVDUezotKBzYpL rect.text{fill:none;stroke-width:0;}#mermaid-svg-FsdVDUezotKBzYpL .icon-shape,#mermaid-svg-FsdVDUezotKBzYpL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FsdVDUezotKBzYpL .icon-shape p,#mermaid-svg-FsdVDUezotKBzYpL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FsdVDUezotKBzYpL .icon-shape .label rect,#mermaid-svg-FsdVDUezotKBzYpL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FsdVDUezotKBzYpL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FsdVDUezotKBzYpL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FsdVDUezotKBzYpL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 窗口 ≥ 840vp
窗口 < 840vp
📋 LG:双栏
左侧 35%:步骤列表
(可滚动点击)
右侧 65%:步骤详情
(大字体 + 食材清单)
底部导航栏
📱 SM/MD:Swiper 沉浸式
全屏滑动步骤
步骤文字居中 80% 宽
底部步骤列表
图二解读 :两种布局不是"覆盖"关系,而是"切换"关系。Swiper 在中小屏幕提供沉浸式焦点,双栏在大屏幕提供信息密度和快速导航。用户旋转或展开设备时,布局会自动无缝切换------当前选中的步骤索引在切换过程中保持不变。

4.4 与已有布局的兼容
RecipeDetailPage 中原本已有三个布局:
| 布局方法 | 触发条件 | 用途 |
|---|---|---|
buildPhoneLayout() |
本机模式 SM/MD | Swiper 沉浸式 |
buildTabletLayout() |
流转到平板(分布式) | 分布式流转后的平板布局 |
buildSmartScreenLayout() |
流转到智慧屏 | 视频播放 + 步骤 |
buildLGLayout() |
本机模式 LG(本篇新增) | 本机平板大屏双栏 |
buildLGLayout() 与 buildTabletLayout() 的区别:前者是本机断点判断,后者是分布式流转后的目标设备布局。两者不冲突------前者通过 currentBreakpoint === Breakpoint.LG 触发,后者通过 flowState === FlowState.FLOWED 触发。
五、代码增删改清单
| 文件 | 新增/修改 | 行数 | 说明 |
|---|---|---|---|
Index.ets |
修改 | +6 | Grid columnsTemplate 改为动态 getGridColumns() |
RecipeDetailPage.ets |
修改 | +80 | build() 增加 LG 分流 + 新增 buildLGLayout() |
RecipeDetailPage.ets |
修改 | +5 | aboutToAppear 新增断点初始化 |
六、设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| LG 才触发双栏,MD 保持 Swiper | 680vp 折叠屏展开,Swiper 体验优于拥挤双栏 | 左侧列表需要至少 200vp 才不截断中文 |
getGridColumns() 返回字符串而非数字 |
columnsTemplate('1fr 1fr 1fr 1fr') 直接做 CSS Grid 模板 |
比数值转换更直观,也便于未来做不等宽列 |
| 双栏左侧 35% / 右侧 65% | 步骤列表中文需 10-15 字宽度 | 65% 的详情区有充足呼吸空间,适合大字体阅读 |
新增独立 buildLGLayout() 而非修改现有 |
与分布式流转布局隔离 | 本机断点和分布式流转是两套触发机制,不应耦合 |
| 不新增断点基础设施 | 复用已有 Breakpoint.ets + windowSizeChange |
最小侵入原则 |
七、本阶段总结与下篇预告
本篇用约 90 行新增代码,让《灵犀厨房》在折叠屏和平板上从"勉强能看"变为"量身定做":
- 动态瀑布流:手机 2 列 → 折叠屏 3 列 → 平板 4 列,屏幕越大信息密度越高
- 菜谱双栏布局:平板横屏自动切换为左列表右详情,像原生平板应用
- 零破坏性:手机布局完全不受影响,SM/MD 用户的体验不变
- 断点决策清晰:MD 不双栏是有意为之------拥挤双栏体验不如沉浸式 Swiper
现在的大屏体验:
📱 手机竖屏 → 打开折叠屏 → 瀑布流从 2 列变成 3 列 → 完全展开 → 菜谱详情自动切换双栏!
下篇预告:第 27 篇《并发优化:TaskPool 加速图片分析》。让 AI 食材识别在后台线程运行,不再阻塞 UI------手指滑动依然流畅,识别结果静默返回。这是性能优化的第一步。
📚 本系列持续更新中:下一篇将进入并发编程领域,让 App 的多任务处理能力上一个大台阶。
🔗 专栏入口:《HarmonyOS6.1全场景实战》合集
📦 获取基线版本源码包 :包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对您有所帮助,麻烦您动动发财之手点赞 👍、收藏 ⭐ 和评论 💬。谢谢大家!!