diff --git a/vite-tailwindcss/src/composables/drawXiSeal.js b/vite-tailwindcss/src/composables/drawXiSeal.js
new file mode 100644
index 0000000..7622611
--- /dev/null
+++ b/vite-tailwindcss/src/composables/drawXiSeal.js
@@ -0,0 +1,46 @@
+/**
+ * 在 canvas 上绘制圆内「囍」印章。
+ * 用位图避免 html2canvas 对 CJK + transform/flex 居中错位。
+ */
+export function drawXiSeal(canvas, {
+ size = 28,
+ fontSize = 13,
+ color = '#d4af37',
+ borderColor = '#d4af37',
+ fill = 'rgba(0, 0, 0, 0.15)',
+ dpr = Math.min(3, window.devicePixelRatio || 2),
+} = {}) {
+ if (!canvas) return
+ const s = Math.max(12, Number(size) || 28)
+ const fs = Math.max(8, Number(fontSize) || Math.round(s * 0.46))
+ const ratio = Math.max(1, Number(dpr) || 2)
+
+ canvas.width = Math.round(s * ratio)
+ canvas.height = Math.round(s * ratio)
+ canvas.style.width = `${s}px`
+ canvas.style.height = `${s}px`
+
+ const ctx = canvas.getContext('2d')
+ if (!ctx) return
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
+ ctx.clearRect(0, 0, s, s)
+
+ const cx = s / 2
+ const cy = s / 2
+ const r = s / 2 - 0.75
+
+ ctx.beginPath()
+ ctx.arc(cx, cy, r, 0, Math.PI * 2)
+ ctx.fillStyle = fill
+ ctx.fill()
+ ctx.lineWidth = 1
+ ctx.strokeStyle = borderColor
+ ctx.stroke()
+
+ ctx.fillStyle = color
+ ctx.font = `700 ${fs}px "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", "SimSun", serif`
+ ctx.textAlign = 'center'
+ ctx.textBaseline = 'middle'
+ // 轻微上移做视觉光学居中(非 CSS transform,导出安全)
+ ctx.fillText('囍', cx, cy + fs * 0.04)
+}
diff --git a/vite-tailwindcss/src/views/hb/index.vue b/vite-tailwindcss/src/views/hb/index.vue
index 141cd90..cd352f3 100644
--- a/vite-tailwindcss/src/views/hb/index.vue
+++ b/vite-tailwindcss/src/views/hb/index.vue
@@ -129,6 +129,7 @@ import html2canvas from 'html2canvas'
import { getConfig, getPosterConfig } from '@/api/wedding'
import { normalizePosterConfig, posterSideMeta, resolvePosterSide } from '@/composables/posterDefaults'
import { renderFestiveQr } from '@/composables/renderFestiveQr'
+import { drawXiSeal } from '@/composables/drawXiSeal'
import { useAudio } from '@/composables/useAudio'
import ClassicPoster from '@/views/hb/templates/ClassicPoster.vue'
import SplitPoster from '@/views/hb/templates/SplitPoster.vue'
@@ -375,6 +376,18 @@ async function savePosterImage() {
try {
cleanupBake = await bakeObjectFitImages(el)
cleanupQr = await mountCaptureQr(el)
+ // 导出前重绘囍印章位图,避免 html2canvas 对 CJK 文本错位
+ try { await document.fonts?.ready } catch { /* ignore */ }
+ el.querySelectorAll('canvas.sp-xi-mid, canvas.hb-xi-circle').forEach((c) => {
+ const isClassic = c.classList.contains('hb-xi-circle')
+ drawXiSeal(c, {
+ size: isClassic ? 22 : 28,
+ fontSize: isClassic ? 11 : 13,
+ color: '#d4af37',
+ borderColor: '#d4af37',
+ fill: isClassic ? 'rgba(155, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.18)',
+ })
+ })
// 按元素实际宽度抬升倍率:目标约 1600px 宽,最高 3x,避免手机导出糊成一片
const baseW = Math.max(1, el.getBoundingClientRect().width || el.offsetWidth || 390)
const scale = Math.min(3, Math.max(2.75, 1600 / baseW))
diff --git a/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue b/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue
index f114278..8fcac4a 100644
--- a/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue
+++ b/vite-tailwindcss/src/views/hb/templates/ClassicPoster.vue
@@ -61,7 +61,7 @@
class-name="hb-name calligraphy"
@animation-complete="namesRightReady = true"
/>
- 囍
+