1. 迎宾海报
This commit is contained in:
@@ -216,6 +216,7 @@
|
||||
:src="displayAt(imgIdx)"
|
||||
class="preview-img g-heart-img"
|
||||
decoding="async"
|
||||
:style="{ objectPosition: heartFocusCssAt(page, imgIdx) }"
|
||||
@click.stop="previewImage(originalAt(imgIdx))"
|
||||
/>
|
||||
</div>
|
||||
@@ -230,6 +231,7 @@
|
||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { borderClass } from '@/composables/borderStyles'
|
||||
import { createSequentialMount } from '@/composables/useSequentialMount'
|
||||
import { HEART_SLOTS, heartSlotStyle, heartFocusCssAt } from '@/composables/heartCollage'
|
||||
|
||||
const NINE_STEP_MS = 300
|
||||
const NINE_SETTLE_MS = 400
|
||||
@@ -264,23 +266,6 @@ const SEQUENTIAL_SELF_TYPES = new Set([
|
||||
'heart-collage',
|
||||
])
|
||||
|
||||
/** 爱心拼接槽位:百分比绝对定位,轮廓接近 ♥(顶开叉、中最宽、底收尖) */
|
||||
const HEART_SLOTS = [
|
||||
{ l: 10, t: 0, w: 32, h: 13 }, // 0 左上叶
|
||||
{ l: 58, t: 0, w: 32, h: 13 }, // 1 右上叶(中间约 16% 开叉)
|
||||
{ l: 0, t: 14, w: 14, h: 15.5 }, // 2 左外上
|
||||
{ l: 0, t: 30.5, w: 14, h: 15.5 }, // 3 左外下
|
||||
{ l: 15, t: 14, w: 26, h: 32 }, // 4 左大图
|
||||
{ l: 42, t: 14, w: 16, h: 32 }, // 5 中轴
|
||||
{ l: 59, t: 14, w: 26, h: 32 }, // 6 右大图
|
||||
{ l: 86, t: 14, w: 14, h: 15.5 }, // 7 右外上
|
||||
{ l: 86, t: 30.5, w: 14, h: 15.5 }, // 8 右外下
|
||||
{ l: 10, t: 47, w: 13, h: 16 }, // 9 左下收腰
|
||||
{ l: 24, t: 47, w: 52, h: 16 }, // 10 中下横
|
||||
{ l: 77, t: 47, w: 13, h: 16 }, // 11 右下收腰
|
||||
{ l: 38, t: 64, w: 24, h: 14 }, // 12 心尖
|
||||
]
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Object, required: true },
|
||||
/** 九宫格:开启串行 v-reveal;关闭则一次展示 */
|
||||
@@ -659,15 +644,6 @@ function heartReveal(i) {
|
||||
return 'up'
|
||||
}
|
||||
|
||||
function heartSlotStyle(slot) {
|
||||
return {
|
||||
left: `${slot.l}%`,
|
||||
top: `${slot.t}%`,
|
||||
width: `${slot.w}%`,
|
||||
height: `${slot.h}%`,
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
78
vite-tailwindcss/src/composables/heartCollage.js
Normal file
78
vite-tailwindcss/src/composables/heartCollage.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { clampHeroFocus, heroFocusCss } from '@/composables/posterDefaults'
|
||||
|
||||
/** 爱心拼接槽位:百分比绝对定位(顶开叉、中最宽、底收尖) */
|
||||
export const HEART_SLOTS = [
|
||||
{ l: 10, t: 0, w: 32, h: 13 }, // 0 左上叶
|
||||
{ l: 58, t: 0, w: 32, h: 13 }, // 1 右上叶
|
||||
{ l: 0, t: 14, w: 14, h: 15.5 }, // 2 左外上
|
||||
{ l: 0, t: 30.5, w: 14, h: 15.5 }, // 3 左外下
|
||||
{ l: 15, t: 14, w: 26, h: 32 }, // 4 左大图
|
||||
{ l: 42, t: 14, w: 16, h: 32 }, // 5 中轴
|
||||
{ l: 59, t: 14, w: 26, h: 32 }, // 6 右大图
|
||||
{ l: 86, t: 14, w: 14, h: 15.5 }, // 7 右外上
|
||||
{ l: 86, t: 30.5, w: 14, h: 15.5 }, // 8 右外下
|
||||
{ l: 10, t: 47, w: 13, h: 16 }, // 9 左下收腰
|
||||
{ l: 24, t: 47, w: 52, h: 16 }, // 10 中下横
|
||||
{ l: 77, t: 47, w: 13, h: 16 }, // 11 右下收腰
|
||||
{ l: 38, t: 64, w: 24, h: 14 }, // 12 心尖
|
||||
]
|
||||
|
||||
export const HEART_SLOT_COUNT = HEART_SLOTS.length
|
||||
|
||||
export const HEART_SLOT_LABELS = [
|
||||
'左上叶', '右上叶', '左外上', '左外下', '左大图', '中轴', '右大图',
|
||||
'右外上', '右外下', '左下', '中下横', '右下', '心尖',
|
||||
]
|
||||
|
||||
export function heartSlotStyle(slot) {
|
||||
return {
|
||||
left: `${slot.l}%`,
|
||||
top: `${slot.t}%`,
|
||||
width: `${slot.w}%`,
|
||||
height: `${slot.h}%`,
|
||||
}
|
||||
}
|
||||
|
||||
export function heartSlotLabel(i) {
|
||||
return HEART_SLOT_LABELS[i] || `图${i + 1}`
|
||||
}
|
||||
|
||||
export function heartFocusAt(page, i, axis = 'x') {
|
||||
const arr = axis === 'y' ? page?.imagesFocusY : page?.imagesFocusX
|
||||
const v = Array.isArray(arr) ? arr[i] : 50
|
||||
return clampHeroFocus(v, 50)
|
||||
}
|
||||
|
||||
export function heartFocusCssAt(page, i) {
|
||||
return heroFocusCss(heartFocusAt(page, i, 'x'), heartFocusAt(page, i, 'y'))
|
||||
}
|
||||
|
||||
/** 保证 imagesFocusX/Y 与 images 等长;非爱心版式则清除 */
|
||||
export function syncImagesFocus(page) {
|
||||
if (!page || page.type !== 'heart-collage' || !Array.isArray(page.images)) {
|
||||
if (page) {
|
||||
delete page.imagesFocusX
|
||||
delete page.imagesFocusY
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(page.imagesFocusX)) page.imagesFocusX = []
|
||||
if (!Array.isArray(page.imagesFocusY)) page.imagesFocusY = []
|
||||
const n = page.images.length
|
||||
while (page.imagesFocusX.length < n) page.imagesFocusX.push(50)
|
||||
while (page.imagesFocusY.length < n) page.imagesFocusY.push(50)
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
page.imagesFocusX[i] = clampHeroFocus(page.imagesFocusX[i], 50)
|
||||
page.imagesFocusY[i] = clampHeroFocus(page.imagesFocusY[i], 50)
|
||||
}
|
||||
page.imagesFocusX.length = n
|
||||
page.imagesFocusY.length = n
|
||||
}
|
||||
|
||||
export function setHeartFocus(page, i, x, y) {
|
||||
if (!page || page.type !== 'heart-collage') return
|
||||
syncImagesFocus(page)
|
||||
if (i < 0 || i >= page.images.length) return
|
||||
page.imagesFocusX[i] = clampHeroFocus(x, 50)
|
||||
page.imagesFocusY[i] = clampHeroFocus(y, 50)
|
||||
}
|
||||
@@ -1094,13 +1094,82 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="page.type === 'heart-collage'" class="w-full text-[11px] text-[#8A8680] mb-1">
|
||||
共 13 张:按心形槽位排列(上叶 → 中段对称 → 下横 → 心尖)。建议竖图放中轴,横图放上下叶。
|
||||
<template v-else-if="page.type === 'heart-collage'">
|
||||
<div class="w-full">
|
||||
<div class="text-[11px] text-[#8A8680] mb-2 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<span>
|
||||
<strong>单击</strong>两格对调顺序;格内<strong>拖拽</strong>调整展示焦点(同海报)。
|
||||
空格点击上传。
|
||||
</span>
|
||||
<a-button
|
||||
v-if="heartSwapSelected?.page === page"
|
||||
size="small"
|
||||
type="link"
|
||||
class="!px-0 !h-auto !text-[#a88c6b]"
|
||||
@click="heartSwapSelected = null"
|
||||
>取消选择(已选:{{ heartSlotLabel(heartSwapSelected.idx) }})</a-button>
|
||||
</div>
|
||||
<div class="heart-admin-board">
|
||||
<div
|
||||
v-for="(slot, iidx) in HEART_SLOTS"
|
||||
:key="iidx"
|
||||
class="heart-admin-slot"
|
||||
:style="heartSlotStyle(slot)"
|
||||
>
|
||||
<div
|
||||
class="heart-admin-cell"
|
||||
:class="{
|
||||
empty: !(page.images[iidx] || page.imagesResized?.[iidx]),
|
||||
selected: heartSwapSelected?.page === page && heartSwapSelected?.idx === iidx,
|
||||
'swap-target': heartSwapSelected?.page === page && heartSwapSelected?.idx !== iidx,
|
||||
dragging: heartFocusDrag?.page === page && heartFocusDrag?.idx === iidx && heartFocusDrag?.moved,
|
||||
}"
|
||||
@pointerdown="onHeartSlotPointerDown($event, page, iidx)"
|
||||
>
|
||||
<img
|
||||
v-if="page.images[iidx] || page.imagesResized?.[iidx]"
|
||||
:src="page.imagesResized?.[iidx] || page.images[iidx]"
|
||||
alt=""
|
||||
draggable="false"
|
||||
:style="{ objectPosition: heartFocusCssAt(page, iidx) }"
|
||||
/>
|
||||
<div v-else class="heart-admin-empty">
|
||||
<span class="text-[16px] leading-none">+</span>
|
||||
<span>{{ heartSlotLabel(iidx) }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="page.images[iidx] || page.imagesResized?.[iidx]"
|
||||
class="hero-focus-crosshair heart-admin-crosshair"
|
||||
:style="{
|
||||
left: heartFocusAt(page, iidx, 'x') + '%',
|
||||
top: heartFocusAt(page, iidx, 'y') + '%',
|
||||
}"
|
||||
/>
|
||||
<div class="heart-admin-toolbar">
|
||||
<span class="heart-admin-label">{{ heartSlotLabel(iidx) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="heart-admin-btn"
|
||||
title="上传/更换"
|
||||
@click.stop="openUpload(u => setOriginalImageAt(page, iidx, u))"
|
||||
>
|
||||
<i class="fa-solid fa-image"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="page.imagesResized?.[iidx]"
|
||||
class="heart-admin-meta"
|
||||
:title="page.imagesResizedMeta?.[iidx] || '展示已生成'"
|
||||
>已缩放</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(img, iidx) in page.images" :key="iidx" class="relative w-[96px]">
|
||||
<image-field v-model="page.images[iidx]"
|
||||
:label="heartSlotLabel(page.type, iidx)"
|
||||
:label="`图${iidx + 1}`"
|
||||
@pick="openUpload(u => setOriginalImageAt(page, iidx, u))"
|
||||
:uploading="uploading" />
|
||||
<div v-if="page.imagesResized?.[iidx]" class="text-center mt-1">
|
||||
@@ -1110,7 +1179,6 @@
|
||||
<button v-if="isFlexible(page.type) && page.images.length > 1"
|
||||
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-[#a88c6b] text-white text-[10px] leading-none flex items-center justify-center shadow"
|
||||
@click="removeImage(page, iidx)" title="移除该图">✕</button>
|
||||
<!-- 排序:前移 / 后移 -->
|
||||
<button v-if="iidx > 0"
|
||||
class="absolute -bottom-2 left-1 w-5 h-5 rounded-full bg-white border border-[#a88c6b]/50 text-[#a88c6b] text-[10px] leading-none flex items-center justify-center shadow hover:bg-[#a88c6b] hover:text-white transition-colors"
|
||||
@click="moveImage(page, iidx, -1)" title="前移"><i class="fa-solid fa-arrow-left"></i></button>
|
||||
@@ -1483,6 +1551,11 @@ import {
|
||||
HERO_FOCUS_GRID, heroFocusGridPoints, clampHeroFocus, heroFocusCss, sideKey, layoutKey,
|
||||
posterSideMeta, posterLayoutMeta, posterRoute,
|
||||
} from '@/composables/posterDefaults'
|
||||
import {
|
||||
HEART_SLOTS, HEART_SLOT_COUNT,
|
||||
heartSlotStyle, heartSlotLabel, heartFocusAt, heartFocusCssAt,
|
||||
syncImagesFocus, setHeartFocus,
|
||||
} from '@/composables/heartCollage'
|
||||
|
||||
echarts.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||
|
||||
@@ -1980,7 +2053,7 @@ function ensurePageShape(p) {
|
||||
delete p.imagesResizedMeta
|
||||
} else {
|
||||
// 固定数量版式:一大五小(6) / 九宫格(9) / 爱心拼接(13);柔性版式:瀑布流/轮播/拍立得,仅首次补齐
|
||||
const fixed = { 'one-large-five-small': 6, 'nine-grid': 9, 'heart-collage': 13, 'masonry': 6, 'carousel': 5, 'polaroid': 6 }
|
||||
const fixed = { 'one-large-five-small': 6, 'nine-grid': 9, 'heart-collage': HEART_SLOT_COUNT, 'masonry': 6, 'carousel': 5, 'polaroid': 6 }
|
||||
const need = fixed[p.type] || 6
|
||||
p.images = Array.isArray(p.images) ? p.images : []
|
||||
if (p.images.length === 0) while (p.images.length < need) p.images.push('')
|
||||
@@ -1989,6 +2062,7 @@ function ensurePageShape(p) {
|
||||
if (p.images.length > need) p.images.length = need
|
||||
}
|
||||
syncImagesResized(p)
|
||||
syncImagesFocus(p)
|
||||
delete p.img1
|
||||
delete p.img2
|
||||
delete p.img1Resized
|
||||
@@ -1996,6 +2070,8 @@ function ensurePageShape(p) {
|
||||
delete p.img1ResizedMeta
|
||||
delete p.img2ResizedMeta
|
||||
}
|
||||
// 非爱心版式清掉焦点字段
|
||||
if (p.type !== 'heart-collage') syncImagesFocus(p)
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.height = p.height || '100vh'
|
||||
p.borderStyle = p.borderStyle || 'mat'
|
||||
@@ -2010,43 +2086,146 @@ function isFlexible(type) {
|
||||
return ['masonry', 'carousel', 'polaroid'].includes(type)
|
||||
}
|
||||
|
||||
const HEART_SLOT_LABELS = [
|
||||
'左上叶', '右上叶', '左外上', '左外下', '左大图', '中轴', '右大图',
|
||||
'右外上', '右外下', '左下', '中下横', '右下', '心尖',
|
||||
]
|
||||
function heartSlotLabel(type, i) {
|
||||
if (type === 'heart-collage' && HEART_SLOT_LABELS[i]) return HEART_SLOT_LABELS[i]
|
||||
return `图${i + 1}`
|
||||
}
|
||||
function addImage(page) {
|
||||
if (!Array.isArray(page.images)) page.images = []
|
||||
page.images.push('')
|
||||
syncImagesResized(page)
|
||||
syncImagesFocus(page)
|
||||
}
|
||||
function removeImage(page, i) {
|
||||
if (page.images.length > 1) {
|
||||
page.images.splice(i, 1)
|
||||
if (Array.isArray(page.imagesResized)) page.imagesResized.splice(i, 1)
|
||||
if (Array.isArray(page.imagesResizedMeta)) page.imagesResizedMeta.splice(i, 1)
|
||||
if (Array.isArray(page.imagesFocusX)) page.imagesFocusX.splice(i, 1)
|
||||
if (Array.isArray(page.imagesFocusY)) page.imagesFocusY.splice(i, 1)
|
||||
syncImagesResized(page)
|
||||
syncImagesFocus(page)
|
||||
}
|
||||
}
|
||||
// 图片排序:前移(-1) / 后移(+1)
|
||||
function moveImage(page, i, dir) {
|
||||
const j = i + dir
|
||||
if (!Array.isArray(page.images) || j < 0 || j >= page.images.length) return
|
||||
const t = page.images[i]; page.images[i] = page.images[j]; page.images[j] = t
|
||||
const swap = (arr) => {
|
||||
if (!Array.isArray(arr) || i >= arr.length || j >= arr.length) return
|
||||
const t = arr[i]; arr[i] = arr[j]; arr[j] = t
|
||||
}
|
||||
swap(page.images)
|
||||
syncImagesResized(page)
|
||||
if (Array.isArray(page.imagesResized)) {
|
||||
const r = page.imagesResized[i]
|
||||
page.imagesResized[i] = page.imagesResized[j]
|
||||
page.imagesResized[j] = r
|
||||
swap(page.imagesResized)
|
||||
swap(page.imagesResizedMeta)
|
||||
syncImagesFocus(page)
|
||||
swap(page.imagesFocusX)
|
||||
swap(page.imagesFocusY)
|
||||
}
|
||||
|
||||
const heartSwapSelected = ref(null)
|
||||
const heartFocusDrag = ref(null)
|
||||
const HEART_FOCUS_DRAG_THRESHOLD = 6
|
||||
|
||||
function swapHeartSlots(page, i, j) {
|
||||
if (!page || !Array.isArray(page.images) || i === j) return
|
||||
if (i < 0 || j < 0 || i >= page.images.length || j >= page.images.length) return
|
||||
syncImagesResized(page)
|
||||
syncImagesFocus(page)
|
||||
const swap = (arr) => {
|
||||
if (!Array.isArray(arr)) return
|
||||
const t = arr[i]
|
||||
arr[i] = arr[j]
|
||||
arr[j] = t
|
||||
}
|
||||
if (Array.isArray(page.imagesResizedMeta)) {
|
||||
const m = page.imagesResizedMeta[i]
|
||||
page.imagesResizedMeta[i] = page.imagesResizedMeta[j]
|
||||
page.imagesResizedMeta[j] = m
|
||||
swap(page.images)
|
||||
swap(page.imagesResized)
|
||||
swap(page.imagesResizedMeta)
|
||||
swap(page.imagesFocusX)
|
||||
swap(page.imagesFocusY)
|
||||
}
|
||||
|
||||
function applyHeartFocusFromClient(clientX, clientY) {
|
||||
const drag = heartFocusDrag.value
|
||||
if (!drag?.el || !drag.page) return
|
||||
const rect = drag.el.getBoundingClientRect()
|
||||
if (!rect.width || !rect.height) return
|
||||
const x = ((clientX - rect.left) / rect.width) * 100
|
||||
const y = ((clientY - rect.top) / rect.height) * 100
|
||||
setHeartFocus(drag.page, drag.idx, x, y)
|
||||
}
|
||||
|
||||
function onHeartSlotPointerDown(e, page, idx) {
|
||||
if (e.target?.closest?.('.heart-admin-btn')) return
|
||||
if (e.button != null && e.button !== 0) return
|
||||
|
||||
const empty = !(page.images?.[idx] || page.imagesResized?.[idx])
|
||||
// 空格:交给 pointerup 做「上传 / 对调」
|
||||
if (empty) {
|
||||
heartFocusDrag.value = {
|
||||
page, idx, el: e.currentTarget,
|
||||
startX: e.clientX, startY: e.clientY,
|
||||
moved: false, empty: true,
|
||||
}
|
||||
window.addEventListener('pointerup', onHeartSlotPointerUp)
|
||||
window.addEventListener('pointercancel', onHeartSlotPointerUp)
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
heartFocusDrag.value = {
|
||||
page, idx, el: e.currentTarget,
|
||||
startX: e.clientX, startY: e.clientY,
|
||||
moved: false, empty: false,
|
||||
}
|
||||
window.addEventListener('pointermove', onHeartSlotPointerMove)
|
||||
window.addEventListener('pointerup', onHeartSlotPointerUp)
|
||||
window.addEventListener('pointercancel', onHeartSlotPointerUp)
|
||||
}
|
||||
|
||||
function onHeartSlotPointerMove(e) {
|
||||
const drag = heartFocusDrag.value
|
||||
if (!drag || drag.empty) return
|
||||
const dx = e.clientX - drag.startX
|
||||
const dy = e.clientY - drag.startY
|
||||
if (!drag.moved && (dx * dx + dy * dy) < HEART_FOCUS_DRAG_THRESHOLD ** 2) return
|
||||
drag.moved = true
|
||||
applyHeartFocusFromClient(e.clientX, e.clientY)
|
||||
}
|
||||
|
||||
function onHeartSlotClickAction(page, idx) {
|
||||
const sel = heartSwapSelected.value
|
||||
const empty = !(page.images?.[idx] || page.imagesResized?.[idx])
|
||||
if (empty && !(sel && sel.page === page)) {
|
||||
openUpload((u) => setOriginalImageAt(page, idx, u))
|
||||
return
|
||||
}
|
||||
if (!sel || sel.page !== page) {
|
||||
heartSwapSelected.value = { page, idx }
|
||||
return
|
||||
}
|
||||
if (sel.idx === idx) {
|
||||
heartSwapSelected.value = null
|
||||
return
|
||||
}
|
||||
swapHeartSlots(page, sel.idx, idx)
|
||||
heartSwapSelected.value = null
|
||||
}
|
||||
|
||||
function onHeartSlotPointerUp(e) {
|
||||
const drag = heartFocusDrag.value
|
||||
window.removeEventListener('pointermove', onHeartSlotPointerMove)
|
||||
window.removeEventListener('pointerup', onHeartSlotPointerUp)
|
||||
window.removeEventListener('pointercancel', onHeartSlotPointerUp)
|
||||
heartFocusDrag.value = null
|
||||
if (!drag?.page) return
|
||||
// 拖过焦点:不对调
|
||||
if (drag.moved) return
|
||||
onHeartSlotClickAction(drag.page, drag.idx)
|
||||
}
|
||||
|
||||
function clearHeartSlotPointer() {
|
||||
heartFocusDrag.value = null
|
||||
window.removeEventListener('pointermove', onHeartSlotPointerMove)
|
||||
window.removeEventListener('pointerup', onHeartSlotPointerUp)
|
||||
window.removeEventListener('pointercancel', onHeartSlotPointerUp)
|
||||
}
|
||||
|
||||
function pageHasOriginal(page) {
|
||||
@@ -2127,6 +2306,7 @@ async function batchResizeBasicImages() {
|
||||
function setOriginalImageAt(page, iidx, url) {
|
||||
page.images[iidx] = url
|
||||
syncImagesResized(page)
|
||||
syncImagesFocus(page)
|
||||
page.imagesResized[iidx] = ''
|
||||
page.imagesResizedMeta[iidx] = ''
|
||||
}
|
||||
@@ -3200,6 +3380,7 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
onHeroFocusPointerUp()
|
||||
clearHeartSlotPointer()
|
||||
disposeVisitCharts()
|
||||
})
|
||||
</script>
|
||||
@@ -3213,6 +3394,144 @@ onUnmounted(() => {
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.heart-admin-board {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
aspect-ratio: 0.8 / 1;
|
||||
margin: 0 auto 8px;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 30%, rgba(201, 120, 120, 0.06), transparent 55%),
|
||||
#faf8f5;
|
||||
border: 1px solid #e7e3db;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.heart-admin-slot {
|
||||
position: absolute;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 1px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.heart-admin-cell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f0ebe3;
|
||||
border: 1px solid #e7e3db;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.15s, border-color 0.15s;
|
||||
}
|
||||
.heart-admin-cell.empty {
|
||||
border-style: dashed;
|
||||
border-color: rgba(168, 140, 107, 0.45);
|
||||
background: #f7f4ef;
|
||||
}
|
||||
.heart-admin-cell.selected {
|
||||
border-color: #a88c6b;
|
||||
box-shadow: 0 0 0 2px rgba(168, 140, 107, 0.65);
|
||||
z-index: 3;
|
||||
}
|
||||
.heart-admin-cell.swap-target:hover {
|
||||
border-color: #c4a484;
|
||||
box-shadow: 0 0 0 2px rgba(196, 164, 132, 0.45);
|
||||
z-index: 2;
|
||||
}
|
||||
.heart-admin-cell.dragging {
|
||||
cursor: grabbing;
|
||||
box-shadow: 0 0 0 2px rgba(168, 140, 107, 0.55);
|
||||
z-index: 4;
|
||||
}
|
||||
.heart-admin-cell img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
.heart-admin-empty {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
color: #a88c6b;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
padding: 2px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.heart-admin-crosshair {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: -6px 0 0 -6px;
|
||||
border-width: 1.5px;
|
||||
}
|
||||
.heart-admin-toolbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 2px;
|
||||
padding: 2px 3px;
|
||||
background: linear-gradient(transparent, rgba(40, 30, 20, 0.55));
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
.heart-admin-label {
|
||||
font-size: 9px;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
.heart-admin-btn {
|
||||
pointer-events: auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #a88c6b;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.heart-admin-btn:hover {
|
||||
background: #a88c6b;
|
||||
color: #fff;
|
||||
}
|
||||
.heart-admin-meta {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
z-index: 2;
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
padding: 2px 3px;
|
||||
border-radius: 3px;
|
||||
background: rgba(168, 140, 107, 0.9);
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
.admin-layout-seg :deep(.ant-segmented-item-selected) {
|
||||
color: #a88c6b;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user