模块三:产品设计与前端实战 | 第04讲:UI 与 UX 的关键差别——页面好看却不好用的 5 个原因
项目:VibeNote 智能笔记(Next.js 14 + React + TypeScript + Tailwind CSS + shadcn/ui)
一句话:UI 是界面长什么样;UX 是用户能不能低摩擦地完成任务。 再美的像素,也救不了迷路、白屏、无声失败。下一讲我们会用组件化把本讲的三态与交互契约固化成「可复用模块」,让 AI 生成代码时更少随机、更多一致。
一、先把定义钉死:UI 与 UX 不是「设计师 vs 开发」
**UI(User Interface)**偏视觉与组件:排版、色彩、字体、间距、图标风格、组件形态。
**UX(User Experience)**偏流程与反馈:用户是否理解自己在哪里、下一步是什么、系统是否在忙、错了怎么恢复、成功后得到了什么确认。
在 VibeNote 里,一个典型错位是:你把 Markdown 预览做得非常漂亮(UI 强),但保存失败时页面毫无提示(UX 崩)。用户不会评价「你的 Shiki 主题真美」,只会觉得「这玩意儿不靠谱」。
这也是为什么在 Vibe Coding 工作流里,我们坚持让 PRD 先写「交互与状态」:模型很擅长生成静态布局,但如果没有明确状态约束,它往往会默认一切请求都成功、一切列表都有数据——这在真实世界里几乎从不成立。把本讲读透,你会更容易一眼看出 AI 生成页面里「看起来像成品、实际上用不了」的隐患。
课程原文「4.1 页面好看却不好用」与「4.2 Loading 空状态错误态」共同指向同一条工程常识:体验问题往往发生在状态切换处,而不是静态页面。
flowchart LR
UI["UI 表现层"] --> UX["UX 行为层"]
UX --> O["任务完成 Outcome"]
二、页面好看却不好用的 5 个高频原因(对照 VibeNote)
原因 1:没有「系统状态」——用户不知道电脑在干嘛
表现:点击 AI 按钮后按钮可重复点、页面像死了一样。
本质:缺少 Loading 与 禁用态,用户无法建立因果预期。
原因 2:空状态=空白——用户不知道下一步
表现:笔记列表为空时一片白。
本质:缺少 Empty State 的引导与主行动作(CTA)。
原因 3:错误被吞掉——「没反应」是最差的反应
表现:网络失败、校验失败只在 console。
本质:缺少 Error State 的可读文案与恢复路径(重试/返回)。
原因 4:信息架构断裂——好看但迷路
表现:编辑器在 A 页面,列表在 B 页面,返回逻辑混乱。
本质:导航模型不清晰(尤其是移动端)。
原因 5:视觉噪音盖过任务
表现:装饰性动效、过多边框、过强对比,导致阅读与写作疲劳。
本质:把「品牌表达」放在「任务效率」之上。
flowchart TB
S["用户触发动作"] --> L{"有 Loading?"}
L -->|否| Bad1["焦虑重复点击"]
L -->|是| D{"数据为空?"}
D -->|是| E{"有 Empty 引导?"}
E -->|否| Bad2["误以为坏了"]
E -->|是| OK["继续"]
D -->|否| R{"请求失败?"}
R -->|是| F{"有 Error 恢复?"}
F -->|否| Bad3["无声失败"]
F -->|是| OK
三、Loading:不是转圈而已,而是「预期管理」
好的 Loading 回答三个问题:是否在处理?还要多久(粗略)?我能不能取消/重试?
对 VibeNote 的 AI 功能:
- 按钮进入
disabled+Loader2图标(shadcn 常见模式) - 长任务显示轻量进度或阶段性文案(「生成摘要中…」)
- 允许用户切换页面时中断请求(
AbortController)
反模式:全屏遮罩挡住一切,用户连「返回列表」都不能。
四、Empty:空状态是「第二首页」
空状态的目标不是「显得不空」,而是推动下一步。VibeNote 列表空时,应提供:
- 一句解释:「还没有笔记」
- 主按钮:「新建笔记」
- 次文案:可选模板或示例(不要一次塞太多)
五、Error:把技术异常翻译成人类行动
分层:
- 可恢复:网络抖动 → 重试
- 输入问题:校验失败 → 指出字段
- 系统问题:5xx → 道歉 + 工单/反馈入口(MVP 可简化为复制错误码)
对 AI:还要处理 内容风险 与 空输出,不要假设模型永远返回非空字符串。
六、设计原则(工程化版):别背教条,背检查项
- 一致性:同类操作同类反馈(保存都用 toast 或都用 inline alert,别混用)
- 可见性:状态可见、位置可见、下一步可见
- 容错:destructive 操作二次确认;支持撤销(至少文本编辑)
- 简约:减少同时出现的主按钮数量(一个屏幕最多一个「最强 CTA」)
- 对齐 shadcn 语义色:用
destructive、muted-foreground等 token,而不是随手#ff0000
七、shadcn/ui 不是皮肤包:它是「组件契约」
shadcn 把 Radix + Tailwind 以可复制源码方式放进你的仓库。它的价值:
- 可访问性默认值更高(焦点、键盘、ARIA)
- 变体体系统一(
cva) - AI 更容易生成一致代码(组件名稳定)
对 VibeNote:优先用 Button、Sheet、Dialog、Tabs、ScrollArea、DropdownMenu、Tooltip、Sonner(toast) 等组合,而不是自己造轮子。
当你告诉 AI「用 shadcn」,请同时告诉它具体组件名与变体名;否则它会混合使用不同写法,导致 className 与交互模式分裂,最终伤害的是 UX 一致性。
八、可运行代码:三态小组件(Next.js Client Component)
下面示例演示 Loading / Empty / Error 的最小结构(可直接粘贴进 components/note-list-states.tsx 后按项目调整)。
// components/note-list-states.tsx
"use client";
import { Loader2, FileWarning } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
type Props = {
loading: boolean;
error: string | null;
items: { id: string; title: string }[];
onRetry: () => void;
onCreate: () => void;
};
export function NoteListStates(p: Props) {
if (p.loading) {
return (
<div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
正在加载笔记列表…
</div>
);
}
if (p.error) {
return (
<div className="p-4">
<Alert variant="destructive">
<FileWarning className="h-4 w-4" />
<AlertTitle>加载失败</AlertTitle>
<AlertDescription className="space-y-3">
<p>{p.error}</p>
<Button size="sm" variant="secondary" onClick={p.onRetry}>
重试
</Button>
</AlertDescription>
</Alert>
</div>
);
}
if (p.items.length === 0) {
return (
<div className="flex flex-col items-start gap-3 p-8">
<div className="text-lg font-medium">还没有笔记</div>
<p className="text-sm text-muted-foreground">
先从一条会议记录或学习笔记开始,VibeNote 会帮你保持结构。
</p>
<Button onClick={p.onCreate}>新建笔记</Button>
</div>
);
}
return (
<ul className="divide-y">
{p.items.map((n) => (
<li key={n.id} className="px-4 py-3 text-sm">
{n.title}
</li>
))}
</ul>
);
}
依赖:需已在项目中
npx shadcn@latest add button alert(版本以你本地 CLI 为准)。
九、把三态写进 PRD(与第03讲联动)
在 PRD「交互与状态」章节,强制每条列表页写清:
- Loading 文案与视觉
- Empty CTA
- Error 恢复
否则 AI 实现时默认 happy path only。
十、UX 文案:用动词开头,避免甩锅用户
差:「非法输入」
好:「标题不能为空,请填写后再保存」
差:「出错」
好:「网络不稳定,点击重试;若持续失败请检查代理」
十一、再谈「好看」:审美要服务任务,而不是绑架任务
很多初学者会用 Dribbble 风格绑架产品:大留白、超大标题、低信息密度。对写作工具(VibeNote)来说,信息密度与阅读效率往往比「视觉冲击力」更重要。不是说不美,而是美的定义要换成:用户在 30 分钟内是否更少疲劳、更少误触、更少迷路。
你可以用三个问题自检当前界面:
- 第一屏有没有浪费大量像素在装饰上,导致核心编辑区被挤压?
- 用户能否在不滚动的情况下找到「保存/AI/返回」中的至少两个关键动作?
- 暗色模式下,正文与辅助文字对比是否仍符合可读性(不仅是「能看清」)?
这套问题比「你觉得好不好看」更接近 UX。
十二、从认知负荷角度看 UI:为什么「少即是多」经常成立
认知负荷分三类:内在负荷(任务本身难)、外在负荷(糟糕界面带来的额外难)、相关负荷(学习新系统带来的必要难)。产品设计的努力,应主要减少外在负荷。
VibeNote 的外在负荷常见来源:
- 同一概念多个名字(笔记/文档/条目)
- 同一动作用多种组件表达(有的用 toast,有的用 alert)
- 移动端与桌面端导航模型不一致
把这些写进设计约束(或 PRD 术语表),AI 生成界面时会更稳。
十三、微交互:不是炫技,是「反馈时差」的管理
人眼对反馈的耐心通常在几百毫秒量级。超过 1 秒没有反馈,就会产生焦虑。微交互(按钮按下态、轻微过渡、loading)的价值是填补反馈时差。
但要避免:
- 过度动画导致「等动画播完才能继续」
- 闪烁式 loading 引发视觉疲劳
shadcn + Tailwind 的 transition-colors 一类轻量过渡通常足够。
十四、表单与笔记编辑器:错误态要靠近犯错点
全局 toast 适合「保存成功」这种结果型反馈;字段级错误应靠近输入控件(FormMessage)。如果 VibeNote 未来有元数据表单(标题、标签),记住:错误离输入越远,用户越烦。
十五、AI 输出场景的特殊 UX:「渐进呈现」与「可审查」
AI 生成内容不是普通数据加载:它可能逐字出现,也可能中途失败。UX 上建议:
- 生成中显示可理解的进行中状态
- 允许用户中止(Abort)
- 完成后提示用户核对(免责声明 + 一键复制/插入)
这不是法务套话,而是降低信任成本的产品设计。
sequenceDiagram
participant U as 用户
participant UI as 编辑器
participant API as Route Handler
U->>UI: 点击 AI 摘要
UI->>UI: Loading + 禁用重复提交
UI->>API: 请求流式输出
API-->>UI: token 增量
UI-->>U: 渐进展示/可中止
Note over UI: 失败则 Error + 重试
十六、shadcn/ui 选型清单(VibeNote 高频)
| 场景 | 推荐组件 | 说明 |
|---|---|---|
| 全局通知 | sonner | 成功/失败轻反馈 |
| 阻断确认 | alert-dialog | 删除笔记等 |
| 侧边导航 | sheet(移动)+ 固定侧栏(桌面) | 下一讲实战深化 |
| 分区布局 | tabs / resizable(可选) | 编辑/预览切换 |
| 列表 | scroll-area | 长列表滚动条可控 |
十七、对比案例:同一个「保存笔记」的两种实现感受
A 版:点击保存 → 无反馈 → 1 秒后跳转。用户不确定是否点中。
B 版:点击保存 → 按钮 loading → 成功 toast → 列表更新时间戳。
B 版未必更「美」,但 UX 显著更好。记住:UX 优先于 UI 炫技。
十八、无障碍(a11y)不是「锦上添花」:它是专业度的分水岭
至少做到:
- 图标按钮
aria-label - 表单控件关联
label - 键盘可操作主要路径
- 焦点可见
shadcn/Radix 帮你打底,但你仍可能在组合时把焦点陷阱搞丢。下一讲会展开更多 a11y 与响应式清单。
十九、设计评审:用「任务走查」代替「品味辩论」
评审时不要问「这颜色好不好」,而是走查:
- 新用户 60 秒能否创建第一条笔记?
- 老用户能否 10 秒找到昨天那条?
- AI 失败时会不会误以为笔记丢了?
这套走查与第01讲验证三步法天然衔接。
二十、把 UI/UX 约束写进给 AI 的提示词(模板)
你是前端工程师。实现 VibeNote 列表页。
约束:
- 使用 shadcn/ui;不得引入第二套组件库
- 必须覆盖 loading / empty / error 三态
- 移动端优先;md 以上双栏
- 所有图标按钮提供 aria-label
- 错误文案中文,给出重试
验收:
- 断网时可重试
- 空列表有 CTA
二十一、可运行代码:保存按钮的三态(演示)
// components/save-button-demo.tsx
"use client";
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
export function SaveButtonDemo() {
const [state, setState] = useState<"idle" | "saving" | "error">("idle");
async function save() {
setState("saving");
await new Promise((r) => setTimeout(r, 800));
const ok = Math.random() > 0.35;
setState(ok ? "idle" : "error");
}
return (
<div className="flex items-center gap-3">
<Button disabled={state === "saving"} onClick={save}>
{state === "saving" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
保存
</Button>
{state === "error" && (
<span className="text-sm text-destructive">保存失败,请重试</span>
)}
</div>
);
}
二十二、常见踩坑:把「Skeleton」当万能药
Skeleton 适合结构稳定的页面;对笔记编辑器这种结构动态场景,滥用 skeleton 反而让用户困惑。此时更优先:明确的 loading 文案 + 禁止重复提交。
二十三、从数据角度理解 UX:事件比感觉可靠
建议你为关键 UX 点埋事件:save_clicked、save_failed、ai_started、ai_failed。没有数据,你只能争论审美;有了数据,你能争论哪段流程在漏人。
二十四、暗色模式下的 UX:注意「对比度」与「语义色」
暗色模式不是把背景变黑那么简单:muted 文本在暗底上更容易过灰;destructive 如果过亮会造成刺眼。shadcn 的主题 token 体系意义就在这里:少用手写 hex,多用语义色,暗色切换才不容易翻车。
二十五、与模块二 AI 写作工具栏的衔接:状态机要一致
模块二 AIWritingToolbar 已演示 busy 与 err 状态。本讲要求你把同样模式推广到所有异步动作:保存、加载列表、导入导出(未来)。一致性会显著降低用户学习成本。
二十六、UX 写作:语气要「并肩」,不要「居高临下」
差:「你输入错误」
好:「我们没能保存,请检查网络后重试」
差:「无效请求」
好:「请求超时,服务器可能繁忙」
技术错误码可以放在次要位置供复制,但主文案要面向普通人。
二十七、页面好看却不好用的「隐藏第六因」:性能错觉
如果滚动卡顿、输入迟滞,用户会把它归因为「难用」而不是「性能」。因此 UX 也包含 交互性能:大文档预览要 useDeferredValue、列表要虚拟化(未来)、动画要克制。
二十八、用检查表结束本讲:上线前 5 分钟 UX 巡检
- 任何按钮在请求中不可重复触发(除非刻意允许)
- 空列表有引导
- 错误有文案 + 恢复路径
- AI 输出有风险提示
- 移动端关键路径可达
二十八补、信息架构:VibeNote 的「用户在哪儿」问题
UI 再精致,如果用户不知道自己处于「列表态 / 编辑态 / 预览态 / AI 处理态」中的哪一种,就会频繁误操作。专业产品会用一致的位置语言解决:侧栏永远表示「空间与集合」,主区表示「当前对象」,顶栏表示「对象级动作」(保存、分享、更多)。这不是唯一正确答案,但你需要选一种并坚持。否则 AI 每次生成页面都会换一种导航逻辑,用户的学习成本会被你自己放大。
再补一张 IA(信息架构)与状态的对应关系图,帮助你在代码层思考路由与布局:
flowchart TB
L["/notes 列表 IA"] --> E["/notes/[id] 编辑 IA"]
E --> P["预览子状态"]
E --> A["AI 子状态"]
P --> E
A --> E
二十九、视觉层次:用「类型尺度」而不是「更多颜色」
新手爱用颜色强调一切,结果页面像圣诞树。更稳的做法是使用 字体尺度(text-sm / text-base / text-lg)与 字重(font-medium)建立层次,把颜色留给语义(成功/失败/链接)。Tailwind 的 text-muted-foreground 适合辅助信息,不要让辅助信息与正文抢对比度。
三十、从「组件视觉」到「品牌一致性」:VibeNote 的最低品牌包
你不需要完整品牌手册,只要定 5 件事:主色语义(primary 用于主 CTA)、圆角策略(rounded-md 统一)、阴影策略(尽量克制)、字体栈(系统字体即可)、图标库(lucide 统一)。写进 AGENTS.md 后,AI 生成 UI 的方差会明显下降。
三十一、错误案例库:三种让用户暴怒的 UX(真实世界常见)
- 静默丢数据:保存失败无提示,用户切换页面后内容消失。
- 无限 loading:请求悬挂,不提供超时与重试。
- 把用户当调试器:弹原生
alert堆栈或 JSON。
对照 VibeNote:只要你守住第03讲 PRD 的验收标准,这三类问题都应被挡在发布门外。
三十二、与 shadcn 主题变量的关系:少写魔法数
优先使用 hsl(var(--primary)) 体系下的 token,而不是页面里到处 text-[#333]。原因很简单:暗色模式切换时,魔法数不会跟着变,UX 会在暗色下突然崩坏。
三十三、用户测试极简版:找 3 个人做「出声思考」
给他们任务:「创建一条笔记并假装明天要找回来」。你不要教,只观察。你会看到令人惊讶的迷路点——它们往往不在 UI 美不美,而在文案是否误导、按钮是否找不到。
三十四、从「好看」回归「好用」:一句口号级别的判断标准
如果只能用一句话验收 UX,我建议用:「用户是否总能知道系统正在发生什么,以及下一步该怎么做。」 这句话涵盖 loading、empty、error,也涵盖导航与反馈,是本讲的主轴。
三十五、把「状态」提升为一等公民:React 层面的组织方式
在实现层,建议你为页面定义显式 viewState:"loading" | "ready" | "empty" | "error",而不是用一堆布尔值 isLoading && !isError 组合出隐含状态。布尔组合会在两周后变成不可读逻辑,也会让 AI 在补丁迭代时更容易引入互斥状态同时成立的 bug。VibeNote 的笔记列表页就是一个典型场景:loading 与 error 绝不能与 items 同时驱动 UI;用联合类型(discriminated union)会更安全。
三十六、与 Next.js App Router 的配合:Suspense 不是替代 Error UI
loading.tsx 能覆盖部分导航加载,但它替代不了业务错误与空列表。很多人误以为加了 Suspense 就「UX 完成了」,结果数据错误仍然无声。正确分工是:路由级 loading.tsx 解决页面切换;组件内三态解决数据请求;error.tsx 解决渲染期异常。三者叠加才专业。
三十七、VibeNote 编辑器 UX:光标、滚动与预览同步的「细节暴力」
分栏预览很爽,但常见痛苦是:编辑区滚动与预览区滚动不同步、长文跳转锚点困难。MVP 可以不做同步滚动,但要在 PRD 里写明「本期不做」,并在 UI 上避免让用户误以为会自动同步(文案提示即可)。这类期望管理属于 UX,不属于 UI。
三十八、国际化预留:中文文案也不要硬编码在组件深处
即便你只做中文,也建议把字符串上移到 const 或 messages 对象中。原因不是立刻要做 i18n,而是让 AI 更容易批量调整文案与语气,减少散落在 JSX 里的碎片字符串。未来要做英文版时,你不会想全局人肉替换。
三十九、对比:用「设计系统 token」约束 AI 比用「审美形容词」有效一万倍
不要说「更现代一点」,要说「使用 rounded-md、border-border、bg-background」。前者让模型猜,后者让模型抄。shadcn 的价值就是把大量选择收敛成 token,让 Vibe Coding 的随机性下降。
四十、从用户情绪曲线理解 UX:峰值与终值
体验记忆往往由峰值与终值决定:如果保存成功那一刻很爽(轻量 toast + 更新时间),用户会原谅中间偶尔的慢;如果最后一步失败且无法恢复,用户会忘记你前面多好看。VibeNote 的 AI 功能尤其要注意终值:生成结束有没有清晰「完成」信号?有没有「插入/替换」明确结果?
四十一、团队协作时的 UX 责任划分:谁对文案负责?
建议:产品/负责人对文案负责,工程师对状态负责。否则会出现「工程师临时编一句话」导致语气不一致。你可以把文案统一放在 copy.ts 或 messages,PR 评审先看文案再看样式。
四十二、本讲与下一讲的桥梁:组件化如何让三态可复制
当你把 NoteListStates 这类组件沉淀为稳定契约,下一讲的组件化与组合才会「越做越快」。否则每个页面都手写一遍三态,AI 再快也会重复犯错。
四十三、复盘清单
- 你能画出 VibeNote 主路径的状态机吗?
- 你是否能指出当前项目最弱的 UX 点?
- 你是否能把三态组件复用到两个页面?
四十四、思考题
- 你最近一个项目里,哪一种状态最常被忽略?为什么?
- VibeNote 的 AI 流式输出,Loading 应该全局还是局部?各自代价?
- 你如何在不增加视觉噪音的前提下表达「正在保存」?
- 列出你项目中三个应使用
AlertDialog的 destructive 动作,并说明文案。 - 如果把 Empty 状态做成营销位,你会如何控制信息密度避免打扰?
- 你会如何为「预览渲染失败(Markdown 语法错误)」设计不吓人的错误体验?
四十五、下讲预告
当你把 UI 当作「皮肤」、把 UX 当作「流程与状态」,你会自然得出本专栏的工程结论:先写状态机与验收,再让 AI 生成组件。这能显著减少「页面像样但不好用」的返工。下一讲我们会把三态组件、工具栏、侧栏等沉淀为可组合模块,让你在面对更大功能面时仍能保持稳定产出。
第05讲讲 组件化开发:如何用 组合 而不是复制粘贴堆功能,让 AI 写前端越写越快。
参考:课程原文 课程内容/4.1 页面好看却不好用?UI与UX的关键差别一次讲透.md、课程内容/4.2 用户最怕什么体验?Loading空状态错误态全攻略.md;若本地存在 reference/advanced/05-ui-ux.md 可补充阅读。
结语:UI 是面子,UX 是里子;做笔记工具,里子出问题会直接把用户送回系统备忘录。把三态做成习惯,你就从「会写页面」进阶到「会做产品」。记住:用户不会为你的渐变点赞,但会为你的可靠留存;可靠来自状态清晰,不靠特效堆叠;把这句话写进你的设计约束里,团队会少吵很多架,AI 也会少生成很多无用样式与结构分叉问题。