Figma 优惠券与营销卡券包系统(Promo Coupon, Discount Voucher & Marketing Card Package Architecture)组件架构实战:从双侧半圆打孔(Radial Notch Cutout)、齿孔虚线分割导轨(Dashed Tear Line)、多态生命周期闭环(Claimed/Used/Expired)到 Variables 金额抵扣状态机与 Dev Mode 现代 CSS 遮罩代码交付
1. 营销卡券系统的布局痛点与视觉工程挑战 #
在电子商务平台、本地生活 O2O、内容订阅与企业级 SaaS 的付费结算转化链路中,优惠券与营销卡券(Promo Coupon & Discount Voucher Card) 是激发用户下单决策、拉升客单价(AOV)与促成首购复购的关键载体。然而,在日常 UI/UX 设计与研发交接中,卡券往往被视为“视觉表现简单但工程实现极度繁琐”的重灾区:
- 位图切图方案的弹性断裂:许多团队习惯将卡券的齿孔、凹槽切为固定 PNG/SVG 背景图。然而当遇到超长活动说明(如“限指定品类且满 200 减 30,不可与拼团优惠券叠加”)或外币结算长符号时,固定切图在响应式拉伸下会导致圆弧凹槽被压扁变形,或边缘像素发虚模糊;
- 打孔(Notch)与虚线撕裂导轨(Tear Line)的定位错位:传统卡券左侧为固定或自适应面额区,右侧为规则与 CTA 操作区。两侧中缝需要严格对称的半圆形内凹打孔与连续垂直虚线。设计师若未采用严谨的原子级对齐结构,当右侧文字折行增加高度时,中缝虚线无法垂直拉伸,上下打孔缺口也会发生像素偏移;
- 多态生命周期混乱与状态爆炸:一张优惠券在整个生命周期中包含:未领取(Claimable)、已领待用(Available / Active)、当前勾选用(Selected)、门槛未满足(Disabled / Insufficient)、已使用(Redeemed)与已过期(Expired)六种互斥状态。如果缺乏结构化的变体(Variants)与 Design Tokens 设计,会导致主色调与灰阶失控;
- 购物车结算链路的动态抵扣脱节:在原型演示中,优惠券的选中状态往往无法反向驱动订单总额的扣减计算,使得高保真可用性测试只能沦为“静态假交互”;
- 研发交付阶段的 CSS 还原鸿沟:工程师若直接使用
overflow: hidden加多层 div 拼凑半圆,不仅 DOM 节点冗余,还会面临复杂背景(如渐变底色、毛玻璃拟态)穿帮的问题。现代前端最佳实践要求基于 CSSmask-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 画布中模拟卡券两侧或上下边缘的半圆形“凹洞”,主要有三种实现路径:
- 方案 A:布尔差集(Boolean Subtract):将主卡片矩形与位于中缝两端的小圆形执行
Subtract selection。- 优点:具有真正通透的透明度通道,放在任何渐变或图片背景上都不会露馅;
- 缺点:在早期版本的 Auto Layout 嵌套中,布尔运算图层偶尔会出现高度未跟随子内容拉伸的几何缓存 Bug。
- 方案 B:同色小圆覆盖法(Overlap Circle with Canvas Color):在中缝上下分别放置两个与页面背景色相同的小半圆,通过绝对定位或负内边距覆盖在主卡片边缘上。
- 缺点:一旦页面切换夜间模式(Dark Mode)或放置在渐变背景上,小半圆的底色会立刻产生视觉色块穿帮,维护成本极高。
- 方案 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 原型连线中配置复合条件逻辑:
- 自动门槛判断:当原型初始化或修改购物车商品数量时,系统判断
cartTotal >= voucherThreshold,若满足则将voucherState设为"available",否则强制置为"disabled"并展示“差 ¥XX 可用,去凑单”; - 点击选中抵扣:用户在结算弹窗中点击可用卡券,触发条件交互:
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)} 凑单 >
</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)进行验收:
- 几何弹性验收:将卡券横向宽度在
320px(窄屏 iPhone SE)至560px(平板弹窗)之间自由拉伸,确认左侧面额宽度固定、右侧内容弹性自适应、中缝虚线不发虚错位; - 多态覆盖率验收:检查是否完整覆盖了“未领/待用/已选/未达标/已用/过期”六态,尤其是未满足满减门槛时的差额计算引导文案;
- 色彩对比度安全:确保置灰状态下的文字对比度仍然满足 WCAG 2.1 AA 规范要求,禁止使用无法辨认的纯淡灰;
- 前端遮罩对齐:在 Dev Mode 标注页中明确指出中缝打孔尺寸,提示前端采用 CSS
mask-image实现矢量裁切,杜绝切图导致的像素失真; - 原型计算闭环:结合 Variables 状态机,确保在购物车增减商品或勾选卡券时,金额数值与结算底栏能够自动实时响应。
核心手册与速查工具
高频手册与实用设计生产力工具直达常见问题与答疑 (FAQ)
高频疑难解析与避坑指南完全不需要。绝大多数核心功能在免费 Starter 计划中均可完整体验与实操练习,无需额外付费购买付费团队版。