Vant Coupon 优惠券选择器与兑换列表(CouponList)实战指南
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
导读
CouponList(优惠券选择器)是 Vant 移动端组件库中用于优惠券兑换与选择的核心组件,它和配套的CouponCell(优惠券单元格)一起,构成了电商下单、结算等场景下完整的一站式优惠券交互方案:单元格展示当前已选优惠券,弹出层内嵌列表完成可用券的勾选与不可用券的展示,顶部兑换栏还支持输入兑换码实时兑换新券。阅读完本篇,你将掌握CouponCell+CouponList的组合用法、全部 Props/Events/Slots 参数语义、CouponInfo数据结构的字段含义,以及底层源码的实现原理与主题定制方案。
组件介绍与引入
优惠券选择器用于优惠券的兑换和选择,它通常与优惠券单元格(CouponCell)配合使用:CouponCell负责在页面上以单元格形式展示“已选券/可选券”的金额汇总,CouponList则负责在弹层中展示可用券、不可用券两个列表并提供兑换入口。
通过以下方式全局注册组件,更多注册方式可参考组件注册:
import { createApp } from 'vue'; import { CouponCell, CouponList } from 'vant'; const app = createApp(); app.use(CouponCell); app.use(CouponList);注册后即可在模板中直接使用<van-coupon-cell>与<van-coupon-list>标签。两个组件都通过 index.ts 中的withInstall包装导出,并同时注册了VanCouponList/VanCouponCell的全局组件类型,享受完整的 TypeScript 提示。
代码演示
基础用法
基础用法是一个“单元格 + 弹层”的组合模式:点击van-coupon-cell打开底部弹出层,弹出层内渲染van-coupon-list,用户勾选券或兑换新券后通过事件回调更新状态并关闭弹层:
<!-- 优惠券单元格 --> <van-coupon-cell :coupons="coupons" :chosen-coupon="chosenCoupon" @click="showList = true" /> <!-- 优惠券列表 --> <van-popup v-model:show="showList" round position="bottom" style="height: 90%; padding-top: 4px;" > <van-coupon-list :coupons="coupons" :chosen-coupon="chosenCoupon" :disabled-coupons="disabledCoupons" @change="onChange" @exchange="onExchange" /> </van-popup>import { ref } from 'vue'; export default { setup() { const coupon = { available: 1, condition: '无门槛\n最多优惠12元', reason: '', value: 150, name: '优惠券名称', startAt: 1489104000, endAt: 1514592000, valueDesc: '1.5', unitDesc: '元', }; const coupons = ref([coupon]); const showList = ref(false); const chosenCoupon = ref(-1); const onChange = (index) => { showList.value = false; chosenCoupon.value = index; }; const onExchange = (code) => { coupons.value.push(coupon); }; return { coupons, showList, onChange, onExchange, chosenCoupon, disabledCoupons: [coupon], }; }, };这段示例完整还原了真实业务链路:
coupons为可用优惠券数组,disabledCoupons为不可用优惠券数组,两者都传入CouponList,组件内部用 Tabs 分成“可使用优惠券”和“不可使用优惠券”两个 Tab 展示;chosenCoupon初始为-1,表示未选中任何券;点击某张券时组件触发change事件并回传选中索引,我们在onChange中关闭弹层并同步chosenCoupon;- 在兑换栏输入兑换码后点击“兑换”按钮,组件触发
exchange事件并回传兑换码,业务方可在onExchange中调用真实兑换接口,成功后把新券 push 进coupons,列表即会实时刷新。
多选用法(chosenCoupon 传数组)
从源码看,chosenCoupon的类型是number | number[](见 CouponList.tsx)。当传入数组时,组件进入多选模式:点击优惠券时change事件回传的是新的选中索引数组,而非单个索引;同时点击底部按钮会回传[](清空全部选择),而不是-1。这一行为在updateChosenCoupon辅助函数中实现——若索引已在数组中则移除,否则追加(CouponList.tsx)。
多选模式的官方示例位于 demo/index.vue:将chosenCoupon绑定为ref<number[]>([]),通过#list-button插槽自定义一个“确定”按钮,点击后把选中的索引数组提交给结算页;同时设置:show-close-button="false"隐藏默认底部按钮,避免多选场景下出现语义不明确的“不使用优惠”按钮。
API
CouponCell Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| title | 单元格标题 | string | 优惠券 |
| chosen-coupon | 当前选中优惠券的索引 | number | number[] | -1 |
| coupons | 可用优惠券列表 | Coupon[] | [] |
| editable | 能否切换优惠券 | boolean | true |
| border | 是否显示内边框 | boolean | true |
| currency | 货币符号 | string | ¥ |
CouponCell的展示逻辑值得留意:它会根据chosenCoupon汇总所有选中券的金额(叠加value或denominations字段),在单元格右侧显示-¥ 15.00这样的汇总金额;当chosenCoupon为-1且存在可选券时显示“N 张可用”,无券时显示“暂无可用券”。该格式化逻辑位于 CouponCell.tsx。editable控制isLink(右侧箭头),为false时单元格变为纯展示。
CouponList Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| v-model:code | 当前输入的兑换码 | string | - |
| chosen-coupon | 当前选中优惠券的索引,支持多选(类型为[]) | number | number[] | -1 |
| coupons | 可用优惠券列表 | CouponInfo[] | [] |
| disabled-coupons | 不可用优惠券列表 | CouponInfo[] | [] |
| enabled-title | 可用优惠券列表标题 | string | 可使用优惠券 |
| disabled-title | 不可用优惠券列表标题 | string | 不可使用优惠券 |
| exchange-button-text | 兑换按钮文字 | string | 兑换 |
| exchange-button-loading | 是否显示兑换按钮加载动画 | boolean | false |
| exchange-button-disabled | 是否禁用兑换按钮 | boolean | false |
| exchange-min-length | 兑换码最小长度 | number | 1 |
| displayed-coupon-index | 滚动至特定优惠券位置 | number | - |
| show-close-button | 是否显示列表底部按钮 | boolean | true |
| close-button-text | 列表底部按钮文字 | string | 不使用优惠 |
| input-placeholder | 输入框文字提示 | string | 请输入优惠码 |
| show-exchange-bar | 是否展示兑换栏 | boolean | true |
| currency | 货币符号 | string | ¥ |
| empty-image | 列表为空时的占位图 | string | - |
| show-count | 是否展示可用 / 不可用数量 | boolean | true |
参数背后有几处值得展开的源码行为(均见 CouponList.tsx):
- 兑换按钮的禁用判定:
buttonDisabled是一个computed,当exchangeButtonLoading为false且(exchangeButtonDisabled为真、兑换码为空、或兑换码长度小于exchangeMinLength)时,兑换按钮自动禁用(CouponList.tsx)。即只要在加载中,按钮永不因其它条件被禁用; - 兑换码的自动清空:点击兑换后触发
exchange事件,若没有使用v-model:code绑定,则内部currentCode会被自动清空;若使用了v-model,则清空与否由外部数据决定(CouponList.tsx)。currentCode的任何变化都会同步触发update:code事件,配合v-model:code形成双向绑定(CouponList.tsx); - 滚动定位:
displayedCouponIndex变化时会通过nextTick后调用对应券项的scrollIntoView()自动滚动到指定优惠券(CouponList.tsx),onMounted时也会执行一次(CouponList.tsx); - 列表高度自适应:
updateListHeight会以「根容器高度(不足时回退为windowHeight)减去兑换栏与 Tab 头高度」来计算列表可视高度(Tab 头固定按 44px 计),并监听窗口高度变化实时重算,保证内部滚动区域精确贴合弹层(CouponList.tsx); - 数量角标:
showCount为true时,Tab 标题会追加(N)形式的可用/不可用数量。
CouponList Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| change | 优惠券切换回调 | index, 选中优惠券的索引 |
| exchange | 兑换优惠券回调 | code, 兑换码 |
事件语义与源码一一对应:点击某张可用券触发change(多选模式回传数组);点击底部“不使用优惠”按钮触发change(单选回传-1,多选回传[]);点击兑换按钮触发exchange。官方测试用例 index.spec.ts 验证了:兑换码为空时点击兑换不会触发exchange,输入有效兑换码后才触发;输入内容还会依次触发update:code事件,未绑定v-model:code时兑换后兑换码会被清空。
CouponList Slots
| 名称 | 说明 |
|---|---|
| list-footer | 优惠券列表底部 |
| disabled-list-footer | 不可用优惠券列表底部 |
| list-button | 自定义底部按钮 |
其中list-footer/disabled-list-footer分别渲染在两个 Tab 的列表末尾(CouponList.tsx 与 CouponList.tsx);list-button用于完全替换底部“不使用优惠”按钮,多选模式下的“确定”按钮正是通过该插槽实现的(CouponList.tsx)。
CouponInfo 数据结构
优惠券列表的每一项是一个CouponInfo对象:
| 键名 | 说明 | 类型 |
|---|---|---|
| id | 优惠券 id | string |
| name | 优惠券名称 | string |
| condition | 满减条件 | string |
| startAt | 卡有效开始时间 (时间戳, 单位秒) | number |
| endAt | 卡失效日期 (时间戳, 单位秒) | number |
| description | 描述信息,优惠券可用时展示 | string |
| reason | 不可用原因,优惠券不可用时展示 | string |
| value | 折扣券优惠金额,单位分 | number |
| valueDesc | 折扣券优惠金额文案 | string |
| unitDesc | 单位文案 | string |
从源码的类型定义看(Coupon.tsx),CouponInfo还支持discount(折扣,如88表示 8.8 折)、denominations(面额,单位分)、originCondition(原始满减门槛)等字段。券面的展示优先级为:
- 优先使用
valueDesc + unitDesc组合文案(如1.5+元、8.8+折); - 否则若有
denominations,显示货币符号 + 格式化面额; - 否则若有
discount,显示X.X折; - 以上都没有则显示为空。
金额与折扣的格式化逻辑(去除无意义小数位)位于 coupon/utils.ts:formatAmount按「整数不保留小数、整十保留 1 位、其余保留 2 位」处理,formatDiscount仅在非整数折扣时保留 1 位小数。有效期文案由startAt、endAt(秒级时间戳)格式化为年.月.日 - 年.月.日(coupon/utils.ts)。此外,当优惠券处于不可用状态(被放入disabledCoupons)时,组件展示reason(不可用原因)而非description,且不再显示勾选复选框(Coupon.tsx)。
类型定义
组件导出以下类型定义:
import type { CouponCellProps, CouponListProps, CouponInfo } from 'vant';CouponListProps由 couponListProps 通过ExtractPropTypes推导而来,CouponCellProps同理;CouponListThemeVars、CouponCellThemeVars则分别定义在 types.ts 与 coupon-cell/types.ts,供 CSS 变量类型提示使用。
主题定制
样式变量
组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件。
| 名称 | 默认值 | 描述 |
|---|---|---|
| --van-coupon-margin | 0 var(--van-padding-sm) var(--van-padding-sm) | - |
| --van-coupon-content-height | 84px | - |
| --van-coupon-content-padding | 14px 0 | - |
| --van-coupon-content-text-color | var(--van-text-color) | - |
| --van-coupon-background | var(--van-background-2) | - |
| --van-coupon-active-background | var(--van-active-color) | - |
| --van-coupon-radius | var(--van-radius-lg) | - |
| --van-coupon-shadow | 0 0 4px rgba(0, 0, 0, 0.1) | - |
| --van-coupon-head-width | 96px | - |
| --van-coupon-amount-color | var(--van-danger-color) | - |
| --van-coupon-amount-font-size | 30px | - |
| --van-coupon-currency-font-size | 40% | - |
| --van-coupon-name-font-size | var(--van-font-size-md) | - |
| --van-coupon-disabled-text-color | var(--van-text-color-2) | - |
| --van-coupon-description-padding | var(--van-padding-xs) var(--van-padding-md) | - |
| --van-coupon-description-border-color | var(--van-border-color) | - |
| --van-coupon-checkbox-color | var(--van-danger-color) | - |
| --van-coupon-list-background | var(--van-background) | - |
| --van-coupon-list-field-padding | 5px 0 5px var(--van-padding-md) | - |
| --van-coupon-list-exchange-button-height | 32px | - |
| --van-coupon-list-close-button-height | 40px | - |
| --van-coupon-list-empty-tip-color | var(--van-text-color-2) | - |
| --van-coupon-list-empty-tip-font-size | var(--van-font-size-md) | - |
| --van-coupon-list-empty-tip-line-height | var(--van-line-height-md) | - |
| --van-coupon-cell-selected-text-color | var(--van-text-color) | - |
其中--van-coupon-*系列变量作用于单个优惠券卡片(面额区、条件区、有效期、描述等),定义于 coupon/index.less;--van-coupon-list-*系列变量作用于整个选择器(背景、兑换栏、底部按钮、空态提示),声明于 coupon-list/index.less;--van-coupon-cell-selected-text-color则作用于单元格选中态文字颜色。所有变量的默认值均复用 Vant 的基础设计令牌(如--van-danger-color、--van-background等),因此通过全局或局部(ConfigProvider)覆盖这些令牌即可实现整套换肤。
源码级原理小结
从整体实现看,CouponList的架构可概括为三个层次:
- 顶部兑换栏(exchange-bar):由
Field(兑换码输入框,最长 20 字符)+Button(兑换按钮)组成,负责v-model:code的双向绑定与exchange事件抛出(CouponList.tsx); - 中部 Tabs 双列表:基于
Tabs/Tab实现“可使用优惠券”与“不可使用优惠券”两个页签,列表高度按弹层动态计算并支持内部滚动,空列表时渲染Empty占位(可自定义emptyImage)(CouponList.tsx); - 底部操作区:默认渲染“不使用优惠”按钮,支持
list-button插槽完全替换(CouponList.tsx)。
单张券的渲染由Coupon组件承担,它负责券面金额、满减条件、有效期、描述/不可用原因与选中态复选框的展示;测试用例 index.spec.ts 覆盖了快照渲染、空列表、自定义空图、兑换事件与插槽渲染等关键行为,可作为你二次开发时的回归参考。
结语
CouponCell+CouponList是 Vant 提供的开箱即用的优惠券选择完整方案:单元格负责汇总展示,列表负责选择、兑换与禁用券浏览,CouponInfo数据结构天然兼容“满减券、折扣券、面额券”三类常见形态。接入时只需维护coupons、disabledCoupons、chosenCoupon三个状态并响应change/exchange两个事件,再结合ConfigProvider与样式变量即可快速融入业务设计体系。
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考