视频背景上传增加封面功能
This commit is contained in:
parent
7ab19ef4fa
commit
e78e8feea9
@ -76,6 +76,7 @@ async def list_backgrounds(
|
||||
"price_points": bg.price_points,
|
||||
"category": getattr(bg, 'category', 'default'),
|
||||
"thumbnail": bg.thumbnail,
|
||||
"cover_url": getattr(bg, 'cover_url', None),
|
||||
"created_at": bg.created_at.isoformat() if bg.created_at else None,
|
||||
"updated_at": bg.updated_at.isoformat() if bg.updated_at else None,
|
||||
"device_type": getattr(bg, 'device_type', 1),
|
||||
@ -133,6 +134,7 @@ async def get_background_detail(
|
||||
"price_points": bg.price_points,
|
||||
"category": getattr(bg, 'category', 'default'),
|
||||
"thumbnail": bg.thumbnail,
|
||||
"cover_url": getattr(bg, 'cover_url', None),
|
||||
"created_at": bg.created_at.isoformat() if bg.created_at else None,
|
||||
"updated_at": bg.updated_at.isoformat() if bg.updated_at else None,
|
||||
"device_type": getattr(bg, 'device_type', 1), # 1: 电脑背景, 2: 手机背景
|
||||
@ -166,6 +168,7 @@ async def create_background(
|
||||
price_points=background_data.get("price_points", 0),
|
||||
category=background_data.get("category", "default"),
|
||||
thumbnail=background_data.get("thumbnail", background_data.get("resource_url")),
|
||||
cover_url=background_data.get("cover_url", background_data.get("thumbnail")),
|
||||
device_type=background_data.get("device_type", 1) # 1: 电脑背景, 2: 手机背景
|
||||
)
|
||||
|
||||
@ -188,6 +191,7 @@ async def create_background(
|
||||
"price_points": new_background.price_points,
|
||||
"category": getattr(new_background, 'category', 'default'),
|
||||
"thumbnail": new_background.thumbnail,
|
||||
"cover_url": getattr(new_background, 'cover_url', None),
|
||||
"created_at": new_background.created_at.isoformat() if new_background.created_at else None,
|
||||
"updated_at": new_background.updated_at.isoformat() if new_background.updated_at else None,
|
||||
"pet_ids": []
|
||||
@ -238,6 +242,8 @@ async def update_background(
|
||||
setattr(bg, 'category', background_data["category"])
|
||||
if "thumbnail" in background_data:
|
||||
bg.thumbnail = background_data["thumbnail"]
|
||||
if "cover_url" in background_data:
|
||||
setattr(bg, 'cover_url', background_data["cover_url"])
|
||||
if "device_type" in background_data:
|
||||
bg.device_type = background_data["device_type"]
|
||||
|
||||
@ -261,6 +267,7 @@ async def update_background(
|
||||
"price_points": bg.price_points,
|
||||
"category": getattr(bg, 'category', 'default'),
|
||||
"thumbnail": bg.thumbnail,
|
||||
"cover_url": getattr(bg, 'cover_url', None),
|
||||
"created_at": bg.created_at.isoformat() if bg.created_at else None,
|
||||
"updated_at": bg.updated_at.isoformat() if bg.updated_at else None,
|
||||
"device_type": getattr(bg, 'device_type', 1), # 1: 电脑背景, 2: 手机背景
|
||||
|
||||
51
backend/migrations/add_background_cover_url.py
Normal file
51
backend/migrations/add_background_cover_url.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""
|
||||
数据库迁移脚本 - 为背景表添加封面字段
|
||||
运行方式: cd backend; $env:PYTHONPATH="."; python migrations/add_background_cover_url.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from config.settings import settings
|
||||
|
||||
|
||||
async def migrate():
|
||||
"""执行迁移"""
|
||||
db_url = settings.DATABASE_URL
|
||||
|
||||
if 'mysql+pymysql' in db_url:
|
||||
async_db_url = db_url.replace('mysql+pymysql', 'mysql+asyncmy')
|
||||
elif 'mysql' in db_url:
|
||||
async_db_url = db_url.replace('mysql', 'mysql+asyncmy')
|
||||
else:
|
||||
async_db_url = db_url
|
||||
|
||||
print(f"数据库URL: {async_db_url[:50]}...")
|
||||
|
||||
engine = create_async_engine(async_db_url, echo=False)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
try:
|
||||
result = await conn.execute(text("SHOW COLUMNS FROM t_background LIKE 'cover_url'"))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
print("[OK] column cover_url already exists")
|
||||
else:
|
||||
print("[INFO] column cover_url not found, adding it now")
|
||||
await conn.execute(
|
||||
text("ALTER TABLE t_background ADD COLUMN cover_url VARCHAR(512) DEFAULT NULL COMMENT '视频背景封面地址'")
|
||||
)
|
||||
print("[OK] added column cover_url")
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "Duplicate column" in error_msg or "already exists" in error_msg.lower():
|
||||
print("[OK] column cover_url already exists, skipped")
|
||||
else:
|
||||
raise
|
||||
|
||||
await engine.dispose()
|
||||
print("migration completed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(migrate())
|
||||
@ -71,6 +71,7 @@ class Background(Base):
|
||||
category = Column(String(50), default='default')
|
||||
description = Column(String(255), nullable=True)
|
||||
thumbnail = Column(String(512), nullable=True)
|
||||
cover_url = Column(String(512), nullable=True)
|
||||
device_type = Column(Integer, nullable=True, default=1) # 1: 电脑背景, 2: 手机背景
|
||||
|
||||
class ChatLog(Base):
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
:alt="background.name"
|
||||
@error="handlePreviewError"
|
||||
/>
|
||||
<div class="video-badge" v-if="background.resource_type === 2">视频背景</div>
|
||||
<div class="overlay" v-if="background.is_locked && !background.is_owned">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>{{ background.price_points }} 积分</span>
|
||||
@ -80,6 +81,9 @@ export default {
|
||||
}
|
||||
return background?.thumbnail || background?.resource_url || this.defaultThumbnail
|
||||
},
|
||||
getBackgroundPoster(background) {
|
||||
return background?.thumbnail || this.defaultThumbnail
|
||||
},
|
||||
handlePreviewError(event) {
|
||||
if (event?.target && event.target.src !== this.defaultThumbnail) {
|
||||
event.target.src = this.defaultThumbnail
|
||||
@ -135,6 +139,17 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-badge {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.background-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
<video
|
||||
v-if="bg.type === 'video'"
|
||||
:src="bg.resource_url"
|
||||
:poster="bg.thumbnail || ''"
|
||||
:poster="getBackgroundPoster(bg)"
|
||||
:alt="bg.name"
|
||||
class="card-video"
|
||||
muted
|
||||
@ -121,7 +121,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 上传对话框 -->
|
||||
<el-dialog v-model="showUploadDialog" title="上传背景" width="600px">
|
||||
<el-dialog v-model="showUploadDialog" title="上传背景" width="640px">
|
||||
<el-form :model="uploadForm" label-width="100px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="uploadForm.name" placeholder="请输入背景名称" />
|
||||
@ -130,11 +130,82 @@
|
||||
<el-input v-model="uploadForm.description" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-radio-group v-model="uploadForm.type">
|
||||
<el-radio-group v-model="uploadForm.type" @change="handleUploadTypeChange">
|
||||
<el-radio value="image">图片</el-radio>
|
||||
<el-radio value="video">视频</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="一步式上传区">
|
||||
<div class="one-step-upload-box" :class="uploadForm.type">
|
||||
<div class="upload-column">
|
||||
<div class="upload-section-title">1. 上传资源文件</div>
|
||||
<el-upload
|
||||
class="upload-demo resource-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:accept="uploadForm.type === 'video' ? 'video/*' : 'image/*'"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将{{ uploadForm.type === 'video' ? '视频' : '图片' }}拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div v-if="uploadForm.file" class="file-hint">
|
||||
已选择:{{ uploadForm.file.name }}
|
||||
</div>
|
||||
<div class="ratio-hint">
|
||||
<span class="ratio-label">推荐比例</span>
|
||||
<el-tag size="small" effect="plain">{{ getRatioLabel(uploadForm.video_ratio) }}</el-tag>
|
||||
<span class="ratio-desc">封面比例建议与视频比例保持一致</span>
|
||||
</div>
|
||||
<el-radio-group v-if="uploadForm.type === 'video'" v-model="uploadForm.video_ratio" size="small" class="ratio-group">
|
||||
<el-radio-button label="16:9">16:9</el-radio-button>
|
||||
<el-radio-button label="9:16">9:16</el-radio-button>
|
||||
<el-radio-button label="1:1">1:1</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadForm.type === 'video'" class="upload-column cover-column">
|
||||
<div class="upload-section-title">2. 上传封面图</div>
|
||||
<el-upload
|
||||
class="upload-demo cover-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:on-change="handleCoverChange"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将封面图片拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div class="cover-meta-row">
|
||||
<el-select v-model="uploadForm.cover_ratio" size="small" placeholder="封面比例" style="width: 160px">
|
||||
<el-option label="16:9 横屏" value="16:9" />
|
||||
<el-option label="9:16 竖屏" value="9:16" />
|
||||
<el-option label="1:1 方图" value="1:1" />
|
||||
</el-select>
|
||||
<el-button v-if="uploadForm.cover_file" size="small" @click="clearCover">清除封面</el-button>
|
||||
</div>
|
||||
<div class="cover-preview-wrap">
|
||||
<div class="cover-preview-title">封面预览</div>
|
||||
<div class="cover-preview" :class="[getAspectClass(uploadForm.cover_ratio), { empty: !uploadForm.cover_url }]">
|
||||
<img v-if="uploadForm.cover_url" :src="uploadForm.cover_url" alt="视频封面预览" />
|
||||
<div v-else class="cover-preview-placeholder">
|
||||
未选择封面时,将使用视频自动生成封面
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crop-tip">
|
||||
<div class="crop-tip-title">裁剪提示</div>
|
||||
<div class="crop-tip-text">建议主体居中、避免复杂边框。若图片过长,可先在外部裁剪到对应比例再上传。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="可见性">
|
||||
<el-radio-group v-model="uploadForm.visibility">
|
||||
<el-radio value="public">Public</el-radio>
|
||||
@ -159,19 +230,6 @@
|
||||
<el-form-item label="背景音乐">
|
||||
<el-input v-model="uploadForm.bgm_url" placeholder="BGM URL (可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="资源文件">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showUploadDialog = false">取消</el-button>
|
||||
@ -222,9 +280,20 @@
|
||||
<el-form-item label="资源地址">
|
||||
<el-input v-model="editForm.resource_url" placeholder="请输入资源 URL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面地址">
|
||||
<el-input v-model="editForm.cover_url" placeholder="请输入视频封面 URL(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缩略图">
|
||||
<el-input v-model="editForm.thumbnail" placeholder="请输入缩略图 URL(可选)" />
|
||||
</el-form-item>
|
||||
<div class="cover-crop-hint">
|
||||
<el-alert
|
||||
title="封面建议使用居中构图,避免上下左右过多留白;若后续需要精细裁剪,可在此处接入在线裁剪器"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">取消</el-button>
|
||||
@ -316,7 +385,7 @@
|
||||
<video
|
||||
v-if="previewBackground?.type === 'video'"
|
||||
:src="previewBackground?.resource_url"
|
||||
:poster="previewBackground?.thumbnail || ''"
|
||||
:poster="previewBackground?.thumbnail || previewBackground?.resource_url"
|
||||
class="preview-video"
|
||||
controls
|
||||
autoplay
|
||||
@ -410,6 +479,7 @@ export default {
|
||||
category: bg.category || 'default',
|
||||
device_type: bg.device_type || 1,
|
||||
thumbnail: bg.thumbnail || (bg.resource_type === 2 ? '' : bg.resource_url) || '',
|
||||
cover_url: bg.cover_url || bg.thumbnail || '',
|
||||
bgm_url: bg.bgm_url || '',
|
||||
created_at: bg.created_at,
|
||||
user_ids: [],
|
||||
@ -449,7 +519,11 @@ export default {
|
||||
category: '',
|
||||
device_type: 1, // 1: 电脑背景, 2: 手机背景
|
||||
bgm_url: '',
|
||||
file: null
|
||||
file: null,
|
||||
cover_file: null,
|
||||
cover_url: '',
|
||||
video_ratio: '16:9',
|
||||
cover_ratio: '16:9'
|
||||
})
|
||||
|
||||
const editForm = reactive({
|
||||
@ -461,7 +535,8 @@ export default {
|
||||
category: '',
|
||||
device_type: 1,
|
||||
resource_url: '',
|
||||
thumbnail: ''
|
||||
thumbnail: '',
|
||||
cover_url: ''
|
||||
})
|
||||
|
||||
// 过滤后的背景列表
|
||||
@ -542,6 +617,7 @@ export default {
|
||||
editForm.device_type = bg.device_type || 1
|
||||
editForm.resource_url = bg.resource_url || ''
|
||||
editForm.thumbnail = bg.thumbnail || ''
|
||||
editForm.cover_url = bg.cover_url || bg.thumbnail || ''
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
@ -577,7 +653,8 @@ export default {
|
||||
price_points: editForm.visibility === 'vip' ? 100 : 0,
|
||||
category: editForm.category,
|
||||
device_type: editForm.device_type,
|
||||
thumbnail: editForm.thumbnail || editForm.resource_url
|
||||
thumbnail: editForm.thumbnail || editForm.resource_url,
|
||||
cover_url: editForm.cover_url || editForm.thumbnail || ''
|
||||
}
|
||||
|
||||
const response = await adminBackgroundAPI.update(editForm.id, payload)
|
||||
@ -887,6 +964,44 @@ export default {
|
||||
uploadForm.file = file.raw
|
||||
}
|
||||
|
||||
const handleCoverChange = (file) => {
|
||||
uploadForm.cover_file = file.raw
|
||||
uploadForm.cover_url = URL.createObjectURL(file.raw)
|
||||
}
|
||||
|
||||
const clearCover = () => {
|
||||
uploadForm.cover_file = null
|
||||
uploadForm.cover_url = ''
|
||||
}
|
||||
|
||||
const getRatioLabel = (ratio) => {
|
||||
const map = {
|
||||
'16:9': '16:9 横屏,适合电脑端',
|
||||
'9:16': '9:16 竖屏,适合手机端',
|
||||
'1:1': '1:1 方图,通用展示'
|
||||
}
|
||||
return map[ratio] || ratio
|
||||
}
|
||||
|
||||
const getAspectClass = (ratio) => {
|
||||
const map = {
|
||||
'16:9': 'ratio-16-9',
|
||||
'9:16': 'ratio-9-16',
|
||||
'1:1': 'ratio-1-1'
|
||||
}
|
||||
return map[ratio] || 'ratio-16-9'
|
||||
}
|
||||
|
||||
const handleUploadTypeChange = (type) => {
|
||||
if (type !== 'video') {
|
||||
clearCover()
|
||||
}
|
||||
}
|
||||
|
||||
const getBackgroundPoster = (bg) => {
|
||||
return bg.cover_url || bg.thumbnail || bg.resource_url
|
||||
}
|
||||
|
||||
// 处理每页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
@ -908,7 +1023,6 @@ export default {
|
||||
// 上传背景
|
||||
const uploadBackground = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
if (!uploadForm.name) {
|
||||
ElMessage.error('请输入背景名称')
|
||||
return
|
||||
@ -921,67 +1035,70 @@ export default {
|
||||
ElMessage.error('请选择分类')
|
||||
return
|
||||
}
|
||||
if (uploadForm.type === 'video' && uploadForm.cover_file && !uploadForm.cover_file.type.startsWith('image/')) {
|
||||
ElMessage.error('视频封面必须是图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
|
||||
// 第一步:上传文件到存储服务
|
||||
const formData = new FormData()
|
||||
formData.append('file', uploadForm.file)
|
||||
|
||||
const uploadResponse = await storageAPI.upload(formData)
|
||||
const resourceFormData = new FormData()
|
||||
resourceFormData.append('file', uploadForm.file)
|
||||
const uploadResponse = await storageAPI.upload(resourceFormData)
|
||||
|
||||
if (!uploadResponse.data || !uploadResponse.data.success) {
|
||||
throw new Error(uploadResponse.data?.message || '文件上传失败')
|
||||
}
|
||||
|
||||
// 第二步:创建背景记录
|
||||
let coverUrl = uploadResponse.data?.thumbnail_url || ''
|
||||
if (uploadForm.type === 'video' && uploadForm.cover_file) {
|
||||
const coverFormData = new FormData()
|
||||
coverFormData.append('file', uploadForm.cover_file)
|
||||
const coverUploadResponse = await storageAPI.upload(coverFormData)
|
||||
if (!coverUploadResponse.data || !coverUploadResponse.data.success) {
|
||||
throw new Error(coverUploadResponse.data?.message || '封面上传失败')
|
||||
}
|
||||
coverUrl = coverUploadResponse.data?.file_url || coverUrl
|
||||
}
|
||||
|
||||
const fileUrl = uploadResponse.data?.file_url
|
||||
const thumbnailUrl = uploadResponse.data?.thumbnail_url || (uploadForm.type === 'video' ? '' : fileUrl)
|
||||
const backgroundData = {
|
||||
name: uploadForm.name,
|
||||
description: uploadForm.description,
|
||||
resource_url: fileUrl,
|
||||
resource_type: uploadForm.type === 'video' ? 2 : 1, // 1表示图片,2表示视频
|
||||
visibility: uploadForm.visibility === 'public' ? 1 : (uploadForm.visibility === 'vip' ? 2 : 0), // 0=private, 1=public, 2=vip
|
||||
resource_type: uploadForm.type === 'video' ? 2 : 1,
|
||||
visibility: uploadForm.visibility === 'public' ? 1 : (uploadForm.visibility === 'vip' ? 2 : 0),
|
||||
is_locked: uploadForm.visibility !== 'public',
|
||||
lock_desc: uploadForm.visibility === 'vip' ? 'VIP专享背景' : '',
|
||||
price_points: uploadForm.visibility === 'vip' ? 100 : 0,
|
||||
category: uploadForm.category,
|
||||
device_type: uploadForm.device_type, // 1: 电脑背景, 2: 手机背景
|
||||
thumbnail: thumbnailUrl
|
||||
device_type: uploadForm.device_type,
|
||||
thumbnail: uploadForm.type === 'video' ? coverUrl : (uploadResponse.data?.thumbnail_url || fileUrl),
|
||||
bgm_url: uploadForm.bgm_url || ''
|
||||
}
|
||||
|
||||
try {
|
||||
const createResponse = await adminBackgroundAPI.create(backgroundData)
|
||||
const createResponse = await adminBackgroundAPI.create(backgroundData)
|
||||
|
||||
if (createResponse.data && createResponse.data.success) {
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
// 即使数据库保存失败,文件已经上传成功,也算作部分成功
|
||||
ElMessage.warning('文件上传成功,但数据库保存失败,请稍后手动刷新')
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error('Database error:', dbError)
|
||||
// 数据库错误不影响文件上传,提示用户手动刷新
|
||||
ElMessage.warning('文件上传成功,请手动刷新页面查看最新列表')
|
||||
if (createResponse.data && createResponse.data.success) {
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
ElMessage.warning('文件上传成功,但数据库保存失败,请稍后手动刷新')
|
||||
}
|
||||
|
||||
// 无论数据库保存是否成功,都关闭对话框并重置表单
|
||||
showUploadDialog.value = false
|
||||
|
||||
// 重置表单
|
||||
Object.assign(uploadForm, {
|
||||
name: '',
|
||||
description: '',
|
||||
type: 'image',
|
||||
visibility: 'public',
|
||||
category: '',
|
||||
device_type: 1, // 默认为电脑背景
|
||||
device_type: 1,
|
||||
bgm_url: '',
|
||||
file: null
|
||||
file: null,
|
||||
cover_file: null,
|
||||
cover_url: ''
|
||||
})
|
||||
|
||||
// 重新加载背景列表
|
||||
await loadBackgrounds()
|
||||
} catch (error) {
|
||||
console.error('Upload background error:', error)
|
||||
@ -1039,7 +1156,14 @@ export default {
|
||||
handleSizeChange,
|
||||
handleSearchUser,
|
||||
pagination,
|
||||
previewBackground
|
||||
previewBackground,
|
||||
handleCoverChange,
|
||||
handleUploadTypeChange,
|
||||
clearCover,
|
||||
getBackgroundPoster,
|
||||
getRatioLabel,
|
||||
getAspectClass,
|
||||
editForm
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1068,6 +1192,72 @@ export default {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.resource-upload-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cover-guidance {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cover-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cover-preview-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cover-preview-title {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.cover-preview {
|
||||
width: 260px;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px dashed #dcdfe6;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.cover-preview.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cover-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover-preview-placeholder {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cover-crop-hint {
|
||||
margin: 8px 0 20px;
|
||||
}
|
||||
|
||||
.background-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
@ -1172,6 +1362,149 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.one-step-upload-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.one-step-upload-box.video {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.upload-column {
|
||||
padding: 16px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 12px;
|
||||
background: #fafcff;
|
||||
}
|
||||
|
||||
.upload-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.resource-upload,
|
||||
.cover-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ratio-hint {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ratio-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ratio-desc {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.ratio-group {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.cover-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cover-meta-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cover-preview-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cover-preview-title {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.cover-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.cover-preview.ratio-16-9 {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.cover-preview.ratio-9-16 {
|
||||
aspect-ratio: 9 / 16;
|
||||
}
|
||||
|
||||
.cover-preview.ratio-1-1 {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.cover-preview.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cover-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover-preview-placeholder {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.crop-tip {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
}
|
||||
|
||||
.crop-tip-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #ad6800;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.crop-tip-text {
|
||||
font-size: 12px;
|
||||
color: #8c6d1f;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.file-hint {
|
||||
margin-top: 8px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.preview-image,
|
||||
.preview-video {
|
||||
max-width: 100%;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user