跳转到内容
关系图谱

donate:文章赞赏卡

1,879 字 7 分钟

属性 1
series
VaultKit 包指南

@vaultkit/vitepress-plugin-donate 是文章尾部赞赏卡 Vue 组件:支付宝 / 微信收款码页签切换 + PayPal 外链,三渠道按配置门控——配了什么就显示什么,全缺时整卡消失,宿主连 v-if 都不用包。交互是居中的「赞赏」按钮点击展开面板(非模态),SSR 首帧收起态一致、无水合差异。

零依赖:无弹窗库无页签库,颜色走 --vp-c-* 变量明暗自适应,文案经 labels 全量注入,prefers-reduced-motion 下关闭展开过渡。本文覆盖全部导出、channels 三渠道与 labels 四字段的逐项详解,以及按配置门控的设计逻辑。

为什么需要它

独立博客的变现动线里,「赞赏」是最轻的一种:不挂广告、不设付费墙,读者觉得文章有价值就扫码请作者喝杯咖啡。但自己写赞赏组件有几个繁琐点:

  • 中文场景要支付宝 + 微信双码,海外读者要 PayPal——渠道组合因人而异,组件必须能按配置伸缩
  • 收款码常驻展示突兀,需要「按钮 → 展开」的两段式交互
  • 展开态在 SSR 下容易翻车(服务端渲染展开、客户端收起,水合闪一下)

本包把这些都处理好了,peer 只有 vue ^3.5.0

安装

sh
pnpm add -D @vaultkit/vitepress-plugin-donate

使用 @vaultkit/vitepress-theme-vault 主题则开箱即带(主题内 NoteDonate 适配层从站点配置读渠道,配置门控——不配渠道则整个功能不出现)。

快速上手

推荐挂在 Layout 的 #doc-after 插槽,正文后、版权卡前(先转化、后声明):

.vitepress/theme/Layout.vue
vue
<script setup lang="ts">
import { DonateCard } from '@vaultkit/vitepress-plugin-donate'
import DefaultTheme from 'vitepress/theme'
</script>

<template>
  <DefaultTheme.Layout>
    <template #doc-after>
      <DonateCard
        :channels="{
          alipay: { qrcode: '/donate-alipay.png', label: '支付宝' },
          wechat: { qrcode: '/donate-wechat.png', label: '微信' },
          paypal: { url: 'https://paypal.me/yourname' },
        }"
        :labels="{
          button: '赞赏支持',
          hint: '如果这篇文章帮到了你,可以请作者喝杯咖啡 ☕',
          qrAlt: name => `${name}收款码`,
          aria: '赞赏支持',
        }"
      />
    </template>
  </DefaultTheme.Layout>
</template>

收款码图片放进 public/ 目录即可按绝对路径引用。

Important

本包发布的是含 .vue 源文件的产物,消费方 VitePress 项目必须把它加进 vite.ssr.noExternal,否则 SSR 构建会报错:

ts
// .vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  vite: { ssr: { noExternal: ['@vaultkit/vitepress-plugin-donate'] } },
})

公开导出一览

导出形态说明
DonateCardVue 组件(具名 + 默认导出)赞赏卡本体
DonateChannels类型三渠道配置形状
DonateLabels类型四字段文案注入

Props 逐项详解

只有两个 props:channels(必填)与 labels(可选)。字段不多,但每个都有门控语义,值得逐个说清。

channels

  • 类型:DonateChannels必填
  • 语义:渠道配置,配置即开关。三个字段全部可选,但形态分两类:
字段类型形态缺省显示名
alipay{ qrcode: string, label?: string }二维码页签Alipay
wechat{ qrcode: string, label?: string }二维码页签WeChat Pay
paypal{ url: string, label?: string }外链页签(新窗口直开)PayPal

alipay / wechatqrcode 是收款码图片地址(public/ 下的绝对路径或完整 URL),label 覆盖页签显示名——中文站点务必传 '支付宝' / '微信',否则页签显示英文缺省名。显示名同时用于页签文字、二维码下方 figcaption 与 qrAlt 函数的入参。

paypalurlpaypal.me/... 收款链接。它不是二维码——页签形态为外链,点击直接新窗口打开target="_blank" rel="noopener"),不切换面板内容,页签上带外链角标图标提示这个差异。

三种典型组合:

ts
// 只面向国内读者:双码
const channels = {
  alipay: { qrcode: '/alipay.png', label: '支付宝' },
  wechat: { qrcode: '/wechat.png', label: '微信' },
}

// 只面向海外读者:仅 PayPal(渲染为独立外链按钮,无页签条)
const channels2 = {
  paypal: { url: 'https://paypal.me/yourname', label: '请我喝咖啡' },
}

// 全渠道
const channels3 = {
  alipay: { qrcode: '/alipay.png', label: '支付宝' },
  wechat: { qrcode: '/wechat.png', label: '微信' },
  paypal: { url: 'https://paypal.me/yourname' },
}

labels

  • 类型:DonateLabels,默认 {}
  • 语义:文案注入,四个字段逐一说明:
字段类型默认值位置与作用
buttonstring'Sponsor'居中的赞赏按钮文案(前置 lucide 爱心图标)
hintstring无(缺省不渲染展开面板顶部的提示语整句
qrAlt(channel: string) => string`${name} QR code`二维码图片的 alt 文案,入参是渠道显示名
ariastring'Sponsor'卡片容器的 aria-label

中文文案完整示例:

ts
const labels = {
  button: '赞赏支持',
  hint: '如果这篇文章帮到了你,可以请作者喝杯咖啡 ☕',
  qrAlt: (name: string) => `${name}收款码`,
  aria: '赞赏支持',
}

两个细节:hint 与版权卡的 notice 同款语义——缺省整行不渲染,而非回退英文;qrAlt 是函数注入(与 reading-time 的 label 同理),因为 alt 文案里要嵌渠道名,传入 label 后生成的就是「支付宝收款码」这样读屏可懂的描述。

设计机制

按配置门控:配置形状即渲染逻辑

组件内部把 channels 归一化成两个 computed:

ts
const qrTabs = computed(() =>
  (['alipay', 'wechat'] as const).flatMap((key) => {
    const channel = props.channels[key]
    return channel?.qrcode
      ? [{ key, name: channel.label ?? QR_DEFAULTS[key], qrcode: channel.qrcode }]
      : []
  }),
)
const hasChannels = computed(() => qrTabs.value.length > 0 || paypal.value !== null)

由此推导出全部渲染分支,宿主不用写任何门控代码:

  • 全缺hasChannels 为假,整卡消失(连赞赏按钮都没有)
  • 只配一个二维码渠道 → 无页签条(单选无意义),直接展示二维码
  • 配两个二维码渠道 → 页签切换,首个配置的渠道为缺省页签activeTab 回退 qrTabs[0]
  • 二维码 + PayPal → 页签条里 PayPal 以外链形态并列
  • 只配 PayPal → 无页签条、无二维码,渲染一枚独立的描边外链按钮

页签条的显示条件是 qrTabs.length > 1 || (qrTabs.length && paypal)——恰好覆盖「需要选择」的所有情形,不多渲染一个像素。

SSR 安全:非模态展开 + 确定性首帧

面板不是弹窗(无遮罩、不锁滚动、不抢焦点),就是按钮下方原位展开的一块 max-width: 320px 的软底卡片——移动端不用处理弹层滚动穿透,无障碍上也免去焦点陷阱的复杂度。open 状态初始恒为 false,SSR 输出与客户端首帧一致(都是收起态),无水合差异。按钮带 aria-expanded 语义,读屏用户能感知展开状态。

其余细节

  • 二维码 <img> 固定 width/height 180loading="lazy"——尺寸预留防布局位移,懒加载不抢正文带宽;白底圆角处理,暗色模式下扫码对比度不受影响
  • 展开过渡是 0.25s 的淡入 + 6px 上移,prefers-reduced-motion: reduce 下连同按钮 hover 位移一并关闭
  • 颜色全部走 --vp-c-brand-* / --vp-c-bg-soft 等 VitePress 语义变量,明暗自适应零配置

高级与边界

  • 与版权卡的顺序:本站 #doc-after 的顺序是赞赏 → 版权 → 评论——转化入口放最前(读者读完情绪最热时),声明与讨论其后
  • 动态渠道channels 是响应式 prop,可按 frontmatter 对特定文章关闭赞赏(传空对象整卡消失)
  • 页签记忆:切换页签的 active 状态是组件局部 ref,路由切换后重置为首个渠道——刻意不持久化,赞赏面板的状态没有跨页记忆的价值
  • 微信「赞赏码」与「收款码」:组件不关心图片内容,两种码都行;建议用微信官方赞赏码,扫码体验带留言功能

常见问题

Q:整个卡片不显示?channels 全空,或二维码渠道漏传 qrcode、PayPal 漏传 url——字段缺失即视为未配置,这是门控的设计行为。

Q:页签显示英文 Alipay / WeChat Pay? 没传 label。渠道显示名缺省英文,中文站点在每个渠道里传 label: '支付宝' / label: '微信'

Q:点 PayPal 页签怎么面板没切换? 按设计:PayPal 是外链形态,直接新窗口打开收款页,页签上的外链角标就是这个提示。

Q:SSR 构建报 .vue 无法解析? 没配 vite.ssr.noExternal,见上文安装一节。

相关链接