1. 修复海报图片
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
class-name="hb-name calligraphy"
|
||||
@animation-complete="namesRightReady = true"
|
||||
/>
|
||||
<span class="hb-xi-circle">囍</span>
|
||||
<canvas ref="xiSealRef" class="hb-xi-circle" width="44" height="44" aria-hidden="true"></canvas>
|
||||
<BlurText
|
||||
v-if="namesRightReady"
|
||||
:text="nameRight"
|
||||
@@ -141,10 +141,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import BlurText from '@/components/vue-bits/BlurText.vue'
|
||||
import TextType from '@/components/vue-bits/TextType.vue'
|
||||
import { heroFocusCss } from '@/composables/posterDefaults'
|
||||
import { drawXiSeal } from '@/composables/drawXiSeal'
|
||||
|
||||
const props = defineProps({
|
||||
cfg: { type: Object, required: true },
|
||||
@@ -155,12 +156,24 @@ defineEmits(['invite'])
|
||||
const stage = ref(0)
|
||||
const lineStage = ref(0)
|
||||
const namesRightReady = ref(false)
|
||||
const xiSealRef = ref(null)
|
||||
let timers = []
|
||||
|
||||
const heroObjectPosition = computed(() => heroFocusCss(props.cfg))
|
||||
const nameLeft = computed(() => `${props.cfg.sonLabel || ''}${props.cfg.sonName || ''}`)
|
||||
const nameRight = computed(() => `${props.cfg.daughterInLawLabel || ''}${props.cfg.daughterInLawName || ''}`)
|
||||
|
||||
async function paintXiSeal() {
|
||||
try { await document.fonts?.ready } catch { /* ignore */ }
|
||||
drawXiSeal(xiSealRef.value, {
|
||||
size: 22,
|
||||
fontSize: 11,
|
||||
color: '#d4af37',
|
||||
borderColor: '#d4af37',
|
||||
fill: 'rgba(155, 0, 0, 0.5)',
|
||||
})
|
||||
}
|
||||
|
||||
function later(fn, ms) {
|
||||
const id = setTimeout(fn, ms)
|
||||
timers.push(id)
|
||||
@@ -194,6 +207,17 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(paintXiSeal)
|
||||
window.addEventListener('resize', paintXiSeal, { passive: true })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', paintXiSeal)
|
||||
timers.forEach(clearTimeout)
|
||||
timers = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -326,15 +350,38 @@ watch(
|
||||
.hb-xi-l { left: 8%; }
|
||||
.hb-xi-r { right: 8%; }
|
||||
.hb-names {
|
||||
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
min-height: 28px; margin-bottom: 8px; opacity: 0; transition: opacity 0.4s ease;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.hb-names.is-show { opacity: 1; }
|
||||
.hb-name { color: #f7efe0 !important; font-size: 12px; letter-spacing: 0.12em; justify-content: center; }
|
||||
.hb-name {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
color: #f7efe0 !important;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
justify-content: center;
|
||||
}
|
||||
.hb-name:first-child { padding-right: 18px; justify-content: flex-end; }
|
||||
.hb-name:last-child { padding-left: 18px; justify-content: flex-start; }
|
||||
.hb-xi-circle {
|
||||
width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--hb-gold);
|
||||
color: var(--hb-gold); font-size: 11px; display: inline-flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0; background: rgba(155, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-left: -11px;
|
||||
margin-top: -11px;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.hb-title-wrap { margin: 4px 0 12px; min-height: 48px; opacity: 0; transition: opacity 0.35s ease; }
|
||||
.hb-title-wrap.is-show { opacity: 1; }
|
||||
|
||||
@@ -79,12 +79,12 @@
|
||||
</div>
|
||||
|
||||
<div class="sp-couple" :class="{ 'is-show': stage >= 5 }">
|
||||
<div class="sp-person">
|
||||
<div class="sp-person sp-person-l">
|
||||
<span class="sp-role calligraphy">新郎</span>
|
||||
<BlurText v-if="stage >= 5" :text="groom" :active="true" animate-by="letters" :delay="40" class-name="sp-name calligraphy" />
|
||||
</div>
|
||||
<span class="sp-xi-mid">囍</span>
|
||||
<div class="sp-person">
|
||||
<canvas ref="xiSealRef" class="sp-xi-mid" width="56" height="56" aria-hidden="true"></canvas>
|
||||
<div class="sp-person sp-person-r">
|
||||
<span class="sp-role calligraphy">新娘</span>
|
||||
<BlurText v-if="stage >= 5" :text="bride" :active="true" animate-by="letters" :delay="40" class-name="sp-name calligraphy" @animation-complete="bumpStage(6)" />
|
||||
</div>
|
||||
@@ -118,10 +118,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import BlurText from '@/components/vue-bits/BlurText.vue'
|
||||
import TextType from '@/components/vue-bits/TextType.vue'
|
||||
import { heroFocusCss } from '@/composables/posterDefaults'
|
||||
import { drawXiSeal } from '@/composables/drawXiSeal'
|
||||
|
||||
const props = defineProps({
|
||||
cfg: { type: Object, required: true },
|
||||
@@ -131,6 +132,7 @@ defineEmits(['invite'])
|
||||
|
||||
const stage = ref(0)
|
||||
const lineStage = ref(0)
|
||||
const xiSealRef = ref(null)
|
||||
let timers = []
|
||||
|
||||
const heroObjectPosition = computed(() => heroFocusCss(props.cfg))
|
||||
@@ -147,6 +149,17 @@ const bodyLines = computed(() => {
|
||||
const groom = computed(() => props.cfg.groomName || props.cfg.sonName || '')
|
||||
const bride = computed(() => props.cfg.brideName || props.cfg.daughterInLawName || '')
|
||||
|
||||
async function paintXiSeal() {
|
||||
try { await document.fonts?.ready } catch { /* ignore */ }
|
||||
drawXiSeal(xiSealRef.value, {
|
||||
size: 28,
|
||||
fontSize: 13,
|
||||
color: '#d4af37',
|
||||
borderColor: '#d4af37',
|
||||
fill: 'rgba(0, 0, 0, 0.18)',
|
||||
})
|
||||
}
|
||||
|
||||
function later(fn, ms) {
|
||||
timers.push(setTimeout(fn, ms))
|
||||
}
|
||||
@@ -179,6 +192,17 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(paintXiSeal)
|
||||
window.addEventListener('resize', paintXiSeal, { passive: true })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', paintXiSeal)
|
||||
timers.forEach(clearTimeout)
|
||||
timers = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -335,18 +359,40 @@ watch(
|
||||
.sp-cursor { color: var(--hb-gold); }
|
||||
|
||||
.sp-couple {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; align-items: center; justify-content: center; gap: 14px;
|
||||
margin: 6px 0 14px; opacity: 0; transition: opacity 0.4s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 6px 0 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.sp-couple.is-show { opacity: 1; }
|
||||
.sp-person { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 72px; }
|
||||
.sp-person {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.sp-person-l { padding-right: 22px; }
|
||||
.sp-person-r { padding-left: 22px; }
|
||||
.sp-role { font-size: 11px; color: rgba(247, 239, 224, 0.7); letter-spacing: 0.3em; }
|
||||
.sp-name { color: var(--hb-gold-soft) !important; font-size: 18px; letter-spacing: 0.2em; justify-content: center; }
|
||||
/* canvas 位图印章:不用 CSS transform,避免 html2canvas 错位 */
|
||||
.sp-xi-mid {
|
||||
width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--hb-gold);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; color: var(--hb-gold); background: rgba(0, 0, 0, 0.15);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-left: -14px;
|
||||
margin-top: -14px;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sp-meta {
|
||||
|
||||
Reference in New Issue
Block a user