左栏
208px导航与归属
- 我的订阅(跨社区切换)
- 本社区热门标签(取自信息流的真实分布)
lg 及以上显示;sticky 吸顶,随页面滚动保持在视口内。这里**不再**列「社区导航」—— 那和上方 megamenu 是同一批条目,首屏重复一次,移动端还要多占一整屏。
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.
Layout & Menu System
本页定义五件事:① 三栏布局作为产品主界面;② 单社区页支持无限级别的横向 megamenu;③ 每个菜单都有自己的 slug 与独立 SFC 页面,按需动态加载并支持 SSR;④ 一套「信息优先」的视觉语言与万级社区目录的浏览设计;⑤ 多语言如何改变路由、数据与排版。可运行的实现在 /c 与 /c/php。
左栏承担导航与归属,中栏承载内容,右栏承担信任与辅助。左右两栏可以「消失」,但中栏永远不能因为栅格问题被压扁。
导航与归属
lg 及以上显示;sticky 吸顶,随页面滚动保持在视口内。这里**不再**列「社区导航」—— 那和上方 megamenu 是同一批条目,首屏重复一次,移动端还要多占一整屏。
当前 slug 页面
min-w-0 是必须的:否则内部表格与长文本会把栅格撑破。全站**没有面包屑** —— 位置感由 megamenu 的选中态与页面标题承担,深链回退由「上级」单链负责。
辅助与信任
xl 及以上显示;页面可用 #right 插槽整体替换。
| 视口 | 栏数 | 行为 |
|---|---|---|
| < 1024px(lg 以下) | 单栏 | 左右栏隐藏,megamenu 收起为「展开菜单」按钮 + 递归折叠树 |
| ≥ 1024px(lg) | 两栏 | 左栏 + 中栏;右栏内容下移或按需折叠 |
| ≥ 1280px(xl) | 三栏 | 左栏 + 中栏 + 右栏,完整形态 |
语言不只是「换一层文案」—— 它同时改路由结构、改数据维度、改排版容差, 所以这些规则与栏数、断点写在同一页,而不是散在各页的文案表里。
语言是 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 → 浏览器语言判定语言,取词典文案,并如实显示被请求的地址。
一级横向铺开,悬停展开大面板;面板内按分组横向分列;如果某个条目还有下级,它右侧会出现箭头,继续悬停就再向右飞出一个面板 —— 如此递归,深度由数据决定而非代码。
| 规范项 | 约定 | 理由 |
|---|---|---|
| 一级项目数 | 建议 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 里;隐藏的扁平索引让爬虫与读屏用户都能发现全部页面 |
菜单是数据,页面是组件,中间用一张注册表连接。这样「加一个板块」就不再等于「改一次代码发一次版」。下面是从 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 重定向 |
这一版的界面几乎全部重画。原因很具体:上一版是「SaaS 官网」的语言 —— 靛蓝渐变、大圆角、大留白、全大写小标签、卡片悬浮位移。它在落地页上很好看,但在一个用户要连续滚动几百条内容的社区里,每一处都在挤压信息密度。
改前 · 官网语言
改后 · 信息优先
| 令牌 | 取值 | 用途 |
|---|---|---|
| canvas | #eaedef | 页面画布:带灰,让白卡片自己浮起来 |
| card / white | #ffffff | 所有内容表面,静态不带阴影 |
| line | #dfe3e7 | 1px 实描边,唯一的「分隔」手段 |
| ink-900 | #14171a | 标题与正文主色 |
| ink-600 | #55626b | 次要文字 |
| ink-400 | #98a4ac | 占位、注释、禁用 |
| brand-500 | #d93d16 | 唯一行动色:主按钮 / 赞同 / 选中 / 品牌 |
| brand-50 | #fef4f1 | 行动色的浅底与高亮背景 |
| link | #0b6bcb | 正文链接,不借用行动色 |
| down | #5a7fd6 | 反对票 |
| online | #0d8a4f | 在线人数 / 成功态 |
圆角只留四档
| 4px | rounded / r-chip | 小标签、状态点 | |
| 6px | rounded-md / rounded-lg | 按钮、输入框、列表行、面板 | |
| 12px | rounded-2xl | 卡片(帖子卡、社区卡、信息卡) | |
| 999px | rounded-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 components | Tailwind 4 的工具类住在 @layer utilities;无层样式会压过任何 @layer,所以层外的 .r-chip 会让 class 里的 hidden 永久失效。 |
| 列宽预算 | 列宽由「文字实际长度」反推,而不是反过来 | 先量出「简介」这格在 1440 视口只有 184px(约 14 汉字),再把简介文案压到 14 字。文案服从格子,不是格子服从文案。 |
| 表头与数据行 | 固定列宽写在两处,必须同步 | 表头占位写错 11px,整组数字就会与数据行错开。收尾占位只写按钮宽度,组内 gap 已经替它留了间距。 |
重做之后有人仍觉得「列表挤、字竖着排」。查下来不是配色问题,是宽度算错了 —— 下面三处都是先量数、再改代码。
两层三栏栅格套在一起
文档布局本身已是 224 / 1fr / 264,目录页自己又套了一层 200 / 1fr / 264 —— 五列挤在 1440 里。修法:目录页接管右栏(ownsRail),把 264px 让给它自己的榜单。
按视口写断点
这个页面能拿到多少宽度由布局决定,与视口无关。@container 挂在页面根节点后,页头、栅格、左栏吸顶全部按「自身实际宽度」判断。
组件类没进级联层
层外的 .r-chip 压过了 @layer utilities 里的 hidden,于是窄屏上领域标签照样渲染,把「加入」按钮顶出卡片、被 overflow-hidden 裁掉半个。
验收方式:在 13 个视口宽度(360 → 1920)上自动断言三件事 —— 文档无横向溢出、overflow:hidden 容器内无内容被裁、 表头与数据行的列右边界像素级一致。13/13 通过后才算改完。
相关文件
数据从哪来:确定性程序化生成
目录里的 1.3万 个社区不是手写数组,而是由 16 个领域 × 话题 × 变体 在模块作用域生成的: PRNG 用固定种子(mulberry32),因此服务端与客户端跑出完全相同的序列 —— 既不会 hydration mismatch,也不需要把上万条数据序列化进 HTML payload。
成员数刻意用幂律分布而不是均匀分布:真实平台的社区规模就是长尾 —— 极少数几十万上百万的大社区,绝大多数是几百人的小社区。均匀分布一眼就能看出是假数据。
当前累计 1.1亿 位成员、133万 人在线。 接入真实后端后,把 dirCommunities 换成接口即可, 页面的数据形状不用改。
打开社区目录 →