1. 优化视觉效果、后端规范化
This commit is contained in:
@@ -505,20 +505,58 @@
|
||||
<a-textarea v-model:value="page.desc" :rows="2" />
|
||||
</a-form-item>
|
||||
<!-- 图片上传区:按版式渲染 -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 mt-2 mb-2">
|
||||
<div class="text-[11px] text-[#8A8680]">
|
||||
原图用于预览;展示图为等比缩放后的体积(不裁切)。
|
||||
<span v-if="resizeProgress" class="text-[#a88c6b] ml-1">{{ resizeProgress }}</span>
|
||||
</div>
|
||||
<a-button
|
||||
size="small"
|
||||
type="primary"
|
||||
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]"
|
||||
:loading="resizingPage"
|
||||
:disabled="uploading || !pageHasOriginal(page)"
|
||||
@click="batchResizeAndUpload(page)"
|
||||
>
|
||||
<i class="fa-solid fa-compress mr-1"></i>一键缩放并上传
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 mt-2">
|
||||
<template v-if="page.type==='single'">
|
||||
<image-field v-model="page.img1" @pick="openUpload(u => page.img1 = u)" :uploading="uploading" />
|
||||
<div class="relative w-[96px]">
|
||||
<image-field v-model="page.img1" @pick="openUpload(u => setOriginalAndClearResized(page, 'img1', u))" :uploading="uploading" />
|
||||
<div v-if="page.img1Resized" class="text-center mt-1">
|
||||
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
|
||||
<div v-if="page.img1ResizedMeta" class="resized-meta">{{ page.img1ResizedMeta }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page.type==='overlap'">
|
||||
<image-field v-model="page.img1" label="图一" @pick="openUpload(u => page.img1 = u)" :uploading="uploading" />
|
||||
<image-field v-model="page.img2" label="图二" @pick="openUpload(u => page.img2 = u)" :uploading="uploading" />
|
||||
<div class="relative w-[96px]">
|
||||
<image-field v-model="page.img1" label="图一" @pick="openUpload(u => setOriginalAndClearResized(page, 'img1', u))" :uploading="uploading" />
|
||||
<div v-if="page.img1Resized" class="text-center mt-1">
|
||||
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
|
||||
<div v-if="page.img1ResizedMeta" class="resized-meta">{{ page.img1ResizedMeta }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative w-[96px]">
|
||||
<image-field v-model="page.img2" label="图二" @pick="openUpload(u => setOriginalAndClearResized(page, 'img2', u))" :uploading="uploading" />
|
||||
<div v-if="page.img2Resized" class="text-center mt-1">
|
||||
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
|
||||
<div v-if="page.img2ResizedMeta" class="resized-meta">{{ page.img2ResizedMeta }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(img, iidx) in page.images" :key="iidx" class="relative">
|
||||
<div v-for="(img, iidx) in page.images" :key="iidx" class="relative w-[96px]">
|
||||
<image-field v-model="page.images[iidx]"
|
||||
:label="`图${iidx+1}`"
|
||||
@pick="openUpload(u => page.images[iidx] = u)"
|
||||
@pick="openUpload(u => setOriginalImageAt(page, iidx, u))"
|
||||
:uploading="uploading" />
|
||||
<div v-if="page.imagesResized?.[iidx]" class="text-center mt-1">
|
||||
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
|
||||
<div v-if="page.imagesResizedMeta?.[iidx]" class="resized-meta">{{ page.imagesResizedMeta[iidx] }}</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -698,6 +736,7 @@ import { useAdminStore } from '@/stores/admin'
|
||||
import ImageField from '@/components/ImageField.vue'
|
||||
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
|
||||
import { resolveDanmakuSwatch } from '@/utils/danmakuColors'
|
||||
import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage'
|
||||
|
||||
echarts.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||
|
||||
@@ -732,6 +771,8 @@ const activeTab = ref(readStoredTab())
|
||||
const activePhotoTab = ref('0')
|
||||
const saving = ref(false)
|
||||
const uploading = ref(false)
|
||||
const resizingPage = ref(false)
|
||||
const resizeProgress = ref('')
|
||||
const uploadConfigSaving = ref(false)
|
||||
const tabLoading = ref(false)
|
||||
const rsvpLoading = ref(false)
|
||||
@@ -850,10 +891,42 @@ function onHeightFieldCustomVh(target, key, val) {
|
||||
target[key] = (val || 100) + 'vh'
|
||||
}
|
||||
|
||||
function syncImagesResized(p) {
|
||||
if (!Array.isArray(p.images)) {
|
||||
delete p.imagesResized
|
||||
delete p.imagesResizedMeta
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(p.imagesResized)) p.imagesResized = []
|
||||
if (!Array.isArray(p.imagesResizedMeta)) p.imagesResizedMeta = []
|
||||
while (p.imagesResized.length < p.images.length) p.imagesResized.push('')
|
||||
while (p.imagesResizedMeta.length < p.images.length) p.imagesResizedMeta.push('')
|
||||
if (p.imagesResized.length > p.images.length) p.imagesResized.length = p.images.length
|
||||
if (p.imagesResizedMeta.length > p.images.length) p.imagesResizedMeta.length = p.images.length
|
||||
}
|
||||
|
||||
function ensurePageShape(p) {
|
||||
if (p.type === 'single') { p.img1 = p.img1 || ''; delete p.img2; delete p.images }
|
||||
else if (p.type === 'overlap') { p.img1 = p.img1 || ''; p.img2 = p.img2 || ''; delete p.images }
|
||||
else {
|
||||
if (p.type === 'single') {
|
||||
p.img1 = p.img1 || ''
|
||||
p.img1Resized = p.img1Resized || ''
|
||||
p.img1ResizedMeta = p.img1ResizedMeta || ''
|
||||
delete p.img2
|
||||
delete p.img2Resized
|
||||
delete p.img2ResizedMeta
|
||||
delete p.images
|
||||
delete p.imagesResized
|
||||
delete p.imagesResizedMeta
|
||||
} else if (p.type === 'overlap') {
|
||||
p.img1 = p.img1 || ''
|
||||
p.img2 = p.img2 || ''
|
||||
p.img1Resized = p.img1Resized || ''
|
||||
p.img2Resized = p.img2Resized || ''
|
||||
p.img1ResizedMeta = p.img1ResizedMeta || ''
|
||||
p.img2ResizedMeta = p.img2ResizedMeta || ''
|
||||
delete p.images
|
||||
delete p.imagesResized
|
||||
delete p.imagesResizedMeta
|
||||
} else {
|
||||
// 固定数量版式:一大五小(6) / 九宫格(9);柔性版式:瀑布流/轮播/拍立得,仅首次补齐
|
||||
const fixed = { 'one-large-five-small': 6, 'nine-grid': 9, 'masonry': 6, 'carousel': 5, 'polaroid': 6 }
|
||||
const need = fixed[p.type] || 6
|
||||
@@ -863,9 +936,15 @@ function ensurePageShape(p) {
|
||||
while (p.images.length < need) p.images.push('')
|
||||
if (p.images.length > need) p.images.length = need
|
||||
}
|
||||
delete p.img1; delete p.img2
|
||||
syncImagesResized(p)
|
||||
delete p.img1
|
||||
delete p.img2
|
||||
delete p.img1Resized
|
||||
delete p.img2Resized
|
||||
delete p.img1ResizedMeta
|
||||
delete p.img2ResizedMeta
|
||||
}
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
|
||||
p.height = p.height || '100vh'
|
||||
p.borderStyle = p.borderStyle || 'mat'
|
||||
// 单图宽度设置(仅 single 版式使用,逐页配置)
|
||||
@@ -878,13 +957,118 @@ function ensurePageShape(p) {
|
||||
function isFlexible(type) {
|
||||
return ['masonry', 'carousel', 'polaroid'].includes(type)
|
||||
}
|
||||
function addImage(page) { if (!Array.isArray(page.images)) page.images = []; page.images.push('') }
|
||||
function removeImage(page, i) { if (page.images.length > 1) page.images.splice(i, 1) }
|
||||
function addImage(page) {
|
||||
if (!Array.isArray(page.images)) page.images = []
|
||||
page.images.push('')
|
||||
syncImagesResized(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)
|
||||
syncImagesResized(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
|
||||
syncImagesResized(page)
|
||||
if (Array.isArray(page.imagesResized)) {
|
||||
const r = page.imagesResized[i]
|
||||
page.imagesResized[i] = page.imagesResized[j]
|
||||
page.imagesResized[j] = r
|
||||
}
|
||||
if (Array.isArray(page.imagesResizedMeta)) {
|
||||
const m = page.imagesResizedMeta[i]
|
||||
page.imagesResizedMeta[i] = page.imagesResizedMeta[j]
|
||||
page.imagesResizedMeta[j] = m
|
||||
}
|
||||
}
|
||||
|
||||
function pageHasOriginal(page) {
|
||||
return getSlotResizeSpecs(page).some((s) => !!s.getOriginal())
|
||||
}
|
||||
|
||||
function setOriginalAndClearResized(page, key, url) {
|
||||
page[key] = url
|
||||
page[`${key}Resized`] = ''
|
||||
page[`${key}ResizedMeta`] = ''
|
||||
}
|
||||
|
||||
function setOriginalImageAt(page, iidx, url) {
|
||||
page.images[iidx] = url
|
||||
syncImagesResized(page)
|
||||
page.imagesResized[iidx] = ''
|
||||
page.imagesResizedMeta[iidx] = ''
|
||||
}
|
||||
|
||||
/** 缺 meta,或只有像素没有文件大小时,自动探测并补全 */
|
||||
async function fillMissingResizedMeta(page) {
|
||||
if (!page) return
|
||||
if (page.type === 'single' || page.type === 'overlap') {
|
||||
const keys = page.type === 'single' ? ['img1'] : ['img1', 'img2']
|
||||
await Promise.all(keys.map(async (key) => {
|
||||
const original = page[key]
|
||||
const resized = page[`${key}Resized`]
|
||||
const metaKey = `${key}ResizedMeta`
|
||||
if (!resized || !original || metaHasFileSize(page[metaKey])) return
|
||||
try {
|
||||
page[metaKey] = await probeResizedMeta(original, resized)
|
||||
} catch { /* 跨域/失效图忽略 */ }
|
||||
}))
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(page.images)) return
|
||||
syncImagesResized(page)
|
||||
await Promise.all(page.images.map(async (original, i) => {
|
||||
const resized = page.imagesResized[i]
|
||||
if (!resized || !original || metaHasFileSize(page.imagesResizedMeta[i])) return
|
||||
try {
|
||||
page.imagesResizedMeta[i] = await probeResizedMeta(original, resized)
|
||||
} catch { /* ignore */ }
|
||||
}))
|
||||
}
|
||||
|
||||
async function batchResizeAndUpload(page) {
|
||||
const slots = getSlotResizeSpecs(page).filter((s) => !!s.getOriginal())
|
||||
if (!slots.length) {
|
||||
message.warning('请先上传原图')
|
||||
return
|
||||
}
|
||||
resizingPage.value = true
|
||||
resizeProgress.value = `0/${slots.length}`
|
||||
let ok = 0
|
||||
let fail = 0
|
||||
try {
|
||||
for (let i = 0; i < slots.length; i += 1) {
|
||||
const slot = slots[i]
|
||||
resizeProgress.value = `${i + 1}/${slots.length}`
|
||||
try {
|
||||
const { file, meta } = await resizeSlotToFile(
|
||||
slot.getOriginal(),
|
||||
slot.maxWidth,
|
||||
slot.maxHeight,
|
||||
`resized-${page.type}-${slot.key}-${Date.now()}.jpg`,
|
||||
)
|
||||
const res = await uploadImage(file)
|
||||
if (!res?.url) throw new Error('上传无返回地址')
|
||||
slot.setResized(res.url, meta)
|
||||
ok += 1
|
||||
} catch (err) {
|
||||
fail += 1
|
||||
console.warn('[resize]', slot.key, err)
|
||||
}
|
||||
}
|
||||
if (fail === 0) message.success(`已生成 ${ok} 张展示图`)
|
||||
else if (ok > 0) message.warning(`成功 ${ok} 张,失败 ${fail} 张(常见原因:跨域无法读取原图)`)
|
||||
else message.error('缩放上传全部失败,请确认原图可跨域读取')
|
||||
} finally {
|
||||
resizingPage.value = false
|
||||
resizeProgress.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 相册页操作
|
||||
@@ -1063,7 +1247,13 @@ function applySectionData(section, loaded) {
|
||||
if (typeof loaded.nineGridAnimate !== 'boolean') loaded.nineGridAnimate = false
|
||||
}
|
||||
Object.assign(cfg, loaded)
|
||||
if (section === 'photos') setActivePhotoIndex(currentPhotoIndex.value)
|
||||
if (section === 'photos') {
|
||||
setActivePhotoIndex(currentPhotoIndex.value)
|
||||
nextTick(() => {
|
||||
const page = cfg.photoPages[currentPhotoIndex.value]
|
||||
if (page) fillMissingResizedMeta(page)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfigSection(section) {
|
||||
@@ -1281,6 +1471,16 @@ watch(activeTab, (tab) => {
|
||||
onTabActivate(tab)
|
||||
}, { immediate: true })
|
||||
|
||||
// 进入相册页 / 切换页签时,为缺 meta 的展示图自动探测尺寸
|
||||
watch(
|
||||
() => [activeTab.value, currentPhotoIndex.value],
|
||||
([tab]) => {
|
||||
if (tab !== 'photos') return
|
||||
const page = cfg.photoPages[currentPhotoIndex.value]
|
||||
if (page) fillMissingResizedMeta(page)
|
||||
},
|
||||
)
|
||||
|
||||
const onResize = () => { regionChart?.resize() }
|
||||
|
||||
onMounted(() => {
|
||||
@@ -1294,6 +1494,14 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.resized-meta {
|
||||
margin-top: 2px;
|
||||
font-size: 9px;
|
||||
line-height: 1.35;
|
||||
color: #8A8680;
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
}
|
||||
.admin-tabs :deep(.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn) { color: #a88c6b; }
|
||||
.admin-tabs :deep(.ant-tabs-ink-bar) { background: #a88c6b; }
|
||||
.admin-tabs :deep(.ant-tabs-tab:hover .ant-tabs-tab-btn) { color: #967b5c; }
|
||||
|
||||
Reference in New Issue
Block a user