!

This page is not translated yet

The interface, navigation and community content are bilingual. The long-form body of this page is still in Chinese. The translation work is tracked as task I18N-10.

See translation tasks

Layout & Menu System

界面、菜单与视觉规范

本页定义五件事:① 三栏布局作为产品主界面;② 单社区页支持无限级别的横向 megamenu;③ 每个菜单都有自己的 slug 与独立 SFC 页面,按需动态加载并支持 SSR;④ 一套「信息优先」的视觉语言与万级社区目录的浏览设计;⑤ 多语言如何改变路由、数据与排版。可运行的实现在 /c 与 /c/php。

目录社区
1.3万 个
领域分类
16 类
PHP 菜单节点
82 个
已登记 slug 页面
49 个

一、三栏布局

左栏承担导航与归属,中栏承载内容,右栏承担信任与辅助。左右两栏可以「消失」,但中栏永远不能因为栅格问题被压扁。

PHP① 社区顶栏 56px —— logo 显示的是社区名(不是平台名)站内搜索 · 居中加入+ 发帖社区主页资讯版本更新记录PHP 百科网址大全扩展包精选最佳实践问答招聘② 横向 megamenu · 无限级 · 本社区唯一的菜单左栏 · 208px我的订阅(跨社区)· /c/php· /c/vue· /c/rust本社区热门标签教程性能提问sticky 吸顶不再列「社区导航」——那和上方 megamenu是同一批条目,重复中栏 · 自适应(minmax(0,1fr))无面包屑 · 页面标题当前 slug 对应的 SFC 页面defineAsyncComponent 按需加载 · SSR 直出右栏 · 264px关于本社区社区规则版主外部资源本页位置xl 显示可被页面 #right 插槽覆盖(用于放本页目录 / 相关页)

左栏

208px

导航与归属

  • 我的订阅(跨社区切换)
  • 本社区热门标签(取自信息流的真实分布)

lg 及以上显示;sticky 吸顶,随页面滚动保持在视口内。这里**不再**列「社区导航」—— 那和上方 megamenu 是同一批条目,首屏重复一次,移动端还要多占一整屏。

中栏

自适应(minmax(0,1fr))

当前 slug 页面

  • 页面标题与主操作
  • 动态加载的 SFC 页面
  • 上次/下次导航

min-w-0 是必须的:否则内部表格与长文本会把栅格撑破。全站**没有面包屑** —— 位置感由 megamenu 的选中态与页面标题承担,深链回退由「上级」单链负责。

右栏

264px

辅助与信任

  • 社区资料与统计
  • 社区规则
  • 版主
  • 外部资源
  • 本页位置

xl 及以上显示;页面可用 #right 插槽整体替换。

视口栏数行为
< 1024px(lg 以下)单栏左右栏隐藏,megamenu 收起为「展开菜单」按钮 + 递归折叠树
≥ 1024px(lg)两栏左栏 + 中栏;右栏内容下移或按需折叠
≥ 1280px(xl)三栏左栏 + 中栏 + 右栏,完整形态

布局里的多语言(v3.3 新增)

语言不只是「换一层文案」—— 它同时改路由结构、改数据维度、改排版容差, 所以这些规则与栏数、断点写在同一页,而不是散在各页的文案表里。

  • 语言是 URL 的第一段

    每个页面都真实存在于 /zh-CN/… 与 /en/… 两个地址,并用 hreflang 互相声明(另附 x-default)。不做「同一个 URL 靠 cookie 变语言」—— 那样页面无法被分别索引,也无法分享给另一个人。

  • 没选过语言的旧链接 301,选过的 302

    判据只有一条:目标是不是确定的。/ui 在用户没有显式选择语言时收敛到默认语言 /zh-CN/ui,目标固定,可以安全地 301;用户选过语言(ruyi_lang cookie)时改用 302,因为这个结果依赖 cookie,301 会被中间层缓存并扩散。

  • 顶栏语言切换器:宽度固定

    地球图标 + 当前语言短码,固定 62px。中文「中」与英文「EN」字宽不同,控件不定宽的话切换语言会让整条顶栏产生横向位移。

  • 同一入口不出现两次

    「社区目录」在 lg 以下放在顶栏右侧操作区,lg 起主导航里已经有同名一项,操作区就把它撤掉 —— 英文标签平均比中文宽 1.7 倍(实测 1024 视口下英文顶栏内容 1240px、可用仅 1013px),重复一个入口的代价是整条顶栏放不下。

  • megamenu 装不下就整行换行

    菜单项文字一律不折行(whitespace-nowrap),空间不足时让菜单栏整体叠成两行。让「Web directory」自己折行得到参差不齐的两行文字,让整行换行得到两行整齐的菜单 —— 后者看起来是有意为之。

  • 未翻译的正文要显式标注

    界面与社区内容已双语,长篇设计文档正文仍是中文。英文版这类页面顶部会出现一条横幅,写明「本页正文尚未翻译」并指向翻译任务 —— 不用「英文外壳 + 中文正文」糊过去。

  • 错误页也要跟着语言走

    404 / 500 是最容易被记住的一屏,恰恰也最容易漏翻:框架默认会把内部 statusMessage 原样打出来,而这些内部信息往往是中文的。所以自己写一份 app/error.vue,按 URL 首段 → cookie → 浏览器语言判定语言,取词典文案,并如实显示被请求的地址。

二、无限级横向 megamenu

一级横向铺开,悬停展开大面板;面板内按分组横向分列;如果某个条目还有下级,它右侧会出现箭头,继续悬停就再向右飞出一个面板 —— 如此递归,深度由数据决定而非代码。

资讯版本更新记录PHP 百科网址大全扩展包精选最佳实践问答悬停展开(140ms 关闭缓冲)PHP · 板块网址大全收录生态中值得长期使用的资源按「大类 → 小类 → 条目」组织进入板块 →框架LaravelSymfonySlimHyperf包与依赖ComposerPackagist国内镜像工具链静态分析格式化与重构调试与性能测试学习资源官方文档书籍与教程优质博客L3 · 调试与性能XdebugBlackfireTidewaysL4 · 再往下…数据有 children 就继续深度不设上限PHP 社区实测规模82 个菜单节点,其中 63 个叶子各自对应一条独立 slug最大层级 4 级:网址大全 → 工具链 → 静态分析 → PHPStan全站 5 个社区合计 125 条 slug 路径
规范项约定理由
一级项目数建议 5–9 个超过 9 个说明板块过细,考虑合并或降级为二级分组
面板分列每个「有子级的分组」占一列无子级的条目合并为「直达」列,避免出现大量单链接空列
继续深入条目右侧出现箭头即代表还有下一层箭头是唯一提示,必须与飞出行为严格对应
飞出方向一律向右(left-full)向左飞出会遮住上级面板,且在小视口下容易超出边界
层级上限不设硬上限,但 slug 段数 ≤ 8超过 8 段基本可以断定信息架构需要收敛
展开触发一级悬停(140ms 关闭缓冲);深层 hover + focus-within缓冲是为了斜向移动时不闪断;focus-within 保证键盘可用
键盘←/→ 切换一级,Esc 关闭,Tab 进入自动展开Tab 到一级项即调用 open(),否则键盘用户无法用 megamenu
移动端降级为 MegaTree 递归折叠按深度缩进、只展开当前分支,四层以上仍可阅读
SEO 与无障碍面板之外另渲染一份「全量菜单索引」(sr-only)面板是悬停才渲染的,深层 slug 的链接不会出现在 SSR 的 HTML 里;隐藏的扁平索引让爬虫与读屏用户都能发现全部页面

三、每个菜单一个 slug 独立页

菜单是数据,页面是组件,中间用一张注册表连接。这样「加一个板块」就不再等于「改一次代码发一次版」。下面是从 URL 到组件的完整解析链路。

浏览器请求/c/php/directory/tools路由解析community=phppath=[directory,tools]菜单树定位findNodeByPath()得到标题与层级注册表查找php/directory/tools命中 → 加载器动态渲染defineAsyncComponentSSR 直出 + 客户端水合未命中注册表回退 StubPage 骨架页,不返回 404左右两栏保持不变只有中栏被替换,切页面不重新拉社区信息

URL 规范

社区根/c/{community}/c/php
一级菜单/c/{community}/{slug}/c/php/news
深层菜单/c/{community}/{slug…}/c/php/directory/tools/static/phpstan
社区目录/c全部社区
slug 唯一性(community_id, slug) 唯一不同社区可以有同名 slug
改名禁止改 slug,只改 label确需改动必须写 301 重定向

相关文件

  • app/data/communityMenus.ts社区 + 菜单树数据(真实项目里来自 MySQL)
  • app/data/menuRegistry.tsslug → SFC 加载器注册表,动态 import
  • app/pages/c/index.vue社区目录,演示主题色随社区切换
  • app/pages/c/[community]/[...path].vue单社区页动态路由:解析 slug → 定位节点 → 动态加载组件
  • app/components/CommunityShell.vue三栏布局外壳 + 社区 header + megamenu 挂载点
  • app/components/MegaMenu.vue横向一级菜单 + 二级大面板
  • app/components/MegaFlyout.vue递归飞出面板(无限级的关键)
  • app/components/MegaTree.vue移动端递归折叠树
  • app/components/apps/**/*.vue各菜单的独立页面组件
  • app/components/apps/StubPage.vue未实现 slug 的通用骨架页

新增一个菜单页面的完整流程

  1. 1第一步 · 加菜单数据在 community_menu_items 里插入一条记录:label、slug、parent_id、position、description(悬浮说明)。前台立刻出现入口。
  2. 2第二步 · 决定是否要专属页面如果内容形态与已有页面相同(列表 / 详情 / 对比 / 聚合),直接复用已有 SFC,只加一条 community_menu_apps 记录。
  3. 3第三步 · 需要新形态时写 SFC新建 components/apps/{community}/{Name}Page.vue,接收 props { community, node, path },在注册表加一行 key → import。
  4. 4第四步 · 什么都不做也可以未登记的 slug 会落到 StubPage:自动生成层级指示、下级菜单与接入指引,绝不 404。

四、视觉语言(v3 重做)

这一版的界面几乎全部重画。原因很具体:上一版是「SaaS 官网」的语言 —— 靛蓝渐变、大圆角、大留白、全大写小标签、卡片悬浮位移。它在落地页上很好看,但在一个用户要连续滚动几百条内容的社区里,每一处都在挤压信息密度。

改前 · 官网语言

  • 靛蓝 / 紫色的渐变主色,一眼「又一个 SaaS」
  • 纯白页面底 + 大柔光阴影,层次全靠阴影
  • rounded-2xl(16px)大圆角,控件也是
  • 章节之间 56–64px,一屏只能看两块内容
  • 全大写 + 0.16em 字距的小标签(中文毫无意义)
  • 卡片悬停 translateY(-2px) + 大阴影
  • 社区目录是静态卡片墙,无搜索、无分类、无排序

改后 · 信息优先

  • 唯一行动色:暖橙红 #d93d16;正文链接单独用 #0b6bcb
  • 带灰画布 #eaedef + 纯白卡片 + 1px 实描边,层次靠底色差
  • 圆角收紧:控件 6px、卡片 12px、胶囊只给可点的小东西
  • 章节间距 20–24px,一屏能看到四到五块内容
  • 分组标签 11px / 700 / 0.04em,去掉大写与超大字距
  • 只过渡颜色与透明度,不做位移与缩放(100–150ms)
  • 目录改为搜索 + 16 领域 + 4 排序 + 3 密度,并按万级数据实测
令牌取值用途
canvas#eaedef页面画布:带灰,让白卡片自己浮起来
card / white#ffffff所有内容表面,静态不带阴影
line#dfe3e71px 实描边,唯一的「分隔」手段
ink-900#14171a标题与正文主色
ink-600#55626b次要文字
ink-400#98a4ac占位、注释、禁用
brand-500#d93d16唯一行动色:主按钮 / 赞同 / 选中 / 品牌
brand-50#fef4f1行动色的浅底与高亮背景
link#0b6bcb正文链接,不借用行动色
down#5a7fd6反对票
online#0d8a4f在线人数 / 成功态

圆角只留四档

4pxrounded / r-chip小标签、状态点
6pxrounded-md / rounded-lg按钮、输入框、列表行、面板
12pxrounded-2xl卡片(帖子卡、社区卡、信息卡)
999pxrounded-full胶囊按钮、头像、分段控件

密度约定

顶栏高度56px不是 64/72px。省下的 8–16px 在长页面里就是内容。
正文字号14px / 行高 1.55中文在 14px 的阅读效率高于 16px,且每屏多出约 1/5 行数。
元信息字号12px作者、时间、计数等;再小就影响可读性,再大就抢标题。
分组标签11px / 700 / 0.04em不用全大写与超大字距 —— 中文没有大小写,那个效果只是廉价感。
卡片间距8–12px列表项间距大于 12px 会让一屏装不下 8 条。
章节间距20–24px文档页章节之间;此前是 56–64px,属于官网式留白。
动画时长100–150ms只做颜色与透明度的过渡,不做位移与缩放。
阴影仅浮层静态卡片用描边建立边界;只有下拉、飞出面板用阴影。

五、社区目录:万级规模下的浏览设计

原来的目录页是一堵静态卡片墙 —— 5 个社区时好看,1.3万 个社区时完全不可用。现在它由四层导航构成:搜索 → 领域 → 排序 → 密度,每一层都在帮用户减少「要看的条数」。下面每一条都已在 /c 上按 1.3万 条数据实测。

搜索

1.3万 条

关键词同时匹配名称、简介、话题与领域;支持 / 唤起,条件写进 URL

领域

16 类

每类带实时计数,把「从万条里挑」收敛成「从十几类里选」

排序

4 档

最活跃 / 成员最多 / 最新创建 / 按名称

密度

3 档

列表(横向比数据)/ 卡片(看简介)/ 紧凑(快速扫标题)

维度做法为什么
搜索唯一能对抗万级数据的手段放在最显眼位置,支持 / 快捷键唤起、Esc 清空,状态写回 URL query(q)
领域分类16 个领域带实时计数把「从 1 万条里挑」收敛成「从 16 类里选」,再在类内看几十条
排序最活跃 / 成员最多 / 最新创建 / 按名称活跃度是社区目录的第一排序依据,成员数只排第二
阅读密度列表 / 卡片 / 紧凑三档列表用于横向比较数据,卡片用于看简介,紧凑用于快速扫标题
分批渲染首屏 36 条,滚动到底部追加 36 条不把 1 万条一次挂到 DOM;生产环境进一步做视口虚拟滚动
URL 状态q / cat / sort / view / lang / verified筛选条件即链接,服务端能直出已筛选的首屏,前进后退可用
未登录可浏览加入按钮即点即生效订阅状态放在 composable + useState,跨页面保持且 SSR 一致
空状态给出可执行的下一步不是「没有数据」,而是「换个关键词或重置筛选,也可以创建一个新社区」
窄容器断点用容器查询,不用视口断点目录页嵌在文档布局的中栏里,能拿到多少宽度取决于布局,与视口无关。按 xl: 写必然算错,实测会把中栏压到 349px。
组件类归属必须写进 @layer componentsTailwind 4 的工具类住在 @layer utilities;无层样式会压过任何 @layer,所以层外的 .r-chip 会让 class 里的 hidden 永久失效。
列宽预算列宽由「文字实际长度」反推,而不是反过来先量出「简介」这格在 1440 视口只有 184px(约 14 汉字),再把简介文案压到 14 字。文案服从格子,不是格子服从文案。
表头与数据行固定列宽写在两处,必须同步表头占位写错 11px,整组数字就会与数据行错开。收尾占位只写按钮宽度,组内 gap 已经替它留了间距。

v3.1 · 这一页曾经只有 349px 宽

重做之后有人仍觉得「列表挤、字竖着排」。查下来不是配色问题,是宽度算错了 —— 下面三处都是先量数、再改代码。

两层三栏栅格套在一起

中栏 349px→中栏 653px

文档布局本身已是 224 / 1fr / 264,目录页自己又套了一层 200 / 1fr / 264 —— 五列挤在 1440 里。修法:目录页接管右栏(ownsRail),把 264px 让给它自己的榜单。

按视口写断点

xl: 断点全部算错→改容器查询

这个页面能拿到多少宽度由布局决定,与视口无关。@container 挂在页面根节点后,页头、栅格、左栏吸顶全部按「自身实际宽度」判断。

组件类没进级联层

手机上标签吃掉 88px→按钮不再被裁

层外的 .r-chip 压过了 @layer utilities 里的 hidden,于是窄屏上领域标签照样渲染,把「加入」按钮顶出卡片、被 overflow-hidden 裁掉半个。

验收方式:在 13 个视口宽度(360 → 1920)上自动断言三件事 —— 文档无横向溢出、overflow:hidden 容器内无内容被裁、 表头与数据行的列右边界像素级一致。13/13 通过后才算改完。

社区目录共 1.3 万个社区 · 累计 2.4 亿位成员 · 今日新帖 38.6 万当前在线 128.4 万 · 我的订阅 2 · 领域 16sticky top-56px · backdrop-blur搜索社区名称、简介、话题…/最活跃成员最多最新名称中文仅认证筛出 2,184 个已显示 36 / 2,184按领域浏览全部社区1.3万编程语言880前端开发1,020后端架构940数据与存储900运维基建920人工智能840精选社区带独立站点与自定义菜单PHPPHP 社区r/php48.2万 成员 · 3.1千 在线LinLinux 社区r/linux86.4万 成员 · 5.2千 在线LLLLM 社区r/llm52.8万 成员 · 4.1千 在线全部社区1.3万 条社区简介成员在线r/php-frameworksLaravel · Symfony · Hyperf 对比12.4万842加入滚动到底部自动追加 36 条(IntersectionObserver,rootMargin 600px)今日最活跃1 r/llm-agent992 r/kubernetes973 r/rust-async964 r/mysql-perf94关于本页生产环境:Meilisearch 召回 +MySQL keyset 分页 + 视口虚拟滚动看这项能力排在哪个版本 →

相关文件

  • app/data/communityDirectory.ts16 个领域 × 话题 × 变体,确定性种子生成万级社区;含分类聚合、榜单与目录→社区适配器
  • app/composables/useSubscriptions.ts跨页面共享的「已加入」集合,SSR 与客户端同一份状态
  • app/pages/c/index.vue目录页:吸顶工具栏 + 分类栏 + 结果区 + 榜单栏
  • app/components/DirectoryCard.vue卡片视图:领域色左边条 + 四行主信息
  • app/components/DirectoryRow.vue列表 / 紧凑两档行视图,列对齐便于纵向比较
  • app/components/CommunityAvatar.vue文字头像 + 领域色描边环 + 在线点
  • app/components/JoinButton.vue加入 / 已加入(悬停提示退出)
  • app/components/VoteRail.vue竖排与横排两向的投票控件
  • app/components/PostCard.vue帖子卡:标准与紧凑两态,含操作栏与更多菜单

数据从哪来:确定性程序化生成

目录里的 1.3万 个社区不是手写数组,而是由 16 个领域 × 话题 × 变体 在模块作用域生成的: PRNG 用固定种子(mulberry32),因此服务端与客户端跑出完全相同的序列 —— 既不会 hydration mismatch,也不需要把上万条数据序列化进 HTML payload。

成员数刻意用幂律分布而不是均匀分布:真实平台的社区规模就是长尾 —— 极少数几十万上百万的大社区,绝大多数是几百人的小社区。均匀分布一眼就能看出是假数据。

当前累计 1.1亿 位成员、133万 人在线。 接入真实后端后,把 dirCommunities 换成接口即可, 页面的数据形状不用改。

打开社区目录 →