Files
justme 12980ba6fe
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful
docs: describe login-only spaces and the 8.0.0 contract [CI]
2026-10-16 06:46:16 +08:00

360 lines
29 KiB
Markdown

# 界面设计
前端的视觉与交互权威文档。协议、接口以 [protocol.md](protocol.md)、[api.md](api.md) 为准,本文只定义它们在界面上如何呈现。
## 1. 原则
1. **工具感的实用极简**:界面是载体,内容是主角。信息密度适中,层级靠字号、字重、间距,不靠装饰。
2. **诚实**:安全状态如实呈现,不夸大。服务端能看到的、不能保证的,界面必须说清楚(见 §7)。
3. **手机与电脑同一套骨架**:同一组组件,按宽度重排,不做两套界面。
4. **克制的质感**:只在两处用 blur,只在浮层用阴影。
## 2. 反 AI 味清单(禁止项)
| 禁止 | 原因 |
|---|---|
| 装饰性渐变、霓虹色、彩色大色块 | 默认 AI 模板特征。渐变只允许用来表达状态或层次,见 §4 |
| 超过 12px 的圆角、pill 形容器 | 同上 |
| 卡片套卡片、满屏悬浮玻璃面板 | 层级混乱,且耗性能 |
| 眉标签(eyebrow)、装饰性副标题、营销式文案 | 工具不需要 |
| Tailwind 默认式重阴影 | 见 §4 |
| 默认的 Inter / Roboto,默认细线图标集原样使用 | 无个性 |
## 3. 色彩
灰白为主,亮暗两套。默认跟随系统(`prefers-color-scheme`),顶栏的外观菜单可手动指定「跟随系统 / 亮色 / 暗色」,选择只存在本机 `localStorage`,启动时在渲染前应用,避免闪烁。暗色变量在 `tokens.css` 里出现两次(跟随系统与手动指定),修改必须同步。
| token | 用途 |
|---|---|
| `--bg` / `--surface` | 页面底 / 输入、浮层底 |
| `--line` | 1px 分隔线,替代卡片边框 |
| `--text` / `--text-dim` | 正文 / 次要信息(时间、设备名) |
| `--accent` | 主按钮、焦点、选中态,与正文同为墨色 |
| `--ok` | enc 与已认证完整 |
| `--warn` | raw 模式、不完整、下载次数将尽 |
| `--danger` | 篡改、错误、删除 |
| `--busy` | 进行中、重连中 |
**语义色只表达安全与状态,不做装饰。** 颜色字面量只允许出现在 `src/styles/tokens.css`,其余 CSS 一律 `var(--*)`,由 stylelint 强制。亮暗两套的语义色都要过对比度(正文 ≥ 4.5:1)。
## 4. 形状、阴影、blur
| 项 | 规则 |
|---|---|
| 圆角 | 三档:`--radius-sm` 4、`--radius-md` 8、`--radius-lg` 12。只能用 token |
| 分隔 | 列表用 1px `--line`,不用阴影、不用卡片 |
| 阴影 | 只有 `--shadow-pop`,只给菜单、弹窗、Toast |
| blur | **仅顶栏与手机底部动作栏**使用 `backdrop-filter: var(--blur-bar)`,半径 10px |
渐变:低饱和、小面积,只表达状态或层次,定义为 `tokens.css` 里的 token(`--fade-down`、`--tile-gradient`、`--progress-gradient`、`--warn-bar`、`--*-soft` 底色):
| 用处 | 做法 |
|---|---|
| 顶栏下沿 | 向下渐隐到页面底色,代替硬分隔线 |
| raw 警示 | 顶栏下沿换成暖色渐隐条 |
| 传输进度 | 细条,左浅右深 |
| 冲突、篡改、警示行 | 左侧色条 + 向右渐隐的同色底 |
| 在线点 | 缓慢呼吸,不用发光阴影 |
blur 的性能约束:
- 全页面同时存在的 `backdrop-filter` 元素不超过 2 个。
- 禁止用于滚动列表中的元素,以及弹窗遮罩(遮罩用纯色 `--scrim`)。
- 必须有回退:`@supports not (backdrop-filter: blur(1px))` 与 `prefers-reduced-transparency: reduce` 时,背景退化为不透明的 `--bg`。
- 玻璃背景下的文字对比度按「最差底色」验证,不得依赖背后内容。
## 5. 字体与图标
| 用途 | 字体 |
|---|---|
| 界面文字 | 思源黑体(`--font-sans`,系统无衬线兜底) |
| 链接、哈希、密钥、文件大小 | 等宽(`--font-mono`) |
字号档位不超过 5 档,字重只用 400 与 600。图标自绘 SVG,统一 1.5px 描边、24 网格,放在 `ui/icons/`,不引入图标库。
## 6. 布局与页面
WebDrop 是**暂存区**,频道工作区:聊天按 thread 展示,文件按 thread/文件夹组织;发送设备作为来源信息。
### 6.1 手机:频道导航与聊天/文件切换
进入频道就是纯聊天界面:单行顶栏(≡、# 频道名、加密图标、搜索、⋯)加底部输入框,没有页签。空间切换、频道列表、本空间的「文件 / 成员 / 邀请 / 分享」、账号与外观都在左侧抽屉(≡ 打开,返回键关闭);点「文件」「成员」整页打开,左上角 ‹ 回到聊天。⋯ 里是空间设置、分享、邀请。触屏没有悬停,消息上不常驻删除按钮,点一下消息弹出菜单(复制、删除)。新建频道自动创建“常规”thread。文件页与桌面共用目录路径、上传、文件夹、排序、移动和预览组件。
频道加载中先画出页面框架(骨架屏:顶栏、频道列表、消息、输入框的灰色占位),不留白屏;等待不足约 300ms 时不显示,避免闪烁;占位轻微呼吸闪动,尊重「减少动效」。不用假进度条。加载失败时在骨架屏上给出错误与「重试」「回到首页」。
### 6.2 电脑:频道与 threads | 聊天 | 线程文件
左侧固定频道导航与 thread 列表,可直接切换已加入频道、新建频道、新建/重命名 thread,并打开账号与设备设置。中间完整高度展示当前 thread 的聊天,底部输入支持文字和文件附件。右侧是资源管理器式文件区:路径面包屑、上传文件、新建文件夹、按名称/大小/时间排序、多选下载、移动、双击预览。
设备与分享收进顶栏弹层,不占第四列。预览在独立弹层打开,不挤占聊天。加密目录先同步文件名、大小与 thread/folder 归属;受限文件必须显式点击“解密并预览文件”或下载后才读取内容,不能因选择列表项或刷新消耗次数。
### 6.3 顶栏
| 控件 | 作用 |
|---|---|
| 频道 id + 加密/明文徽标 | 当前频道与安全状态(§7.1) |
| 在线点 + `N/M` | 设备在线情况;手机上点开设备与分享抽屉 |
| 连接状态 | 仅在重连中或离线时显示 |
| 搜索 | 展开关键字输入(文本内容或文件名);再次点击关闭并清除关键字。类型分组一直可见 |
| 外观 | 跟随系统 / 亮色 / 暗色 |
| 设备与分享(仅手机) | 打开抽屉 |
### 6.4 首页(初始状态)与页面
首页把「便捷分享」与「空间」做成两个并列分区(宽屏左右并列,窄屏上下排列,中间一条细线分隔),各有图标和一句话定位:
- **便捷分享**(左):临时 · 只读 · 对方无需账号,端到端加密。输入区(文字、拖入/选择/粘贴文件)加「创建分享」,下面紧跟「我的分享」列表。是写完即发的操作区。
- **空间**(右):长期 · 多人 · 登录使用。先列「我的空间」(名称、加密、角色、人数上限),底部是「新建空间」「加入空间」。是进入长期工作区的入口。
主页右上角提供外观菜单,右下角显示前端和后端的 tag 与短 commit hash 及上海时间的构建时间;无法取得 tag 时如实显示「未标记」。
| 操作 | 结果 |
|---|---|
| 快速分享(输入文本、拖入、粘贴、选择、相册、拍照) | 先在首页编辑临时文本并收集文件;点「创建临时分享」后建立加密草稿并带入内容;点「完成分享」后生成链接与二维码 |
| 新建频道 | 登录后创建,自动成为该频道所有者;可选择是否端到端加密,默认启用;成员人数上限、TTL、受限下载次数和上传权限仍用同一频道模型 |
| 加入频道 | 扫码或粘贴链接 → 加入预览 → 加入 |
| 我的频道 | 点一行进入;垃圾桶从当前账号资源清单移除,**不会删除频道**,需二次确认;账号清单移除会同步到其他设备 |
| 账号与同步 | 用户名密码注册或登录;本地频道/分享凭据在 Worker 加密后同步,另一台设备登录后恢复;退出时撤销会话 |
登录/注册页是居中的单列卡片(标题与标语在上,卡片在下);创建账号需要再输入一次密码确认,两次不一致时就地提示并禁用提交。账号设置是左侧导航 + 右侧单页(个人资料 / 我的设备 / 安全 / 空间邀请 / 同步与登录;手机上导航变为顶部横向页签),结果统一用 toast 反馈,设备重命名就地编辑,移除设备与退出登录需确认。
账号密码不提供找回流程。忘记密码会导致账号保险库无法解密;界面需在注册区域明确提示。本机记住登录通过 IndexedDB 保存 session 和不可导出的保险库 CryptoKey,刷新后恢复身份;退出或设备被撤销后清除。
未登录访问首页时先显示注册/登录页;收到的分享链接任何人可读;收到的频道链接要求登录,未登录先显示登录页,登录后继续加入。频道加入页按当前规则显示密码输入。右上角频道设置集中管理加入方式(登录后持链接加入 / 仅受邀账号)、频道密码、成员角色、邀请与撤销,不另设权限入口。
| 路由 | 页面 |
|---|---|
| `/` | 首页 |
| `/c/:id` | 已在「我的频道」里 → 频道暂存区;否则链接里带密钥 → 整页加入预览;否则「无法打开此频道」 |
别人发来的链接打开时,页面一加载就把 `#` 后面的密钥取走并清掉地址栏,再显示加入预览,确认后才加入。
频道分享面板与分享页里的二维码只给**成员链接**;所有者链接含管理权限,只提供复制,不出二维码。二维码固定深色码点、浅色底,暗色主题下也不反转,否则很多扫码器识别不了。
### 6.4.1 便捷分享
Share 是独立的只读发布资源,不创建频道成员或 threads。创建者在草稿中添加内容和设置,明确确认后封存;服务端清除 owner_hash,前端丢弃管理密钥,此后只有读取入口。访客打开分享链接直接读取加密清单,下载受限文件时计次数,不经过频道加入流程。详见 protocol.md §15。
### 6.5 频道 thread、文件管理与聊天
频道内桌面布局为左侧频道/thread 导航、中央聊天、右侧线程文件管理器。新建频道自动创建“常规”thread;用户可以继续创建和命名其他 thread,在各自 thread 中发言或上传文件。聊天消息显示发送设备名称和时间。
左栏按 thread 显示虚拟文件夹与文件。可以创建/重命名 thread、创建/重命名文件夹、多选文件并移动到当前 thread 的文件夹。文件预览仍使用中央预览区;消息不混入文件列表。手机上 thread 和文件树可横向/纵向收缩,聊天输入固定在文件列表下方、上传动作栏上方。
频道名、文件夹名和移动操作通过后端结构接口管理,其他设备收到通知后重新读取。文件名称和类型仍通过加密资源元数据同步,GATED 内容仅在显式操作后读取。
其他弹层:
| 名称 | 说明 |
|---|---|
| 条目详情 | 手机为底部抽屉,电脑为内联预览区;内容一致 |
| 扫码加入 | 相机识别二维码(浏览器自带 `BarcodeDetector`,不支持或无权限时退回「粘贴链接」)→ 加入预览(模式、权限)→ 加入后进入该频道 |
| 写文本 | 抽屉,`Ctrl+Enter` 发送;首页上用于快速分享 |
| 频道设置 | 顶栏齿轮打开设置面板;所有者可修改名称、成员人数上限、成员上传、保留时间和受限文件下载次数 |
| 设备管理 | 设备面板分别显示成员人数与已连接设备;成员人数按账号去重,同一账号可在多台设备登录;设备可重命名,所有者可移除任意设备 |
扫到的链接含密钥,只放在内存里,不写日志、不持久化。
> 原型说明:前端目前是验证设计的框架,频道列表只在内存里,刷新即清空;真实实现把链接存进 IndexedDB(architecture §6)。
### 6.5 条目
列表里的一行按内容类型决定画法,**不大的文本、JSON、视频直接画在行里,不用点开**:
| 内容 | 列表里的画法 | 快捷操作 |
|---|---|---|
| 文本 | 内容本身(没有标题);太长只显示前 6 行,点击进详情 | 复制 |
| JSON(能解析成对象或数组) | 格式化后等宽显示;超过 14 行或 1200 字符截断 | 复制 |
| 不大的视频(≤ 25 MB) | 行内播放器(`controls`、`preload="metadata"`) | 下载 |
| 图片 | 缩略图 + 标题 + 小字 | 下载 |
| 压缩包、其他文件、大视频 | 类型图标 + 标题 + 小字 | 下载 |
小字依次为:设备 · 时间 · 大小 · 剩余下载次数,后面跟状态标签:`v3`(有历史)、`有更新`、`冲突`。传输中在行内显示细进度条。点标题行或文本区域打开详情(手机底部抽屉,电脑内联预览区)。
**不能烧下载次数**:按协议,文件超过 1 MiB 或设了下载上限就是 GATED,每次拉取都计一次下载(api.md)。所以:
- 文本与 JSON 内容本来就在内存里,一律直接画。
- 没有下载上限的视频、图片自动加载;**有下载上限的**不自动拉取:图片显示类型图标,视频显示占位「点击播放 · 会消耗 1 次下载」,点了才加载。
- 滚动列表、刷新页面都不应该消耗下载次数。
- 长列表使用 `content-visibility: auto`。注意它会让后代的 `position: fixed` 失效,弹层必须用 portal 挂到 `body`。
- **新的在前**。
- 内联渲染一律是纯文本或原生媒体元素,不执行内容(threat-model §4)。
预览:
| 类型 | 呈现 |
|---|---|
| 图片、视频、音频 | 原生元素播放 blob URL,不执行内容 |
| 文本、JSON | 纯文本等宽展示,JSON 格式化,最多读取前 200,000 字符 |
| 压缩包 | 只列出目录树与大小,不解压、不执行 |
| 其他 | 提示「暂不支持预览」,保留下载 |
### 6.6 修改、历史与便签
- **修改 = 追加新版本,后写者胜**(不改协议)。文本编辑后显示「已编辑」;两台设备同时改同一条时,后到者生效,先到者的内容以冲突提示展示:「某设备同时改过:… [用他的][保留我的]」。
- **文件修改历史**:新版本是一个替代旧条目的新条目,界面把它们折叠成同一个文件。条目显示 `v3`;别的设备更新了你还没看过的文件时显示「有更新」,详情里有一行「某设备 更新了此文件 [知道了]」;详情下方列出历史版本,可单独下载旧版本。需要在加密的 `meta_json` 里加可选字段 `replaces`(旧 `item_id`),后端不解析 meta,待写入 protocol §7。
- **便签**是频道唯一的快照槽(protocol §9),多设备实时同步,最后写入者胜,不做字符级合并。默认折叠,折叠时显示第一行;他人正在编辑时显示「某设备 正在编辑」。
- 暂不做字符级协同编辑(需要新增协议条目类型,属于后续议题)。
## 7. 状态与诚实提示
### 7.1 频道安全徽标(顶栏)
| 状态 | 呈现 |
|---|---|
| enc | 小号锁图标 + 「加密」,`--ok`,点开说明「内容在浏览器内加密,服务端只见密文」 |
| raw | **整条顶栏下沿加 `--warn` 色条**,文字「明文 · 服务端可见」,不可收起 |
### 7.2 条目状态
条目状态只有以下几种,由帧推导,组件按判别联合穷尽处理:
| 状态 | 条件 | 呈现 |
|---|---|---|
| `receiving` | 无 FINAL,帧连续 | `--busy` 进度,标注「不完整」 |
| `complete` | 已认证 FINAL 且 0..n 连续 | 正常 |
| `incomplete` | 缺口,或长时间无 FINAL | `--warn`,可重试拉取 |
| `tampered` | 认证失败 | **整条替换为警示行**,`--danger`,说明「内容被篡改,已丢弃」,不展示任何内容 |
| `expired` / `removed` | tombstone | 淡出后移除,可短暂保留一条「已过期 / 已删除」提示 |
### 7.3 连接状态(顶栏)
`在线` · `重连中` · `离线(N 条待发送)`。离线时仍可添加内容,发送进入队列,恢复后自动补发,无需用户操作。
### 7.4 必须如实表述的内容
| 场景 | 文案原则 |
|---|---|
| `ui_restrict_copy_download` | 说明这是界面限制,「成员仍可通过其他方式保存内容」,不暗示安全保证 |
| 下载次数 | 下载前提示「此文件剩余 N 次下载」;达上限提示「已被删除」 |
| 链接 | 分享前说明「持有链接即可访问」;成员链接与所有者链接分开展示,所有者链接标注「含管理权限」 |
| 历史可读 | 加入说明处写明「持有链接的人可读取此频道全部历史」 |
| 元数据 | 创建与设置页注明「不隐藏 IP 与访问时间」 |
### 7.5 频道文件预览与下载限制
- 新上传文件默认允许单帧内容直接随频道同步。加入频道的成员可直接看到文件名、类型、大小;PNG 等图片和 TXT 等文本可直接预览,不消耗下载次数。文件名与类型仍只存在于加密帧中。
- 超出单帧容量的文件保持 GATED,列表不自动读取内容。成员主动打开预览时才取回文件,并消耗一次下载次数;取回后显示解密出的文件名、类型和大小。EXE 等不支持内联预览的类型仍提供下载。
- 上传者可关闭直接预览并为文件设置下载次数上限。
- 设备标签优先显示浏览器提供的机型和系统版本,例如「Pixel 9 (Android 16) · A1B2C3D4」「Windows 11 · 1234ABCD」。浏览器没有暴露具体机型时显示能识别的平台名称;保留端点短标识以区分同型号设备。
### 7.6 空与错误状态
| 码 | 页面 |
|---|---|
| `members_full` | 加入预览页:「成员人数已满」,告知联系所有者 |
| `not_joined`(被释放) | 频道页整体替换为「你已被移出此频道」 |
| `not_found` / `gone` | 「频道不存在或已过期」 |
| `rate_limited` | Toast,带重试倒计时 |
| 链接缺密钥 / 模式不符 | 拒绝进入,明确说明原因(降级防御,见 protocol §2) |
## 8. 交互
| 场景 | 行为 |
|---|---|
| 发送文本 | 点「写」弹出输入,`Ctrl+Enter` 发送 |
| 粘贴 | 窗口任意位置 `Ctrl+V` 直接发送;手机点「粘贴」读取剪贴板(无权限时退回「写」)。文件或图片直接入队,先出现「传输中」条目 |
| 拖放 | 桌面整页接收拖放;拖入时出现覆盖提示,不改变布局 |
| 文件上传 | 上传前可设置是否让单帧文件直接同步预览,以及受限文件的下载次数上限 |
| 频道设置 | 顶栏齿轮打开;设置通过现有 `PATCH /channels/{id}/settings` 更新,服务端广播变更给在线成员 |
| 复制 | 点击复制,按钮短暂变为「已复制」,不弹 Toast |
| 删除 | 确认删除后提交删除,并同步频道墓碑 |
| 新条目到达 | 若用户已滚到底部则自动跟随,否则底部出现「N 条新内容」 |
| 触控 | 可点击区域 ≥ 44px;底栏避开安全区(`env(safe-area-inset-*)`) |
## 9. 动效
使用 React `ViewTransition`(稳定版 React 19.3 已包含),原则是**只在有空间关系处动**:
| 场景 | 动画 |
|---|---|
| 首页 ↔ 频道 ↔ 设置(层级导航) | 方向性滑入 `nav-forward` / `nav-back` |
| 条目新增 | CSS 淡入(`row-in`)。store 经 `useSyncExternalStore` 订阅,更新不在 transition 内,`ViewTransition` 不会触发,所以不用它 |
| 开屏 | 单线手写 `WebDrop` 逐笔画出,约 1.2s,仅每个会话首次出现,点击或按键可跳过;加载未完成时停在末笔 |
| 桌面左栏切换频道 | 淡入淡出,不滑动 |
| 条目状态变化、重连、后台同步 | **无动画**(`default="none"`) |
规则:
- 一律 `default="none"`,显式开启需要的触发。
- 时长:退出 150ms、进入 210ms、移动 400ms,取自 `--duration-*`。
- 必须尊重 `prefers-reduced-motion`(已在 `base.css`)。
- 顶栏、底栏用 `viewTransitionName` 隔离,并采用 backdrop-blur 的专用写法,避免闪烁。
- 不支持的浏览器直接无动画,功能不受影响。
## 10. 无障碍
- 所有交互元素可键盘操作,焦点环可见(`--accent`)。
- 状态不只靠颜色,配图标与文字。
- 动态内容(新条目、连接状态)用 `aria-live="polite"`。
- 触控目标 ≥ 44px;文本可缩放到 200% 不破版。
## 11. 文案
中文为默认语言;文案用语直白,不用感叹号与营销口吻。暂不做多语言,文案直接写在组件里。
## 12. 手机适配(iPhone / Android)
| 方面 | 做法 |
|---|---|
| 刘海与手势条 | `viewport-fit=cover`;顶栏用 `env(safe-area-inset-top)`,底部动作栏与抽屉用 `inset-bottom`,手机布局两侧用 `inset-left/right`(横屏) |
| 视口高度 | `100vh` 兜底 + `100dvh`,避开地址栏伸缩 |
| 软键盘 | 需要输入的抽屉(写文本)从**顶部**出来,iOS 不缩小布局视口,底部抽屉会被键盘遮住;Android 用 `interactive-widget=resizes-content` |
| 输入框缩放 | 触屏上 input / textarea 字号 16px,避免 iOS 聚焦时自动放大 |
| 点按 | `touch-action: manipulation` 去掉双击缩放延迟;去掉点按高亮;触屏上可点区域 ≥ 44px;`hover` 样式只在支持 hover 的设备生效,避免粘在上一次点击处 |
| 返回键 | 弹层打开时压入一条历史记录,Android 返回键 / iOS 边缘滑动关闭弹层而不是离开页面(`lib/useBackToClose.ts`,这是唯一直接操作 history 的地方) |
| 状态栏颜色 | `theme-color` 跟随当前页面底色,手动切换亮暗和系统切换时都会更新 |
| Safari 旧版 | `backdrop-filter` 同时写 `-webkit-` 前缀 |
| 横屏 / 矮屏 | 高度 < 500px 时隐藏「最新」卡、动作栏改为横排;宽 ≥ 900 **且**高 ≥ 500 才用电脑布局,横屏手机不会被当成电脑 |
| 窄屏 | < 360px 时顶栏只留在线点,条目小字允许换行 |
| 相机 | 「拍照」用 `<input capture="environment">`,iOS 与 Android 都会直接拉起相机;扫码在 Android Chrome 用 `BarcodeDetector`,iOS Safari 不支持,提示用系统相机扫码后打开链接,或粘贴链接。相机、剪贴板读取都要求 https(`localhost` 除外) |
| 剪贴板 | iOS 读取时会弹出系统「粘贴」确认,Android 会要求权限;被拒绝时退回「写」 |
| 图片格式 | iPhone 的 HEIC 照片在非 Safari 浏览器里无法显示,预览失败时提示直接下载 |
| 下载 | iOS 上 `a[download]` 配合 blob 会弹出存储菜单;大文件优先走 OPFS 临时存储;不支持时 Blob 回退最多 64 MiB |
## 13. 空间、频道与频道分组
界面层级统一为「空间 → 频道分组(可选)→ 频道 → 聊天与文件夹」。原顶层频道对用户称为空间,原 thread 对用户称为频道。新建空间自动创建“常规”频道。空间负责成员、邀请、成员人数上限和加密边界;账号内设备不重复计入人数,设备只是账号的属性,只在账号设置里管理。频道分组只组织导航,不改变权限或加密密钥。
桌面最左是 64px 的空间窄栏(每个空间一个首字方块,当前空间反色;下方是新建空间与加入空间),其右 240px 的频道列:顶部是空间名与加密状态(点击打开空间设置),下面按分组展示频道,分组标题小号灰字,悬停才显示 + 与 ⋯,频道行 32px、选中为整行淡底;便签并入频道列底部。账号卡片贴在左下角横跨两栏。手机上这些都收进左侧抽屉:顶部是横向一排空间方块,其下是空间名栏与频道列表。支持新建/重命名分组、折叠分组、在分组内新建频道、将已有频道移入其他分组或移回未分组。旧 thread 自动作为未分组频道展示。中间显示当前频道聊天,右侧显示当前频道文件夹和文件。
空间设置集中管理成员、邀请和空间权限;频道管理提供名称与所属分组。分组名及归属由后端持久化并通过通知刷新,刷新与其他设备保持一致。协议/API 内部保留 channels/thread_id 等标识,避免因界面术语变更重建已有数据。
### 13.1 成员栏、邀请与通知
右侧栏顶部是「文件 / 成员 n」页签,默认文件;顶栏的人数按钮直接打开成员页,空间主还有一个 + 直接打开邀请弹窗。手机上没有页签,「文件」「成员」放在左侧抽屉里。
成员页以用户为单位,按 Discord 的分组排列:在线 — n、离线 — n、待接受 — n(仅空间主可见,有「撤销」)。每行头像、昵称(有昵称时下方小字显示用户名),空间主带「空间主」标签;一个用户有多台设备时小字显示「在线设备/设备数 台设备在线」。**设备不再单独列出,只是用户的属性。**在线状态与设备数由后端按账号统计(`GET /channels/{id}/members` 的 `online`、`devices`、`online_devices`,在线 = 该账号在本频道有活动订阅),前端在成员页可见期间每 8 秒读取一次,不改动订阅流。空间主在页头有醒目的「+ 邀请成员」,成员行悬停出现「移除」(需确认)。邀请与成员管理不再放在空间设置里,设置里只留一句指引。
邀请弹窗只收用户名;成功、撤销、移除、失败都弹 toast。toast 在右下角,4 秒消失(带操作按钮的 10 秒),最多 4 条,成功 / 失败 / 提示用左侧色条区分,`aria-live`。被邀请方登录后每 8 秒轮询账号邀请,出现新邀请就弹「xxx 邀请你加入 yyy [接受]」,点「接受」直接加入并进入空间;账号设置里的邀请收件箱同样显示空间名与邀请人。空间名与邀请人放在邀请 payload 的加密内容里(`ChannelGrant.name / invitedBy`,均为可选字段),服务端不可见;旧邀请没有这两个字段时回退为「空间 + ID 前 8 位」。
## 14. 文件管理器交互
文件区支持单击选中、Ctrl/Command 多选、Shift 连选、双击预览、右键菜单和键盘 Enter/Delete/Ctrl+A/Escape。文件夹单击选中、双击或菜单“打开文件夹”进入;面包屑用于快速返回。文件行提供更多菜单按钮,触屏也能访问操作。
右键文件提供预览、下载、复制名称、移动和删除;文件夹提供打开和重命名;空白区域提供上传、新建文件夹、全选。菜单和悬停卡通过 portal 挂载到 body,贴近指针且限制在视口内,支持 Escape、外部点击和键盘导航。
文件可拖入当前频道的文件夹或面包屑目录,多选文件可一起移动;跨空间/频道的内部拖放拒绝。外部文件可拖入指定文件夹上传。内部拖拽移动由后端更新资源归属,不重新加密或拉取文件。
悬停卡延迟显示名称、类型、大小、时间;已同步且非 GATED 的小图片/文本可显示缩略预览。GATED、上传中或校验失败的文件不会自动取内容,悬停不消耗下载次数。右侧文件区与聊天之间可拖动分隔条调整宽度,分隔条也支持键盘左右键。
## 15. 频道中的文件消息与发送者
聊天时间线采用 Discord 式布局:所有消息靠左,同一条左边线,左侧头像、右侧「名称 + 时间」一行与占满宽度的正文,不再区分自己和他人。同一发送者 5 分钟内连发的消息合并,只缩进对齐,悬停时在头像位置显示时间;悬停整行有淡底,右上角出现删除。文件消息是 360px 以内的文件卡片(图标、名称、大小与类型,右侧预览与下载);没有头像时显示名称首字。受限文件不自动加载内容或媒体缩略图。
输入区是整体圆角框(左 + 附件、中间自增高输入、右发送),占位文案「给 #频道名 发消息」,Enter 发送、Shift+Enter 换行(输入法组合中不触发)。
**乐观发送**:点发送的同一帧就清空输入框并在时间线底部画出消息(半透明,aria-busy),后台再加密上传,不等后端;可以连续快速发送。worker 发布真实条目时带回 localId,在同一次状态更新里替换占位项,不闪烁、不重复。失败时占位项保留原文本并显示「发送失败 · 重试」,可删除;离线沿用本机发送队列。文件同理:确认上传选项后卡片立刻出现并显示进度。
发送者名称与头像从后端账号资料读取,回退固定用户名。资源作者由后端记录,昵称修改后其他设备同步更新展示。没有账号归属(未登录访客)时统一显示「访客」;界面里不再展示设备名,设备只在账号设置的「我的设备」里管理。
头像缩略图在 Worker 内生成,最大 64×64、编码后不超过 8 KB,仅使用 PNG/JPEG/WebP data URL;不使用第三方头像链接。圆形头像使用专门的 --radius-avatar token,消息气泡仍遵守现有圆角规范。
## 16. 记住账号和密码
登录/注册表单提供“记住账号和密码(本机)”选项,默认勾选。成功验证后才保存本机加密凭据,下次需要登录时自动填充用户名和密码;有效会话仍优先自动恢复。取消勾选时,只保留当前页面会话,后台同步不能再次写入自动登录数据。
正常退出登录清除会话,但保留用户明确选择保存的填充信息。“清除本机保存”同时清除保存的账号密码和自动恢复会话;如果当前已登录,可以继续使用当前页面,刷新后需重新登录。会话被服务端拒绝时清除本机恢复资料。界面提示只在自己的设备使用记住功能,并兼容浏览器自带密码管理器。