# 查价系统 产品 UI/UX 设计方案 v1.0

| 版本 | 日期 | 作者 | 说明 |
|------|------|------|------|
| v1.0 | 2026-06-16 | 产品设计/UI/UX | 可直接用于前端开发与 Cursor 生成代码 |

> 配套文档：[查价系统-PRD.md](./查价系统-PRD.md) · [查价系统-技术设计.md](./查价系统-技术设计.md)  
> 技术栈：Next.js 15 App Router · Tailwind CSS · shadcn/ui（定制 token）· Geist 字体

---

## 1. 信息架构（Information Architecture）

### 1.1 页面列表

| 编号 | 页面名称 | 路由 | 角色 | 优先级 |
|------|----------|------|------|--------|
| P01 | 登录页 | `/login` | 全部 | P0 |
| P02 | 工作台 | `/dashboard` | customer / operator / admin | P0 |
| P03 | 卡派查价页 | `/quote` | customer | P0 |
| P04 | 询价结果页 | `/quote/result/[quoteId]` | customer | P0 |
| P05 | 询价历史 | `/history` | customer | P0 |
| P06 | 加价配置 | `/admin/markup` | operator / admin | P0 |
| P07 | 预警中心 | `/admin/alerts` | admin | P0 |
| P08 | 403 无权限 | `/403` | 全部 | P1 |
| P09 | 404 未找到 | `/not-found` | 全部 | P1 |

**MVP 不包含页面**：注册页、个人设置、CC 对接页、下单页、多币种设置页。

### 1.2 页面关系图

```
登录页 (/login)
    │
    ├─ customer ──► 工作台 (/dashboard)
    │                    │
    │                    ├─► 卡派查价 (/quote)
    │                    │         │
    │                    │         └─► 询价结果 (/quote/result/:quoteId)
    │                    │
    │                    └─► 询价历史 (/history)
    │                              │
    │                              └─► 询价结果 (/quote/result/:quoteId)
    │
    ├─ operator ──► 工作台 (/dashboard)
    │                    └─► 加价配置 (/admin/markup)
    │
    └─ admin ──► 工作台 (/dashboard)
                     ├─► 加价配置 (/admin/markup)
                     └─► 预警中心 (/admin/alerts)
```

**全局规则**：
- 未登录访问受保护路由 → 重定向 `/login?redirect={path}`
- `customer` 访问 `/admin/*` → `/403`
- `operator` 访问 `/admin/alerts` → `/403`
- 登录成功默认跳转 `/dashboard`

### 1.3 路由结构（Next.js App Router）

```
app/
├── login/page.tsx                    # P01
├── dashboard/page.tsx                  # P02
├── quote/
│   ├── page.tsx                      # P03
│   └── result/[quoteId]/page.tsx     # P04
├── history/page.tsx                  # P05
├── admin/
│   ├── markup/page.tsx               # P06
│   └── alerts/page.tsx               # P07
├── 403/page.tsx                      # P08
└── not-found.tsx                     # P09
```

| 路由 | 鉴权 | 布局 |
|------|------|------|
| `/login` | 公开 | `AuthLayout`（无侧边栏） |
| `/dashboard` | JWT | `AppLayout` |
| `/quote` | JWT + customer | `AppLayout` |
| `/quote/result/[quoteId]` | JWT + customer（本人） | `AppLayout` |
| `/history` | JWT + customer | `AppLayout` |
| `/admin/markup` | JWT + operator/admin | `AdminLayout` |
| `/admin/alerts` | JWT + admin | `AdminLayout` |

### 1.4 导航结构

**客户侧 `AppLayout` 顶栏**：
- Logo「美美与共 · 卡派查价」
- 导航：工作台 | 卡派查价 | 询价历史
- 右侧：客户 ID + 退出

**管理侧 `AdminLayout` 顶栏 + 侧栏**：
- 侧栏：工作台 | 加价配置 | 预警中心（admin 可见）
- 顶栏：角色标签 + 退出

---

## 2. 交互原型（Interaction Design）

### 2.1 全局交互规则

| 规则 ID | 规则 |
|---------|------|
| IX-01 | 所有提交按钮点击后 `disabled` + `loading`，最短 3s |
| IX-02 | 表单字段 `onBlur` 触发校验；错误文案显示在字段下方 |
| IX-03 | API 错误统一用 `ErrorBanner` 展示在页面主内容区顶部 |
| IX-04 | 查价接口 `Cache-Control: no-store`，禁止浏览器缓存 |
| IX-05 | 金额展示固定 `USD`，格式 `$1,234.56` |
| IX-06 | 所有异步操作最长等待 30s，超时展示可重试入口 |

---

### 2.2 P01 登录页 `/login`

**页面结构**：
- Header：Logo + 产品名
- 主区：登录卡片（用户名、密码）
- 操作区：登录按钮

**状态**：`idle` | `loading` | `error` | `success`

| 状态 | UI |
|------|-----|
| idle | 表单可编辑，按钮「登录」 |
| loading | 按钮 loading，输入框 disabled |
| error | ErrorBanner「用户名或密码错误」 |
| success | 跳转 `/dashboard` |

**按钮行为**：点击 → `POST /api/auth/login` → 存 token → 按角色跳转

---

### 2.3 P02 工作台 `/dashboard`

**页面结构**：
- Header：欢迎语 + 角色
- 主区：快捷入口卡片（按角色渲染）
- 操作区：无

**客户卡片**：「卡派查价」「询价历史」  
**运营卡片**：「加价配置」  
**管理员卡片**：「加价配置」「预警中心」+ 未处理预警数量角标

---

### 2.4 P03 卡派查价页 `/quote`（核心）

**页面结构**：
```
┌─────────────────────────────────────────────┐
│ Header（AppLayout 顶栏）                      │
├─────────────────────────────────────────────┤
│ 页面标题：卡派查价                             │
│ 副标题：填写货物信息，获取实时报价（约 30 秒内） │
├──────────────────┬──────────────────────────┤
│ 主输入区（左 60%） │ 结果区（右 40%，空时占位）   │
│ QuoteForm        │ QuoteResultPanel           │
├──────────────────┴──────────────────────────┤
│ 操作区：获取报价（PrimaryButton）              │
└─────────────────────────────────────────────┘
```

**移动端（<768px）**：单列；输入区在上，结果区在下。

**页面状态机**：

| 状态 | 触发 | 输入区 | 结果区 | 操作区 |
|------|------|--------|--------|--------|
| `idle` | 进入页面 | 可编辑 | 空状态插画 +「填写信息后获取报价」 | 按钮可点 |
| `validating` | 点击提交 | 只读 | 不变 | 按钮 loading |
| `processing` | POST 成功且 status=processing | 只读 | Skeleton +「正在查询报价…」+ 进度条（0–30s） | 按钮 disabled |
| `success` | GET status=done 且 is_realtime=true | 可编辑（新询价生成新 request_id） | QuoteCard 双等级 | 按钮可点 |
| `fallback` | GET status=done 且 is_realtime=false | 同 success | QuoteCard + WarningBanner「非实时报价，仅供参考」 | 按钮可点 |
| `error` | GET status=failed 或超时 | 可编辑 | ErrorBanner + 重试按钮 | 按钮可点 |
| `expired` | 倒计时归零 | 可编辑 | 蒙层 +「报价已过期」ConfirmDialog | 确认后回到 idle |

**关键交互**：

1. 点击「获取报价」→ 前端生成 `request_id`(UUID v4) → `POST /api/quotes`
2. 若立即 `done`（L1/L2 命中）→ 直接 `success`/`fallback`
3. 若 `processing` → 每 2s `GET /api/quotes/{quoteId}`，最多 15 次
4. `success` 后展示 3 分钟倒计时 `CountdownTimer`；<30s 时文字变 Warning 色
5. 防重复：提交后按钮 disabled 3000ms；相同 request_id 由后端幂等

**表单字段布局（QuoteForm）**：

| 分组 | 字段 |
|------|------|
| 提货地址 | 街道、城市、州（2 字母）、邮编 |
| 派送地址 | 同上 |
| 货物信息 | 重量+单位、长×宽×高+单位、件数、货物类型（Select） |
| 偏好 | 单位制（公制/英制）、默认展示等级（标准/保证送达） |

**即时校验**：

| 字段 | 规则 | 错误文案 |
|------|------|----------|
| 邮编 | `^\d{5}(-\d{4})?$` | 邮编格式无效 |
| 重量 | 1–50000（换算后 lb） | 重量超出范围 |
| 件数 | 1–999 整数 | 件数无效 |

---

### 2.5 P04 询价结果页 `/quote/result/[quoteId]`

**页面结构**：
- Header：面包屑「卡派查价 / 报价详情」
- 主区：QuoteCard（双等级 Tab）+ 明细 breakdown
- 操作区：「重新询价」「查看历史」

**状态**：`loading` | `success` | `fallback` | `error` | `expired`

| 状态 | UI |
|------|-----|
| loading | 全页 Skeleton |
| success | QuoteCard + CountdownTimer |
| fallback | QuoteCard + WarningBanner |
| error | ErrorBanner「无法获取报价」 |
| expired | 过期提示 + 跳转 `/quote` 按钮 |

**数据来源**：`GET /api/quotes/{quoteId}`

---

### 2.6 P05 询价历史 `/history`

**页面结构**：
- Header：标题「询价历史」
- 主区：表格（时间、路线摘要、价格、来源、状态）
- 操作区：分页器（page/size 必填）

**状态**：`loading` | `success` | `empty` | `error`

| 状态 | UI |
|------|-----|
| loading | 表格 Skeleton 5 行 |
| success | 数据表格 + 分页 |
| empty | EmptyState「暂无询价记录」+ 按钮「去查价」 |
| error | ErrorBanner |

**行点击**：跳转 `/quote/result/{quoteId}`

---

### 2.7 P06 加价配置 `/admin/markup`

**页面结构**：
- Header：标题 + 搜索框（customer_id）
- 主区：表格（客户 ID、加价比例、操作人、更新时间、操作）
- 操作区：分页

**编辑交互**：行内「编辑」→ 右侧 Drawer
- 字段：加价比例（NumberInput，0–30，步长 0.1）
- 按钮：保存（Primary）/ 取消（Secondary）

**状态**：`loading` | `success` | `empty` | `error` | `saving`

| 状态 | UI |
|------|-----|
| saving | 保存按钮 loading，Drawer 内字段 disabled |
| 校验失败 | 字段下「加价比例不能超过 30%」 |

---

### 2.8 P07 预警中心 `/admin/alerts`

**页面结构**：
- Header：标题 + 筛选（类型、状态 open/resolved）
- 主区：表格（时间、类型、quote_id、偏差%、状态）
- 详情：行点击展开 Drawer（detail_json）
- 操作区：「标记已处理」

**预警类型标签色**：

| 类型 | 标签色 |
|------|--------|
| PRICE_DEVIATION | Warning |
| STALE_FALLBACK | Warning |
| RPA_FAILED | Error |
| RPA_CAPTCHA | Error |
| STRUCT_CHANGE | Error |

---

## 3. 视觉设计（Visual Design System）

### 3.1 色彩系统

| Token | HEX | 用途 |
|-------|-----|------|
| `--color-primary` | `#0D9488` | 主按钮、链接、焦点环 |
| `--color-primary-hover` | `#0F766E` | 主按钮 hover |
| `--color-primary-active` | `#115E59` | 主按钮 active |
| `--color-success` | `#16A34A` | 成功状态、实时报价标识 |
| `--color-warning` | `#D97706` | 非实时角标、倒计时 <30s、预警 |
| `--color-error` | `#DC2626` | 错误、失败状态 |
| `--color-bg` | `#F8FAFC` | 页面背景 |
| `--color-surface` | `#FFFFFF` | 卡片、表单背景 |
| `--color-border` | `#E2E8F0` | 分割线、输入框边框 |
| `--color-text-primary` | `#0F172A` | 标题、正文 |
| `--color-text-secondary` | `#64748B` | 辅助说明、标签 |
| `--color-text-disabled` | `#94A3B8` | 禁用文字 |

**Tailwind 映射**：primary=`teal-600`，neutral=`slate`

### 3.2 字体系统

| Token | 字体 | 字号 | 字重 | 行高 | 用途 |
|-------|------|------|------|------|------|
| `--font-display` | Geist Sans | 30px / 1.875rem | 600 | 1.2 | 页面 H1 |
| `--font-h2` | Geist Sans | 24px / 1.5rem | 600 | 1.3 | 区块标题 |
| `--font-h3` | Geist Sans | 18px / 1.125rem | 600 | 1.4 | 卡片标题 |
| `--font-body` | Geist Sans | 16px / 1rem | 400 | 1.6 | 正文 |
| `--font-caption` | Geist Sans | 14px / 0.875rem | 400 | 1.5 | 辅助说明 |
| `--font-label` | Geist Sans | 14px / 0.875rem | 500 | 1.4 | 表单标签 |
| `--font-mono` | Geist Mono | 16px / 1rem | 500 | 1.4 | 金额、quote_id |

**规则**：
- 金额、单号使用 `font-mono`
- 正文最大宽度 `max-w-[65ch]`

### 3.3 间距系统（Spacing）

| Token | 值 | 用途 |
|-------|-----|------|
| `--space-1` | 4px | 图标与文字间距 |
| `--space-2` | 8px | 字段内 padding、标签间距 |
| `--space-3` | 12px | 紧凑列表项 |
| `--space-4` | 16px | 表单项间距、卡片内 padding |
| `--space-6` | 24px | 区块间距 |
| `--space-8` | 32px | 页面边距、卡片间距 |

**页面边距**：`px-4 md:px-6 lg:px-8`；内容区 `max-w-7xl mx-auto`

### 3.4 圆角系统

| Token | 值 | 用途 |
|-------|-----|------|
| `--radius-sm` | 6px | 输入框、标签 |
| `--radius-md` | 8px | 按钮 |
| `--radius-lg` | 12px | 卡片 |
| `--radius-xl` | 16px | 弹窗 |

### 3.5 阴影系统

| Token | 值 | 用途 |
|-------|-----|------|
| `--shadow-card` | `0 1px 3px rgba(15,23,42,0.06), 0 1px 2px rgba(15,23,42,0.04)` | 卡片 |
| `--shadow-modal` | `0 10px 25px rgba(15,23,42,0.12)` | 弹窗、Drawer |
| `--shadow-none` | none | 表格行、内嵌面板 |

### 3.6 组件风格

#### Button

| 变体 | 背景 | 文字 | 边框 | 高度 |
|------|------|------|------|------|
| Primary | `#0D9488` | `#FFFFFF` | none | 40px |
| Secondary | `#FFFFFF` | `#0F172A` | `#E2E8F0` | 40px |
| Ghost | transparent | `#0D9488` | none | 40px |
| Danger | `#DC2626` | `#FFFFFF` | none | 40px |

`border-radius: 8px`；`padding: 0 16px`；loading 时左侧 16px 旋转 Spinner

#### Input

- 高度 40px；`border: 1px solid #E2E8F0`；`radius: 6px`
- Focus：`ring-2 ring-teal-600/20 border-teal-600`
- Error：`border-error` + 下方 12px 红色 caption

#### Card（QuoteCard）

- 背景 `#FFFFFF`；`border: 1px solid #E2E8F0`；`radius: 12px`；`padding: 24px`；`shadow-card`
- 顶部 Tab：「标准」「保证送达」
- 价格区：`font-mono text-3xl font-semibold`
- 非实时角标：右上角 Warning 色 pill「非实时报价，仅供参考」

#### Loading

- 全页/区块：Skeleton（灰条 `bg-slate-200 animate-pulse`）
- 按钮内：`Spinner` 16×16，`border-2 border-white/30 border-t-white`
- 查价进度：线性进度条 + 文案「正在查询报价…（{elapsed}s / 30s）」

#### Error 提示

- `ErrorBanner`：背景 `#FEF2F2`，左边框 4px `#DC2626`，内边距 16px
- `WarningBanner`：背景 `#FFFBEB`，左边框 4px `#D97706`

---

## 4. 设计规范（Design System for AI Coding）

### 4.1 UI 组件清单

| 组件名 | 路径 | 用途 |
|--------|------|------|
| `PrimaryButton` | `components/ui/primary-button.tsx` | 主操作 |
| `SecondaryButton` | `components/ui/secondary-button.tsx` | 取消、次要操作 |
| `InputField` | `components/ui/input-field.tsx` | 文本输入 + label + error |
| `SelectField` | `components/ui/select-field.tsx` | 货物类型、州 |
| `QuoteForm` | `components/quote/quote-form.tsx` | 查价表单 |
| `QuoteCard` | `components/quote/quote-card.tsx` | 双等级报价展示 |
| `QuoteResultPanel` | `components/quote/quote-result-panel.tsx` | 结果区容器（含状态切换） |
| `CountdownTimer` | `components/quote/countdown-timer.tsx` | 3 分钟倒计时 |
| `LoadingSpinner` | `components/ui/loading-spinner.tsx` | 按钮/行内 loading |
| `Skeleton` | `components/ui/skeleton.tsx` | 骨架屏 |
| `ErrorBanner` | `components/ui/error-banner.tsx` | 错误提示 |
| `WarningBanner` | `components/ui/warning-banner.tsx` | 非实时/预警提示 |
| `EmptyState` | `components/ui/empty-state.tsx` | 空列表 |
| `ConfirmDialog` | `components/ui/confirm-dialog.tsx` | 过期重询确认 |
| `DataTable` | `components/ui/data-table.tsx` | 历史/加价/预警表格 |
| `PageHeader` | `components/layout/page-header.tsx` | 标题区 |
| `AppLayout` | `components/layout/app-layout.tsx` | 客户布局 |
| `AdminLayout` | `components/layout/admin-layout.tsx` | 管理端布局 |

### 4.2 状态规范（统一枚举）

```typescript
// 查价页
type QuotePageStatus =
  | 'idle'
  | 'validating'
  | 'processing'
  | 'success'
  | 'fallback'
  | 'error'
  | 'expired';

// 通用数据页
type DataPageStatus = 'loading' | 'success' | 'empty' | 'error';

// 按钮
type ButtonState = 'default' | 'loading' | 'disabled';
```

| 状态 | 视觉 | 交互 |
|------|------|------|
| loading | Skeleton 或 Spinner | 禁止重复提交 |
| success | 正常内容 + Success/Primary 强调 | 可操作 |
| error | ErrorBanner | 展示重试 |
| empty | EmptyState 插画 + CTA | 引导主路径 |
| disabled | opacity 0.5 + cursor-not-allowed | 不响应点击 |

### 4.3 命名规范

| 类型 | 规则 | 示例 |
|------|------|------|
| React 组件 | PascalCase | `QuoteCard` |
| 文件 | kebab-case | `quote-card.tsx` |
| Props 接口 | 组件名 + Props | `QuoteCardProps` |
| 页面状态 hook | use + 页面 + Status | `useQuotePageStatus` |
| Tailwind class | 语义组合，禁止随意 magic number | `text-text-secondary` 用 CSS 变量 |
| API 状态字段 | 与 PRD 一致 | `status`, `is_realtime`, `source_type` |

### 4.4 UI 约束规则（写死）

| ID | 规则 |
|----|------|
| UI-01 | 所有触发 API 的按钮必须有 `loading` 状态 |
| UI-02 | 所有 API 请求必须有 `error` 状态展示（ErrorBanner） |
| UI-03 | 所有表单字段必须有 `validation` 状态（字段下错误文案） |
| UI-04 | 查价提交按钮提交后 disabled 3000ms |
| UI-05 | 轮询最长 30s，超时进入 `error` 状态 |
| UI-06 | `is_realtime=false` 必须显示 WarningBanner，不可隐藏 |
| UI-07 | 金额必须用 `font-mono`，保留 2 位小数 |
| UI-08 | 界面文案全部中文 |
| UI-09 | 禁止使用 emoji 作为 UI 图标；使用 `@phosphor-icons/react` |
| UI-10 | 移动端所有页面单列布局，禁止横向滚动 |
| UI-11 | 全高区域使用 `min-h-[100dvh]`，禁止 `h-screen` |

### 4.5 Tailwind 配置片段（globals.css）

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --color-primary: #0d9488;
  --color-primary-hover: #0f766e;
  --color-success: #16a34a;
  --color-warning: #d97706;
  --color-error: #dc2626;
  --color-bg: #f8fafc;
  --color-surface: #ffffff;
  --color-border: #e2e8f0;
  --color-text-primary: #0f172a;
  --color-text-secondary: #64748b;
  --radius-md: 8px;
  --radius-lg: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
}

body {
  @apply bg-[var(--color-bg)] text-[var(--color-text-primary)] antialiased;
  font-family: var(--font-geist-sans), system-ui, sans-serif;
}
```

### 4.6 响应式断点

| 断点 | 宽度 | 布局规则 |
|------|------|----------|
| default | <768px | 单列；查价表单全宽；隐藏侧栏改底部 Tab |
| `md` | ≥768px | 查价页左右 60/40 分栏 |
| `lg` | ≥1024px | 管理端显示固定侧栏 240px |
| `xl` | ≥1280px | 内容区 `max-w-7xl` 居中 |

### 4.7 页面 ↔ 组件映射（Cursor 直接生成）

| 页面 | 主要组件 |
|------|----------|
| `/login` | `InputField` ×2, `PrimaryButton`, `ErrorBanner` |
| `/dashboard` | `PageHeader`, 快捷入口 Card ×N |
| `/quote` | `QuoteForm`, `QuoteResultPanel`, `QuoteCard`, `CountdownTimer`, `ConfirmDialog` |
| `/quote/result/[id]` | `QuoteCard`, `CountdownTimer`, `WarningBanner` |
| `/history` | `DataTable`, `EmptyState`, 分页 |
| `/admin/markup` | `DataTable`, Drawer + `InputField`, `PrimaryButton` |
| `/admin/alerts` | `DataTable`, 类型 Tag, Drawer 详情 |

---

## 附录 A：与 PRD / 技术设计映射

| PRD/技术项 | UI 实现 |
|------------|---------|
| GD-5 异步轮询 | `/quote` processing 状态 + 2s 轮询 |
| 双等级报价 | `QuoteCard` Tab：标准 / 保证送达 |
| 3 分钟有效期 | `CountdownTimer` |
| is_realtime=false | `WarningBanner` |
| 过期重询 | `ConfirmDialog` |
| 运营加价 | `/admin/markup` Drawer |
| 预警中心 | `/admin/alerts` 表格 + 处理按钮 |

## 附录 B：文案清单（中文）

| 场景 | 文案 |
|------|------|
| 提交查价 | 获取报价 |
| 处理中 | 正在查询报价，请稍候… |
| 非实时 | 非实时报价，仅供参考 |
| 失败 | 暂时无法获取报价，请稍后重试 |
| 超时 | 查询超时，请重试 |
| 过期确认 | 报价已过期，是否重新询价？ |
| 空历史 | 暂无询价记录 |

## 附录 C：图标（Phosphor）

| 场景 | 图标名 |
|------|--------|
| 查价 | `Truck` |
| 历史 | `ClockCounterClockwise` |
| 预警 | `Warning` |
| 成功 | `CheckCircle` |
| 错误 | `XCircle` |
| 加载 | `CircleNotch`（旋转） |
