1. 修复海报图片
This commit is contained in:
@@ -653,14 +653,14 @@
|
||||
</a-card>
|
||||
</section>
|
||||
|
||||
<!-- 海报(独立 poster_configs:1 男方 /hb-n,2 女方 /hb-nv,3 回门宴 /hb-hm) -->
|
||||
<!-- 海报(独立 poster_configs:1 男方 /hb-n,2 女方 /hb-nv,3 新人模板 /hb-xr) -->
|
||||
<section v-if="activeTab === 'poster'">
|
||||
<a-card :bordered="false" class="!shadow-sm">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
message="每一方同时维护竖版(/1)与横版(/2)。例:男方竖版 /hb-n/1,横版 /hb-n/2;女方 /hb-nv/1|/2;回门 /hb-hm/1|/2。"
|
||||
message="每一方同时维护竖版(/1)与横版(/2)。例:男方竖版 /hb-n/1,横版 /hb-n/2;女方 /hb-nv/1|/2;新人模板 /hb-xr/1|/2。"
|
||||
/>
|
||||
<a-tabs v-model:activeKey="posterSideTab" size="small" type="card" class="mb-2">
|
||||
<a-tab-pane v-for="s in POSTER_SIDES" :key="s.keyStr" :tab="`${s.label}(${s.key})`" />
|
||||
@@ -862,11 +862,17 @@
|
||||
<a-textarea v-model:value="posterLinesText" :rows="5" placeholder="每行一条正文" />
|
||||
</a-form-item>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<a-form-item label="新郎">
|
||||
<a-input v-model:value="posterCfg.groomName" />
|
||||
<a-form-item :label="posterNameLabels.left">
|
||||
<div class="flex gap-2">
|
||||
<a-input v-model:value="posterCfg.sonLabel" class="!w-[100px]" :placeholder="posterNameLabels.leftPh" />
|
||||
<a-input v-model:value="posterCfg.sonName" placeholder="姓名" @update:value="v => posterCfg.groomName = v" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="新娘">
|
||||
<a-input v-model:value="posterCfg.brideName" />
|
||||
<a-form-item :label="posterNameLabels.right">
|
||||
<div class="flex gap-2">
|
||||
<a-input v-model:value="posterCfg.daughterInLawLabel" class="!w-[100px]" :placeholder="posterNameLabels.rightPh" />
|
||||
<a-input v-model:value="posterCfg.daughterInLawName" placeholder="姓名" @update:value="v => posterCfg.brideName = v" />
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="喜宴时间">
|
||||
<a-input v-model:value="posterCfg.dateText" />
|
||||
@@ -882,20 +888,47 @@
|
||||
<a-input v-model:value="posterCfg.loveFooter" placeholder="Love you" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<a-button type="primary" :loading="posterSaving" @click="onSavePosterConfig"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]">
|
||||
<i class="fa-solid fa-floppy-disk mr-1"></i>保存海报配置
|
||||
</a-button>
|
||||
<a-button @click="openPosterPreview(posterSideTab, posterLayoutTab)">
|
||||
<i class="fa-solid fa-eye mr-1"></i>打开 {{ currentPosterPreviewPath }} 预览
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
<div class="poster-action-bar-spacer" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<div v-if="activeTab === 'poster'" class="poster-action-bar">
|
||||
<div class="poster-action-bar-inner">
|
||||
<div class="poster-action-bar-meta">
|
||||
<span>{{ currentPosterSide.label }} · {{ currentPosterLayout.label }}</span>
|
||||
<span class="poster-action-bar-path">{{ currentPosterPreviewPath }}</span>
|
||||
</div>
|
||||
<div class="poster-action-bar-btns">
|
||||
<a-button
|
||||
v-if="posterUndoSnapshot"
|
||||
size="small"
|
||||
@click="undoPosterImport"
|
||||
>
|
||||
<i class="fa-solid fa-rotate-left mr-1"></i>撤回导入
|
||||
</a-button>
|
||||
<a-button size="small" :loading="posterCopying" @click="copyPosterTemplate">
|
||||
<i class="fa-solid fa-copy mr-1"></i>复制
|
||||
</a-button>
|
||||
<a-button size="small" :loading="posterImporting" @click="importPosterTemplateDirect">
|
||||
<i class="fa-solid fa-file-import mr-1"></i>导入
|
||||
</a-button>
|
||||
<a-button size="small" @click="openPosterPreview(posterSideTab, posterLayoutTab)">
|
||||
<i class="fa-solid fa-eye mr-1"></i>预览
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="posterSaving"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]"
|
||||
@click="onSavePosterConfig"
|
||||
>
|
||||
<i class="fa-solid fa-floppy-disk mr-1"></i>保存
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="activeTab === 'photos'">
|
||||
<div class="admin-panel !p-4 mb-3">
|
||||
<div class="flex justify-between items-center gap-3 flex-wrap">
|
||||
@@ -1419,7 +1452,7 @@ import {
|
||||
metaHasFileSize,
|
||||
} from '@/composables/resizeImage'
|
||||
import {
|
||||
defaultPosterBundle, normalizePosterBundle, defaultPosterForLayout,
|
||||
defaultPosterBundle, normalizePosterBundle, defaultPosterForLayout, normalizePosterConfig,
|
||||
LOADING_EFFECTS, ENTER_EFFECTS, POSTER_SIDES, POSTER_LAYOUTS, POSTER_TEMPLATES, MOBILE_LAYOUTS,
|
||||
HERO_FOCUS_GRID, heroFocusGridPoints, clampHeroFocus, heroFocusCss, sideKey, layoutKey,
|
||||
posterSideMeta, posterLayoutMeta, posterRoute,
|
||||
@@ -1616,6 +1649,12 @@ const uploadCfg = reactive(defaultUploadConfig())
|
||||
const posterSideTab = ref('1')
|
||||
const posterLayoutTab = ref('1')
|
||||
const posterBundle = reactive(defaultPosterBundle())
|
||||
const posterImporting = ref(false)
|
||||
const posterCopying = ref(false)
|
||||
/** 本会话最近一次复制的模板,导入时优先/兜底使用 */
|
||||
let posterTemplateCache = null
|
||||
/** 导入前快照,用于撤回 { s, l, config } */
|
||||
const posterUndoSnapshot = ref(null)
|
||||
const posterCfg = computed(() => {
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
@@ -1737,7 +1776,7 @@ const posterNameLabels = computed(() => {
|
||||
return { left: '女儿称谓 / 姓名', right: '女婿称谓 / 姓名', leftPh: '女儿:', rightPh: '女婿:' }
|
||||
}
|
||||
if (s === '3') {
|
||||
return { left: '新郎称谓 / 姓名', right: '新娘称谓 / 姓名', leftPh: '新郎:', rightPh: '新娘:' }
|
||||
return { left: '新人左侧称谓 / 姓名', right: '新人右侧称谓 / 姓名', leftPh: '新郎:', rightPh: '新娘:' }
|
||||
}
|
||||
return { left: '儿子称谓 / 姓名', right: '儿媳称谓 / 姓名', leftPh: '儿子:', rightPh: '儿媳:' }
|
||||
})
|
||||
@@ -2026,9 +2065,17 @@ async function batchResizeBasicImages() {
|
||||
console.warn('[resize-basic]', slot.key, err)
|
||||
}
|
||||
}
|
||||
if (fail === 0) message.success(`已生成 ${ok} 张展示图`)
|
||||
else if (ok > 0) message.warning(`成功 ${ok} 张,失败 ${fail} 张(常见原因:跨域无法读取原图)`)
|
||||
else message.error('缩放上传全部失败,请确认原图可跨域读取')
|
||||
if (ok > 0) {
|
||||
try {
|
||||
await saveConfigSection('basic', pickSectionData('basic'))
|
||||
if (fail === 0) message.success(`已生成并保存 ${ok} 张展示图`)
|
||||
else message.warning(`成功 ${ok} 张并已保存,失败 ${fail} 张`)
|
||||
} catch (e) {
|
||||
message.warning(`已生成 ${ok} 张展示图,但自动保存失败:${e?.message || '请手动保存'}`)
|
||||
}
|
||||
} else {
|
||||
message.error('缩放上传全部失败,请确认原图可跨域读取')
|
||||
}
|
||||
} finally {
|
||||
resizingBasic.value = false
|
||||
resizeProgress.value = ''
|
||||
@@ -2285,6 +2332,7 @@ async function onSavePosterConfig() {
|
||||
posterBundle['1'] = bundle['1']
|
||||
posterBundle['2'] = bundle['2']
|
||||
posterBundle['3'] = bundle['3']
|
||||
posterUndoSnapshot.value = null
|
||||
message.success('海报配置已保存')
|
||||
} catch (e) {
|
||||
message.error(e?.message || '海报配置保存失败')
|
||||
@@ -2293,6 +2341,141 @@ async function onSavePosterConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildPosterTemplatePayload() {
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
const slot = ensurePosterSlot(s, l)
|
||||
return {
|
||||
version: 1,
|
||||
kind: 'poster-slot',
|
||||
side: Number(s),
|
||||
layout: Number(l),
|
||||
sideLabel: posterSideMeta(s).label,
|
||||
layoutLabel: posterLayoutMeta(l).label,
|
||||
route: posterRoute(s, l),
|
||||
config: JSON.parse(JSON.stringify(slot)),
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPosterTemplate() {
|
||||
posterCopying.value = true
|
||||
try {
|
||||
const payload = buildPosterTemplatePayload()
|
||||
posterTemplateCache = payload
|
||||
const text = JSON.stringify(payload, null, 2)
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
message.success(`已复制「${currentPosterSide.value.label} · ${currentPosterLayout.value.label}」`)
|
||||
} catch (e) {
|
||||
message.error(e?.message || '复制失败,请检查浏览器剪贴板权限')
|
||||
} finally {
|
||||
posterCopying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function extractPosterImportConfig(parsed) {
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
if (parsed.kind === 'poster-slot' && parsed.config && typeof parsed.config === 'object') {
|
||||
return parsed.config
|
||||
}
|
||||
if (parsed.config && typeof parsed.config === 'object' && !Array.isArray(parsed.config)) {
|
||||
return parsed.config
|
||||
}
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
const sideObj = parsed[s] || parsed[Number(s)]
|
||||
if (sideObj && typeof sideObj === 'object') {
|
||||
const slot = sideObj[l] || sideObj[Number(l)]
|
||||
if (slot && typeof slot === 'object') return slot
|
||||
}
|
||||
if ('template' in parsed || 'heroImg' in parsed || 'lines' in parsed || 'sonName' in parsed || 'groomName' in parsed) {
|
||||
return parsed
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function readPosterImportSource() {
|
||||
// 1) 剪贴板 2) 本会话刚复制的缓存
|
||||
try {
|
||||
if (navigator?.clipboard?.readText) {
|
||||
const clip = String(await navigator.clipboard.readText() || '').trim()
|
||||
if (clip) {
|
||||
try {
|
||||
const parsed = JSON.parse(clip)
|
||||
if (extractPosterImportConfig(parsed)) return parsed
|
||||
} catch { /* not json */ }
|
||||
}
|
||||
}
|
||||
} catch { /* no permission */ }
|
||||
if (posterTemplateCache && extractPosterImportConfig(posterTemplateCache)) {
|
||||
return posterTemplateCache
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function applyImportedConfig(rawCfg) {
|
||||
const s = sideKey(posterSideTab.value)
|
||||
const l = layoutKey(posterLayoutTab.value)
|
||||
posterUndoSnapshot.value = {
|
||||
s,
|
||||
l,
|
||||
config: JSON.parse(JSON.stringify(ensurePosterSlot(s, l))),
|
||||
}
|
||||
const next = normalizePosterConfig(rawCfg, s, l)
|
||||
next.side = Number(s)
|
||||
next.layout = Number(l)
|
||||
if (next.sonName && !next.groomName) next.groomName = next.sonName
|
||||
if (next.daughterInLawName && !next.brideName) next.brideName = next.daughterInLawName
|
||||
if (next.groomName && !next.sonName) next.sonName = next.groomName
|
||||
if (next.brideName && !next.daughterInLawName) next.daughterInLawName = next.brideName
|
||||
posterBundle[s][l] = next
|
||||
}
|
||||
|
||||
async function importPosterTemplateDirect() {
|
||||
posterImporting.value = true
|
||||
try {
|
||||
const parsed = await readPosterImportSource()
|
||||
if (!parsed) {
|
||||
message.warning('请先复制模板,再点导入')
|
||||
return
|
||||
}
|
||||
const rawCfg = extractPosterImportConfig(parsed)
|
||||
if (!rawCfg) {
|
||||
message.error('无法识别的模板数据')
|
||||
return
|
||||
}
|
||||
applyImportedConfig(rawCfg)
|
||||
message.success(`已导入到「${currentPosterSide.value.label} · ${currentPosterLayout.value.label}」,可撤回或保存`)
|
||||
} catch (e) {
|
||||
message.error(e?.message || '导入失败')
|
||||
} finally {
|
||||
posterImporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function undoPosterImport() {
|
||||
const snap = posterUndoSnapshot.value
|
||||
if (!snap?.config) {
|
||||
message.info('没有可撤回的导入')
|
||||
return
|
||||
}
|
||||
posterSideTab.value = snap.s
|
||||
posterLayoutTab.value = snap.l
|
||||
posterBundle[snap.s][snap.l] = snap.config
|
||||
posterUndoSnapshot.value = null
|
||||
message.success('已撤回导入')
|
||||
}
|
||||
|
||||
function pickSectionData(section) {
|
||||
const keys = SECTION_KEYS[section] || []
|
||||
const data = {}
|
||||
@@ -3514,4 +3697,57 @@ onUnmounted(() => {
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.poster-action-bar-spacer {
|
||||
height: 72px;
|
||||
}
|
||||
.poster-action-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 40;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(168, 140, 107, 0.2);
|
||||
box-shadow: 0 -8px 24px rgba(88, 68, 42, 0.08);
|
||||
}
|
||||
.poster-action-bar-inner {
|
||||
max-width: 1220px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.admin-shell--top ~ .poster-action-bar .poster-action-bar-inner,
|
||||
.admin-shell--top .poster-action-bar-inner {
|
||||
max-width: 80vw;
|
||||
}
|
||||
.poster-action-bar-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #5c5348;
|
||||
}
|
||||
.poster-action-bar-path {
|
||||
font-size: 11px;
|
||||
color: #8A8680;
|
||||
}
|
||||
.poster-action-bar-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.poster-action-bar-meta { width: 100%; }
|
||||
.poster-action-bar-btns { width: 100%; justify-content: stretch; }
|
||||
.poster-action-bar-btns :deep(.ant-btn) { flex: 1 1 auto; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user