Files
pingxiangcard/docs/superpowers/specs/2026-06-26-ui-ux-standardization-design.md
jacobxu666andClaude 1b12c1a6b7 docs: final spec refinements per second review
- Fix type import syntax in mock example (Scenic[] → Scenic)
- Add pagination example to mock mechanism
- Clarify $uv-placeholder-color placement in uni.scss
- Separate $color-price vs $uv-error in migration table
- Split task 1.1 into 1.1a/1.1b/1.1c sub-tasks
- Add App.vue global style example
- Add SCSS import rules

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 09:19:09 +08:00

596 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI/UX 标准化设计规范
> 湖南12301文旅专区微信小程序(C端)
> 2026-06-26
## 背景
项目现有 35 个 .vue 文件,存在以下问题:
- 双 token 系统冲突(`uni.scss` vs `_tokens.scss`)
- 14 个页面自定义局部 SCSS 变量,绕过全局体系
- 3 套字体族混用(SourceHanSerifCN / Noto Serif SC / 系统字体)
- 55+ 种硬编码颜色值(4 种品牌蓝、4 种错误红、5 种背景灰)
- 19 种字号仅 8 种有对应变量
- 9 种按钮高度
- mock 数据分散在各页面,格式不统一
目标:统一所有页面的字体、字号、颜色、间距、组件规范,以及 mock 数据管理方式,为后续后端 API 对接做好准备。
## 已确认的决策
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 颜色体系 | uni.scss 体系 | 偏暖偏亮,`#3c9cff` 主色 |
| 字体族 | 思源宋体系 | 文化/旅游感强 |
| 模拟数据 | 集中管理 + 类型先行 | 对接 API 时页面零改动 |
| 执行节奏 | 基础设施先行 → 核心链路 → 剩余页面 | 风险最小 |
---
## 1. Token 体系统一
### 1.1 Source of Truth
- 保留 `src/uni.scss` 作为唯一 token 文件(uni-app 自动注入到所有 `<style lang="scss">`)
- `_tokens.scss` 中与 `uni.scss` 冲突的变量值改为直接引用 `uni.scss` 变量,或删除
- 禁止任何页面在 `<style>` 中重新定义 `$font`、`$primary`、`$text-main` 等局部变量
### 1.2 字号体系
10 级字号,消灭所有游离值:
| 变量 | 值 | 场景 |
|------|-----|------|
| `$font-size-xxs` | 18rpx | 辅助标注、库存提示、角标 |
| `$font-size-xs` | 20rpx | 标签、小徽标 |
| `$font-size-sm` | 24rpx | 次要说明、辅助文字 |
| `$font-size-base` | 28rpx | 正文、按钮文字 |
| `$font-size-lg` | 32rpx | 卡片标题、区域小标题 |
| `$font-size-xl` | 36rpx | 区域标题 |
| `$font-size-2xl` | 44rpx | 页面大标题 |
| `$font-size-3xl` | 56rpx | 大号展示数字 |
| `$font-size-4xl` | 64rpx | 促销大数字(介于 3xl 和 display 之间) |
| `$font-size-display` | 72rpx | 超大展示(极少使用) |
**游离值迁移对照**:
| 原值 | 迁移到 | 理由 |
|------|--------|------|
| 18rpx | 新增 `$font-size-xxs` | 使用频率高,值得单独层级 |
| 22rpx | `$font-size-xs` (20rpx) | 视觉差异极小 |
| 26rpx | `$font-size-base` (28rpx) | 正文统一 28rpx |
| 30rpx | `$font-size-lg` (32rpx) | 卡片标题统一 32rpx |
| 34rpx | `$font-size-xl` (36rpx) | 差异极小 |
### 1.3 字重
| 变量 | 值 | 场景 |
|------|-----|------|
| `$font-weight-normal` | 400 | 正文、说明 |
| `$font-weight-medium` | 500 | 次要标题、按钮 |
| `$font-weight-bold` | 700 | 主标题、价格数字、强调 |
禁止使用 CSS 关键字 `bold`,统一用数字变量。
### 1.4 行高
| 变量 | 值 | 场景 |
|------|-----|------|
| `$line-height-tight` | 1.2 | 标题 |
| `$line-height-normal` | 1.5 | 正文 |
| `$line-height-relaxed` | 1.75 | 长段落、说明文 |
---
## 2. 颜色体系
### 2.1 品牌 & 功能色
以 `uni.scss` 现有值为准,不做修改:
| 变量 | 值 | 用途 |
|------|-----|------|
| `$uv-primary` | `#3c9cff` | 主色/品牌蓝 |
| `$uv-error` | `#e53935` | 错误/危险红 |
| `$uv-warning` | `#f9ae3d` | 警告/橙 |
| `$uv-success` | `#5ac725` | 成功/绿 |
### 2.2 中性色(文字 & 背景)
沿用 `uni.scss` 现有 `$uv-*` 命名,补充缺失值,替代现有 55+ 种硬编码灰值:
**主变量(uni.scss 已有,保持不变)**:
| 变量 | 值 | 用途 |
|------|-----|------|
| `$uv-main-color` | `#111111` | 标题、重要正文 |
| `$uv-content-color` | `#333333` | 正文 |
| `$uv-tips-color` | `#666666` | 次要说明 |
| `$uv-placeholder-color` | `#999999` | 占位符(**新增**,放在 `$uv-tips-color` 下方) |
| `$uv-disabled-color` | `#c8c9cc` | 不可操作文字 |
| `$uv-bg-color` | `#f3f4f6` | 页面背景 |
| `$uv-bg-color-hover` | `#f1f1f1` | 按压态背景 |
| `$uv-border-color` | `#eeeeee` | 分割线、边框 |
| `$color-card` | `#ffffff` | 卡片/模块背景(uni.scss 已有) |
**语义别名(向后兼容,指向上述主变量)**:
| 别名 | 指向 | 说明 |
|------|------|------|
| `$color-foreground` | `$uv-main-color` | uni.scss 已有 |
| `$color-muted-foreground` | `$uv-content-color` | uni.scss 已有 |
| `$color-background` | `$uv-bg-color` | uni.scss 已有 |
| `$color-border` | `$uv-border-color` | uni.scss 已有 |
> 迁移时优先使用主变量(`$uv-*`),语义别名仅在已有代码中保留,新代码禁止使用别名。
### 2.3 语义色
订单状态、标签等场景专用:
| 变量 | 值 | 用途 |
|------|-----|------|
| `$color-price` | `#e53935` | 价格红 |
| `$color-price-highlight` | `#FFD600` | 促销亮黄(首页大卡专用) |
| `$color-status-pending` | `#ff9500` | 待处理/待支付 |
| `$color-status-success` | `#00b42a` | 已完成/已支付 |
| `$color-status-cancel` | `#86909c` | 已取消/已关闭 |
### 2.4 透明度规范
暗底白字(5 档):
```
rgba(255, 255, 255, 0.35) — 最弱(信任标签等)
rgba(255, 255, 255, 0.55) — 弱(次要说明)
rgba(255, 255, 255, 0.7) — 中(描述文字)
rgba(255, 255, 255, 0.85) — 强(正文)
rgba(255, 255, 255, 0.95) — 最强(标题)
```
白底投影(3 档):
```
rgba(0, 0, 0, 0.04) — 轻投影(卡片)
rgba(0, 0, 0, 0.08) — 中投影(浮层)
rgba(0, 0, 0, 0.12) — 重投影(弹窗)
```
### 2.5 硬编码颜色迁移对照
| 现有硬编码值 | 迁移到 |
|-------------|--------|
| `#111`, `#1A1A1A`, `#222` | `$uv-main-color` |
| `#333` | `$uv-content-color` |
| `#666` | `$uv-tips-color` |
| `#999`, `#888888` | `$uv-placeholder-color` |
| `#cccccc`, `#BBBBBB`, `#dadbde` | `$uv-disabled-color` / `$uv-light-color` |
| `#e5e7eb`, `#E2E8F0` | `$uv-border-color` |
| `#f7f8fa`, `#f5f5f5`, `#f7f7f7`, `#FAFAFA`, `#F8FAFC` | `$uv-bg-color` |
| `#fff`, `#ffffff`, `#FFFFFF` | `$color-card` |
| `#e53935`(价格场景) | `$color-price`(价格红,值 = `$uv-error`) |
| `#EF4444`, `#f53f3f`, `#E74C3C`(错误/状态场景) | `$uv-error` |
| `#3c9cff`, `#0052FF`, `#4A90D9`, `#4D7CFF` | `$uv-primary` |
| `#5ac725`, `#10B981`, `#00b42a` | `$uv-success` 或 `$color-status-success` |
| `#f9ae3d`, `#F59E0B`, `#ff9500`, `#ff7d00` | `$uv-warning` 或 `$color-status-pending` |
---
## 3. 字体族 & 排版
### 3.1 字体声明
```scss
// uni.scss 现状(display 和 body 都用了思源宋体):
$font-family-display: 'SourceHanSerifCN', '思源宋体', serif; // ✅ 保持不变
$font-family-body: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', sans-serif; // ⚠️ 需修改
// 修改后:
$font-family-display: 'SourceHanSerifCN', '思源宋体', serif; // 标题装饰字体
$font-family-body: -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif; // 正文系统字体
```
> **为什么要改 `$font-family-body`**:uni.scss 现状把思源宋体(衬线体)用于正文,导致所有文字都是衬线风格,阅读体验偏重且加载慢。改为系统字体后,正文干净利落,仅标题/品牌名保留思源宋体的文化感。
- `$font-family-display`:标题、品牌名、卡名等装饰性文字
- `$font-family-body`:正文、按钮、表单、说明文字(默认字体,加载快)
全局在 `App.vue` 中声明 `font-family: $font-family-body`,仅需要装饰性的元素单独指定 `$font-family-display`。
### 3.2 字号场景对照
| 场景 | 变量 | 值 |
|------|------|-----|
| 页面大标题 | `$font-size-2xl` | 44rpx |
| 区域标题 | `$font-size-xl` | 36rpx |
| 卡片标题 | `$font-size-lg` | 32rpx |
| 正文/按钮 | `$font-size-base` | 28rpx |
| 次要说明 | `$font-size-sm` | 24rpx |
| 辅助标注/标签 | `$font-size-xs` | 20rpx |
| 极小文字/角标 | `$font-size-xxs` | 18rpx |
---
## 4. 组件规范
### 4.1 按钮
从 9 种高度收敛到 3 种:
| 尺寸 | 高度 | 字号 | 场景 |
|------|------|------|------|
| sm | 64rpx | `$font-size-sm` (24rpx) | 列表内操作、小按钮 |
| md | 80rpx | `$font-size-base` (28rpx) | 页面内主要操作 |
| lg | 96rpx | `$font-size-lg` (32rpx) | 底部固定栏、支付按钮 |
- 统一圆角 `$radius-full`(胶囊形)
- 主操作:`$uv-primary` 填充色 + 白色文字
- 次要操作:描边样式(border `$uv-primary` + `$uv-primary` 文字)
- 按压态:`transform: scale(0.95)` + `transition: transform $transition-fast`
### 4.2 卡片
```scss
圆角:$radius-lg (24rpx)
投影:$shadow-base → 0 4rpx 16rpx rgba(15, 23, 42, 0.08)
内边距:$spacing-4 (32rpx)
卡片间距:$spacing-3 (24rpx)
背景:$color-card (#ffffff)
```
### 4.3 箭头/Chevron
统一一种实现:
- 尺寸:12rpx × 12rpx
- border-width:2rpx
- 颜色:`$uv-placeholder-color`
- 右箭头:`transform: rotate(45deg)`
- 下箭头:`transform: rotate(135deg)`
### 4.4 间距规则
**推荐使用**(日常开发只用这 7 个):
| 变量 | 值 | 场景 |
|------|-----|------|
| `$spacing-1` | 8rpx | 图标与文字间隙 |
| `$spacing-2` | 16rpx | 紧凑元素间距 |
| `$spacing-3` | 24rpx | 列表项间距、卡片间距 |
| `$spacing-4` | 32rpx | 区域内容边距 |
| `$spacing-5` | 40rpx | 区块间距 |
| `$spacing-6` | 48rpx | 页面顶部/底部安全区 |
| `$spacing-8` | 64rpx | 大区块分隔 |
**保留但少用**(uni.scss 已有,仅特殊场景允许):
| 变量 | 值 | 场景 |
|------|-----|------|
| `$spacing-0` | 0 | 重置间距 |
| `$spacing-10` | 80rpx | 超大区块分隔 |
| `$spacing-12` | 96rpx | 页面级留白 |
| `$spacing-16` | 128rpx | 极少使用 |
| `$spacing-20` | 160rpx | 极少使用 |
禁止硬编码间距值(如 `padding: 20rpx`)。
### 4.5 圆角规则
| 变量 | 值 | 场景 |
|------|-----|------|
| `$radius-sm` | 8rpx | 标签、小徽标 |
| `$radius-base` | 16rpx | 输入框、小卡片 |
| `$radius-lg` | 24rpx | 卡片、弹窗 |
| `$radius-xl` | 32rpx | 大面板(少用) |
| `$radius-full` | 9999rpx | 胶囊按钮、圆形头像 |
### 4.6 投影规则
以 `uni.scss` 现有值为准(使用 `rgba(15, 23, 42, ...)` 冷灰色基底):
| 变量 | 值 | 场景 |
|------|-----|------|
| `$shadow-sm` | `0 2rpx 8rpx rgba(15,23,42,0.04)` | 轻投影 |
| `$shadow-base` | `0 4rpx 16rpx rgba(15,23,42,0.08)` | 卡片默认 |
| `$shadow-lg` | `0 8rpx 32rpx rgba(15,23,42,0.12)` | 浮层、弹窗 |
| `$shadow-xl` | `0 16rpx 48rpx rgba(15,23,42,0.16)` | 大弹窗/浮层 |
### 4.7 过渡动画
| 变量 | 值 | 场景 |
|------|-----|------|
| `$transition-fast` | `150ms ease` | 按压、hover |
| `$transition-base` | `250ms ease` | 展开/收起、淡入淡出 |
禁止使用 `0.15s ease`、`0.2s ease` 等硬编码值。
---
## 5. 类型定义 & Mock 数据
### 5.1 文件结构
```
src/
├── api/
│ ├── types.ts # 所有业务类型(唯一来源)
│ ├── request.ts # HTTP 封装(已有,不动)
│ ├── scenic.ts # 景区接口
│ ├── card.ts # 年卡接口
│ ├── order.ts # 订单接口
│ ├── user.ts # 用户接口(已有,补充类型)
│ └── reserve.ts # 预约接口
├── mock/
│ ├── index.ts # 统一导出 + USE_MOCK 开关
│ ├── scenic.ts # 景区 mock
│ ├── card.ts # 年卡 mock
│ ├── order.ts # 订单 mock
│ ├── reserve.ts # 预约记录 mock
│ ├── notice.ts # 公告 mock
│ └── images.ts # 占位图片 URL 常量
```
### 5.2 types.ts 业务类型
```typescript
interface Scenic {
id: number
name: string
cover: string
address: string
lat: number
lng: number
openTime: string
description: string
ticketPrice: number
freeTimes: number
}
interface ScenicSession {
id: number
scenicId: number
date: string
startTime: string
endTime: string
stock: number
remainStock: number
}
interface Card {
id: number
name: string
price: number
originalPrice: number
cover: string
benefits: string
scenicIds: number[]
}
interface Order {
id: number
cardId: number
cardName: string
amount: number
status: 'pending' | 'paid' | 'cancelled' | 'closed'
payTime: string | null
createTime: string
}
interface OrderDetail extends Order {
orderNo: string
userId: number
userName: string
phone: string
payMethod: string
}
interface ReserveRecord {
id: number
scenicId: number
scenicName: string
date: string
session: string
status: 'pending' | 'confirmed' | 'cancelled' | 'used'
}
interface Notice {
id: number
title: string
content: string
publishTime: string
isTop: boolean
}
interface QuickAction {
id: number
icon: string
label: string
path: string
}
interface Announcement {
id: number
title: string
content: string
}
// 分页通用
interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
// 分页请求参数
interface PageQuery {
page: number
pageSize: number
}
```
### 5.3 Mock 切换机制
```typescript
// mock/index.ts
export const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'
// api/scenic.ts
import { USE_MOCK } from '@/mock'
import { mockScenicList } from '@/mock/scenic'
import type { Scenic } from './types'
export function getScenicList(): Promise<Scenic[]> {
if (USE_MOCK) return Promise.resolve(mockScenicList)
return request.get('/mp-api/scenic/list')
}
// 分页示例
export function getScenicListPaged(query: PageQuery): Promise<PageResult<Scenic>> {
if (USE_MOCK) {
const start = (query.page - 1) * query.pageSize
return Promise.resolve({
list: mockScenicList.slice(start, start + query.pageSize),
total: mockScenicList.length,
page: query.page,
pageSize: query.pageSize,
})
}
return request.get('/mp-api/scenic/list', query)
}
```
- `.env` 文件中 `VITE_USE_MOCK=true` 控制开关
- 对接 API 时改为 `false`,页面代码零改动
### 5.4 Mock 状态管理
对于需要交互的 mock 数据(如订单状态变更、预约操作),提供简单的内存状态管理:
```typescript
// mock/store.ts
import { ref } from 'vue'
import type { Order, ReserveRecord } from '@/api/types'
import { mockOrders } from './order'
import { mockReserves } from './reserve'
export const orderStore = ref<Order[]>([...mockOrders])
export const reserveStore = ref<ReserveRecord[]>([...mockReserves])
export function updateOrderStatus(orderId: number, status: Order['status']) {
const order = orderStore.value.find(o => o.id === orderId)
if (order) order.status = status
}
export function addReserve(record: Omit<ReserveRecord, 'id'>) {
const id = Math.max(...reserveStore.value.map(r => r.id)) + 1
reserveStore.value.push({ ...record, id })
}
```
> 仅 mock 模式使用。对接 API 后这些操作改为 API 调用,页面逻辑不变。
### 5.5 占位图片规范
统一收到 `mock/images.ts`:
```typescript
export const IMG_SCENIC_COVER = 'https://images.unsplash.com/...'
export const IMG_AVATAR = 'https://...'
export const IMG_CARD_BANNER = 'https://...'
// ...
```
对接 API 后这些常量直接废弃,不影响页面代码。
---
## 6. 图片资源规范
| 类型 | 尺寸 | 格式 | 说明 |
|------|------|------|------|
| TabBar 图标 | 81px × 81px | PNG | 无透明通道,选中/未选中各一套 |
| 页面分享图 | 500px × 400px | JPEG | < 128KB |
| 年卡封面 | 750px × 420px | JPEG | < 200KB |
| 景区封面 | 750px × 420px | JPEG | < 200KB,统一裁切比例 16:9 |
| 背景图 | 优先 CSS 渐变 | — | 避免大图,减小包体积 |
- 网络图片必须在微信公众平台配置 `downloadFile` 合法域名
- 本地图片放 `src/static/` 目录,构建时自动打包
- 占位图统一使用 `mock/images.ts` 中的常量
---
## 7. 无障碍规范
- 所有可交互元素(`<view @click>`、`<button>`)必须设置 `role` 或 `aria-role` 属性
- `<image>` 标签必须提供有意义的描述(小程序用 `aria-label` 或父容器 `aria-role`)
- 颜色对比度至少 4.5:1(WCAG AA 标准),正文文字不得仅靠颜色传达信息
- 错误状态必须配合图标 + 文字,不能只用红色标识
- 表单输入框必须关联 `<label>` 或使用 `aria-label`
---
## 8. 执行计划
### 第1步:基础设施(不改任何页面)
| 序号 | 任务 | 验证方式 |
|------|------|---------|
| 1.1a | `uni.scss` 新增缺失变量:`$font-size-xxs` (18rpx)、`$font-size-4xl` (64rpx)、`$uv-placeholder-color` (#999999) | 编译通过 |
| 1.1b | `uni.scss` 新增语义色变量:`$color-price`、`$color-price-highlight`、`$color-status-pending`、`$color-status-success`、`$color-status-cancel` | 编译通过 |
| 1.1c | `uni.scss` 修改 `$font-family-body` 为系统字体(去掉思源宋体前缀) | 编译通过 |
| 1.2 | 处理 `_tokens.scss`:冲突值改为引用 `uni.scss` 或删除 | 编译通过,无变量冲突 |
| 1.3 | `api/types.ts` 补全所有业务 interface(含 `PageResult<T>`、`PageQuery`) | `pnpm run type-check` 通过 |
| 1.4 | 创建 `src/mock/` 目录,迁移所有硬编码数据,实现 `USE_MOCK` 开关和 MockStore | mock 数据可正常 import |
| 1.5 | `App.vue` 全局样式重置(见下方示例) | 构建预览正常 |
| 1.6 | 删除 14 个页面中的局部 `$font`、`$primary`、`$text-main` 等变量声明 | 编译通过 |
**1.5 App.vue 全局样式示例**:
```scss
// App.vue <style lang="scss">
page {
font-family: $font-family-body;
font-size: $font-size-base;
color: $uv-content-color;
background-color: $uv-bg-color;
line-height: $line-height-normal;
-webkit-font-smoothing: antialiased;
}
```
**SCSS 导入规则**:
- `uni.scss`:uni-app 自动注入,所有 `.vue` 文件的 `<style lang="scss">` 可直接使用变量,无需 `@import`
- `App.vue`:通过 `<style>` 设置全局 `page` 样式和默认字体
- 页面/组件中**不需要**再次 `@import uni.scss`
### 第2步:核心链路迁移(4 个页面)
迁移页面:`首页 → 年卡列表 → 年卡详情 → 购买/订单`
每页迁移清单:
1. 删除局部 SCSS 变量,改用全局 token
2. 硬编码颜色值 → 全局变量
3. 硬编码字号 → 对应 `$font-size-*` 变量
4. 硬编码间距 → 对应 `$spacing-*` 变量
5. 模板中硬编码数据 → import mock 数据
6. 组件样式对齐规范(按钮高度、卡片圆角、箭头样式)
验证:`pnpm run dev:mp-weixin` 构建 + 微信开发者工具预览,确认视觉无回退。
### 第3步:剩余页面迁移(~21 个页面)
- 同样 6 项清单逐页执行
- 分包页面(`packages/user/pages/card/`)6 个文件同步处理
- 纯 CSS 组件(`card-item.vue`、`empty-state.vue`、`McGlass.vue`)改为 SCSS + 全局变量
---
## 9. 迁移规则(禁止项)
- ❌ 禁止在 `<style>` 中定义 `$font`、`$primary`、`$text-main` 等局部变量
- ❌ 禁止使用 `font-weight: bold` 关键字,必须用 `$font-weight-bold`
- ❌ 禁止硬编码颜色值(如 `#333`、`#666`),必须用全局变量
- ❌ 禁止硬编码间距值(如 `padding: 20rpx`),必须用 `$spacing-*`
- ❌ 禁止在页面中硬编码业务数据,必须从 `mock/` 或 `api/` import
- ❌ 禁止使用 `0.15s ease` 等硬编码过渡值,必须用 `$transition-*`