3.4 UI 与 UX 的关键差别——页面好看却不好用的 5 个原因

5 阅读18分钟

模块三:产品设计与前端实战 | 第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:把技术异常翻译成人类行动

分层:

  1. 可恢复:网络抖动 → 重试
  2. 输入问题:校验失败 → 指出字段
  3. 系统问题:5xx → 道歉 + 工单/反馈入口(MVP 可简化为复制错误码)

对 AI:还要处理 内容风险空输出,不要假设模型永远返回非空字符串。


六、设计原则(工程化版):别背教条,背检查项

  1. 一致性:同类操作同类反馈(保存都用 toast 或都用 inline alert,别混用)
  2. 可见性:状态可见、位置可见、下一步可见
  3. 容错:destructive 操作二次确认;支持撤销(至少文本编辑)
  4. 简约:减少同时出现的主按钮数量(一个屏幕最多一个「最强 CTA」)
  5. 对齐 shadcn 语义色:用 destructivemuted-foreground 等 token,而不是随手 #ff0000

七、shadcn/ui 不是皮肤包:它是「组件契约」

shadcn 把 Radix + Tailwind 以可复制源码方式放进你的仓库。它的价值:

  • 可访问性默认值更高(焦点、键盘、ARIA)
  • 变体体系统一cva
  • AI 更容易生成一致代码(组件名稳定)

对 VibeNote:优先用 ButtonSheetDialogTabsScrollAreaDropdownMenuTooltipSonner(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 分钟内是否更少疲劳、更少误触、更少迷路

你可以用三个问题自检当前界面:

  1. 第一屏有没有浪费大量像素在装饰上,导致核心编辑区被挤压?
  2. 用户能否在不滚动的情况下找到「保存/AI/返回」中的至少两个关键动作?
  3. 暗色模式下,正文与辅助文字对比是否仍符合可读性(不仅是「能看清」)?

这套问题比「你觉得好不好看」更接近 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 与响应式清单。


十九、设计评审:用「任务走查」代替「品味辩论」

评审时不要问「这颜色好不好」,而是走查:

  1. 新用户 60 秒能否创建第一条笔记?
  2. 老用户能否 10 秒找到昨天那条?
  3. 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_clickedsave_failedai_startedai_failed。没有数据,你只能争论审美;有了数据,你能争论哪段流程在漏人


二十四、暗色模式下的 UX:注意「对比度」与「语义色」

暗色模式不是把背景变黑那么简单:muted 文本在暗底上更容易过灰;destructive 如果过亮会造成刺眼。shadcn 的主题 token 体系意义就在这里:少用手写 hex,多用语义色,暗色切换才不容易翻车。


二十五、与模块二 AI 写作工具栏的衔接:状态机要一致

模块二 AIWritingToolbar 已演示 busyerr 状态。本讲要求你把同样模式推广到所有异步动作:保存、加载列表、导入导出(未来)。一致性会显著降低用户学习成本。


二十六、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(真实世界常见)

  1. 静默丢数据:保存失败无提示,用户切换页面后内容消失。
  2. 无限 loading:请求悬挂,不提供超时与重试。
  3. 把用户当调试器:弹原生 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 的笔记列表页就是一个典型场景:loadingerror 绝不能与 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。


三十八、国际化预留:中文文案也不要硬编码在组件深处

即便你只做中文,也建议把字符串上移到 constmessages 对象中。原因不是立刻要做 i18n,而是让 AI 更容易批量调整文案与语气,减少散落在 JSX 里的碎片字符串。未来要做英文版时,你不会想全局人肉替换。


三十九、对比:用「设计系统 token」约束 AI 比用「审美形容词」有效一万倍

不要说「更现代一点」,要说「使用 rounded-mdborder-borderbg-background」。前者让模型猜,后者让模型抄。shadcn 的价值就是把大量选择收敛成 token,让 Vibe Coding 的随机性下降。


四十、从用户情绪曲线理解 UX:峰值与终值

体验记忆往往由峰值终值决定:如果保存成功那一刻很爽(轻量 toast + 更新时间),用户会原谅中间偶尔的慢;如果最后一步失败且无法恢复,用户会忘记你前面多好看。VibeNote 的 AI 功能尤其要注意终值:生成结束有没有清晰「完成」信号?有没有「插入/替换」明确结果?


四十一、团队协作时的 UX 责任划分:谁对文案负责?

建议:产品/负责人对文案负责,工程师对状态负责。否则会出现「工程师临时编一句话」导致语气不一致。你可以把文案统一放在 copy.tsmessages,PR 评审先看文案再看样式。


四十二、本讲与下一讲的桥梁:组件化如何让三态可复制

当你把 NoteListStates 这类组件沉淀为稳定契约,下一讲的组件化与组合才会「越做越快」。否则每个页面都手写一遍三态,AI 再快也会重复犯错。


四十三、复盘清单

  • 你能画出 VibeNote 主路径的状态机吗?
  • 你是否能指出当前项目最弱的 UX 点?
  • 你是否能把三态组件复用到两个页面?

四十四、思考题

  1. 你最近一个项目里,哪一种状态最常被忽略?为什么?
  2. VibeNote 的 AI 流式输出,Loading 应该全局还是局部?各自代价?
  3. 你如何在不增加视觉噪音的前提下表达「正在保存」?
  4. 列出你项目中三个应使用 AlertDialog 的 destructive 动作,并说明文案。
  5. 如果把 Empty 状态做成营销位,你会如何控制信息密度避免打扰?
  6. 你会如何为「预览渲染失败(Markdown 语法错误)」设计不吓人的错误体验?

四十五、下讲预告

当你把 UI 当作「皮肤」、把 UX 当作「流程与状态」,你会自然得出本专栏的工程结论:先写状态机与验收,再让 AI 生成组件。这能显著减少「页面像样但不好用」的返工。下一讲我们会把三态组件、工具栏、侧栏等沉淀为可组合模块,让你在面对更大功能面时仍能保持稳定产出。

第05讲组件化开发:如何用 组合 而不是复制粘贴堆功能,让 AI 写前端越写越快。


参考:课程原文 课程内容/4.1 页面好看却不好用?UI与UX的关键差别一次讲透.md课程内容/4.2 用户最怕什么体验?Loading空状态错误态全攻略.md;若本地存在 reference/advanced/05-ui-ux.md 可补充阅读。

结语:UI 是面子,UX 是里子;做笔记工具,里子出问题会直接把用户送回系统备忘录。把三态做成习惯,你就从「会写页面」进阶到「会做产品」。记住:用户不会为你的渐变点赞,但会为你的可靠留存;可靠来自状态清晰,不靠特效堆叠;把这句话写进你的设计约束里,团队会少吵很多架,AI 也会少生成很多无用样式与结构分叉问题。