Figma 优惠券与营销卡券包系统(Promo Coupon, Discount Voucher & Marketing Card Package Architecture)组件架构实战:从双侧半圆打孔(Radial Notch Cutout)、齿孔虚线分割导轨(Dashed Tear Line)、多态生命周期闭环(Claimed/Used/Expired)到 Variables 金额抵扣状态机与 Dev Mode 现代 CSS 遮罩代码交付

2026 年 11 月 15 日

1. 营销卡券系统的布局痛点与视觉工程挑战 #

在电子商务平台、本地生活 O2O、内容订阅与企业级 SaaS 的付费结算转化链路中,优惠券与营销卡券(Promo Coupon & Discount Voucher Card) 是激发用户下单决策、拉升客单价(AOV)与促成首购复购的关键载体。然而,在日常 UI/UX 设计与研发交接中,卡券往往被视为“视觉表现简单但工程实现极度繁琐”的重灾区:

  1. 位图切图方案的弹性断裂:许多团队习惯将卡券的齿孔、凹槽切为固定 PNG/SVG 背景图。然而当遇到超长活动说明(如“限指定品类且满 200 减 30,不可与拼团优惠券叠加”)或外币结算长符号时,固定切图在响应式拉伸下会导致圆弧凹槽被压扁变形,或边缘像素发虚模糊;
  2. 打孔(Notch)与虚线撕裂导轨(Tear Line)的定位错位:传统卡券左侧为固定或自适应面额区,右侧为规则与 CTA 操作区。两侧中缝需要严格对称的半圆形内凹打孔与连续垂直虚线。设计师若未采用严谨的原子级对齐结构,当右侧文字折行增加高度时,中缝虚线无法垂直拉伸,上下打孔缺口也会发生像素偏移;
  3. 多态生命周期混乱与状态爆炸:一张优惠券在整个生命周期中包含:未领取(Claimable)、已领待用(Available / Active)、当前勾选用(Selected)、门槛未满足(Disabled / Insufficient)、已使用(Redeemed)与已过期(Expired)六种互斥状态。如果缺乏结构化的变体(Variants)与 Design Tokens 设计,会导致主色调与灰阶失控;
  4. 购物车结算链路的动态抵扣脱节:在原型演示中,优惠券的选中状态往往无法反向驱动订单总额的扣减计算,使得高保真可用性测试只能沦为“静态假交互”;
  5. 研发交付阶段的 CSS 还原鸿沟:工程师若直接使用 overflow: hidden 加多层 div 拼凑半圆,不仅 DOM 节点冗余,还会面临复杂背景(如渐变底色、毛玻璃拟态)穿帮的问题。现代前端最佳实践要求基于 CSS mask-image: radial-gradient 实现真正的无感打孔。

在 Figma 中,通过深度运用 Auto Layout 自动布局、嵌套组件属性与变体 以及 Variables 变量联动状态机,我们可以打造一套能够应对多端屏幕适配、支持实时金额联动、且完美对齐前端工程规范的高阶卡券组件体系。


2. 优惠券核心组件拓扑与 Auto Layout 几何拆解 #

一套工业级的营销卡券必须由三个横向解耦的容器组合而成:左侧面额区(Value Module)、中缝打孔撕裂导轨(Divider & Notch Module) 与 右侧信息及操作区(Content & Action Module)。

Coupon Card Master Container (Auto Layout Horizontal, Hug/Fill Width, Min-Height: 96px)
├── Left Value Slot (Auto Layout Vertical, Fixed Width: 110px, Hug Height)
│   ├── Currency Symbol & Amount Group (Auto Layout Horizontal, Baseline Align)
│   │   ├── Currency Prefix ("¥" / "$", Font-Size: 18px, Bold)
│   │   └── Amount Number ("50" / "8.5", Font-Size: 36px, Black/Heavy)
│   └── Threshold Label ("满 299 元可用" / "无门槛抵扣", Font-Size: 12px)
├── Center Tear Notch Divider (Auto Layout Vertical, Width: 16px, Fill Height)
│   ├── Top Half-Circle Notch (Width: 16px, Height: 8px, Arc Inside)
│   ├── Vertical Dashed Line (Vector Stroke / Border-Left Dashed, Fill Height)
│   └── Bottom Half-Circle Notch (Width: 16px, Height: 8px, Arc Inside)
├── Right Content & Action Slot (Auto Layout Vertical, Fill Container, Padding: 12px 14px)
│   ├── Title & Expiry Header (Auto Layout Horizontal, Space-Between)
│   │   ├── Coupon Title ("品类专享神券", Font-Weight: 600, Truncate/Wrap)
│   │   └── Expiry Countdown Badge ("仅剩 2 天", Pill Shape, Danger/Warning)
│   ├── Rules Description ("仅限数码配件与影音外设使用...", Max-Lines: 2)
│   └── Bottom Action Footer (Auto Layout Horizontal, Space-Between, Align Bottom)
│       ├── Valid Period Text ("有效期至 2026.12.31 23:59")
│       └── Action CTA Slot (Button Atom: "立即使用" / "去凑单" / "已领取")
└── Floating Corner Ribbon (Absolute Positioning: Top Right, State: "Hot" / "Exclusive")

2.1 双侧半圆打孔(Radial Notch Cutout)的无穿帮 Figma 几何实现 #

在 Figma 画布中模拟卡券两侧或上下边缘的半圆形“凹洞”,主要有三种实现路径:

  1. 方案 A:布尔差集(Boolean Subtract):将主卡片矩形与位于中缝两端的小圆形执行 Subtract selection。
    • 优点:具有真正通透的透明度通道,放在任何渐变或图片背景上都不会露馅;
    • 缺点:在早期版本的 Auto Layout 嵌套中,布尔运算图层偶尔会出现高度未跟随子内容拉伸的几何缓存 Bug。
  2. 方案 B:同色小圆覆盖法(Overlap Circle with Canvas Color):在中缝上下分别放置两个与页面背景色相同的小半圆,通过绝对定位或负内边距覆盖在主卡片边缘上。
    • 缺点:一旦页面切换夜间模式(Dark Mode)或放置在渐变背景上,小半圆的底色会立刻产生视觉色块穿帮,维护成本极高。
  3. 方案 C:左右分体双卡片拼接(Decoupled Left/Right Split Frames - 官方推荐工程方案):
    • 将卡券拆分为左矩形卡片与右矩形卡片;
    • 在左卡片的右上角与右下角设置内凹弧度,或者通过精细的 SVG 矢量切片进行两端拼接;
    • 中间通过一个固定宽度的中缝导轨(Divider Track)串联,中缝两端放置圆弧透明遮罩。

2.2 垂直虚线齿孔(Dashed Tear Line)与动态弹性拉伸 #

右侧规则文本在移动端窄屏或多语言环境下会产生 2 行至 4 行的不确定折行,这意味着卡券的高度必须是弹性自适应的(Hug contents)。

配置要点:

  • 中缝容器设置为 Auto Layout Vertical,高度严格设置为 Fill container(填充容器);
  • 顶部半圆与底部半圆固定高度(如 8px),对齐方式为顶部吸附与底部吸附;
  • 中间的虚线图元必须设置为 Fill container(填充剩余纵向空间),并配置 Stroke: 1px, Dashes: 3, 3(3 像素虚线与 3 像素留白)。这样无论右侧文字增加多少行,中缝虚线都能严丝合缝地自动撑满,永远不会出现虚线断层或溢出。

2.3 悬浮角标(Floating Ribbon)的绝对定位锚定 #

对于“热门专享”、“限时秒杀”或“新人特权”等特殊标识,可将角标组件放入卡券容器中,启用右侧属性面板中的 绝对定位(Absolute position):

  • 约束条件(Constraints)设置为 Right 与 Top;
  • 偏移量设置为 X: 0, Y: 0;
  • 在角标内部配置 padding: 2px 8px 与左下角 4px 单向圆角,使其自然悬浮于卡券右上角,且不会干扰下方 Auto Layout 文字排版的流式流转。

相关绝对定位排障规范可查阅 Figma 常见报错与疑难故障诊断手册。


3. Design Tokens 分层与 Variables 动态抵扣状态机建模 #

卡券组件的状态转换需要遵循 Design Tokens 标准对照手册 构建分层设计令牌体系,以实现多主题与多品牌的一键切换。

3.1 核心 Token 映射架构 #

Tier 1: Global Tokens (全局基础色彩)
├── color-orange-50: #FFF7ED
├── color-orange-500: #F97316
├── color-orange-600: #EA580C
├── color-neutral-200: #E5E7EB
├── color-neutral-400: #9CA3AF
└── color-neutral-900: #111827

Tier 2: Semantic Tokens (语义决策层 - 响应 Light / Dark 模式)
├── voucher-bg-active: Alias(color-orange-50 / color-neutral-800)
├── voucher-border-active: Alias(color-orange-500)
├── voucher-text-amount: Alias(color-orange-600 / color-orange-400)
├── voucher-bg-disabled: Alias(color-neutral-100 / color-neutral-800)
├── voucher-text-disabled: Alias(color-neutral-400 / color-neutral-500)
└── voucher-tear-line: Alias(color-orange-200 / color-neutral-700)

Tier 3: Component Tokens (卡券私有属性)
├── voucher-notch-diameter: 16px
├── voucher-min-height: 96px
└── voucher-amount-font-size: 36px

3.2 Variables 变量集合定义与高阶原型联动 #

在 Figma 的 Local Variables 中新建 CheckoutPromotionEngine 集合,建立以下变量并绑定至组件:

变量名称 (Variable Name) 数据类型 (Type) 默认值 (Default) 绑定的界面图元与交互响应
cartTotal Number 350 购物车原始订单总金额,用于判断门槛与最终扣减
voucherThreshold Number 299 该券的满减使用门槛(如满 299 元)
voucherDiscount Number 50 优惠券抵扣面额(50 元现金券)
isVoucherSelected Boolean false 控制卡券右侧单选框高亮、边框发光与抵扣生效
finalPayableAmount Number 350 最终实付金额:cartTotal - (isVoucherSelected ? voucherDiscount : 0)
voucherState String "available" 状态枚举:claimable / available / disabled / used

在 Figma 原型连线中配置复合条件逻辑:

  1. 自动门槛判断:当原型初始化或修改购物车商品数量时,系统判断 cartTotal >= voucherThreshold,若满足则将 voucherState 设为 "available",否则强制置为 "disabled" 并展示“差 ¥XX 可用,去凑单”;
  2. 点击选中抵扣:用户在结算弹窗中点击可用卡券,触发条件交互:
    • isVoucherSelected = not isVoucherSelected;
    • finalPayableAmount = cartTotal - (isVoucherSelected ? voucherDiscount : 0);
    • 结算底栏的“立即支付 ¥XXX”实时刷新金额数值,达成高度逼真的动态原型演示。

更多变量表达式的高阶用法请参考 Variables 变量联动状态机。


4. 营销卡券生命周期与交互决策矩阵 #

不同业务状态下,卡券的视觉语言、操作反馈与无障碍属性存在清晰的规范界限:

卡券生命周期状态 视觉风格与 Token 表现 核心 CTA 按钮形态 门槛与说明文案特点 屏幕阅读器 (ARIA) 朗读提示
未领取 (Claimable) 高饱和主色边框与浅底色,面额数字高亮 “立即领取” / “一键全领” (Primary 实体按钮) 突出新手或大促专享福利 aria-label="新人满299减50元专享神券,点击立即领取"
已领可用 (Available) 强调有效时间与可用标识,右上角可选“推荐”徽标 “去使用” / 单选勾选圈 (Ghost 或 Icon 按钮) 显示到期倒计时(如“仅剩3小时”) aria-label="满299减50元券,已领取,可使用"
当前勾选 (Selected) 主色加粗实线边框,右下角带有实体对勾角标,卡片微投影 选中的高亮单选圆圈 (Radio Checked) 提示“已为您推荐最优减免 ¥50” aria-checked="true", role="checkbox"
未达门槛 (Disabled) 整体低对比中性灰色系 (neutral-400),半圆齿孔淡化 “去凑单” (Link 幽灵按钮或文本链接) 明确指出差额:“还差 ¥49 可用” aria-disabled="true", 描述缺失差额
已使用 (Redeemed) 盖印章多态(斜向 45° “已使用”双线复古水印章),低对比底色 置灰禁用文本 “已于 11-12 抵扣” 记录抵扣订单编号或使用时间戳 aria-label="该优惠券已在历史订单中使用"
已过期 (Expired) 灰色系降噪,表面覆盖 “已过期” 浅灰印章 灰色禁用按钮 “已失效” 提示过期时间 “2026.11.10 已失效” aria-label="该优惠券已过期失效"

5. Dev Mode 研发协同与前端现代 CSS 遮罩 + React 生产级交付 #

在 Figma Dev Mode 开发者模式 下,工程师往往会为卡券两侧的圆孔内凹犯愁。直接使用背景图或拼装小圆的做法早已过时。在现代 CSS 中,推荐使用 mask-image 配合径向渐变(radial-gradient) 实现 100% 响应式矢量无损内凹切角,无论背景是复杂照片还是渐变色,均能实现真正的物理透明镂空。

5.1 现代 CSS 径向打孔遮罩原理 #

/* 使用 CSS Mask 在左右两侧边缘各镂空一个半径为 8px 的半圆凹槽 */
.coupon-mask {
  --notch-radius: 8px;
  --notch-position: 110px; /* 左侧面额区的固定宽度 */
  
  mask-image: 
    radial-gradient(circle var(--notch-radius) at var(--notch-position) 0%, transparent 100%, black 100%),
    radial-gradient(circle var(--notch-radius) at var(--notch-position) 100%, transparent 100%, black 100%);
  mask-composite: intersect;
  -webkit-mask-composite: destination-in;
}

5.2 生产级 React + TypeScript + WAI-ARIA 代码实现 #

import React from 'react';

export interface CouponItem {
  id: string;
  title: string;
  discount: number;
  threshold: number;
  expiresAt: string;
  category: string;
  status: 'claimable' | 'available' | 'selected' | 'disabled' | 'redeemed' | 'expired';
}

interface CouponCardProps {
  data: CouponItem;
  cartSubtotal?: number;
  onSelect?: (id: string) => void;
  onClaim?: (id: string) => void;
  onNavigateToShop?: () => void;
}

export const CouponCard: React.FC<CouponCardProps> = ({
  data,
  cartSubtotal = 0,
  onSelect,
  onClaim,
  onNavigateToShop
}) => {
  const isMeetThreshold = cartSubtotal >= data.threshold;
  const isSelected = data.status === 'selected';
  const isDisabled = data.status === 'disabled' || (!isMeetThreshold && data.status === 'available');

  return (
    <article
      className={`relative flex w-full max-w-md rounded-xl transition-all duration-200 overflow-hidden border ${
        isSelected
          ? 'border-orange-500 bg-orange-50/50 shadow-md shadow-orange-500/10'
          : isDisabled
          ? 'border-slate-200 bg-slate-50 text-slate-400'
          : 'border-orange-200 bg-white hover:border-orange-300'
      }`}
      role="region"
      aria-label={`${data.title} - 面额 ${data.discount} 元`}
    >
      {/* 左侧面额区 */}
      <div className="flex flex-col items-center justify-center w-28 shrink-0 py-4 px-2 border-r border-dashed border-orange-200/80">
        <div className="flex items-baseline font-black tracking-tight">
          <span className={`text-sm ${isDisabled ? 'text-slate-400' : 'text-orange-600'}`}>¥</span>
          <span className={`text-3xl ml-0.5 ${isDisabled ? 'text-slate-400' : 'text-orange-600'}`}>
            {data.discount}
          </span>
        </div>
        <span className="text-xs text-slate-500 mt-1 text-center font-medium">
          {data.threshold > 0 ? `满 ¥${data.threshold} 可用` : '无门槛抵扣'}
        </span>
      </div>

      {/* 右侧核心信息与操作区 */}
      <div className="flex flex-col justify-between flex-1 p-3.5 pl-4">
        <div>
          <div className="flex items-start justify-between gap-2">
            <h4 className={`text-sm font-semibold line-clamp-1 ${isDisabled ? 'text-slate-400' : 'text-slate-800'}`}>
              {data.title}
            </h4>
            {data.status === 'claimable' && (
              <span className="inline-flex px-1.5 py-0.5 text-[10px] font-medium text-orange-600 bg-orange-100 rounded">
                限时神券
              </span>
            )}
          </div>
          <p className="text-xs text-slate-400 mt-1 line-clamp-1">
            {data.category}
          </p>
        </div>

        <div className="flex items-center justify-between mt-3 pt-2 border-t border-slate-100/80">
          <span className="text-[11px] text-slate-400">
            有效期至 {data.expiresAt}
          </span>

          {/* 状态操作分流 */}
          {data.status === 'claimable' && (
            <button
              type="button"
              onClick={() => onClaim?.(data.id)}
              className="px-3 py-1 text-xs font-semibold text-white bg-orange-500 hover:bg-orange-600 rounded-full transition-colors focus:ring-2 focus:ring-orange-400 focus:outline-none"
            >
              立即领取
            </button>
          )}

          {data.status === 'available' && isMeetThreshold && (
            <button
              type="button"
              onClick={() => onSelect?.(data.id)}
              className={`px-3 py-1 text-xs font-semibold rounded-full transition-all ${
                isSelected
                  ? 'bg-orange-500 text-white shadow-sm'
                  : 'bg-orange-50 text-orange-600 hover:bg-orange-100'
              }`}
              aria-pressed={isSelected}
            >
              {isSelected ? '已选用' : '使用'}
            </button>
          )}

          {isDisabled && data.status !== 'redeemed' && data.status !== 'expired' && (
            <button
              type="button"
              onClick={onNavigateToShop}
              className="text-xs text-orange-500 hover:underline font-medium"
            >
              还差 ¥{(data.threshold - cartSubtotal).toFixed(1)} 凑单 &gt;
            </button>
          )}

          {data.status === 'redeemed' && (
            <span className="text-xs text-slate-400 font-medium">已于本单抵扣</span>
          )}

          {data.status === 'expired' && (
            <span className="text-xs text-slate-300 font-medium">已失效</span>
          )}
        </div>
      </div>
    </article>
  );
};

6. 总结与企业级营销组件交付标准核验清单 #

在完成营销卡券设计与交接时,建议对照以下标准化审查清单(Checklist)进行验收:

  1. 几何弹性验收:将卡券横向宽度在 320px(窄屏 iPhone SE)至 560px(平板弹窗)之间自由拉伸,确认左侧面额宽度固定、右侧内容弹性自适应、中缝虚线不发虚错位;
  2. 多态覆盖率验收:检查是否完整覆盖了“未领/待用/已选/未达标/已用/过期”六态,尤其是未满足满减门槛时的差额计算引导文案;
  3. 色彩对比度安全:确保置灰状态下的文字对比度仍然满足 WCAG 2.1 AA 规范要求,禁止使用无法辨认的纯淡灰;
  4. 前端遮罩对齐:在 Dev Mode 标注页中明确指出中缝打孔尺寸,提示前端采用 CSS mask-image 实现矢量裁切,杜绝切图导致的像素失真;
  5. 原型计算闭环:结合 Variables 状态机,确保在购物车增减商品或勾选卡券时,金额数值与结算底栏能够自动实时响应。

❓ 常见问题与答疑 (FAQ)

高频疑难解析与避坑指南
A

完全不需要。绝大多数核心功能在免费 Starter 计划中均可完整体验与实操练习,无需额外付费购买付费团队版。