Files
UHD_Media_Tool/docs/design/ui-controls.md
T
rootandClaude Opus 5.5 c9b4ceb301 fix(web): 控制台界面细节与 uhd_web 控件规范对齐
- 顶栏搜索移进右侧玻璃胶囊;输入框改淡玻璃(边线 0.46 → 0.16,悬停加深,聚焦 / 出错保留光晕)
- 新增 SegmentedControl(滑动 seg-thumb),替换站点、下载、订阅、文件、智能体列表及对话框里的实心选中块;Tabs 改为同一枚滑块
- 开关型按钮统一 outline + aria-pressed 选中外观;页头 / 工具栏次操作描边、图标钮描边圆;文件排序改 Select;下载总速度加标签
- 分页一页放得下时只留总数、空列表不渲染、按容器宽度收起按钮
- 运行状态存储区重排;智能体列表页与会话页共用外壳路由,标题与左栏不再重挂载
- 站点卡片「⋯」不再单独换行;设置导航、状态计数、多选条件改玻璃选中态

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-08 12:02:48 +08:00

76 lines
9.1 KiB
Markdown

# 控件使用标准
> 迁入自 /opt/uhd_web `docs/features/`(2026-09-27),2026-10-08 与 uhd_web 同步。文中流媒体、播放器、落地页、套餐等段落是 uhd_web 的原始上下文,本项目没有这些页面,只作材质与 token 用法参考;以本目录 README 的约定为准。
配合[设计 token](design-tokens.md)与[界面材质](interface-materials.md)。开发态的陈列页 `/dev/ui`(只在开发构建注册,[routes.ts](../../apps/web/app/routes.ts))把三块壳下的 token、材质和控件排在一页,换皮或改 token 先看它。
## 按钮只有一套
全站按钮(门户、公共页、测速、流媒体、播放器浮层)是同一套外观,与动漫站同一思路:形状一律胶囊或正圆,颜色与材质只由角色决定。
| 角色 | 门户写法 | 流媒体写法 | 外观 |
|---|---|---|---|
| 主操作 | `<Button>` | `btn-glass btn-glass-primary` | lens 玻璃胶囊 + 左上棱边的光谱高光(静止,悬停沿边扫过一小段),不做实心 |
| 次操作 | `<Button variant="outline">`(`secondary` 同样子) | `btn-glass` | lens 玻璃胶囊(边光 + 外侧细线) |
| 图标钮 | `<Button variant="outline" size="icon*">` | `btn-glass-icon` | lens 玻璃正圆 |
| 只有文字 / 图标 | `<Button variant="ghost">` | 各自的 ghost 行 | 悬停出淡底 |
| 危险操作 | `<Button variant="destructive">` | — | 淡红底红字胶囊 |
一屏(或一张卡片)只放一个主操作,并列的其余动作退为描边;前置条件没满足时,真正该做的那一步是主操作(例如团队页没有团队套餐时,「创建团队套餐」是主操作,「添加成员」退为描边)。
两边的底、边、字色、投影都引用 `--mat-lens-*` / `--mat-pill-*`,改外观只改参数。弹窗底部的按钮行间距 12px(`gap-3`):按钮带外侧细线与光谱高光,8px 会挤在一起。放进玻璃胶囊里的按钮(控制台顶栏的通知、日夜切换、账户)用 `ghost`,不再自带玻璃。调用处只给布局(宽度、高度、间距、定位),不改圆角、底色、边框、投影和字色;不是 `<button>` 的元素(`Link`、`a`)用 `buttonVariants()` 拿同一套类名。分段标签、菜单行、列表行不算按钮,按各自控件规范;分页当前页、角标、选中的季 / 集 / 选项、日历选中日用静态的 `pill-selected`,不借主按钮样式;分段控件(侧栏、SegmentedTabs、求片页的分段)的滑块用 `seg-thumb`。放胶囊或分段控件的横向滚动容器加 `scroll-bleed`:`overflow-x-auto` 会连带裁掉纵向溢出,不加的话胶囊的阴影和描边会被切成直边。
### 本项目(控制台)的落地
- **单选分段**用 `SegmentedControl`([segmented.tsx](../../apps/web/app/components/ui/segmented.tsx)):筛选(全部 / 已启用 / 需要处理)、视图(卡片 / 列表 / 日历,只有图标时给 `title`)、分组(进行中 / 已完成)、对话框里的二选一 / 三选一。选中项由一枚滑动的 `seg-thumb` 承担,文字提一档,没有底轨、没有实心块;数量放 `count`,弱一档显示。radix `Tabs` 的 `TabsList` 用同一枚滑块(`useSlidingThumb`),页面级标签页不另写。横向放不下时外面包一层 `overflow-x-auto scroll-bleed`。
- **多选条件**(搜索结果的分辨率 / 站点、整理记录顶部的状态计数)是胶囊,选中用 `pill-selected`,不用分段控件。
- **开关型按钮**(批量、按作品分组、显示例行记录、显示隐藏文件)是 `variant="outline"` 加 `aria-pressed`,按下时 `.pill-secondary[aria-pressed="true"]` 给出与 `pill-selected` 相同的外观;分页的当前页同理(`aria-current="page"`)。不再用 `secondary` / `ghost` 切换表示开关。
- **页头与工具栏**:一屏一个主操作,其余是描边胶囊;工具栏里的图标按钮是描边圆钮(`variant="outline" size="icon-sm"`)。`ghost` 只留给行内文字动作(返回链接、表格行尾的「⋯」、卡片里的「详情」「清除选择」)和顶栏胶囊里的按钮。下拉一律用 `Select`,不用原生 `<select>`。
- **分页**(`Pagination`):一页放得下时只留「共 N 条」(超过最小档 10 条时保留每页条数),没有条目时不渲染;卡片网格下面传 `hideSingle`,一页时整块不显示。翻页按钮按容器宽度收起(`@container`),窄卡片里不折行。
- **顶栏搜索**在右侧玻璃胶囊的第一格(与 uhd_web 流媒体顶栏一致):桌面是平涂淡底胶囊加 Ctrl K 提示,手机只留图标,都打开命令面板。
右上角账户模块全站共用 `account-popover.tsx`:用户中心用头像 + 名字的胶囊,流媒体与测速用圆头像;菜单里是另外两块的跳转(当前所在的不列,流媒体与测速需有生效订阅)与退出登录。
其余控件仍分两处实现:门户原语在 `app/components/ui/`(shadcn 语义 token),流媒体控件在 `app/routes/stream/_sub/`、`app/routes/stream/player/controls.tsx`(浏览页 `content` / `wash`,播放器 `on-media` 族)。需要相同行为时各自实现。
## 尺寸与圆角
- 图标钮:桌面 36px,触屏 40px(`touch:` 变体,见下文「触屏判断」);播放键等主钮 44 / 48。
- 圆角角色:控件 `rounded-md`、小按钮 `rounded-lg`、输入框与下拉选择 `rounded-xl`(由 `.field-glass` 的原语给)、卡片 `rounded-xl`、面板 `rounded-2xl`、抽屉与大卡 `rounded-3xl`、药丸 `rounded-full`、徽章与骨架屏 `rounded-tight`。
- 字号:控件文字 `text-control`(15),菜单项 `text-body` / `text-caption`,面板小标签 `text-label` 大写 `tracking-[0.18em]`。
## 状态反馈
- hover:洗层 `hover:bg-wash/[0.06]`(播放器 `on-media-wash/[0.07]`),文字提一档(secondary → strong)。
- 按压:`active:scale-[0.95]`(圆形主钮 0.92),`transition-[transform,background-color,color] duration-micro`。
- 选中:`pill-selected`(静态半透明底 + 稍深描边);单选列表项左侧 Check 标记。进度用细线表示,不做实心填充。
- 打开中的触发钮:`bg-wash/[0.08]` 保持高亮。
- 焦点:门户走 shadcn 的 `focus-visible:ring`,玻璃族 `focus-visible:ring-2 ring-brand/50`。
- 禁用:`opacity-50 cursor-not-allowed`,不改颜色。
- 危险:移除、删除这类动作平时是中性描边,悬停时换成 `destructive` 的边框与文字。
## 动效
`duration-micro` 微反馈、`duration-base` 颜色透明度、`duration-layout` 尺寸位置、`duration-slow` 及以上只给入场与装饰;缓动默认 `ease-standard`,滑块与按压回弹 `ease-spring`,抽屉 `ease-sheet`,弹层 `animate-pg-pop`。尊重 reduced motion。
## 大写小标签
`SectionLabel`(section-label.tsx),三种口味:`portal`(门户表单字段名与分组,`text-xs font-semibold uppercase text-muted-foreground`)、`page`(浏览页,`text-label uppercase tracking-[0.18em] text-content-muted`)、`media`(播放器弹层,同上换 on-media)。`as` 指定元素(label / span / p)。
还有几种写法没收进来,值不同、收进来就是像素变化,待设计决定:门户里 `text-xs font-medium uppercase tracking-wider`(团队订单弹窗)、`text-micro … tracking-[0.15em]`(侧栏分组、邀请页)、`tracking-widest` 一处;落地页 hero 的 `tracking-[0.32em] text-white/45` 与分区标题的 `text-meta font-bold tracking-[.22em] text-brand-strong` 是刻意的装饰用法,单列为 hero kicker 与 section kicker。
## 弹层结构
触发钮 + 面板,面板由 `usePopover` 管开关(点外面 / Esc 关闭)。面板顶部一行大写小标签说明这是什么,下面是列表;列表项分两种:单选行(左勾选 + 标签 + 右侧提示)与动作行(左图标 + 文字)。四件在 `player/controls.tsx`:`MenuTrigger`、`MenuPanel`、`MenuOption`、`MenuButton`。`MenuPanel` 通吃三处:`placement` 上方(播放器)/ 下方(浏览页 header),`tone` 决定表头口味,`role` 给 listbox,内边距、宽度、z 与偏移经 className 传入,tailwind-merge 让它们覆盖默认值,所以三处的 class 集合与各自收拢前完全一致(用 react-dom/server 渲出来逐项核对过)。账户菜单的头像表头与排序菜单的单选行样式不同,仍各自写在页面里。
## 收拢原则
- 同一份 class 串出现在两处以上就抽原语,抽的时候 class 串逐字不改,渲染不变。
- 参数只到必要:尺寸档、是否激活、标签、图标;不把整页状态传给控件。
- 只有一处用的局部组件留在页面文件里,不为了"以后复用"提前抽。
- 两个页面展示同一种信息时共用零件:套餐卡用 plan-card-parts.tsx(名称价格、规格表、线路提醒、共有权益),电影与剧集详情用 DetailExtras.tsx(规格徽章、影片信息、可播放版本、更多推荐)。
## 触屏判断
触屏还是鼠标由 [input-modality.ts](../../apps/web/app/lib/input-modality.ts) 统一判断,写在 `<html data-input>` 上:样式用 `touch:` 变体(代替 `[@media(pointer:coarse)]:`),组件用 `useTouchInput()`,处理指针事件用 `pointerKind(e)`,不要直接比较 `pointerType`。车机、触屏一体机常把触屏报告成鼠标,只看 `(pointer: coarse)` 会按电脑处理。`hover:` 在触屏模式下不生效,触屏上必须能用的操作不要只靠悬停出现。界面上不给用户选;排障时可在地址加 `?input=touch|mouse|auto` 手动指定。