- 前端
- CMS
【免费下载链接】wp-calypso
The JavaScript and API powered WordPress.com
引言
在 WordPress.com 的 Calypso 界面中,无论是套餐购买页、Jetpack 产品商店还是订阅续费入口,几乎每一个涉及"钱"的地方都需要一个统一的组件来渲染计划(Plan)的价格。PlanPrice正是这样一个开箱即用的 React 组件:它既能直接展示后端返回的预格式化价格字符串(productDisplayPrice),也能接收原始数字(rawPrice)自动完成货币符号、千分位、小数位等本地化格式化,还能通过original/discounted两个属性优雅地呈现"划线原价 + 折扣现价"的促销组合。
本文基于 wp-calypso 仓库 packages/components/src/plan-price/README.md 为核心骨架,结合组件源码、样式表、单元测试与真实业务使用场景,完整讲解该组件的全部 Props、渲染优先级、底层货币格式化原理及实战用法,帮助你在自己的页面中快速接入规范化的价格展示。
PlanPrice 是什么
PlanPrice 是一个用于展示"计划价格"的通用 React 组件,定义于 packages/components/src/plan-price/index.tsx(通过@automattic/components包对外导出)。它的核心定位是:
- 展示一个计划的价格,可带货币符号(如
$、€、kr.); - 支持折扣场景:当需要强调价格已打折时,官方推荐的做法是用两个
<PlanPrice>并排放入一个 flexbox 容器中,一个打上original(原价,显示删除线),另一个打上discounted(折扣价,绿色高亮); - 支持价格区间:传入一个二元数组
rawPrice即可渲染出$99.99-139.99这样的区间价格; - 可以用于任何需要展示计划价格的页面。
它位于@automattic/components组件库中,因此任何依赖该包的 Calypso 应用(包括client下的各个 section)都可以直接复用。
快速上手:从 README 中的两个经典用法说起
官方 README 给出了两种最典型的数据来源与用法。
用法一:直接使用后端返回的productDisplayPrice
productDisplayPrice是后端(/purchases、/plans等 REST 端点)返回的已经格式化好的价格字符串(可能还包含 HTML 标记),它通常会被存入 Redux 状态。此时组件会原样输出该字符串,不做任何货币格式化:
function MyComponent( { purchaseId } ) { const purchase = useSelector( ( state ) => getRawByPurchaseId( state, purchaseId ) ); return <PlanPrice productDisplayPrice={ purchase?.product_display_price } />; }也可以同时传入税款信息与促销标记:
<PlanPrice productDisplayPrice={ purchase.productDisplayPrice } taxText={ purchase.taxText } isOnSale={ !! purchase.saleAmount } />;用法二:传入rawPrice原始数字,交给组件自动格式化
当你有的是"纯数字"(例如从购买记录或续费价格函数中计算出来的数值)时,使用rawPrice并配合currencyCode,组件会调用getCurrencyObject完成货币符号、千分位、小数位等本地化处理:
<PlanPrice productDisplayPrice={ purchase.productDisplayPrice } rawPrice={ getRenewalPrice( purchase ) } currencyCode={ purchase.currencyCode } taxText={ purchase.taxText } isOnSale={ !! purchase.saleAmount } />;注意:上面的写法中同时传入了
productDisplayPrice与rawPrice,而二者的优先级规则在"渲染优先级"一节会详细说明——关键在于,只有当rawPrice是数组(价格区间)时它才会覆盖productDisplayPrice。
价格区间:rawPrice传数组
README 明确指出:如果向rawPrice传入一个包含两个数字的数组,组件将展示一个价格区间:
<PlanPrice rawPrice={ [ 132.2, 110.4 ] } original /> <PlanPrice rawPrice={ [ 99.99, 87 ] } discounted />这是 README 中给出的完整示例。它来自 packages/components/src/plan-price/docs/example.jsx 和 index.stories.jsx 中展示的所有场景,包括:
| 场景 | 代码示例 |
|---|---|
| 标准价格 | <PlanPrice rawPrice={ 99.8 } /> |
| 价格区间 | <PlanPrice rawPrice={ [ 99.99, 139.99 ] } /> |
| 零价格 | <PlanPrice rawPrice={ 0 } /> |
| 折扣价(原价+现价) | flexbox 中<PlanPrice rawPrice={ 8.25 } original />+<PlanPrice rawPrice={ 2 } discounted /> |
| 折扣价格区间 | original用[ 8.25, 20.23 ],discounted用[ 2.25, 3 ] |
| 带税信息 | 追加taxText="10%" |
| 简单视图 | 追加isInSignup(signup 流程中的简化展示) |
Props 全解析:从源码接口看 14 个可用属性
PlanPrice 的所有 Props 都定义在同文件的PlanPriceProps接口中(packages/components/src/plan-price/index.tsx#L93-L203),同时组件类在render()中给出了各属性的默认值。下表汇总了完整清单:
| 属性 | 类型 | 默认值 | 作用说明 |
|---|---|---|---|
rawPrice | number \| [ number, number ] \| null | — | 待格式化的原始价格;单个数字展示单价格,二元数组展示价格区间;若数组元素含 0 则不渲染;当与productDisplayPrice同时出现时,数组形式的rawPrice优先级更高 |
isSmallestUnit | boolean | — | 为true时,rawPrice中的数字被解释为货币最小单位(如美分)的整数,而非浮点金额;对productDisplayPrice无效 |
original | boolean | — | 添加is-originalCSS 类,呈现划线原价样式 |
discounted | boolean | — | 添加is-discountedCSS 类,呈现绿色折扣价样式 |
currencyCode | string \| null | 'USD' | 货币代码(ISO 4217)。未设置或为undefined时回退到USD;显式为null时组件什么都不渲染(仅在使用rawPrice时生效) |
className | string | — | 追加到组件输出元素的 className 上 |
isOnSale | boolean | — | 为true时在价格旁显示 "Sale" 徽章;对productDisplayPrice与displayFlatPrice无效 |
taxText | string | — | 在价格旁渲染(+X tax)文本(X为传入值);对productDisplayPrice与displayFlatPrice无效 |
displayPerMonthNotation | boolean | — | 在价格旁显示 "per month"(每月)标注;对productDisplayPrice与displayFlatPrice无效 |
productDisplayPrice | string \| TranslateResult | — | 后端预格式化的价格字符串(可含 HTML),原样输出;仅当rawPrice为数组时被忽略 |
omitHeading | boolean | — | 为true时渲染<span>而非默认的<h4>标题标签 |
displayFlatPrice | boolean | — | 平铺展示价格:不再把整数部分与小数部分拆成两个元素,而是输出完整价格文本;开启后多数格式化选项(tax、sale、per-month、wrapper class)失效 |
priceDisplayWrapperClassName | string | — | 为每个价格渲染一个带该 class 的<div>包裹层;对displayFlatPrice与productDisplayPrice无效 |
isLargeCurrency | boolean | — | 添加is-large-currency类,以大字号渲染(适用于大面额货币如韩元、印尼盾) |
渲染优先级(重要)
从render()的实现(index.tsx#L36-L52)可以看出两条明确的优先级规则:
productDisplayPrice优先:只要productDisplayPrice有值、且rawPrice不是长度大于 1 的数组,就直接原样输出productDisplayPrice,忽略currencyCode、taxText、isOnSale、displayPerMonthNotation等所有格式化选项;rawPrice数组覆盖:当rawPrice是数组(长度 ≥ 2)时,反过来忽略productDisplayPrice,走完整的货币格式化流程。
此外还有两个"渲染为空"的边界情况:
currencyCode为null且使用rawPrice→ 返回null;rawPrice为二元数组且其中包含0(如[10, 0])→ 返回null,避免渲染出$10-0这种荒谬的区间。
这些规则全部有对应单元测试背书(见下文"测试验证"一节)。
底层原理:getCurrencyObject 如何完成货币本地化
当走rawPrice分支时,组件把格式化的重活委托给@automattic/number-formatters的getCurrencyObject。从 packages/format-currency/src/index.ts 的实现可以看到(注意number-formatters底层复用了该逻辑),它利用Intl.NumberFormat将金额拆解成以下结构:
symbol:货币符号(如$、€、kr.、C$);symbolPosition:'before'或'after',决定符号在数字左侧还是右侧(例如法语加拿大fr-CA环境下,48 CAD渲染为48C$,符号在右侧);integer:整数部分(字符串,已按语言习惯包含千分位分组,如44,700);fraction:小数部分(含小数点分隔符,如.50);hasNonZeroFraction:布尔值,标记小数部分是否非零;sign:符号(如负数)。
PlanPrice 正是依据这些字段拼装出最终 DOM。以 HtmlPriceDisplay 为例:
- 整数部分放入
<span class="plan-price__integer">; - 小数部分放入
<sup class="plan-price__fraction">,且仅当hasNonZeroFraction为真时才渲染——这也是为什么44700(DKK)会渲染成kr.44,700而不会出现多余的.00; - 货币符号放进
<sup class="plan-price__currency-symbol">,位置由symbolPosition决定。
isSmallestUnit 的换算
isSmallestUnit意味着传入的是"最小货币单位"整数(例如美分5001表示$50.01)。测试用例 test/index.tsx#L40-L46 验证了<PlanPrice rawPrice={ 5001 } isSmallestUnit />渲染为$50.01。这对处理分、仙等无小数的整数金额非常有用,可以避免浮点运算误差。
折扣展示与样式体系
README 的核心使用建议是:要突出"价格已打折",就用两个 PlanPrice 并排放在 flexbox 容器中:
import { PlanPrice } from '@automattic/components'; export default class extends React.Component { static displayName = 'MyPlanPrice'; render() { return ( <div> <span className="my-plan-price-with-flexbox"> <PlanPrice rawPrice={ 99 } original /> <PlanPrice rawPrice={ 30 } discounted /> </span> <span className="my-plan-price-with-flexbox"> <PlanPrice rawPrice={ [ 132.2, 110.4 ] } original /> <PlanPrice rawPrice={ [ 99.99, 87 ] } discounted /> </span> </div> ); } }上述示例完整来自 README.md,可直接复制运行。样式的核心规则在 style.scss:
.plan-price.is-original:使用中性浅色--color-neutral-light,并通过::before伪元素绘制一条rotate(-16deg)的 2px 删除线(--color-accent),模拟经典的"划线原价";.plan-price.is-discounted:使用成功色--color-success(绿色),突出折扣后的现价;.plan-price__currency-symbol/.plan-price__fraction/.plan-price__tax-amount:均以上标(vertical-align: super)呈现,字号为$font-body-extra-small;.plan-price__term:每月标注的样式,0.875rem字号,左对齐。
组件还会根据omitHeading决定外层标签:默认渲染<h4>(测试 test/index.tsx#L246-L268 验证了h4与span的切换),所以在页面中它天然是一个语义化的价格标题。
单元测试验证的行为契约
packages/components/src/plan-price/test/index.tsx 使用@testing-library/react+ jsdom 环境编写了 30+ 条用例,基本锁定了组件的行为契约。以下是最值得注意的几条(均可作为你在集成时对行为的确认依据):
- 零与区间:
rawPrice={ 0 }渲染$0;rawPrice={ [10, 0] }不渲染$10-0(L22-L69); - 优先级:
rawPrice为数字时用productDisplayPrice;rawPrice为数组时反过来覆盖productDisplayPrice;设置productDisplayPrice时currencyCode被忽略(L71-L122); - 货币:
currencyCode="EUR"渲染€5.05;undefined回退$;null什么都不渲染;DKK下整数44700无小数位、浮点44700.5显示.50(L124-L159); - FlatPrice:
displayFlatPrice下输出完整文本(kr.44,700.50作为一个整体文本节点),且忽略isOnSale、taxText、displayPerMonthNotation(L161-L244); - 符号位置:
en-US下 CAD 渲染为C$48(符号在左),fr-CA下渲染为48C$(符号在右)(L354-L375); - CSS 类:
original/discounted/isLargeCurrency分别产生is-original、is-discounted、is-large-currency类(L312-L352)。
在真实业务页面中的使用
在client侧,PlanPrice 被广泛用于套餐与产品价格展示。例如 Jetpack 产品商店的价格明细组件 client/my-sites/plans/jetpack-plans/product-store/pricing-breakdown/render-price.tsx 中:
import { PlanPrice } from '@automattic/components'; // ... <PlanPrice rawPrice={ price } currencyCode={ currencyCode } displayFlatPrice />它选择displayFlatPrice模式,从产品数据中取出原始价格price与currencyCode,由组件统一完成本地化格式化,无需每个页面重复实现货币逻辑。类似的使用点还包括 client/my-sites/plans/jetpack-plans/product-lightbox/payment-plan.tsx、client/my-sites/plans/woo-express-plans-page/index.tsx 等。
这印证了组件的设计初衷:把"价格格式化 + 折扣视觉表现"收敛到一个组件里,业务页面只关心"给数字还是给字符串"。
常见问题与边界行为小结
- 想要区间价格就传数组:
rawPrice={[ low, high ]},中间自动渲染为{{smallerPrice}}-{{higherPrice}}(经由translate处理,支持 i18n); - 不要传 0 进区间数组:会被当作"无意义区间"整体不渲染;
productDisplayPrice可以带 HTML:组件用dangerouslySetInnerHTML原样输出,因此只应传入可信的后端数据;- 同时传两个 prop 时的选择:单价格时
productDisplayPrice生效;区间时rawPrice生效; - 语义化标题:默认是
<h4>,在列表/段落内嵌场景请用omitHeading降级为<span>; - 大面额货币:日元、韩元、印尼盾等面额很大时,用
isLargeCurrency获得更小的字号(对应样式表$font-title-large与is-large-currency类的配合)。
延伸阅读
- 组件文档与示例:packages/components/src/plan-price/README.md、docs/example.jsx、index.stories.jsx
- 组件实现:packages/components/src/plan-price/index.tsx
- 样式:packages/components/src/plan-price/style.scss
- 测试:packages/components/src/plan-price/test/index.tsx
- 底层货币格式化工具:packages/format-currency/src/index.ts 与 packages/format-currency/README.md
- 前端
- CMS
【免费下载链接】wp-calypso
The JavaScript and API powered WordPress.com
相关推荐
如何利用IP-Adapter-FaceID革新旅游业:虚拟导游与人脸定制服务完全指南
如何利用IP Adapter FaceID革新旅游业:虚拟导游与人脸定制服务完全指南 IP Adapter FaceID作为一款基于Stable Diffusi
计算机视觉大模型Leantime 自托管项目管理完整指南:部署、建项目、管任务一次跑通
Leantime 自托管项目管理完整指南:部署、建项目、管任务一次跑通 Leantime 是一个开源免费的项目管理系统,把任务看板、里程碑时间线、个人待办和工作
后端项目管理企业应用Godot 3 Demos最佳实践总结:10个提升游戏开发效率的关键技巧
Godot 3 Demos最佳实践总结:10个提升游戏开发效率的关键技巧 Godot 3 Demos是一个为Godot游戏引擎提供数十个免费开源演示的项目,涵盖
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考