14 KiB
响应式适配计划(Popiart SkillHub 前端)
本文档为移动端响应式适配的执行计划,按阶段推进。分支:
responsive(切自version/1.2.14)。
Context(背景与目标)
本项目 README 定位为 PC 端(≥1280px)网站,移动端基本未做真正重排。当前"适配"靠 src/hooks/useViewportScale.ts 以 1920 设计稿等比缩放,窄屏体验差、弹窗溢出、侧栏挤占。现需让整站在移动端可用。
硬约束:只做 UI 适配,不修改任何业务逻辑 / 数据流 / 接口。
三项决策:
- 移动端导航 = 底部 Tab 栏,纯 CSS 实现(侧栏在窄屏 reflow 到底部,不做抽屉、不加开合 state)。
- 断点范围 = 手机 + 平板(多档断点)。
- 改动边界 = 允许最小"启用型"改动:修 viewport meta、新增
useIsMobile(matchMedia 封装)。不碰任何业务逻辑。
三个关键前提(决定方案骨架):
- viewport 被锁死:
index.html:17的<meta name="viewport" content="width=1280, user-scalable=no">覆盖了第 7 行的width=device-width,真机被强制按 1280 宽渲染。不修此行,一切响应式在真机失效 —— 第 0 步必做。 - 900px 是天然主断点:
useViewportScale.ts:17的LAYOUT_SCALE_DISABLE_BELOW = 900,窗口 <900px 时自动关闭等比缩放交给媒体查询。所有"重排型"断点必须 ≤900px,否则与缩放坐标系打架。≥900px(含平板横屏 1024)仍在缩放系统内,只做轻量微调,不做重排。 - 样式几乎全在 SCSS,Tailwind 基本空转:2724 处 className 绝大多数是 SCSS 语义化 BEM 类;Tailwind 响应式变体
sm:/md:/lg:用了 0 处,原子类仅 ~19 处。Tailwind 的断点/栅格只在 className 生效,无法作用于.scss里的@media,而本次要改的 77 处@media全在 SCSS。→ 结论:主战场是手写 SCSS@media,不重写 JSX 为原子类,也不自建平行 token 系统。 - Arco 用得极少(仅 7 个组件):Message(35)/Dropdown(14)/Modal(8)/Input(6)/Tooltip(6)/Button(3)/Spin(2)。未用栅格 Row/Col、未用 Table、未用 Form——布局 100% 原生 SCSS,Arco 帮不上布局响应式。唯一痛点是 Modal 固定宽度,且散在约 9 处(见阶段 2)。无
ConfigProvider。
断点体系(对齐 Tailwind 默认,不自建平行系统)
数值直接沿用 Tailwind v4 默认口径,仅保留一个自定义主断点 900(与缩放开关硬耦合):
| 名称 | 范围 | 对应 Tailwind | 处理策略 |
|---|---|---|---|
| desktop | ≥1024px | ≥lg | 现状 + 等比缩放,基本不动 |
| tablet-landscape | 900–1024px | lg 区间 | 仍在缩放系统内,仅轻量微调,不重排 |
| mobile-main(主断点) | ≤900px | 自定义 | 缩放已自动关闭,重排主战场:侧栏→底部 Tab、多列→单/双列、弹窗→全屏 |
| tablet-portrait | ≤768px | md | 进一步收敛单列、隐藏次要信息 |
| small-phone | ≤640px | sm | 最小尺寸微调:字号、间距、图标化、按钮全宽 |
统一用
max-width媒体查询(与现有方向一致)。现有 77 处@media不重构;新增的直接写@media (max-width: 900/768/640px),数值口径与 Tailwind 一致,将来若在新 JSX 用md:/lg:亦不冲突。是否加 SCSS@mixin respond-to属可选糖,非必需。
阶段 0 · 全局基建(先做,是后续所有改造的抓手)
基建刻意做薄:不引入平行 token 系统、不重构现有 @media、不把 JSX 改写成 Tailwind 原子类。
| # | 事项 | 文件/位置 | 说明 |
|---|---|---|---|
| 0.1 | 修 viewport | index.html:17 |
删除/改为 width=device-width, initial-scale=1(保留第 7 行即可)。这是一切前提 |
| 0.2 | 约定断点数值 | 无需新文件;沿用 Tailwind 默认口径 | 统一用 900(主)/768(md)/640(sm)/1024(lg)。新增 @media 直接写数值;如需 SCSS mixin 仅加一个轻量 @mixin respond-to(可选)。若个别新 JSX 要响应式类,直接用 Tailwind 自带 md:/lg:,不自建 |
| 0.3 | useIsMobile hook |
新增 src/hooks/useIsMobile.ts + 挂 src/hooks/index.ts barrel |
matchMedia("(max-width: 900px)") 封装,与 useViewportScale 的 900 对齐;仅供少数"隐藏 PC 专属 UI"的条件渲染(不改业务逻辑)。底部 Tab 为纯 CSS,不依赖它 |
| 0.4 | 全局兜底 | src/index.css |
全局 *{min-width:0} 防 flex/grid 溢出、img/video{max-width:100%}、overflow-x:hidden 兜底、移动端 body 字号基准 |
阶段 1 · 应用外壳 Layout → 底部 Tab 栏(纯 CSS)
核心文件:src/layouts/Layout/Layout.tsx、src/layouts/Layout/Layout.scss
改造锚点(均在 Layout.scss):
--layout-sidebar-width: 90px(:50-51)、grid-template-columns(:61)、grid-template-rows、.layoutShell__sidebar(:840)、.layoutShell__topbar(:159)、.layoutShell__contentFrame(:1069)。
≤900px 的 CSS 重排(不动 TSX 结构、不加 state):
.layoutShell网格从"左栏 + 右内容"改为纵向三行:顶栏 / 内容区(1fr) / 底部导航。grid-template-columns: 1fr,grid-template-rows: auto 1fr auto。.layoutShell__sidebar:grid-row改为最后一行、grid-column:1;flex-direction: row、横向排布、position: sticky/fixed; bottom:0、高度约 56–64px、overflow-x:auto(导航项多时横滑)、加env(safe-area-inset-bottom)安全区。品牌 logo(__brand) 在窄屏隐藏。主导航__sideNav与底部__footer(subscribe/profile)合并为一条底部 Tab(复用现有LAYOUT_SIDE_NAV_ITEMS(Layout.tsx:93) /LAYOUT_FOOTER_NAV_ITEMS(Layout.tsx:286) 渲染出的 DOM,纯 CSS 重排,不改数据)。.layoutShell__topbar:精简——保留登录/头像/积分,隐藏次要项(教程、客服二维码可收进头像菜单,纯 CSS 隐藏);grid-column:1。.layoutShell__contentFrame/__contentScroll:单列,底部留出 Tab 栏高度的padding-bottom。- 与缩放系统协调:≤900px
useViewportScale已返回 1(不缩放),故.layoutShellScaleInner(Layout.scss:32-45)在窄屏zoom/transform自动为 1,无需改 hook;仅需确认 spacer 占位在 scale=1 时不产生多余留白(Layout.scss内layoutShellScaleSpacer窄屏归零)。
阶段 2 · 弹窗统一改造(抓手:src/dialog/index.tsx)
主注入点:openDialogWithHistory()(src/dialog/index.tsx ~L96-190)内 barrel 弹窗汇聚到同一个 Modal.confirm({...config})。在此对 config.style 做窄屏降级可一处覆盖多数 barrel 弹窗:≤900px 统一 width:100%、maxWidth:100vw、近全屏(复用已有 layoutFullscreenDetailModalShellStyle(:57-67)作窄屏 shell)。
⚠️ Modal 非单点收口,另有约 9 个散点需单独覆盖:
- 直接调
Modal.confirm的(3 文件):src/dialog/pay-subscribe/index.tsx、src/pages/Create/panels/FolderPanel/index.tsx、src/pages/Profile/index.tsx。 - 直接写
<Modal>JSX 的(5 文件):src/dialog/LoginRegisterModal/index.tsx、src/dialog/asset-details/index.tsx及其子AssetExpandImageModal/AssetDetailRepairModal、src/components/DeleteConfirmModal/index.tsx。 - 每处按同一窄屏 shell 规则给
style/className。可考虑抽一个共享的窄屏 modal style 常量(放src/dialog/index.tsx导出)复用,避免重复。
不走 barrel 的自定义 overlay: src/dialog/alice-live-class/(--dialog-layout-scale 缩放)、src/dialog/app-download-mac/。
弹窗逐个(≤900px 目标 = 全屏/单列,内部并排→堆叠):
| 弹窗目录 | 现状宽度 | 窄屏风险 | 适配要点 |
|---|---|---|---|
character-picker |
1699px/80vw | 极高 | 重灾区:固定 1699 画布 → 全屏、卡片网格降为 2/1 列 |
material-picker |
1699px/80vw | 极高 | 同上,全屏 + 单列 |
voice-audio-upload |
1042px/92vw | 高 | 全屏,波形/表单纵向堆叠 |
points-detail-modal |
fit/100vw-32 | 高 | 大表格:改卡片式列表或横向滚动容器 |
pay-subscribe |
fit/100vw-32 | 高 | 套餐并排→纵向堆叠(配合组件 subscribe-plan-list) |
character-detail |
全屏 shell | 中(内容级) | 宽度 OK,内部"预览+信息"双栏→单列纵向 |
asset-details |
全屏 shell | 中(内容级) | 同上;子弹窗 AssetExpandImageModal/AssetDetailRepairModal 已有 @media,补齐 |
folders-asset-details |
全屏 shell | 中 | 内部双栏→单列 |
LoginRegisterModal |
fit/100vw-32 | 中 | 已有 @media540,补到 480;表单全宽 |
points-recharge |
fit/100vw-32 | 中 | 已有 @media720/480,校验网格 repeat(4,87px) |
user-edit |
fit/100vw-32 | 中 | 表单单列全宽 |
gateway-apikey |
fit/100vw-32 | 中 | 已有 @media768,补齐 |
copyright-deposit |
885px | 中 | 全屏/单列 |
voice-library-picker |
833px | 中 | 全屏;依赖组件 VoiceLibraryPicker(见阶段 4) |
contact-service |
454px/80vw | 低 | 微调 |
insufficient-points |
fit | 低 | 微调 |
alice-live-class |
自定义缩放 | 中 | 单独:窄屏改真实全屏布局,脱离 --dialog-layout-scale |
app-download-mac |
overlay | 低 | 单独:已有 @media720,补齐 |
清理:
src/dialog/Untitled(21 字节杂散文件)随手删除。
阶段 3 · 页面逐个适配(≤900px 为主,≤768/≤480 收敛)
统一原则:多列网格降列(→2→1)、并排双栏→纵向、定宽 min-width/固定px→min-width:0+流式、overflow-x 区域改可控横滑或换行、按钮/输入窄屏全宽。均只改 .scss 与必要 className,不动组件逻辑。
优先级 A — 无/弱响应式,窄屏高风险(重点投入)
| 页面 | 文件 | 适配要点 |
|---|---|---|
| CharactersCreate | src/pages/CharactersCreate/index.{tsx,scss}(最大 scss 1833 行)+ 子 CreateRoleModal |
画布型编辑器,多处 min-width:720/660/494/430。窄屏:画布区改纵向流、工具/结果面板堆叠、去定宽。工作量最大 |
| Voice | src/pages/Voice/index.scss(4055 行)+ components/music/*、components/speech/VoiceSpeechPanel.tsx(3226 行) |
左表单/右画廊/播放器/歌词/波形裁剪。窄屏:repeat(2,…)→1、min-width:280/160/125去定宽、播放器 bar 贴底、裁剪/歌词编辑纵向 |
| Profile | src/pages/Profile/index.{tsx,scss} |
仅有 @media900:补 768/480;用户信息头行重排、画廊网格降列、批量选择栏适配 |
| Create/InspirationLibraryPanel | panels/InspirationLibraryPanel/*、BackgroundAssets/* |
无断点:新增网格降列 |
优先级 B — 已有较完整响应式(补齐 + 校验)
| 页面 | 文件 | 适配要点 |
|---|---|---|
| index(首页) | src/pages/index/index.scss(1709 行) |
营销页多处三栏定宽栅格(150px 1fr 150px 等);已有 1320/1100/860/640,补 480、去定宽段 |
| Explore(+子 feed) | src/pages/Explore/* |
响应式最完整(6→1 列);仅校验 Hero minmax(420px,633px) 定宽段 ≤480 |
| Teaching | src/pages/Teaching/index.scss |
已 5→1 列;校验 hero 双栏与 minmax(160px,192fr) |
| TeachingDocument | src/pages/TeachingDocument/* |
402px 定宽侧栏已有单列降级;校验富文本多列 --rich-column-count |
| Characters | src/pages/Characters/* |
卡片墙 + 横滑区,已有 1280/900/560;补 480 |
| Create 容器 + 其余面板 | src/pages/Create/*(MyCreationsPanel/FolderPanel/MyFavoritePanel/TemplateAssets/CharacterAssetsPanel) |
多含 overflow-x 与 repeat(auto-fill,168px) 定宽单元格;FolderPanel(1687 行 scss)为重点,补断点 |
| subscribe | src/pages/subscribe/* + Swiper |
已 820 降单列;校验 repeat(4,87px) 积分网格、Swiper 窄屏 slidesPerView |
| content/page | src/pages/content/page/* |
已 @media768,基本 OK |
| NotFound | src/pages/NotFound/index.tsx |
纯 Tailwind,天然自适应,无需改 |
阶段 4 · 公共组件适配(src/components/)
| 组件 | 适配要点 |
|---|---|
subscribe-plan-list |
重:并排套餐卡(已 @media820)→ 窄屏纵向堆叠/横滑 |
VoiceLibraryPicker |
重:列表+试听并排 → 纵向 |
AvatarCropModal |
重:图片裁剪画布,校验触摸手势与固定尺寸(已 @media768/520) |
UserAvatarMenu |
下拉菜单定宽(已 @media860),窄屏防越界 |
AssetGalleryTile / PreviewImageThumb |
网格瓦片:流式宽度、min-width:0 |
AssetAudioPlayer |
固定宽 → 流式全宽 |
UserGuide |
新手引导覆盖层:定位随窄屏元素校准 |
CategoryDropdown / FolderMoveSubmenu / DeleteConfirmModal |
固定宽 → max-width:calc(100vw-32px) |
subscribe-qa-section(已768)、TabPill/SectionPill、HorizontalWheelScroll、ErrorBoundary、UploadPendingIndicator、RouteLoading |
小改或无需改,逐个校验不溢出 |
执行顺序建议
阶段 0(基建)→ 阶段 1(外壳 Tab 栏,先让整体骨架能用)→ 阶段 2(弹窗统一注入,一处覆盖多数)→ 阶段 3A(重灾页面)→ 阶段 3B(补齐页面)→ 阶段 4(组件)。每阶段独立可提交(Conventional Commits,如 style(layout): 移动端底部导航栏)。
验证
- 本地
npm run dev(端口 5195,--host 0.0.0.0可真机同网访问)。 - 用 Chrome DevTools 设备模拟逐屏核对:390px(手机)/ 768px(平板竖)/ 900px(主断点临界)/ 1024px(平板横)/ 1280px(桌面回归)。
- 重点验证清单:① 首屏无横向滚动条(各断点);② 底部 Tab 可点、内容不被遮挡(safe-area);③ 全部弹窗窄屏不溢出、可关闭;④ 重灾区页面(CharactersCreate/Voice)无内容被裁切;⑤ ≥900px 桌面视觉零回归(缩放系统未被破坏)。
- 收尾
npm run lint(零 warning)+npm run build(tsc 类型检查通过)。 - 提示:≤900 与 ≥900 两侧要交叉验证,因为 900 是缩放开关的临界点。
注意事项 / 不做
- 不改任何
src/api/*、src/store/*业务逻辑与数据流。 - 不新增业务组件;
useIsMobile仅为条件渲染纯 UI(如隐藏/切换展示),非业务分支。 - 遵循
.cursor/skills/react-best-practices:SCSS 嵌套跟随 DOM、语义化 class;如需新增任何可见文案须走 i18n(本次应尽量零新增文案)。