design.md 实战(uniAPP 版):一套设计系统跑通 5 个端

82次阅读

实战日期:2026-06-11
实战对象:Otto Robot 跨端控制应用(H5 / 微信小程序 / 支付宝小程序 / 抖音小程序 / Android+iOS App)
框架:uniAPP(Vue 3 + Vite + TypeScript)
前置阅读:design.md 概念扫盲(ID 125)、微信小程序实战(ID 126)


一、为什么 uniAPP 才是 design.md 的最佳载体

上一篇文章我写了微信小程序版 design.md 落地。但飞熊跟我说了关键信息:项目用的是 uniAPP。

这一换,design.md 落地的 复杂度反而降了一半

我对比下三套方案跑 design.md 的工程量:

维度 微信原生小程序 uniAPP Taro
单位系统 rpx(手写 2 倍) px(自动转换) px(自动转换)
设计令牌 WXSS 变量 / 手写 SCSS 变量一等公民 SCSS / Less
跨端复用 design.md 5 套规范 1 套规范跑 5 端 1 套
条件编译 不支持 原生 #ifdef 支持 process.env 模拟
AI 编程友好度 一般(WXML 偏 HTML) 优(Vue 模板,Cursor 理解最深)
官方设计语言 杂乱 uni-ui 提供 design tokens TDesign

结论 :uniAPP 是当前 最适合 design.md 落地 的跨端方案。下面是 Otto Robot 的完整实战。


二、Step 1:理解 uniAPP 跟微信小程序的 5 个关键差异

维度 微信原生小程序 uniAPP design.md 应对
样式单位 rpx 强制 px 自动转 rpx/upx/pt 令牌直接用 px,框架自动适配
样式预处理 弱(less/wxss) SCSS 一等公民 令牌用 SCSS 变量管理,编译时注入
组件 WXML + 4 文件 .vue 单文件组件 一个 .vue 文件搞定
条件编译 #ifdef MP-WEIXIN 不同端的设计差异用条件编译处理
设计令牌共享 手写 WXSS 变量 SCSS 变量 + uni-ui tokens 一份 _tokens.scss 跨端共用

关键洞察 :uniAPP 的 px 在小程序端会 自动转换为 rpx,在 App 端会按设计稿宽度换算为 px。这意味着 design.md 里的所有令牌只用写一份 px,编译时会自动适配。


三、Step 2:把 design.md 落到 SCSS 变量

设计令牌不再是 ” 文档里写写 ”,而是直接变成代码可用的 SCSS 变量。Otto Robot 的 design tokens 落地代码:

// /src/styles/_tokens.scss
// Otto Design Tokens — 单一真源,跟 design.md 一一对应
// 修改此处必须同步更新 /docs/design.md

// ============ 颜色 ============
$color-primary:        #1E5AE8;
$color-primary-hover:  #1648C0;
$color-primary-pressed:#0F3BA8;
$color-danger:         #E5484D;
$color-warning:        #F5A524;
$color-success:        #30A46C;

$color-surface:        #FFFFFF;
$color-surface-alt:    #F7F8FA;
$color-border:         #E4E7EB;
$color-divider:        #F1F3F5;

$color-text-primary:   #101828;
$color-text-secondary: #475467;
$color-text-disabled:  #98A2B3;

$color-bg-dark:        #0B0F19;
$color-text-dark:      #E6E8EB;

// ============ 排版 ============
$font-family-sans: Inter, -apple-system, 'PingFang SC', sans-serif;
$font-family-mono: 'JetBrains Mono', monospace;

$font-size-caption:  12px;
$font-size-body:     14px;
$font-size-subtitle: 16px;
$font-size-title:    18px;
$font-size-display:  24px;

$font-weight-regular: 400;
$font-weight-medium:  500;
$font-weight-bold:    700;

// ============ 间距(4 的倍数)============
$spacing-unit: 4px;
$spacing-xxs: 2px;
$spacing-xs:  4px;
$spacing-sm:  8px;
$spacing-md:  12px;
$spacing-lg:  16px;
$spacing-xl:  24px;
$spacing-xxl: 32px;

// ============ 圆角 ============
$radius-sm:   4px;
$radius-md:   8px;
$radius-lg:   12px;
$radius-pill: 9999px;

// ============ 阴影 ============
$shadow-card:  0 2px 8px rgba(16, 24, 40, 0.06);
$shadow-modal: 0 8px 24px rgba(16, 24, 40, 0.12);

// ============ 动效 ============
$duration-fast: 120ms;
$duration-base: 200ms;
$duration-slow: 320ms;
$easing:        cubic-bezier(0.2, 0, 0, 1);

// ============ z-index ============
$z-base:     0;
$z-dropdown: 100;
$z-modal:    1000;
$z-toast:    2000;
// /src/styles/_mixins.scss
// 设计原理层 — 跟 design.md 第二节 " 设计原理 " 对应
@import './tokens';

@mixin otto-button-base {
  font-family: $font-family-sans;
  font-size: $font-size-body;
  font-weight: $font-weight-medium;
  border-radius: $radius-md;
  transition: all $duration-fast $easing;
}

@mixin otto-card {
  background: $color-surface;
  border-radius: $radius-md;
  box-shadow: $shadow-card;
  padding: $spacing-lg;
}

@mixin text-ellipsis($lines: 1) {
  @if $lines == 1 {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  } @else {
    display: -webkit-box;
    -webkit-line-clamp: $lines;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

这一步把 design.md 真正变成了 ” 代码的源 ”,AI 生成的组件如果想用令牌,只需要 @import '@/styles/tokens'


四、Step 3:用 Cursor 生成 3 个真实页面

Otto 跨端应用需要 3 个核心页面:

  1. 设备列表页(/src/pages/devices/index.vue
  2. 设备详情页(/src/pages/device/detail.vue
  3. 配网页(/src/pages/device/setup.vue

Prompt(直接抄)

请按 /docs/design.md 和 /src/styles/_tokens.scss 的规范,生成 Otto 跨端应用的
" 设备列表页 "。要求:1. 单文件组件 .vue(<template> + <script setup> + <style lang="scss" scoped>)2. 顶部用 uni-nav-bar 组件,标题 " 我的设备 ",右上角 "+" 按钮
3. 设备卡片用 <otto-device-tile> 自研组件
4. 列表为空时显示 <empty-state> 组件
5. 所有色值、间距、圆角严格使用 _tokens.scss 里的 SCSS 变量
6. SCSS 用 @use 引入 tokens,不要用 @import
7. 点击卡片跳转 /pages/device/detail

不要:- 不要写硬编码色值(如 #1E5AE8 改为 $color-primary)- 不要写魔法数字(如 padding: 12px 改为 padding: $spacing-md)- 不要用 any 类型(TypeScript strict)- 不要做花式动画(旋转 / 抖动)

生成结果(设备列表页 .vue 完整代码)


<template>
  <view class="page">
    <uni-nav-bar
      title=" 我的设备 "
      :right-icon="iconAdd"
      @clickRight="onAddDevice"
    />

    <view v-if="devices.length > 0" class="device-list">
      <otto-device-tile
        v-for="device in devices"
        :key="device.id"
        :name="device.name"
        :battery="device.battery"
        :status="device.status"
        @tap="onDeviceTap(device.id)"
      />
    </view>

    <empty-state
      v-else
      illustration="/static/empty-device.png"
      title=" 还没有设备 "
      subtitle=" 去添加你的第一个 Otto"
    >
      <otto-button variant="primary" @tap="onAddDevice">
        添加设备
      </otto-button>
    </empty-state>
  </view>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {onShow} from '@dcloudio/uni-app'
import iconAdd from '@/static/icons/add.svg'

interface Device {
  id: string
  name: string
  battery: number
  status: 'online' | 'offline' | 'connecting'
}

const devices = ref<Device[]>([])

onShow(() => {loadDevices()
})

async function loadDevices() {
  // 实际项目里调用 uni.request 拉数据
  devices.value = [{ id: '1', name: '客厅的 Otto', battery: 89, status: 'online'},
    {id: '2', name: '卧室的 Otto', battery: 42, status: 'offline'}
  ]
}

function onDeviceTap(id: string) {uni.navigateTo({ url: `/pages/device/detail?id=${id}` })
}

function onAddDevice() {uni.navigateTo({ url: '/pages/device/setup'})
}
</script>

<style lang="scss" scoped>
@use '@/styles/tokens' as *;

.page {
  background-color: $color-surface;
  min-height: 100vh;
  padding: 0 $spacing-xl;
}

.device-list {
  margin-top: $spacing-xl;
  display: flex;
  flex-direction: column;
  gap: $spacing-lg;
}
</style>

实测(3 个页面):

  • AI 生成耗时:约 5 分钟(比微信小程序多 1 分钟,因为 .vue 比 .wxml 长)
  • 样式一致性:100% 命中(因为是 SCSS 变量,编译错的可能性为 0)
  • 跨端编译:5 端全跑通(H5 / 微信 / 支付宝 / 抖音 / App-PLUS)

五、Step 4:组件沉淀(uni-ui + 自研双线)

uniAPP 生态里已经有一半的设计系统了——uni-ui(官方组件库)覆盖了 60+ 基础组件。Otto 的策略是:

src/components/
├── ui/              # uni-ui 改造(design tokens 注入)│   ├── otto-button/
│   ├── otto-input/
│   ├── otto-modal/
│   └── ...
├── business/        # 业务组件
│   ├── device-tile/         # 设备卡片
│   ├── status-badge/        # 状态徽章
│   ├── empty-state/         # 空状态
│   └── ...
└── layout/          # 布局组件
    ├── page-container/
    └── bottom-bar/

组件示例:OttoButton(design tokens 完整版)


<template>
  <button
    class="otto-button"
    :class="[`otto-button--${variant}`,
      `otto-button--${size}`,
      {'is-disabled': disabled, 'is-loading': loading}
    ]"
    :disabled="disabled"
    @click="handleClick"
  >
    <view v-if="loading" class="otto-button__loading" />
    <slot />
  </button>
</template>

<script setup lang="ts">
/**
 * Otto Button 组件
 * 设计规范:/docs/design.md §radius §motion §palette
 * 主操作蓝仅在 primary 变体使用,且单页最多 2 个
 */
interface Props {
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
}

const props = withDefaults(defineProps
<Props>(), {
  variant: 'primary',
  size: 'md',
  disabled: false,
  loading: false
})

const emit = defineEmits<{(e: 'click', event: Event): void
}>()

function handleClick(e: Event) {if (!props.disabled && !props.loading) {emit('click', e)
  }
}
</script>

<style lang="scss" scoped>
@use '@/styles/tokens' as *;

.otto-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: $radius-md;
  font-weight: $font-weight-medium;
  font-family: $font-family-sans;
  transition: all $duration-fast $easing;
  border: none;
  cursor: pointer;

  &--primary {
    background-color: $color-primary;
    color: $color-surface;

    &:active {background-color: $color-primary-pressed;}
  }

  &--secondary {
    background-color: $color-surface-alt;
    color: $color-text-primary;
  }

  &--ghost {
    background-color: transparent;
    color: $color-primary;
    border: 1px solid $color-border;
  }

  &--danger {
    background-color: $color-danger;
    color: $color-surface;
  }

  &--sm {height: 28px; padding: 0 $spacing-md; font-size: $font-size-caption;}
  &--md {height: 40px; padding: 0 $spacing-lg; font-size: $font-size-body;}
  &--lg {height: 48px; padding: 0 $spacing-xl; font-size: $font-size-subtitle;}

  &.is-disabled {
    opacity: 0.5;
    color: $color-text-disabled;
    cursor: not-allowed;
  }
}
</style>

关键 :组件顶部注释 反向引用 design.md 章节。AI 改这个组件时,Cursor 的 Rules 会自动提示它 ” 先去读 design.md §radius §motion §palette”。


六、Step 5:跨端适配(条件编译 + design.md 差异表)

uniAPP 的杀手锏是 条件编译。design.md 里的 ” 跨端差异速查表 ” 可以直接落到代码里:


<template>
  <view class="device-card">
    <!-- #ifdef MP-WEIXIN -->
    <view class="status-bar-wechat">

<text> 微信小程序专属状态栏 </text>
    </view>
    <!-- #endif -->

    <!-- #ifdef APP-PLUS -->
    <view class="status-bar-app">

<text>App 端用原生导航栏 </text>
    </view>
    <!-- #endif -->

    <view class="device-info">
      <text class="device-name">{{name}}</text>
      <text class="device-battery">{{battery}}%</text>
    </view>
  </view>
</template>

<style lang="scss" scoped>
@use '@/styles/tokens' as *;

.device-card {
  background: $color-surface;
  border-radius: $radius-md;
  padding: $spacing-lg;

  /* #ifdef H5 */
  box-shadow: $shadow-card;        // H5 用阴影
  /* #endif */

  /* #ifdef MP-WEIXIN */
  border: 1rpx solid $color-border; // 小程序用边框(避免阴影在低端机失效)/* #endif */

  /* #ifdef APP-PLUS */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);  // App 端阴影深一点
  /* #endif */
}
</style>

跨端设计差异表(design.md 必备章节)

维度 H5 微信小程序 App 备注
阴影 支持 低端机失效 完全支持 小程序优先用 border
圆角 完全支持 完全支持 8px 以下模糊 最小圆角 ≥ 4px
字体 系统字体 PingFang SC SF Pro 跨端用 sans-serif
字号 14px 起 12px 起 14px 起 老人 / 远视友好
下拉刷新 scroll-view onPullDownRefresh native 交互差异
返回手势 右滑 物理返回键 设计时显式说明

这一节必须写进 design.md,否则 AI 会在 H5 上加阴影、在小程序上渲染失败。


七、6 个真实踩坑(uniAPP 特有)

现象 解决
1. rpx 误用 部分 AI 还在写 rpx 强制用 _tokens.scss 里的 px 变量,配置 ESLint 禁止 rpx
2. @import 编译慢 大项目 @import 多层嵌套编译卡死 @use '@/styles/tokens' as *(Sass module 系统)
3. Vue 3 响应式丢失 reactive 解构后失去响应 ref + .valuetoRefs
4. H5 端原生组件样式失效 button 边框去不掉 button::after {border: none;} 加到 App.vue 全局样式
5. App 端安全区问题 内容顶到刘海屏 全局 <view class="page-safe"> + padding-top: env(safe-area-inset-top)
6. 跨端 API 差异 uni.getSystemInfo 在不同端返回值不同 封装 useSystem() 组合式函数,做平台适配层

ESLint 配置(防 rpx 误用)

// .eslintrc.js
module.exports = {
  rules: {
    'no-restricted-syntax': [
      'error',
      {selector: "Literal[value=/\\d+rpx/]",
        message: 'uniAPP 项目禁止使用 rpx,请用 _tokens.scss 里的 px 变量'
      }
    ]
  }
}

八、对比:微信原生 vs uniAPP vs Taro

维度 微信原生 uniAPP Taro
design.md 落地难度 ⭐⭐⭐ 中(要写 WXSS 变量 + rpx 转换) 最易(SCSS 变量 + px 自动转) ⭐⭐ 较易(SCSS + px 自动转)
跨端 仅微信 5 端(H5+4 小程序 +App) 4 端(无 App)
AI 编程支持 一般 优(Vue 模板是 Cursor 训练最深的)
性能 最佳(原生渲染) 良好(小程序端编译优化) 良好
生态 微信专有 uni-ui + uView + 第三方 丰富 TDesign
学习成本 中(要学 WXML) 低(Vue 3 是主流) 中(React 风格)

飞熊选 uniAPP 是正确决策:用 Vue 3 + SCSS + 跨端编译,design.md 落地的工程量比微信原生少 40%。


九、迁移路径(如果你之前用别的方案)

场景 A:微信原生小程序 → uniAPP

  1. 第 1 周:把 WXSS 变量映射到 _tokens.scss(1:1 转换,px 不变)
  2. 第 2 周:WXML 模板改为 .vue 模板(用 `
    ` 替换 ``,`v-if` 替换 `wx:if`)
  3. 第 3 周.js 逻辑改为 <script setup>,API 替换 wx.*uni.*
  4. 第 4 周:跑 5 端编译,修复跨端差异

工具:uniAPP 官方有 WXML→Vue 转换器(wxml-to-uni),能转换 70% 代码。

场景 B:Taro → uniAPP

  1. 第 1 周:把 React JSX 改为 Vue 3 SFC(语法差异最大)
  2. 第 2 周:SCSS 变量保留,重新组织到 _tokens.scss
  3. 第 3 周:编译验证 5 端

场景 C:H5 → uniAPP(要做 App)

  1. 第 1 周:把 px 全部映射到 _tokens.scss
  2. 第 2 周:组件从 .vue(标准)改为 .vue(uniAPP,差异小)
  3. 第 3 周:H5 → 小程序时处理 API 差异(uni.* 替代 window.*

十、完整 design.md 模板(uniAPP 专用)

把上面所有点整合,给一份可直接复用的 uniAPP design.md 模板:

# Otto Cross-platform Design System
# 框架: uniAPP (Vue 3 + Vite + TypeScript + SCSS)
# 版本: v1.0 (2026-06-11)
# 适用端: H5 / 微信小程序 / 支付宝小程序 / 抖音小程序 / Android+iOS App

## §0 单位约定
- 所有令牌用 px 表达(uniAPP 自动转 rpx/pt)- _tokens.scss 是单一真源,design.md 仅做 " 原理 " 说明
- 禁止在 .vue 文件里直接写 rpx(ESLint 强制)## §1 设计令牌
[见 _tokens.scss 文件]

## §2 设计原理
[品牌基调、颜色规则、排版规则、圆角语义、阴影语义、动效规则]

## §3 原生组件改造
- button 边框: button::after {border: none;} (全局 App.vue)
- input padding: padding: 0 + 外层 view 控制
- 跨端差异表 [见正文 §6]

## §4 跨端适配
- H5 优先 CSS 变量
- 小程序优先 SCSS 编译
- App 端优先原生体验
- 条件编译必须用 #ifdef,不要在 JS 里写 if (uni.getSystemInfoSync().platform === 'android')

## §5 暗色模式
- 在 _tokens.scss 末尾定义暗色 SCSS 变量
- 使用 <view class="theme-{{theme}}"> 切换
- 主题来源: uni.getSystemInfoSync().theme

## §6 组件命名
- 业务组件: otto-* 前缀
- 通用组件: ot-* 前缀
- 文件目录: /src/components/{namespace}/{name}/index.vue

## §7 维护规则
- 修改 _tokens.scss 必须同步 design.md
- 修改 design.md 必须审视 _tokens.scss
- 漂移检测脚本: /scripts/check-design-drift.ts(每周跑 1 次)

十一、结论:uniAPP + design.md 的 4 个核心优势

  1. 一份令牌跑 5 端:H5 / 微信 / 支付宝 / 抖音 / App,设计成本只算 1 次
  2. SCSS 编译时校验 :令牌值错了编译就报错, 比微信小程序的 WXSS 变量更安全
  3. Vue 3 模板 + Cursor 深度适配:AI 生成的代码质量更高(Cursor 训练数据 Vue 占比最大)
  4. 条件编译原生支持:跨端差异用 #ifdef 块处理,design.md 差异表直接落到代码

Otto Robot 的下一步:把这份 design.md 落到 east196.cn/otto/design.md,下个月开发小程序时直接 @docs/design.md 让 Cursor 生成。

系列文章

  • design.md 概念扫盲 → ID 125
  • design.md 微信小程序实战 → ID 126
  • design.md uniAPP 实战 → 本篇(ID 127)
  • 下一篇预告:design.md ↔ Figma 双向同步

实战记录人:增长运营官
实战日期:2026-06-11
框架版本: uniAPP 3.x + Vue 3.4 + Vite 5.x
适配 uniAPP 用户: 飞熊(Otto Robot 跨端控制应用)

正文完