- Add $font-size-4xl (64rpx) to font scale - Fix line-height-tight to match uni.scss (1.2) - Use existing $uv-* naming for neutral colors, add $uv-placeholder-color - Clarify $font-family-body change (serif → system fonts for body) - Split spacing into recommended vs reserved tiers - Fix $radius-md → $radius-base, add $radius-xl - Fix shadow values to match uni.scss rgba(15,23,42,...) base - Add PageResult<T> pagination type - Add MockStore for interactive mock data - Add image resource specs (sizes, formats) - Add accessibility guidelines - Fix page count (27 → 21) Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
UI/UX 标准化设计规范
湖南12301文旅专区微信小程序(C端)
2026-06-26
背景
项目现有 35 个 .vue 文件,存在以下问题:
- 双 token 系统冲突(
uni.scssvs_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 |
占位符(新增,uni.scss 缺失) |
$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, #EF4444, #f53f3f, #E74C3C |
$uv-error 或 $color-price |
#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 字体声明
// 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 卡片
圆角:$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 业务类型
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 切换机制
// 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')
}
.env文件中VITE_USE_MOCK=true控制开关- 对接 API 时改为
false,页面代码零改动
5.4 Mock 状态管理
对于需要交互的 mock 数据(如订单状态变更、预约操作),提供简单的内存状态管理:
// 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:
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.1 | 清理 uni.scss:补齐 $font-size-xxs、$font-size-4xl、$uv-placeholder-color、语义色、修改 $font-family-body 为系统字体 |
编译通过 |
| 1.2 | 处理 _tokens.scss:冲突值改为引用 uni.scss 或删除 |
编译通过,无变量冲突 |
| 1.3 | api/types.ts 补全所有业务 interface |
pnpm run type-check 通过 |
| 1.4 | 创建 src/mock/ 目录,迁移所有硬编码数据,实现 USE_MOCK 开关 |
mock 数据可正常 import |
| 1.5 | App.vue 全局字体声明统一为 $font-family-body |
构建预览正常 |
| 1.6 | 删除 14 个页面中的局部 $font、$primary、$text-main 等变量声明 |
编译通过 |
第2步:核心链路迁移(4 个页面)
迁移页面:首页 → 年卡列表 → 年卡详情 → 购买/订单
每页迁移清单:
- 删除局部 SCSS 变量,改用全局 token
- 硬编码颜色值 → 全局变量
- 硬编码字号 → 对应
$font-size-*变量 - 硬编码间距 → 对应
$spacing-*变量 - 模板中硬编码数据 → import mock 数据
- 组件样式对齐规范(按钮高度、卡片圆角、箭头样式)
验证: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-*