feat(upload): 封面上传支持横版与前端压缩
- 新增 imageCompress 与 imagePresets,上传前按规格高质量等比压缩 - 短剧封面拆分为竖版/横版两个上传位,带建议尺寸提示 - 轮播图、创作者中心封面与分集封面接入压缩与提示
This commit is contained in:
1 parent
dea414739e
commit
923b7dce1e
9 files changed
+486
-87
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
import { pageDramasApi } from '@/api/admin/drama'
|
||||
import { useRbac } from '@/stores/rbac'
|
||||
import { uploadFile } from '@/utils/uploader'
|
||||
import { BANNER_IMAGE, compressForPreset } from '@/views/contentManagement/upload/imagePresets'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
|
||||
const { can } = useRbac()
|
||||
@@ -218,12 +219,13 @@ async function uploadImage(options) {
|
||||
imagePercent.value = 0
|
||||
imageError.value = ''
|
||||
try {
|
||||
const asset = await uploadFile(options.file, {
|
||||
const file = await compressForPreset(options.file, BANNER_IMAGE)
|
||||
const asset = await uploadFile(file, {
|
||||
bizType: 'BANNER',
|
||||
onProgress: (percent) => (imagePercent.value = percent)
|
||||
})
|
||||
form.imageUrl = asset.url
|
||||
imageFileName.value = options.file.name
|
||||
imageFileName.value = file.name
|
||||
await resetValidation()
|
||||
ElMessage.success('图片已上传')
|
||||
} catch {
|
||||
@@ -467,7 +469,9 @@ onMounted(async () => {
|
||||
{{ imageDisplayName }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="banner-image-hint">支持 JPG、PNG、WebP,单张不超过 5MB。</p>
|
||||
<p class="banner-image-hint"
|
||||
>{{ BANNER_IMAGE.hint }};支持 JPG、PNG、WebP,单张不超过 5MB。</p
|
||||
>
|
||||
<el-alert
|
||||
v-if="imageError"
|
||||
:title="imageError"
|
||||
|
||||
@@ -26,6 +26,11 @@ import { formatCount } from '@/utils/format'
|
||||
import { listContentTagsApi } from '@/api/admin/contentTag'
|
||||
import { statusClass } from '@/utils/status'
|
||||
import { uploadFile } from '@/utils/uploader'
|
||||
import {
|
||||
DRAMA_COVER_LANDSCAPE,
|
||||
DRAMA_COVER_PORTRAIT,
|
||||
compressForPreset
|
||||
} from '@/views/contentManagement/upload/imagePresets'
|
||||
|
||||
const router = useRouter()
|
||||
const { can, hasAny } = useRbac()
|
||||
@@ -48,6 +53,10 @@ const uploadingCover = ref(false)
|
||||
const coverPercent = ref(0)
|
||||
const coverError = ref('')
|
||||
const coverFileName = ref('')
|
||||
const uploadingHorizontal = ref(false)
|
||||
const horizontalPercent = ref(0)
|
||||
const horizontalError = ref('')
|
||||
const horizontalFileName = ref('')
|
||||
const editingId = ref(null)
|
||||
const editingCommentCount = ref(0)
|
||||
const editingLikeCount = ref(0)
|
||||
@@ -66,7 +75,8 @@ const form = reactive({
|
||||
heat: 0,
|
||||
isHot: false,
|
||||
sort: 0,
|
||||
cover: ''
|
||||
cover: '',
|
||||
horizontalCover: ''
|
||||
})
|
||||
|
||||
const MAX_COVER_SIZE = 5 * 1024 * 1024
|
||||
@@ -75,6 +85,11 @@ const COVER_TYPES = ['image/jpeg', 'image/png', 'image/webp']
|
||||
const coverDisplayName = computed(
|
||||
() => coverFileName.value || (form.cover ? coverBasename(form.cover) : '尚未选择封面')
|
||||
)
|
||||
const horizontalDisplayName = computed(
|
||||
() =>
|
||||
horizontalFileName.value ||
|
||||
(form.horizontalCover ? coverBasename(form.horizontalCover) : '尚未选择横版封面')
|
||||
)
|
||||
|
||||
const pageCoverPreviewList = computed(() => list.value.map((item) => item.cover).filter(Boolean))
|
||||
|
||||
@@ -192,10 +207,13 @@ function openCreate() {
|
||||
heat: 0,
|
||||
isHot: false,
|
||||
sort: 0,
|
||||
cover: ''
|
||||
cover: '',
|
||||
horizontalCover: ''
|
||||
})
|
||||
coverFileName.value = ''
|
||||
coverError.value = ''
|
||||
horizontalFileName.value = ''
|
||||
horizontalError.value = ''
|
||||
editingCommentCount.value = 0
|
||||
editingLikeCount.value = 0
|
||||
dialogVisible.value = true
|
||||
@@ -223,10 +241,13 @@ function openEdit(row) {
|
||||
heat: toCount(row.views),
|
||||
isHot: !!row.isHot,
|
||||
sort: toCount(row.sort),
|
||||
cover: row.cover || ''
|
||||
cover: row.cover || '',
|
||||
horizontalCover: row.horizontalCover || ''
|
||||
})
|
||||
coverFileName.value = row.cover ? coverBasename(row.cover) : ''
|
||||
coverError.value = ''
|
||||
horizontalFileName.value = row.horizontalCover ? coverBasename(row.horizontalCover) : ''
|
||||
horizontalError.value = ''
|
||||
editingCommentCount.value = toCount(row.commentCount)
|
||||
editingLikeCount.value = toCount(row.likeCount)
|
||||
dialogVisible.value = true
|
||||
@@ -245,7 +266,7 @@ function rowCategoryList(row) {
|
||||
}
|
||||
|
||||
async function saveDrama() {
|
||||
if (saving.value || uploadingCover.value) return
|
||||
if (saving.value || uploadingCover.value || uploadingHorizontal.value) return
|
||||
const titles = payloadLocales(form.titleLocales, textLanguages.value)
|
||||
const missing = textLanguages.value.find((code) => !titles[code])
|
||||
if (!titles.zh || missing) {
|
||||
@@ -282,32 +303,41 @@ async function saveDrama() {
|
||||
}
|
||||
}
|
||||
|
||||
function beforeCoverUpload(file) {
|
||||
coverError.value = ''
|
||||
function validateCoverFile(file, errorRef, label) {
|
||||
errorRef.value = ''
|
||||
if (!COVER_TYPES.includes(file.type)) {
|
||||
coverError.value = '封面仅支持 JPG、PNG 或 WebP 格式'
|
||||
errorRef.value = `${label}仅支持 JPG、PNG 或 WebP 格式`
|
||||
return false
|
||||
}
|
||||
if (file.size > MAX_COVER_SIZE) {
|
||||
coverError.value = '封面大小不能超过 5MB'
|
||||
errorRef.value = `${label}大小不能超过 5MB`
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 封面走分片上传(图片只有一个分片),成功后回填地址。 */
|
||||
function beforeCoverUpload(file) {
|
||||
return validateCoverFile(file, coverError, '竖版封面')
|
||||
}
|
||||
|
||||
function beforeHorizontalUpload(file) {
|
||||
return validateCoverFile(file, horizontalError, '横版封面')
|
||||
}
|
||||
|
||||
/** 封面走分片上传(图片只有一个分片),上传前按展示规格等比压缩,成功后回填地址。 */
|
||||
async function uploadCover(options) {
|
||||
uploadingCover.value = true
|
||||
coverPercent.value = 0
|
||||
coverError.value = ''
|
||||
try {
|
||||
const asset = await uploadFile(options.file, {
|
||||
const file = await compressForPreset(options.file, DRAMA_COVER_PORTRAIT)
|
||||
const asset = await uploadFile(file, {
|
||||
bizType: 'DRAMA_COVER',
|
||||
onProgress: (percent) => (coverPercent.value = percent)
|
||||
})
|
||||
form.cover = asset.url
|
||||
coverFileName.value = options.file.name
|
||||
ElMessage.success('封面已上传')
|
||||
coverFileName.value = file.name
|
||||
ElMessage.success('竖版封面已上传')
|
||||
} catch {
|
||||
coverError.value = '封面上传失败,请检查网络后重试'
|
||||
} finally {
|
||||
@@ -315,6 +345,26 @@ async function uploadCover(options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadHorizontalCover(options) {
|
||||
uploadingHorizontal.value = true
|
||||
horizontalPercent.value = 0
|
||||
horizontalError.value = ''
|
||||
try {
|
||||
const file = await compressForPreset(options.file, DRAMA_COVER_LANDSCAPE)
|
||||
const asset = await uploadFile(file, {
|
||||
bizType: 'DRAMA_COVER',
|
||||
onProgress: (percent) => (horizontalPercent.value = percent)
|
||||
})
|
||||
form.horizontalCover = asset.url
|
||||
horizontalFileName.value = file.name
|
||||
ElMessage.success('横版封面已上传')
|
||||
} catch {
|
||||
horizontalError.value = '横版封面上传失败,请检查网络后重试'
|
||||
} finally {
|
||||
uploadingHorizontal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeCover() {
|
||||
if (uploadingCover.value) return
|
||||
form.cover = ''
|
||||
@@ -322,8 +372,15 @@ function removeCover() {
|
||||
coverError.value = ''
|
||||
}
|
||||
|
||||
function removeHorizontalCover() {
|
||||
if (uploadingHorizontal.value) return
|
||||
form.horizontalCover = ''
|
||||
horizontalFileName.value = ''
|
||||
horizontalError.value = ''
|
||||
}
|
||||
|
||||
function beforeDialogClose(done) {
|
||||
if (saving.value || uploadingCover.value) {
|
||||
if (saving.value || uploadingCover.value || uploadingHorizontal.value) {
|
||||
ElMessage.warning(saving.value ? '正在保存,请稍候' : '封面正在上传,请稍候')
|
||||
return
|
||||
}
|
||||
@@ -572,65 +629,140 @@ onMounted(async () => {
|
||||
<el-form ref="formRef" :model="form" label-position="top" class="drama-editor-form">
|
||||
<section class="editor-section">
|
||||
<h3 class="editor-section-title">封面</h3>
|
||||
<div class="cover-editor">
|
||||
<div class="cover-preview-card">
|
||||
<img v-if="form.cover" :src="form.cover" alt="短剧封面" />
|
||||
<div v-else class="cover-preview-empty">
|
||||
<AppIcon :icon="AppIcons.content" :size="28" />
|
||||
<span>暂无封面</span>
|
||||
<div class="cover-block">
|
||||
<p class="cover-label">竖版封面</p>
|
||||
<div class="cover-editor">
|
||||
<div class="cover-preview-card">
|
||||
<img v-if="form.cover" :src="form.cover" alt="短剧竖版封面" />
|
||||
<div v-else class="cover-preview-empty">
|
||||
<AppIcon :icon="AppIcons.content" :size="28" />
|
||||
<span>暂无封面</span>
|
||||
</div>
|
||||
<div v-if="uploadingCover" class="cover-upload-mask">
|
||||
<span>上传中</span>
|
||||
<b>{{ coverPercent }}%</b>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="uploadingCover" class="cover-upload-mask">
|
||||
<span>上传中</span>
|
||||
<b>{{ coverPercent }}%</b>
|
||||
|
||||
<div class="cover-editor-meta">
|
||||
<div class="cover-status-line">
|
||||
<el-tag :type="form.cover ? 'success' : 'info'" effect="light">
|
||||
{{ form.cover ? '已上传' : '未上传' }}
|
||||
</el-tag>
|
||||
<span class="cover-filename" :title="coverDisplayName">{{ coverDisplayName }}</span>
|
||||
</div>
|
||||
<p class="cover-hint"
|
||||
>{{ DRAMA_COVER_PORTRAIT.hint }};支持 JPG、PNG、WebP,单张不超过 5MB。</p
|
||||
>
|
||||
<el-alert
|
||||
v-if="coverError"
|
||||
:title="coverError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="cover-actions">
|
||||
<el-upload
|
||||
v-if="can('cover:upload')"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
:before-upload="beforeCoverUpload"
|
||||
:http-request="uploadCover"
|
||||
:disabled="uploadingCover"
|
||||
>
|
||||
<el-button :loading="uploadingCover" :disabled="uploadingCover">
|
||||
<AppIcon :icon="AppIcons.upload" />
|
||||
{{ form.cover ? '更换封面' : '选择封面' }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="can('cover:upload') && form.cover"
|
||||
type="danger"
|
||||
:disabled="uploadingCover"
|
||||
@click="removeCover"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
<a
|
||||
v-if="form.cover"
|
||||
class="cover-original-link"
|
||||
:href="form.cover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
查看原图
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cover-editor-meta">
|
||||
<div class="cover-status-line">
|
||||
<el-tag :type="form.cover ? 'success' : 'info'" effect="light">
|
||||
{{ form.cover ? '已上传' : '未上传' }}
|
||||
</el-tag>
|
||||
<span class="cover-filename" :title="coverDisplayName">{{ coverDisplayName }}</span>
|
||||
<div class="cover-block">
|
||||
<p class="cover-label">横版封面</p>
|
||||
<div class="cover-editor">
|
||||
<div class="cover-preview-card cover-preview-card--landscape">
|
||||
<img v-if="form.horizontalCover" :src="form.horizontalCover" alt="短剧横版封面" />
|
||||
<div v-else class="cover-preview-empty">
|
||||
<AppIcon :icon="AppIcons.content" :size="28" />
|
||||
<span>暂无横版封面</span>
|
||||
</div>
|
||||
<div v-if="uploadingHorizontal" class="cover-upload-mask">
|
||||
<span>上传中</span>
|
||||
<b>{{ horizontalPercent }}%</b>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cover-hint">支持 JPG、PNG、WebP,单张不超过 5MB。</p>
|
||||
<el-alert
|
||||
v-if="coverError"
|
||||
:title="coverError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="cover-actions">
|
||||
<el-upload
|
||||
v-if="can('cover:upload')"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
:before-upload="beforeCoverUpload"
|
||||
:http-request="uploadCover"
|
||||
:disabled="uploadingCover"
|
||||
>
|
||||
<el-button :loading="uploadingCover" :disabled="uploadingCover">
|
||||
<AppIcon :icon="AppIcons.upload" />
|
||||
{{ form.cover ? '更换封面' : '选择封面' }}
|
||||
|
||||
<div class="cover-editor-meta">
|
||||
<div class="cover-status-line">
|
||||
<el-tag :type="form.horizontalCover ? 'success' : 'info'" effect="light">
|
||||
{{ form.horizontalCover ? '已上传' : '未上传' }}
|
||||
</el-tag>
|
||||
<span class="cover-filename" :title="horizontalDisplayName">{{
|
||||
horizontalDisplayName
|
||||
}}</span>
|
||||
</div>
|
||||
<p class="cover-hint">
|
||||
{{ DRAMA_COVER_LANDSCAPE.hint }};支持 JPG、PNG、WebP,单张不超过 5MB。
|
||||
</p>
|
||||
<el-alert
|
||||
v-if="horizontalError"
|
||||
:title="horizontalError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="cover-actions">
|
||||
<el-upload
|
||||
v-if="can('cover:upload')"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
:before-upload="beforeHorizontalUpload"
|
||||
:http-request="uploadHorizontalCover"
|
||||
:disabled="uploadingHorizontal"
|
||||
>
|
||||
<el-button :loading="uploadingHorizontal" :disabled="uploadingHorizontal">
|
||||
<AppIcon :icon="AppIcons.upload" />
|
||||
{{ form.horizontalCover ? '更换横版封面' : '选择横版封面' }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="can('cover:upload') && form.horizontalCover"
|
||||
type="danger"
|
||||
:disabled="uploadingHorizontal"
|
||||
@click="removeHorizontalCover"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="can('cover:upload') && form.cover"
|
||||
type="danger"
|
||||
:disabled="uploadingCover"
|
||||
@click="removeCover"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
<a
|
||||
v-if="form.cover"
|
||||
class="cover-original-link"
|
||||
:href="form.cover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
查看原图
|
||||
</a>
|
||||
<a
|
||||
v-if="form.horizontalCover"
|
||||
class="cover-original-link"
|
||||
:href="form.horizontalCover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
查看原图
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import { compressImage } from '@/utils/imageCompress'
|
||||
|
||||
/**
|
||||
* 图片上传规格:建议尺寸来自客户端实际展示位,按 3 倍屏预留像素。
|
||||
*
|
||||
* - 竖版封面 3:4:首页三列 ~118dp、推荐分类两列 ~175dp、追剧收藏 ~118dp,
|
||||
* 同时作为播放器起播首帧铺满竖屏,1080×1440 是兼顾清晰度与体积的平衡点。
|
||||
* - 横版封面 16:9:追剧页「继续观看」横幅(约 2.06:1)与后续横向位共用。
|
||||
* - 推荐轮播:客户端轮播高度固定 420dp、按屏宽铺满(约 1:1.08)。
|
||||
*/
|
||||
export interface ImagePreset {
|
||||
label: string
|
||||
/** 建议尺寸文案,展示在上传按钮旁。 */
|
||||
hint: string
|
||||
maxWidth: number
|
||||
maxHeight: number
|
||||
}
|
||||
|
||||
export const DRAMA_COVER_PORTRAIT: ImagePreset = {
|
||||
label: '竖版封面',
|
||||
hint: '建议 1080×1440(3:4),最小 720×960',
|
||||
maxWidth: 1080,
|
||||
maxHeight: 1440
|
||||
}
|
||||
|
||||
export const DRAMA_COVER_LANDSCAPE: ImagePreset = {
|
||||
label: '横版封面',
|
||||
hint: '建议 1280×720(16:9),用于继续观看横幅等横向位',
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720
|
||||
}
|
||||
|
||||
export const EPISODE_COVER: ImagePreset = {
|
||||
label: '分集封面',
|
||||
hint: '建议 720×960(3:4)',
|
||||
maxWidth: 720,
|
||||
maxHeight: 960
|
||||
}
|
||||
|
||||
export const BANNER_IMAGE: ImagePreset = {
|
||||
label: '推荐轮播图',
|
||||
hint: '建议 1080×1164(约 1:1.08,与客户端 420dp 轮播高度一致)',
|
||||
maxWidth: 1080,
|
||||
maxHeight: 1164
|
||||
}
|
||||
|
||||
/** 按规格压缩:尺寸未超且体积不大时原样返回,不做二次编码。 */
|
||||
export function compressForPreset(file: File, preset: ImagePreset): Promise<File> {
|
||||
return compressImage(file, {
|
||||
maxWidth: preset.maxWidth,
|
||||
maxHeight: preset.maxHeight
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user