- 添加导航条目和分类数据模型 - 实现导航卡片网格展示 - 支持点击卡片打开链接 - 添加主题样式(渐变色、卡片阴影) - 区分系统默认和自定义条目 - 添加分类统计信息"
12 KiB
12 KiB
index3.html 逐条对照
本文档把 index3.html 里的每一条 CSS 规则与 JS 行为,逐条对应到 Flutter 实现的位置,用来回答「有没有跑偏」。
对照方式:index3.html 里的选择器 / 声明 → Flutter 文件与常量。
1. 设计变量
| index3.html | Flutter |
|---|---|
--bg-color: #f7f9fb |
AppColors.background |
--card-bg: #ffffff |
AppColors.surface |
--primary-color: #3b82f6 |
AppColors.primary |
--text-main: #1e293b |
AppColors.textPrimary |
--text-sub: #64748b |
AppColors.textSecondary |
--border-color: #e2e8f0 |
AppColors.border |
--shadow |
AppColors.cardShadow |
--shadow-hover |
AppColors.cardShadowHover |
--sidebar-width: 230px |
AppSizes.sidebarWidth |
--sidebar-collapsed-width: 64px |
AppSizes.sidebarCollapsedWidth |
2. 全局
| index3.html | Flutter |
|---|---|
* { box-sizing: border-box; margin: 0; padding: 0 } |
Flutter 无 box-sizing 概念,等价于默认行为 |
html { scroll-behavior: smooth } |
Scrollable.ensureVisible(..., duration: 300ms) |
body { background-color: var(--bg-color) } |
Scaffold.backgroundColor = AppColors.background |
body { color: var(--text-main) } |
AppTheme.light + 各文字样式自带颜色 |
body { font-family: -apple-system, ... } |
不指定 fontFamily,沿用系统字体栈 |
body { display: flex; min-height: 100vh } |
Scaffold.body = Stack[ Row[侧栏, Expanded(内容)] ] |
3. 侧栏
| index3.html | Flutter |
|---|---|
.sidebar { width: 230px } |
SidebarNav 根 AnimatedContainer.width |
.sidebar { background-color: card-bg } |
AppColors.surface |
.sidebar { border-right: 1px solid border } |
BoxDecoration(border: Border(right: ...)) |
.sidebar { padding: 20px 12px 12px 12px } |
sidebarPaddingTop / Horizontal / Bottom |
.sidebar { flex-direction: column } |
Column |
.sidebar { height: 100vh } |
Row(crossAxisAlignment: stretch) |
.sidebar { transition: width .3s cubic-bezier(.4,0,.2,1) } |
sidebarTransition + Curves.fastOutSlowIn(该曲线即 Material standard easing) |
.sidebar { overflow-x: hidden } |
根部 ClipRect |
body.sidebar-collapsed .sidebar { width: 64px; padding: 20px 8px 12px 8px } |
sidebarCollapsedWidth + sidebarCollapsedPaddingHorizontal |
.sidebar-header { height: 40px; margin-bottom: 24px; 居中 } |
_SidebarHeader(sidebarHeaderHeight / sidebarHeaderGap) |
.logo { font-size: 18px; font-weight: 700; color: primary } |
AppTextStyles.logo |
.logo-icon { font-size: 20px; font-weight: 800; color: primary } |
AppTextStyles.logoIcon |
body.sidebar-collapsed .logo-text { display: none } |
折叠时渲染 logoIcon 分支 |
body.sidebar-collapsed .logo-icon { display: block } |
同上 |
.nav-container { flex: 1; overflow-y: auto; overflow-x: hidden } |
Expanded(child: SingleChildScrollView(...)) |
4. 导航
| index3.html | Flutter |
|---|---|
.nav-item-wrapper { margin-bottom: 4px } |
_NavEntry 的 Padding(bottom: navItemGap) |
.nav-link { padding: 11px 16px; border-radius: 8px } |
navItemPaddingVertical/Horizontal、navItemRadius |
.nav-link { font-size: 14px; font-weight: 500 } |
AppTextStyles.navItem |
.nav-link { color: text-sub } |
AppColors.textSecondary |
.nav-link { justify-content: space-between } |
Row(mainAxisAlignment: spaceBetween) |
.nav-link { cursor: pointer } |
MouseRegion(cursor: SystemMouseCursors.click) |
.nav-link { transition: all .2s } |
AnimatedContainer(duration: 200ms, curve: Curves.ease) |
.nav-link:hover { background-color: #f8fafc; color: primary } |
AppColors.navItemHover + _hovered |
.nav-link.active { background-color: #eff6ff; color: primary } |
AppColors.primaryFaint + selected |
.arrow-icon { font-size: 14px; font-weight: bold; color: #94a3b8; font-family: monospace } |
AppTextStyles.navArrow |
.arrow-icon { transition: transform .3s ease } |
AnimatedRotation(duration: 300ms, curve: Curves.ease) |
.nav-item-wrapper.open > .nav-link .arrow-icon { transform: rotate(90deg) } |
turns: open ? 0.25 : 0 |
.sub-menu { padding-left: 16px; max-height: 0; overflow: hidden } |
subMenuIndent + AnimatedSize(未展开时不渲染子项) |
.sub-menu { transition: max-height .3s cubic-bezier(0,1,0,1) } |
curve: const Cubic(0, 1, 0, 1) |
.open .sub-menu { max-height: 500px; transition: max-height .3s ease-in-out } |
curve: Curves.easeInOut |
.sub-menu .nav-link { padding: 8px 16px; font-size: 13px } |
_SubNavItem + AppTextStyles.subNavItem |
body.sidebar-collapsed .nav-text { display: none } |
折叠时 _NavRow 不渲染任何子节点 |
body.sidebar-collapsed .arrow-icon { display: none } |
折叠时 _NavRow 走无 trailing 分支 |
body.sidebar-collapsed .sub-menu { display: none } |
折叠时根本不构建 AnimatedSize |
body.sidebar-collapsed .nav-link { justify-content: center; padding: 12px 0 } |
navItemCollapsedPaddingVertical + 水平 0 |
5. 侧栏底部
| index3.html | Flutter |
|---|---|
.sidebar-footer { border-top: 1px solid border; padding-top: 12px; margin-top: 8px } |
_SidebarFooter |
.submit-btn { padding: 10px 16px; border-radius: 8px; font-size: 14px; font-weight: 500 } |
submitButtonPaddingVertical/Horizontal、AppTextStyles.submitButton |
.submit-btn { color: text-sub; transition: all .2s } |
AnimatedContainer(curve: Curves.ease) |
.submit-btn:hover { background-color: #f1f5f9; color: primary } |
AppColors.navItemHoverDeep |
.submit-btn 的 title="提交网站" |
Tooltip('提交网站') |
body.sidebar-collapsed .submit-text { display: none } |
折叠时不渲染文字 |
6. 主内容区与顶栏
| index3.html | Flutter |
|---|---|
.main-content { margin-left: 230px / 64px } |
Row[ SizedBox(width: 侧栏), Expanded(内容) ] |
.main-content { transition: margin-left .3s cubic-bezier(.4,0,.2,1) } |
侧栏宽度动画,同一曲线 |
.main-content { padding: 24px 40px 40px 40px } |
contentPaddingTop / Horizontal / Bottom |
.top-bar { gap: 16px; max-width: 750px; margin: 0 auto 36px auto } |
TopBar(topBarGap / topBarMaxWidth / topBarMarginBottom + Center) |
.toggle-btn { width/height: 44px; border-radius: 12px; border: 1px solid border } |
toggleButtonSize / toggleButtonRadius |
.toggle-btn { box-shadow: var(--shadow) } |
AppColors.cardShadow |
.toggle-btn { font-size: 18px; color: text-sub } |
Text('☰', fontSize: 18) |
.toggle-btn { transition: all .2s } |
AnimatedContainer(curve: Curves.ease) |
.toggle-btn:hover { border-color: primary; color: primary } |
_hovered 分支 |
.toggle-btn 的 title="展开/折叠侧边栏" |
Tooltip('展开/折叠侧边栏') |
.search-container { flex: 1 } |
Expanded(child: LinkSearchField) |
.search-box { height: 44px; padding: 0 20px; border-radius: 12px; font-size: 15px } |
searchHeight / searchPaddingHorizontal / searchRadius / AppTextStyles.searchInput |
.search-box { box-shadow: var(--shadow) } |
未聚焦时的 boxShadow |
.search-box:focus { border-color: primary; box-shadow: 0 0 0 3px rgba(59,130,246,.15) } |
聚焦时只用光晕(覆盖默认阴影,与 CSS 一致) |
.search-box { transition: border-color .2s, box-shadow .2s } |
AnimatedContainer(curve: Curves.ease) |
7. 分组与卡片
| index3.html | Flutter |
|---|---|
.section-title { font-size: 16px; font-weight: 600; margin-bottom: 16px } |
AppTextStyles.sectionTitle + sectionTitleGap |
.section-title::before { width: 4px; height: 16px; margin-right: 8px; border-radius: 2px } |
_SectionTitle 的竖条(sectionAccent*) |
.grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px } |
AppBreakpoints.gridColumnsFor + gridGap |
.grid { margin-bottom: 36px } |
每个分区统一 sectionGap 下间距(最后一个也一样) |
.card { border-radius: 10px; padding: 14px 16px; border: 1px solid border } |
cardRadius / cardPadding* / AppColors.border |
.card { box-shadow: var(--shadow); transition: transform .2s, box-shadow .2s } |
AnimatedContainer(curve: Curves.ease) |
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-hover) } |
cardHoverLift + cardShadowHover |
.card-icon { width/height: 36px; border-radius: 8px; background: #f1f5f9; color: primary } |
_CardIcon(cardIconSize / cardIconRadius / AppColors.iconSurface) |
.card-icon { font-weight: 600; margin-right: 12px } |
AppTextStyles.cardIcon + cardIconGap |
.card-icon 未设 font-size,继承 body 的 16px |
AppTextStyles.cardIcon.fontSize = 16 |
.card-info { overflow: hidden } + .card-title/.card-desc 单行省略 |
Expanded + maxLines: 1, overflow: ellipsis |
.card-title { font-size: 14px; font-weight: 600 } |
AppTextStyles.cardTitle |
.card-desc { font-size: 12px; color: text-sub; margin-top: 2px } |
AppTextStyles.cardDesc + cardTitleGap |
8. 回到顶部
| index3.html | Flutter |
|---|---|
.back-to-top { position: fixed; bottom: 30px; right: 30px } |
Positioned(right: 30, bottom: 30)(backToTopOffset) |
.back-to-top { width/height: 44px; border-radius: 50% } |
backToTopSize + BoxShape.circle |
.back-to-top { font-size: 18px; color: text-sub; box-shadow: var(--shadow) } |
Text('↑', fontSize: 18) + cardShadow |
.back-to-top { transition: all .2s ease } |
AnimatedContainer(curve: Curves.ease) |
.back-to-top:hover { color/border-color: primary; transform: translateY(-3px); box-shadow: hover } |
backToTopHoverLift + cardShadowHover |
.back-to-top 的 title="回到顶部" |
Tooltip('回到顶部') |
9. JS 行为
| index3.html 的脚本 | Flutter |
|---|---|
点 #toggleBtn → body.classList.toggle('sidebar-collapsed') |
HomePage._toggleSidebar |
点 .has-sub → 若已折叠先移除 sidebar-collapsed |
HomePage._toggleGroup 里 collapsed 为真时先 _collapsedOverride = false |
点 .has-sub → parentItem.classList.toggle('open') |
同一批 id 集合 _openGroupIds 的增删 |
10. 当前实现没有任何原型之外的行为
以下都是「原型里没有」的行为,都不实现:
| 原型的状态 | 当前实现 |
|---|---|
| 搜索框没有任何 JS | 只接收输入,不过滤内容;没有空状态、没有清空按钮 |
active 写死在「常用推荐」上 |
高亮固定不动,点其它导航项也不会变 |
| 没有媒体查询 | 侧栏始终 230px(点折叠按钮变 64px),主内容内边距始终 24/40/40/40 |
#icon、#stock、#dev、#api 没有对应元素 |
这四个导航项点击后没有任何反应(不滚动、不报错) |
#submit 没有对应元素 |
点「提交网站」没有任何反应 |
只有 #common(3 张卡)与 #design(2 张卡)两段内容 |
内容区就是这两段、五张卡片 |
历史上曾经额外加过、现已全部删除的:滚动的侧栏联动、折叠态首字短标签、分组包含当前分区时的强调色、点二级条目自动展开分组、卡片上弹出网址的 tooltip、搜索过滤与空状态、点导航切换高亮、窄屏自动折叠、补齐缺失的四个分区、提交按钮的提示。
11. 验证方式
flutter analyze→ No issues foundflutter test→ 36 个用例通过flutter build web --no-wasm-dry-run→ 构建成功
其中几条专门守住「不加戏」的边界:
- 滚动主内容时,侧栏第一行仍是同一个 widget 实例(没有被重建),高亮不变。
- 折叠态下
navText('常用推荐')、navText('提交网站')都找不到——文字确实被隐藏了,与display: none一致。 - 输入搜索词后,仍然是 2 个分区、5 张卡片。
- 点
#icon这类没有对应元素的导航,滚动偏移不变。 - 点「提交网站」不会弹出任何东西。