阅读视图

发现新文章,点击刷新页面。

瑞幸 UI 上 pub.dev 了 —— 22 个 Flutter 组件,与微信小程序版双端对齐

瑞幸 UI 上 pub.dev 了 —— 22 个 Flutter 组件,与微信小程序版双端对齐

把 DESIGN.md 当作跨端的"单一真相",一套设计语言同时喂给 WeChat 小程序和 Flutter。

效果截图

瑞幸-fl-首页.png

瑞幸-fl-方案选择.png

瑞幸-fl-等级卡.png

瑞幸-fl-产品.png

瑞幸-fl-左侧导航.png

瑞幸-fl-通知.png

瑞幸-fl-网格.png

瑞幸-fl-头像.png

瑞幸-fl-按钮.png

背景

之前我写了《我从瑞幸咖啡小程序里,拆出了一套 22 个组件的开源 UI 库》,发布了 npm 包 lkcn-ui

验证 DESIGN.md 真的是"可复用的设计规范"吗,那它至少应该能驱动两个不同的运行时。于是有了这一版:

  • GitHubhttps://github.com/qwfy5287/lkcn-ui-flutter
  • pub.devhttps://pub.dev/packages/lkcn_ui
  • 姊妹项目https://github.com/qwfy5287/lkcn-ui(小程序版)

双端对照

两个仓库,一份 DESIGN.md,相同的 22 个组件:

平台 包名 分发 仓库
微信小程序 lkcn-ui npm qwfy5287/lkcn-ui
Flutter lkcn_ui pub.dev qwfy5287/lkcn-ui-flutter

命名这里踩了个小坑:pub.dev 要求 snake_case,不能用连字符,所以 npm 的 lkcn-ui 到 pub.dev 就成了 lkcn_ui。这是 Dart/Flutter 生态的惯例,不算破坏品牌一致性。

版本号策略是 MAJOR.MINOR 对齐 + PATCH 独立——看到 npm 1.2.3 + pub 1.2.1 就知道 API 对齐、只是 Flutter 单独修了两个 bug。

设计语言的「跨端翻译」

如果说小程序版是把 DESIGN.md 翻译成 WXSS + WXML,那 Flutter 版就是翻译成 Dart Widget。这过程有 5 件事需要做决定:

1. Design Token:CSS 变量 → Dart const class

小程序版把 token 写成 CSS 变量,注入到 page {}

page {
  --lkcn-blue: #1A6EFF;
  --lkcn-radius-md: 24rpx;
}

Flutter 没有 CSS 变量这种运行时机制,但它的类型系统更强。我用 const class 做等价物:

class LkcnColors {
  static const Color primary = Color(0xFF002FA7);      // 克莱因蓝
  static const Color accentOrange = Color(0xFFFF6A3D);
  static const Color accentGold = Color(0xFFC9A66B);
}

class LkcnRadius {
  static const double md = 12;
  static const double pill = 999;
}

使用:

Container(
  decoration: BoxDecoration(
    color: LkcnColors.primary,
    borderRadius: BorderRadius.circular(LkcnRadius.md),
  ),
)

好处是编译期常量、IDE 自动补全、类型安全;坏处是换肤没办法像 CSS 变量那样"覆盖即生效"——要彻底换肤得上 ThemeExtension。首版先不折腾这个。

2. 单位:rpx → logical pixels

小程序的 rpx 基于 750 设计稿,Flutter 的 logical pixel 是独立密度单位。换算规则就一条:rpx = lpt × 2

字号 28rpx 对应 14 lpt,间距 24rpx 对应 12 lpt,圆角 16rpx 对应 8 lpt。习惯了之后是肌肉记忆,但第一次做映射表时你会翻 variables.wxss 翻到吐。

3. 组件 API:kebab-case → PascalCase / enum

  • 组件类:lkcn-buttonLkcnButton
  • 枚举属性:type="primary"LkcnButtonType.primary
  • 事件回调:bind:tap="onClick"onTap: () {}

Flutter 的 enum 比字符串属性严格得多——如果你传了个不存在的 type 字符串,小程序只会默默 fallback,Flutter 直接编译不过。对库作者是好事。

4. 插槽:<slot> → Widget 参数

小程序靠 <slot> 传子内容,支持具名插槽。Flutter 对应的是具名参数:

LkcnCard(
  title: '我的资产',
  child: Column(children: [...]),   // 主内容
  footer: Row(...),                  // footer 槽
)

一个命名参数 = 一个插槽,清晰、类型安全、IDE 能提示。

5. Demo 的组织:pages/demo-*example/lib/demos/*

小程序版每个 demo 是独立 page(wxml/wxss/js/json 四件套),通过 pages.json 注册。Flutter 版按 pub.dev 惯例,example/ 是个独立的可运行 app,每个组件对应一个 .dart 文件,用 MaterialPageRoute 跳转:

example/
├── lib/
│   ├── main.dart              # 按 原子/交互/容器/业务 分组的索引页
│   └── demos/
│       ├── button_demo.dart
│       ├── product_card_demo.dart
│       └── ... (21 个)
└── pubspec.yaml               # path: ../ 引用主包

cd example && flutter run 就能跑,iOS / Android / macOS / Web 四端都能看。这比小程序的"打开微信开发者工具"门槛低多了。

几个还原得比较得意的组件

LkcnStepper:加购从 + 展开到 [-] n [+]

瑞幸菜单页最有辨识度的微交互,Flutter 版用 setState 切两个形态:

LkcnStepper(
  value: _quantity,
  onChanged: (v) => setState(() => _quantity = v),
)

弹性动画走 LkcnMotion.bounce(即 Cubic(0.34, 1.56, 0.64, 1)),跟 WXSS cubic-bezier 常量完全一致。

LkcnPrice:三段式价格渲染

"符号小 + 整数大 + 小数小"的层次是瑞幸价格的灵魂:

LkcnPrice(value: 9.9, original: 32, prefix: '预估到手')

内部把 9.9 拆成 9.9 两段不同字号,¥ 给第三种字号,原价走 TextDecoration.lineThrough

LkcnCouponScroll:票据左侧半圆缺口

小程序版靠 CSS clip-path 裁出缺口,Flutter 没有这个 API。我用 CustomPainter 手画 path:

final path = Path()
  ..moveTo(r, 0)
  ..lineTo(size.width - r, 0)
  // ...
  ..lineTo(0, size.height * 0.5 + 6)
  ..arcToPoint(                          // ← 半圆缺口
    Offset(0, size.height * 0.5 - 6),
    radius: const Radius.circular(6),
    clockwise: false,
  )
  ..close();

最终效果和小程序版几乎一致。CustomPainter 写起来比 CSS clip-path 啰嗦,但控制粒度更细。

LkcnMembershipPlan:会员订阅全流程

方案选择器 + 订阅 CTA + 协议勾选,三件事一个 Widget 解决:

LkcnMembershipPlan(
  plans: const [
    LkcnPlan(name: '连续包月', price: 9.9, badge: '爆款天天 9.9 起'),
    LkcnPlan(name: '月卡', price: 19.9),
  ],
  agreement: '开通会员代表接受',
  agreementLinks: const [
    LkcnAgreementLink(text: '《会员服务协议》'),
    LkcnAgreementLink(text: '《自动续费协议》'),
  ],
  onSubscribe: (plan, agreed) {
    // agreed = false 时可以弹 toast 提示勾选
  },
)

快速上手

pubspec.yaml

dependencies:
  lkcn_ui: ^0.1.0

业务代码:

import 'package:flutter/material.dart';
import 'package:lkcn_ui/lkcn_ui.dart';

class MenuPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: LkcnColors.pageBg,
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          LkcnProductCard(
            image: 'https://.../coconut-latte.png',
            title: '生椰拿铁',
            tags: const ['全球销量第一', 'IIAC 金奖'],
            price: 9.9,
            originalPrice: 32,
            pricePrefix: '预估到手',
            onAdd: () {},
          ),
          const SizedBox(height: 16),
          LkcnButton.cta(
            text: '立即开通连续包月 ¥9.9',
            size: LkcnButtonSize.large,
            block: true,
            round: true,
            onTap: () {},
          ),
        ],
      ),
    );
  }
}

22 个组件速览

  • 原子:Button · Tag · Price · Badge · Avatar
  • 交互:SearchBar · Segment · Stepper · Tabs · Tabbar
  • 容器:Card · Grid · Swiper · NoticeBar · LocationBar · FloatingButton · CategorySidebar
  • 业务:ProductCard · CouponScroll · PromoCard · LevelCard · MembershipPlan

每个的 API 尽量跟 npm 版同名、同语义。小程序那边的 bind:add 事件在 Flutter 是 onAdd,小程序的 custom-class 在 Flutter 通过 child/padding 参数调——这些映射关系看完一遍 README 就能对上号。

一些数据

  • 22 个组件,零第三方依赖(只依赖 Flutter SDK)
  • 约 3000 行 Dart 代码(不含 example)
  • flutter analyze / example && flutter analyze0 警告 0 错误
  • lib/ 目录 25 个 .dart 文件
  • Dart SDK:^3.11.3,Flutter:>=3.22.0
  • MIT License

跨端维护的几条经验

做完这版 Flutter 之后,最深的感受是:跨端组件库的真正难点不在代码,在保持纪律。

  1. DESIGN.md 做单一真相:色值 / 间距 / 圆角这些决策写在文档里,而不是写在某一端代码的注释里。PR 有分歧时,以文档为准。
  2. MAJOR.MINOR 对齐 + PATCH 独立:两端版本号不强求完全一致,但 API 变更要同步发版。
  3. Issue 加端标签[wx] / [flutter] / [design] 三类,避免跨端 issue 混战。
  4. demo 先行:改组件前先改 demo,再改源码 —— 这样能强制你想清楚 API 长什么样。

后续计划

  • 每个组件写 widget test,提 pub.dev Like / Popularity 评分
  • ThemeExtension 版的 Design Token,支持运行时换肤
  • 深色模式
  • GitHub Actions CI:analyze + test + 自动 pub publish
  • VitePress 双端文档站(两端 API 并排展示)

觉得有用的话,欢迎 Star / 试用:

  • GitHub:https://github.com/qwfy5287/lkcn-ui-flutter
  • pub.dev:https://pub.dev/packages/lkcn_ui
  • 小程序版姊妹项目:https://github.com/qwfy5287/lkcn-ui

🧑‍💻 顺便求职

目前正在找工作,前端优先,全栈也可以胜任,坐标 厦门

案例集(前端 / 全栈):my.feishu.cn/wiki/XUmGw8…

有合适岗位欢迎评论或私信,感谢。

我从瑞幸咖啡小程序里,拆出了一套 22 个组件的开源 UI 库

我从瑞幸咖啡小程序里,拆出了一套 22 个组件的开源 UI 库

把它的设计语言完整提炼出来,做成了一个可以直接 npm install 的微信小程序组件库。

效果截图

Snipaste_2026-04-15_16-25-37.jpg

Snipaste_2026-04-15_16-27-47.jpg

Snipaste_2026-04-15_16-27-15.jpg

Snipaste_2026-04-15_16-26-37.jpg

Snipaste_2026-04-15_16-26-20.jpg

Snipaste_2026-04-15_16-26-00.jpg

克制的双色系统(蓝+橙),无阴影的卡片层次,菜单页那个从 + 按钮展开到数量步进器的丝滑交互,会员卡页面方案选择器的信息架构……这些细节放在一起,构成了一套非常完整且高辨识度的设计语言。

项目叫 LKCN UI,22 个组件,纯原生微信小程序自定义组件,零依赖,原生 / Taro / uni-app 项目都能直接用。

GitHub: https://github.com/user/lkcn-ui

色彩系统

瑞幸全局只用 两个强调色

色值 用途 使用场景
#1A6EFF Brand Blue 交互元素 TabBar 激活态、按钮、加购圆钮、链接
#FF6B35 Accent Orange 促销与价格 价格数字、CTA 按钮、Badge、优惠券

辅助色包括会员金 #C8A26E、即享绿 #2B7D5B、咖啡棕 #3D2D1F

一个重要发现:瑞幸的卡片没有阴影。整个 App 的层次感完全靠圆角 + 间距 + 背景色差来实现,这使得渲染性能非常好,也让整体视觉特别干净。

字体体系

价格是瑞幸 UI 最有辨识度的元素。它把价格拆成了三段不同大小的文字:

¥(小号加粗) 9(大号加粗) .9(小号加粗)  ¥32(小号灰色删除线)

这种「符号小、整数大、小数小」的层次处理让价格数字极具视觉冲击力,同时原价的删除线灰色处理制造了强烈的价差感知。我在 lkcn-price 组件里完整还原了这个效果。

间距与圆角

间距体系是标准的 8px 递增:4 / 8 / 12 / 16 / 24 / 32(rpx 翻倍)。

圆角有 5 级:4px(标签)→ 8px(按钮、输入框)→ 12px(卡片)→ 20px(弹窗)→ 999px(胶囊)。

所有 Token 都通过 CSS 变量注入,覆盖变量即可全局换肤:

page {
  --lkcn-blue: #1A6EFF;
  --lkcn-orange: #FF6B35;
  --lkcn-radius-md: 24rpx;
  /* ... 60+ 个变量 */
}

22 个组件一览

全部组件从瑞幸小程序的真实页面中提取,不是凭空设计的:

基础组件: Button(6 种类型 × 3 尺寸)、Tag(4 类型 × 4 颜色)、Price(整数/小数自动拆分)、Badge、Avatar

布局容器: Card、Grid(3/4/5 列自适应)、Swiper(胶囊形指示点)、CouponScroll、PromoCard

导航: TabBar(safe-area 适配)、Tabs(滑动下划线)、SegmentControl、SearchBar、CategorySidebar、LocationBar

业务组件: ProductCard(菜单列表项)、Stepper(折叠→展开态)、LevelCard(会员等级)、MembershipPlan(订阅方案选择)、NoticeBar、FloatingButton

1. Stepper:瑞幸的加购交互

瑞幸菜单页的加购交互是我见过最优雅的——数量为 0 时只显示一个蓝色 + 圆钮,点击后展开为 [-] [数字] [+] 三段式控件。

<!-- 使用方式 -->
<lkcn-stepper value="{{count}}" bind:change="onChange" />

组件内部的关键判断:

<!-- value <= min 时只显示 + 按钮 -->
<view wx:if="{{value <= min}}" class="lkcn-stepper__add lkcn-stepper__add--solo">
  <text class="lkcn-stepper__icon">+</text>
</view>
<!-- 否则展开完整控件 -->
<view wx:else class="lkcn-stepper__controls">
  <!-- [-] [count] [+] -->
</view>

加购按钮的 scale(0.88) + cubic-bezier(0.34, 1.56, 0.64, 1) 弹性回弹动画让点击手感特别好。

2. Price:三段式价格渲染

<lkcn-price value="9.9" original="32" prefix="预估到手" />

组件自动将 9.9 拆分为整数 9 和小数 .9,分别用不同字号渲染,currency symbol ¥ 用小号加粗。这种处理在电商类小程序里非常实用,直接拿去用就行。

3. CategorySidebar:菜单页左侧导航

这个组件还原了菜单页左侧的完整细节——激活态的白色背景、左侧橙色指示条、分类标签(新品产地季苦瓜轻体),以及新品小红点。

<lkcn-category-sidebar
  categories="{{categories}}"
  active="{{catActive}}"
  height="100vh"
  bind:change="onCatChange"
/>

数据结构支持纯文字和对象两种格式:

categories: [
  '人气Top',                           // 纯文字
  { text: '周边NEW', tag: '周边NEW', tagColor: 'blue' },  // 带标签
  { text: '果C美式', tag: '苦瓜轻体', tagColor: 'green', dot: true },
]

4. MembershipPlan:会员方案选择器

会员卡页面底部那个方案选择 + 订阅 CTA + 协议勾选的完整流程,一个组件搞定:

<lkcn-membership-plan
  plans="{{plans}}"
  active="{{planActive}}"
  agreement="开通会员代表接受"
  agreement-links="{{[{text:'《服务协议》'}, {text:'《续费说明》'}]}}"
  bind:subscribe="onSubscribe"
/>

为什么选原生而不是 Taro / uni-app

这是我在开发前做的一个关键决策。核心理由就一个——受众最大化

原生微信小程序自定义组件能被所有技术栈引入:

原生组件 → 原生项目 ✅、uni-app 项目 ✅、Taro 项目 ✅
uni-app 组件 → 只有 uni-app 能用 ❌
Taro 组件 → 只有 Taro 能用 ❌

uni-app 引入原生组件只需要放到 wxcomponents/ 目录,在 pages.json 注册即可。Taro 也类似。写一份代码三个生态都能吃到,这是 Vant Weapp 走过的路。

快速上手

npm install lkcn-ui

在微信开发者工具中构建 npm,然后注册组件:

{
  "usingComponents": {
    "lkcn-button": "lkcn-ui/button/index",
    "lkcn-price": "lkcn-ui/price/index",
    "lkcn-product-card": "lkcn-ui/product-card/index"
  }
}

直接使用:

<lkcn-button type="primary" round>立即下单</lkcn-button>

<lkcn-product-card
  image="/images/coconut-latte.png"
  title="生椰拿铁(首创)"
  tags="{{['全球销量第一', 'IIAC金奖']}}"
  price="9.9"
  original-price="32"
  bind:add="onAddToCart"
/>

也可以不用 npm,直接把 packages/ 下需要的组件目录复制到你的项目里。

换肤

所有视觉变量都通过 CSS 变量控制,覆盖即可适配你自己的品牌:

page {
  --lkcn-blue: #7C3AED;    /* 换成你的品牌紫 */
  --lkcn-orange: #F59E0B;  /* 换成你的品牌黄 */
  --lkcn-radius-md: 32rpx; /* 更大的圆角 */
}

不需要改任何组件源码,Design Token 体系的优势就在这里。

项目数据

  • 22 个组件,全部完成
  • 143 个源文件
  • 0 外部依赖
  • 每个组件 4 件套(wxml / wxss / js / json)
  • 60+ Design Token CSS 变量
  • 11 个可交互 demo 页面
  • 包体积 < 90KB(未压缩)

后续计划

  • 组件 TypeScript .d.ts 类型声明
  • VitePress 文档站
  • 暗色模式适配
  • GitHub Actions CI 自动发布

如果你也觉得有用,欢迎 Star:

GitHub: https://github.com/user/lkcn-ui

❌