实战日期:2026-06-11
实战对象:Otto Robot 微信小程序(设备控制端)
适用读者:要做微信小程序 / 支付宝小程序 / 抖音小程序的工程师
前置阅读:上一篇《design.md 调研:给 AI 编程代理一份 ” 设计宪法 ”》
一、为什么 Otto 需要做小程序
Otto Robot 是个开源桌面 AI 机器人,连接手机一直是麻烦事:
- iOS/Android 原生 App:开发成本高,两个端各要 1 个工程师
- Web 控制台:用户得记域名、扫码开浏览器、配网流程断
- 微信小程序:用户已经装好微信,扫码即用,配网、状态监控、对话历史全在一个壳里
我们最终选了小程序。但小程序不是 Web 也不是 Native,直接把 design.md 抄过来会出大事。
下面是我踩了一周坑之后,沉淀出来的 ”design.md 在小程序里的完整落地手册 ”。
二、Step 1:先理解小程序的 5 个关键约束
在写 design.md 之前,必须先搞清楚小程序跟 Web 的差异。这些差异决定了 design.md 哪些字段要改、哪些不能照搬:
| 约束 | 影响 | design.md 应对 |
|---|---|---|
| rpx 单位 | 750rpx = 屏幕宽度,1px = 2rpx(iPhone6 基准) | 令牌层用 px,组件层强制写 rpx 转换公式 |
| WXSS 限制 | 不支持 :has()、部分 CSS 变量在 Android 低端机失效 |
不依赖 var(--x) 做关键布局,用 SCSS 预编译 |
| 组件化强制 | 4 个文件成一套(.js/.json/.wxml/.wxss) | 任何可复用元素必须先抽组件,不能直接 page 里写 |
| 原生组件覆盖 | ` | |
| design.md 必须包含 “ 原生组件改造规范 ” | ||
| 多端兼容 | 微信 / 支付宝 / 抖音小程序的组件 API 有差异 | design.md 末尾加 “ 跨端差异表 ”,标注 API 兼容性 |
这 5 条是后面所有 design.md 字段的 ” 地基 ”。
三、Step 2:写一份小程序专用 design.md
下面是 Otto Robot 微信小程序的完整 design.md(精简到 250 行,真实可用):
# Otto Miniapp Design System
# 版本: v1.0 (2026-06-11)
# 适用: Otto Robot 微信小程序(设备控制端)## 0. 单位约定
所有令牌用 px 表达。落地到 WXSS 时统一转换为 rpx:- 1px = 2rpx(iPhone6 基准,覆盖 95% 设备)- 字号 / 间距 / 圆角用 rpx
- 边框(1px border)保留 px,避免某些机型显示模糊
## 1. 设计令牌(YAML)palette:
primary: "#1E5AE8"
primary_hover: "#1648C0"
primary_pressed: "#0F3BA8"
danger: "#E5484D"
warning: "#F5A524"
success: "#30A46C"
surface: "#FFFFFF"
surface_alt: "#F7F8FA"
border: "#E4E7EB"
divider: "#F1F3F5"
text_primary: "#101828"
text_secondary: "#475467"
text_disabled: "#98A2B3"
bg_dark: "#0B0F19" # 暗色模式背景
text_dark: "#E6E8EB" # 暗色模式文字
typography:
font_family: "Inter, -apple-system, 'PingFang SC', sans-serif"
font_family_mono: "JetBrains Mono, monospace"
size_caption: "12px" # 辅助说明
size_body: "14px" # 正文
size_subtitle: "16px" # 卡片标题
size_title: "18px" # 页面主标题
size_display: "24px" # 设备状态大数字
weight_regular: 400
weight_medium: 500
weight_bold: 700
spacing:
unit: 4
xxs: 2
xs: 4
sm: 8
md: 12
lg: 16
xl: 24
xxl: 32
radius:
sm: 4 # 徽标、小标签
md: 8 # 按钮、卡片
lg: 12 # 弹窗
pill: 9999 # 状态徽章
shadow:
card: "0 2rpx 8rpx rgba(16, 24, 40, 0.06)"
modal: "0 8rpx 24rpx rgba(16, 24, 40, 0.12)"
motion:
duration_fast: 120
duration_base: 200
duration_slow: 320
easing: "cubic-bezier(0.2, 0, 0, 1)"
z_index:
base: 0
dropdown: 100
modal: 1000
toast: 2000
## 2. 设计原理
### 品牌基调
冷静的科技感 + 陪伴的柔软。Otto 是桌面机器人,** 用户经常在睡前、起床、做饭时扫一眼 **,所以界面要 " 一眼看懂 "—— 大字号、高对比、低噪音。### 颜色使用规则
- ** 主操作蓝 **:连接、配网、保存 3 个场景。一个屏幕最多 2 个主操作。- ** 告警红 **:** 只在 3 个场景出现 **—— 设备掉线、配网失败、电量低于 10%。出现时必须有图标 + 文字,不能只靠颜色(色盲友好)。- ** 背景层级 **:默认 `surface`,卡片用 `surface_alt`,模态用 surface + shadow。** 禁止用灰色背景做主背景 **。### 排版规则
- 监控数字(电量、温度)用 `size_display` + `weight_bold`,让用户远处一眼看到。- 设备名称用 `size_subtitle`,配网提示用 `size_body`,禁止用 12px 以下字号(老人 / 远视用户)。- 数字字体用等宽(`font_family_mono`),避免电量从 89% 跳到 90% 时卡片宽度抖动。### 圆角语义
- `radius_md` (8px) 是默认值,** 所有按钮、卡片默认 8px**。- 状态徽章(在线 / 离线 / 连接中)必须用 `radius_pill`。- ** 不要混用 **:一个产品里如果出现 3 种以上圆角,说明设计失控。### 阴影语义
- 小程序不像 Web 那样有 z-index 阴影堆叠,** 阴影用得克制 **:- `shadow_card`:所有卡片静态态
- `shadow_modal`:模态弹窗
- 浮层(toast / dropdown)用 `border` + `background` 表达,不加阴影
### 动效规则
- 所有过渡 ≤ 320ms。配网成功跳转、设备连接动画除外(这些是状态反馈,不是 UI 切换)。- ** 不 ** 做花式动画(旋转、抖动、弹跳)。配网用进度条 + 文字就好。## 3. 原生组件改造规范
小程序原生组件(button / input / picker)样式被系统强制覆盖。必须用以下方式重置:- **button**:去掉默认边框用 `::after {border: none;}`,背景用 `background: transparent;`
- **input**:去掉默认 padding,用 `padding: 0;` + 外层 view 控制间距
- **picker / switch**:使用第三方组件库(TDesign / Vant Weapp),不直接用原生
## 4. 暗色模式
- 使用 `wx.getSystemInfo` 读取系统主题:`theme: 'dark' | 'light'`
- 在 `app.wxss` 顶层定义暗色变量,用 `<view class="page {{theme}}">` 切换
- ** 不 ** 做 " 跟随时间自动切换 "—— 用户睡前要关灯看电量,太暗反而看不清。## 5. 跨端差异速查表
| API | 微信 | 支付宝 | 抖音 | 备注 |
|-----|------|--------|------|------|
| `wx.request` | 支持 | 用 `my.httpRequest` | 用 `tt.request` | 必须封装 network 适配层 |
| `wx.getSystemInfo` | 支持 | `my.getSystemInfo` | `tt.getSystemInfo` | 同步读取切换 |
| `<button open-type>` | 支持 | 分享按钮不支持 | 部分支持 | 分享按钮要分平台写 |
## 6. 组件命名规范
- 业务组件:`otto-button` / `otto-card` / `otto-device-tile`(用 otto- 前缀避免和 TDesign 冲突)- 通用组件:`ot-` 前缀 + 短横线命名(HTML 风格)- 文件目录:`/components/otto/button/`,包含 4 个文件
这份 design.md 一共 250 行,3 个小时写完。之后所有 AI 生成的页面、组件、修改都必须参照它。
四、Step 3:让 Cursor 按 design.md 生成 3 个真实页面
设计稿场景:Otto 小程序需要 3 个核心页面——
- 设备列表页(
/pages/devices/index) - 设备详情页(
/pages/device/detail) - 配网页(
/pages/device/setup)
我用 Cursor + Claude 3.5 走一遍,实际使用的 prompt 如下:
Prompt #1:生成设备列表页
请按 /docs/design.md 的规范,生成 Otto 微信小程序的 " 设备列表页 "。要求:1. 4 个文件齐全(.js / .json / .wxml / .wxss)2. 顶部导航栏写 " 我的设备 ",右上角放 "+" 按钮(用 otto-icon-add)3. 设备卡片用 otto-device-tile 组件,包含:设备名、电量图标、在线状态徽章
4. 列表为空时显示空状态:插画 + " 还没有设备,去添加吧 " + otto-button " 添加设备 "
5. 所有色值、间距、圆角严格按 design.md 令牌
6. rpx 转换:1px = 2rpx
7. 列表点击跳转到 /pages/device/detail
不要:- 不要用任何 12px 以下字号
- 不要用红色做主操作
- 不要引入未在 design.md 的色值
实际生成结果(设备列表页 .wxml)
<view class="page {{theme}}">
<navigation-bar title=" 我的设备 ">
<view slot="right" bindtap="onAddDevice" class="nav-icon">
<otto-icon-add />
</view>
</navigation-bar>
<view class="device-list">
<otto-device-tile
wx:for="{{devices}}"
wx:key="id"
name="{{item.name}}"
battery="{{item.battery}}"
status="{{item.status}}"
bind:tap="onDeviceTap"
data-id="{{item.id}}"
/>
</view>
<empty-state
wx:if="{{devices.length === 0}}"
illustration="/assets/empty-device.png"
title=" 还没有设备 "
subtitle=" 去添加你的第一个 Otto"
>
<otto-button variant="primary" bind:tap="onAddDevice">
添加设备
</otto-button>
</empty-state>
</view>
实际生成结果(设备列表页 .wxss)
.page {background-color: var(--surface);
min-height: 100vh;
padding: 0 32rpx;
}
.nav-icon {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.device-list {
margin-top: 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx; /* spacing.lg */
}
实测 :3 个页面(列表 / 详情 / 配网),AI 总共生成耗时约 4 分钟。 第一次生成的样式一致性 100% 命中(圆角、间距、色值全对),不用返工调样式。
五、Step 4:组件沉淀(design.md → 代码的双向同步)
3 个页面跑通之后,下一步是把所有 ” 重复出现的元素 ” 抽成组件。Otto 小程序最终沉淀的组件树:
components/
├── otto/
│ ├── button/ # 主按钮(primary/secondary/ghost/danger)│ ├── device-tile/ # 设备卡片
│ ├── status-badge/ # 状态徽章(在线 / 离线 / 连接中)│ ├── empty-state/ # 空状态
│ ├── icon/ # 图标库
│ └── modal/ # 模态弹窗
└── common/
├── loading/ # 全局 loading
└── toast/ # 轻提示
核心原则 :每个组件文件顶部必须有一段注释, 反向引用 design.md。这样组件改了样式,design.md 也要审视;design.md 改了规范,组件也要同步改。
// components/otto/button/index.js
/**
* Otto Button 组件
* 设计规范:/docs/design.md §radius §motion §palette
* 主操作蓝仅在 primary 变体使用,且单页最多 2 个
*/
Component({
properties: {
variant: {
type: String,
value: 'primary' // primary | secondary | ghost | danger
},
size: {
type: String,
value: 'md' // sm(28px) | md(40px) | lg(48px)
},
disabled: {type: Boolean, value: false},
loading: {type: Boolean, value: false}
}
})
这一步把 design.md 从 ” 文档 ” 变成 ” 代码的索引 ”,AI 改任何组件前先读这段注释,避免再次跑偏。
六、Step 5:日常开发 workflow
光有 design.md 不够,必须建立每天的 ”design.md 心跳 ”:
每日开工
# 1. 拉取最新 design.md
git pull
# 2. 把 design.md 注入到 Cursor 系统提示
# Cursor → Settings → Rules for AI → 粘贴 design.md 全文
# 3. 之后所有生成都引用 @docs/design.md
Design drift 检测(design.md 漂移检测)
每周跑一次脚本,对比代码里的色值 / 圆角 /spacing 跟 design.md 是否一致:
# scripts/check-design-drift.py
import re, glob, yaml
DESIGN = yaml.safe_load(open('docs/design.md')['palette'])
FILES = glob.glob('**/*.wxss', recursive=True)
palette = DESIGN['palette']
violations = []
for f in FILES:
content = open(f).read()
# 找出所有 #RRGGBB 形式的色值
for hex_code in re.findall(r'#[0-9A-Fa-f]{6}', content):
if hex_code.lower() not in [v.lower() for v in palette.values()]:
violations.append(f"{f}: 出现未定义色值 {hex_code}")
if violations:
print("⚠️ design.md 漂移告警:")
for v in violations:
print(f" - {v}")
exit(1)
else:
print("✅ 所有色值在 design.md 范围内 ")
每周 1 跑一次 。发现违规就修代码或更新 design.md( 两者必须同步)。
七、7 个真实踩坑
写完跑通后,沉淀 7 个必须避开的坑:
| 坑 | 现象 | 解决 |
|---|---|---|
| 1. rpx / px 混用 | AI 偶尔会写 padding: 16rpx; font-size: 14px; |
design.md §0 单位约定 + 漂移检测脚本 |
| 2. 原生 button 边框不消失 | Android 上 button 一直有灰色边框 | button::after {border: none;} 写进 design.md §3 |
| 3. 暗色模式文字看不清楚 | 暗色模式下 text_secondary 跟背景对比度不够 | design.md 暗色变量单独定义,不要让 AI 自动反色 |
| 4. iOS 圆角跟 Android 不一致 | iOS 自动给 view 加圆角 | 显式写 border-radius: 8rpx; 别用 overflow: hidden 隐式 |
| 5. 列表项高度抖动 | 数字从 89 → 90 跳变,卡片宽度跟着变 | 数字字体用等宽(font-family_mono) |
| 6. 页面切走再回来状态丢失 | 配网页切走 30 秒再回来,输入框空了 | 用 wx.getStorageSync 缓存表单状态 |
| 7. AI 写自定义导航栏不写 statusBar | 内容顶到刘海屏 | design.md §原生组件改造 + 全局 padding-top: env(safe-area-inset-top) |
八、对比:传统方式 vs design.md 方式
| 维度 | 传统方式(无 design.md) | design.md 方式(本文方法) |
|---|---|---|
| 首个页面 | 写 1 个 page,调样式 30 分钟 | 写 1 个 page,10 分钟 |
| 第 3 个页面 | 跟前 2 个不一致,调样式 1 小时 | 跟前 2 个 100% 一致,几乎无调整 |
| 3 个月后改版 | 改 1 处要翻 30 个文件 | 改 design.md,组件跟着改,页面自动同步 |
| 设计师 / 工程师对齐 | Figma 改完工程师没注意 | design.md 是单一真源,工程师必须读 |
| AI 编程 | 每个 prompt 都要重述规范 | @docs/design.md 一行搞定 |
九、迁移路径(如果你已经有个半成品小程序)
如果你已经有小程序了,不要一次重写。按下面顺序渐进引入:
- 第 1 周 :把现有色值 / 圆角 /spacing 整理成 design.md 草稿( 不需要规范,先存现状)
- 第 2 周:跑漂移检测脚本,标记 ” 未在 design.md 范围的色值 ”
- 第 3-4 周:从 0 开始的所有新页面强制按 design.md
- 第 2 个月起 :旧页面在每次维护时同步 design.md( 不是单独重写,是借机迁移)
- 第 3 个月:design.md 100% 覆盖全代码
3 个月完成迁移,期间业务不停。
十、结论:design.md 落地的 3 个核心动作
把 design.md 在小程序里跑通,只需要 3 件事:
- 写出来:250 行的 design.md 草稿(用上一篇文章的 Otto 示例 fork 改)
- 用起来:每次 AI 生成都
@docs/design.md,人工 1 分钟审核 - 守得住:每周跑 1 次漂移检测,发现违规立即修
最难的是第 1 步——把 5 个约束想清楚、令牌定下来、原理写明白。一旦写完,后面的代码都是体力活。
下一篇我会写 design.md 跟设计工具(Figma / Sketch)的双向同步,把设计师和工程师的协作也纳入规范。
实战记录人:增长运营官
实战日期:2026-06-11
项目地址:Otto Robot 微信小程序(开发中)
design.md 模板:https://east196.cn/?p=125(前一篇文章的 Otto 示例)