1. 礼物特效

This commit is contained in:
李琦
2026-08-03 13:19:09 +08:00
parent abcb53b88e
commit 8a4b3f4ab7
8 changed files with 532 additions and 173 deletions

View File

@@ -608,6 +608,45 @@
:uploading="uploading"
/>
</a-form-item>
<a-form-item label="照片展示焦点">
<div
v-if="posterCfg.heroImg"
ref="heroFocusPreviewEl"
class="hero-focus-preview hero-focus-preview--drag"
:class="{ dragging: heroFocusDragging }"
@pointerdown="onHeroFocusPointerDown"
>
<img
:src="posterCfg.heroImg"
alt=""
draggable="false"
:style="{ objectPosition: heroFocusCss(posterCfg) }"
/>
<span
class="hero-focus-crosshair"
:style="{ left: posterCfg.heroFocusX + '%', top: posterCfg.heroFocusY + '%' }"
/>
</div>
<div v-else class="hero-focus-preview hero-focus-preview--empty">
请先上传海报图片再拖拽设置焦点
</div>
<div class="hero-focus-pad mt-2" :style="{ '--hero-focus-n': HERO_FOCUS_GRID }">
<button
v-for="pt in heroFocusGrid"
:key="pt.key"
type="button"
class="hero-focus-cell"
:class="{ active: isHeroFocusGridActive(pt) }"
:title="`${Math.round(pt.x)}%, ${Math.round(pt.y)}%`"
@click="setHeroFocus(pt.x, pt.y)"
>
<span class="hero-focus-dot" />
</button>
</div>
<div class="text-[11px] text-[#8A8680] mt-1">
在预览上拖拽十字准星或点下方 9×9 网格当前 {{ Math.round(posterCfg.heroFocusX) }}% · {{ Math.round(posterCfg.heroFocusY) }}%
</div>
</a-form-item>
<a-form-item label="二维码中心图">
<image-field
v-model="posterCfg.qrCenterImg"
@@ -1041,7 +1080,8 @@ import { GIFT_TYPES, emptyGiftCosts, DEFAULT_GIFT_COSTS } from '@/utils/giftType
import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage'
import {
defaultPosterBundle, normalizePosterBundle, normalizePosterConfig,
LOADING_EFFECTS, ENTER_EFFECTS, POSTER_SIDES, POSTER_TEMPLATES, MOBILE_LAYOUTS, sideKey, posterSideMeta,
LOADING_EFFECTS, ENTER_EFFECTS, POSTER_SIDES, POSTER_TEMPLATES, MOBILE_LAYOUTS,
HERO_FOCUS_GRID, heroFocusGridPoints, clampHeroFocus, heroFocusCss, sideKey, posterSideMeta,
} from '@/composables/posterDefaults'
echarts.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
@@ -1147,6 +1187,58 @@ const uploadCfg = reactive(defaultUploadConfig())
const posterSideTab = ref('1')
const posterBundle = reactive(defaultPosterBundle())
const posterCfg = computed(() => posterBundle[sideKey(posterSideTab.value)] || posterBundle['1'])
const heroFocusGrid = heroFocusGridPoints(HERO_FOCUS_GRID)
const heroFocusPreviewEl = ref(null)
const heroFocusDragging = ref(false)
function setHeroFocus(x, y) {
const cfg = posterCfg.value
if (!cfg) return
cfg.heroFocusX = clampHeroFocus(x)
cfg.heroFocusY = clampHeroFocus(y)
}
function isHeroFocusGridActive(pt) {
const cfg = posterCfg.value
if (!cfg) return false
return Math.abs(cfg.heroFocusX - pt.x) < 0.6
&& Math.abs(cfg.heroFocusY - pt.y) < 0.6
}
function heroFocusFromClient(clientX, clientY) {
const el = heroFocusPreviewEl.value
if (!el) return
const rect = el.getBoundingClientRect()
if (rect.width <= 0 || rect.height <= 0) return
const x = ((clientX - rect.left) / rect.width) * 100
const y = ((clientY - rect.top) / rect.height) * 100
setHeroFocus(x, y)
}
function onHeroFocusPointerMove(e) {
if (!heroFocusDragging.value) return
heroFocusFromClient(e.clientX, e.clientY)
}
function onHeroFocusPointerUp() {
if (!heroFocusDragging.value) return
heroFocusDragging.value = false
window.removeEventListener('pointermove', onHeroFocusPointerMove)
window.removeEventListener('pointerup', onHeroFocusPointerUp)
window.removeEventListener('pointercancel', onHeroFocusPointerUp)
}
function onHeroFocusPointerDown(e) {
if (!posterCfg.value?.heroImg) return
e.preventDefault()
heroFocusDragging.value = true
heroFocusFromClient(e.clientX, e.clientY)
window.addEventListener('pointermove', onHeroFocusPointerMove)
window.addEventListener('pointerup', onHeroFocusPointerUp)
window.addEventListener('pointercancel', onHeroFocusPointerUp)
}
const posterMusicSelectOptions = computed(() => {
const list = Array.isArray(posterMusicOptions.value) ? posterMusicOptions.value : []
const opts = list
@@ -2048,6 +2140,7 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('resize', onResize)
onHeroFocusPointerUp()
disposeVisitCharts()
})
</script>
@@ -2407,4 +2500,81 @@ onUnmounted(() => {
width: 10px; height: 10px; border-radius: 9999px; flex-shrink: 0;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08);
}
.hero-focus-pad {
display: grid;
grid-template-columns: repeat(var(--hero-focus-n, 9), 22px);
gap: 4px;
width: fit-content;
padding: 8px;
border-radius: 10px;
background: #f7f4ef;
border: 1px solid #e7e3db;
}
.hero-focus-cell {
width: 22px;
height: 22px;
border-radius: 5px;
border: 1px solid #ddd6cb;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
transition: border-color 0.15s, background 0.15s;
}
.hero-focus-cell:hover { border-color: #a88c6b; }
.hero-focus-cell.active {
border-color: #a88c6b;
background: rgba(168, 140, 107, 0.14);
}
.hero-focus-dot {
width: 5px;
height: 5px;
border-radius: 999px;
background: #c4b8a4;
}
.hero-focus-cell.active .hero-focus-dot { background: #a88c6b; }
.hero-focus-preview {
position: relative;
width: min(100%, 280px);
aspect-ratio: 16 / 11;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e7e3db;
background: #f0ebe3;
user-select: none;
touch-action: none;
}
.hero-focus-preview--drag { cursor: crosshair; }
.hero-focus-preview--drag.dragging { cursor: grabbing; }
.hero-focus-preview--empty {
display: flex;
align-items: center;
justify-content: center;
color: #8A8680;
font-size: 12px;
padding: 12px;
text-align: center;
}
.hero-focus-preview img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.hero-focus-crosshair {
position: absolute;
width: 18px;
height: 18px;
margin: -9px 0 0 -9px;
border: 2px solid #fff;
border-radius: 999px;
box-shadow: 0 0 0 1px rgba(168, 140, 107, 0.85), 0 1px 4px rgba(0, 0, 0, 0.25);
background: rgba(168, 140, 107, 0.35);
pointer-events: none;
z-index: 1;
}
</style>