802 lines
20 KiB
Vue
802 lines
20 KiB
Vue
<template>
|
||
<div class="mix-page">
|
||
<!-- 页面头部 -->
|
||
<div class="mix-page__header">
|
||
<h1 class="mix-page__title">智能混剪</h1>
|
||
<a-button @click="$router.push('/material/list')">
|
||
<template #icon><ArrowLeftOutlined /></template>
|
||
返回素材列表
|
||
</a-button>
|
||
</div>
|
||
|
||
<div class="mix-page__content">
|
||
<!-- 左侧:参数配置 -->
|
||
<div class="mix-page__params">
|
||
<a-card title="混剪参数" :bordered="false">
|
||
<a-form layout="vertical">
|
||
<!-- 分组选择 -->
|
||
<a-form-item label="选择素材分组" required>
|
||
<a-select
|
||
v-model:value="formData.groupId"
|
||
placeholder="请选择素材分组"
|
||
:loading="loadingGroups"
|
||
@change="handleGroupChange"
|
||
>
|
||
<a-select-option v-for="g in groupList" :key="g.id" :value="g.id">
|
||
{{ g.name }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
|
||
<!-- 视频标题 -->
|
||
<a-form-item label="视频标题" required>
|
||
<a-input
|
||
v-model:value="formData.title"
|
||
placeholder="请输入生成视频的标题"
|
||
:maxlength="50"
|
||
show-count
|
||
/>
|
||
</a-form-item>
|
||
|
||
<!-- 生成数量 -->
|
||
<a-form-item label="生成数量">
|
||
<a-radio-group v-model:value="formData.produceCount" button-style="solid" @change="saveProduceCount">
|
||
<a-radio-button :value="1">1个</a-radio-button>
|
||
<a-radio-button :value="3">3个</a-radio-button>
|
||
<a-radio-button :value="5">5个</a-radio-button>
|
||
<a-radio-button :value="10">10个</a-radio-button>
|
||
<a-radio-button :value="15">15个</a-radio-button>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
|
||
<!-- 成品总时长 -->
|
||
<a-form-item label="成品总时长">
|
||
<div class="mix-page__slider-box">
|
||
<a-slider
|
||
v-model:value="formData.totalDuration"
|
||
:min="15"
|
||
:max="30"
|
||
:step="1"
|
||
:marks="{ 15: '15s', 20: '20s', 25: '25s', 30: '30s' }"
|
||
/>
|
||
<div class="slider-value">{{ formData.totalDuration }}秒</div>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 单切片时长 -->
|
||
<a-form-item label="单切片时长">
|
||
<div class="mix-page__slider-box">
|
||
<a-slider
|
||
v-model:value="formData.clipDuration"
|
||
:min="3"
|
||
:max="5"
|
||
:step="1"
|
||
:marks="{ 3: '3s', 4: '4s', 5: '5s' }"
|
||
/>
|
||
<div class="slider-value">{{ formData.clipDuration }}秒</div>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 裁剪模式 -->
|
||
<a-form-item label="裁剪模式">
|
||
<a-radio-group v-model:value="formData.cropMode" button-style="solid">
|
||
<a-radio-button value="center" class="crop-btn">
|
||
居中裁剪
|
||
</a-radio-button>
|
||
<a-radio-button value="fill" class="crop-btn">
|
||
填充模式
|
||
</a-radio-button>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
|
||
<!-- 自动计算的场景数 -->
|
||
<div class="mix-page__scene-info">
|
||
<div class="scene-row">
|
||
<span>场景数</span>
|
||
<strong>{{ sceneCount }} 个</strong>
|
||
</div>
|
||
<div class="scene-row">
|
||
<span>实际总时长</span>
|
||
<strong>{{ actualTotalDuration }}秒</strong>
|
||
</div>
|
||
<div class="scene-row">
|
||
<span>已填充</span>
|
||
<strong :class="{ 'text-green': filledCount === sceneCount }">
|
||
{{ filledCount }} / {{ sceneCount }}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 一键填充按钮 -->
|
||
<a-button
|
||
block
|
||
size="large"
|
||
style="margin-bottom: 12px"
|
||
:disabled="!groupFiles.length"
|
||
@click="autoFillScenes"
|
||
>
|
||
<template #icon><ThunderboltOutlined /></template>
|
||
一键填充
|
||
</a-button>
|
||
|
||
<a-button
|
||
type="primary"
|
||
block
|
||
size="large"
|
||
:loading="submitting"
|
||
:disabled="!canSubmit"
|
||
@click="handleSubmit"
|
||
>
|
||
<template #icon><RocketOutlined /></template>
|
||
开始混剪
|
||
</a-button>
|
||
</a-form>
|
||
</a-card>
|
||
</div>
|
||
|
||
<!-- 右侧:场景格子 + 素材列表 -->
|
||
<div class="mix-page__preview">
|
||
<!-- 场景格子区域 -->
|
||
<a-card title="场景编排" :bordered="false" style="margin-bottom: 16px">
|
||
<template #extra>
|
||
<a-button size="small" @click="clearScenes">清空</a-button>
|
||
</template>
|
||
|
||
<div class="mix-page__scenes">
|
||
<div
|
||
v-for="(scene, index) in scenes"
|
||
:key="index"
|
||
class="mix-page__scene"
|
||
:class="{ 'mix-page__scene--filled': scene.fileId }"
|
||
@click="openSceneSelector(index)"
|
||
>
|
||
<!-- 场景序号 -->
|
||
<span class="scene-index">{{ index + 1 }}</span>
|
||
|
||
<!-- 已填充:显示封面 -->
|
||
<template v-if="scene.fileId">
|
||
<img
|
||
v-if="getFileById(scene.fileId)?.coverBase64"
|
||
:src="getFileById(scene.fileId).coverBase64"
|
||
class="scene-thumb"
|
||
/>
|
||
<div v-else class="scene-placeholder filled">
|
||
<VideoCameraOutlined />
|
||
</div>
|
||
<div class="scene-name">{{ getFileById(scene.fileId)?.fileName }}</div>
|
||
<a-button
|
||
class="scene-remove"
|
||
type="text"
|
||
size="small"
|
||
danger
|
||
@click.stop="removeScene(index)"
|
||
>
|
||
<CloseOutlined />
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- 未填充:空白格子 -->
|
||
<template v-else>
|
||
<div class="scene-placeholder">
|
||
<PlusOutlined />
|
||
</div>
|
||
<div class="scene-hint">点击选择</div>
|
||
</template>
|
||
|
||
<!-- 时长标签 -->
|
||
<span class="scene-duration">{{ formData.clipDuration }}s</span>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<!-- 素材库 -->
|
||
<a-card title="素材库" :bordered="false">
|
||
<a-spin :spinning="loadingFiles">
|
||
<div v-if="groupFiles.length > 0" class="mix-page__grid">
|
||
<div
|
||
v-for="file in groupFiles"
|
||
:key="file.id"
|
||
class="mix-page__item"
|
||
:class="{ 'mix-page__item--used': isFileUsed(file.id) }"
|
||
@click="handleFileClick(file)"
|
||
>
|
||
<!-- 封面图 -->
|
||
<div class="mix-page__thumb">
|
||
<img v-if="file.isVideo && file.coverBase64" :src="file.coverBase64" :alt="file.fileName" />
|
||
<div v-else class="mix-page__placeholder">
|
||
<VideoCameraOutlined />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已使用标记 -->
|
||
<span v-if="isFileUsed(file.id)" class="mix-page__used-badge">
|
||
已使用 ×{{ getFileUsageCount(file.id) }}
|
||
</span>
|
||
|
||
<!-- 文件名 -->
|
||
<div class="mix-page__name" :title="file.fileName">
|
||
{{ file.fileName }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<a-empty v-else description="请先选择素材分组" />
|
||
</a-spin>
|
||
</a-card>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 素材选择弹窗 -->
|
||
<a-modal
|
||
v-model:open="selectorVisible"
|
||
title="选择素材"
|
||
:footer="null"
|
||
width="600px"
|
||
>
|
||
<div class="mix-page__selector-grid">
|
||
<div
|
||
v-for="file in groupFiles"
|
||
:key="file.id"
|
||
class="mix-page__selector-item"
|
||
@click="selectFileForScene(file)"
|
||
>
|
||
<div class="selector-thumb">
|
||
<img v-if="file.isVideo && file.coverBase64" :src="file.coverBase64" />
|
||
<VideoCameraOutlined v-else />
|
||
</div>
|
||
<div class="selector-name">{{ file.fileName }}</div>
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted } from 'vue'
|
||
import { message } from 'ant-design-vue'
|
||
import { useRouter } from 'vue-router'
|
||
import {
|
||
ArrowLeftOutlined,
|
||
RocketOutlined,
|
||
VideoCameraOutlined,
|
||
PlusOutlined,
|
||
CloseOutlined,
|
||
ThunderboltOutlined
|
||
} from '@ant-design/icons-vue'
|
||
import { MaterialService, MaterialGroupService } from '@/api/material'
|
||
import { MixTaskService } from '@/api/mixTask'
|
||
|
||
const router = useRouter()
|
||
|
||
// 表单数据
|
||
const formData = ref({
|
||
groupId: null,
|
||
title: '',
|
||
produceCount: loadProduceCount(),
|
||
totalDuration: 15, // 成品总时长 15-30s
|
||
clipDuration: 3, // 单切片时长 3-5s
|
||
cropMode: 'center' // 裁剪模式,默认居中裁剪
|
||
})
|
||
|
||
// 本地存储键名
|
||
const STORAGE_KEY = 'mix-produce-count'
|
||
|
||
// 从本地存储加载生成数量
|
||
function loadProduceCount() {
|
||
const saved = localStorage.getItem(STORAGE_KEY)
|
||
return saved ? parseInt(saved, 10) : 3
|
||
}
|
||
|
||
// 保存生成数量到本地存储
|
||
function saveProduceCount() {
|
||
localStorage.setItem(STORAGE_KEY, formData.value.produceCount.toString())
|
||
}
|
||
|
||
// 状态
|
||
const loadingGroups = ref(false)
|
||
const loadingFiles = ref(false)
|
||
const submitting = ref(false)
|
||
const selectorVisible = ref(false)
|
||
const currentSceneIndex = ref(-1)
|
||
|
||
// 分组和文件
|
||
const groupList = ref([])
|
||
const groupFiles = ref([])
|
||
|
||
// 场景列表 [{ fileId, fileUrl }, ...]
|
||
const scenes = ref([])
|
||
|
||
// 计算场景数 = 总时长 / 单切片时长
|
||
const sceneCount = computed(() => {
|
||
return Math.floor(formData.value.totalDuration / formData.value.clipDuration)
|
||
})
|
||
|
||
// 实际总时长 = 场景数 × 单切片时长
|
||
const actualTotalDuration = computed(() => {
|
||
return sceneCount.value * formData.value.clipDuration
|
||
})
|
||
|
||
// 已填充数量
|
||
const filledCount = computed(() => {
|
||
return scenes.value.filter(s => s.fileId).length
|
||
})
|
||
|
||
// 监听场景数变化,自动调整场景数组
|
||
watch(sceneCount, (newCount) => {
|
||
const current = scenes.value.length
|
||
if (newCount > current) {
|
||
// 增加空场景
|
||
for (let i = current; i < newCount; i++) {
|
||
scenes.value.push({ fileId: null, fileUrl: null })
|
||
}
|
||
} else if (newCount < current) {
|
||
// 减少场景
|
||
scenes.value = scenes.value.slice(0, newCount)
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// 根据fileId获取文件信息
|
||
const getFileById = (fileId) => {
|
||
return groupFiles.value.find(f => f.id === fileId)
|
||
}
|
||
|
||
// 检查文件是否已使用
|
||
const isFileUsed = (fileId) => {
|
||
return scenes.value.some(s => s.fileId === fileId)
|
||
}
|
||
|
||
// 获取文件使用次数
|
||
const getFileUsageCount = (fileId) => {
|
||
return scenes.value.filter(s => s.fileId === fileId).length
|
||
}
|
||
|
||
// 加载分组列表
|
||
const loadGroups = async () => {
|
||
loadingGroups.value = true
|
||
try {
|
||
const res = await MaterialGroupService.getGroupList()
|
||
if (res.code === 0) {
|
||
groupList.value = res.data || []
|
||
}
|
||
} catch (error) {
|
||
message.error('加载分组失败')
|
||
} finally {
|
||
loadingGroups.value = false
|
||
}
|
||
}
|
||
|
||
// 分组变更时加载素材
|
||
const handleGroupChange = async (groupId) => {
|
||
if (!groupId) {
|
||
groupFiles.value = []
|
||
clearScenes()
|
||
return
|
||
}
|
||
|
||
loadingFiles.value = true
|
||
try {
|
||
const res = await MaterialService.getFilePage({
|
||
groupId,
|
||
fileCategory: 'video',
|
||
pageNo: 1,
|
||
pageSize: 50
|
||
})
|
||
if (res.code === 0) {
|
||
groupFiles.value = res.data.list || []
|
||
clearScenes()
|
||
}
|
||
} catch (error) {
|
||
message.error('加载素材失败')
|
||
} finally {
|
||
loadingFiles.value = false
|
||
}
|
||
}
|
||
|
||
// 打开场景选择器
|
||
const openSceneSelector = (index) => {
|
||
currentSceneIndex.value = index
|
||
selectorVisible.value = true
|
||
}
|
||
|
||
// 为场景选择文件
|
||
const selectFileForScene = (file) => {
|
||
if (currentSceneIndex.value >= 0 && currentSceneIndex.value < scenes.value.length) {
|
||
scenes.value[currentSceneIndex.value] = {
|
||
fileId: file.id,
|
||
fileUrl: file.fileUrl
|
||
}
|
||
}
|
||
selectorVisible.value = false
|
||
}
|
||
|
||
// 点击素材库文件:填充到第一个空场景
|
||
const handleFileClick = (file) => {
|
||
const emptyIndex = scenes.value.findIndex(s => !s.fileId)
|
||
if (emptyIndex >= 0) {
|
||
scenes.value[emptyIndex] = {
|
||
fileId: file.id,
|
||
fileUrl: file.fileUrl
|
||
}
|
||
} else {
|
||
message.info('所有场景已填满')
|
||
}
|
||
}
|
||
|
||
// 移除场景
|
||
const removeScene = (index) => {
|
||
scenes.value[index] = { fileId: null, fileUrl: null }
|
||
}
|
||
|
||
// 清空场景
|
||
const clearScenes = () => {
|
||
scenes.value = Array(sceneCount.value).fill(null).map(() => ({ fileId: null, fileUrl: null }))
|
||
}
|
||
|
||
// 一键填充:随机分配素材到空场景
|
||
const autoFillScenes = () => {
|
||
if (!groupFiles.value.length) {
|
||
message.warning('请先选择素材分组')
|
||
return
|
||
}
|
||
|
||
// 打乱素材顺序
|
||
const shuffled = [...groupFiles.value].sort(() => Math.random() - 0.5)
|
||
let fileIndex = 0
|
||
|
||
// 填充每个空场景
|
||
scenes.value = scenes.value.map(scene => {
|
||
if (!scene.fileId) {
|
||
const file = shuffled[fileIndex % shuffled.length]
|
||
fileIndex++
|
||
return { fileId: file.id, fileUrl: file.fileUrl }
|
||
}
|
||
return scene
|
||
})
|
||
|
||
message.success('已随机填充所有场景')
|
||
}
|
||
|
||
// 是否可提交
|
||
const canSubmit = computed(() => {
|
||
return formData.value.groupId &&
|
||
formData.value.title.trim() &&
|
||
filledCount.value === sceneCount.value
|
||
})
|
||
|
||
// 提交混剪
|
||
const handleSubmit = async () => {
|
||
if (!canSubmit.value) return
|
||
|
||
submitting.value = true
|
||
try {
|
||
// 构建素材列表(带上素材实际时长 fileDuration)
|
||
const materials = scenes.value.map(scene => {
|
||
const file = getFileById(scene.fileId)
|
||
return {
|
||
fileId: scene.fileId,
|
||
fileUrl: scene.fileUrl,
|
||
duration: formData.value.clipDuration,
|
||
fileDuration: file?.duration || null // 素材实际时长
|
||
}
|
||
})
|
||
|
||
const res = await MixTaskService.createTask({
|
||
title: formData.value.title,
|
||
materials: materials,
|
||
produceCount: formData.value.produceCount,
|
||
cropMode: formData.value.cropMode
|
||
})
|
||
|
||
if (res.code === 0) {
|
||
message.success('混剪任务创建成功!')
|
||
router.push('/material/mix-task')
|
||
}
|
||
} catch (error) {
|
||
message.error('提交失败:' + error.message)
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadGroups()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.mix-page {
|
||
padding: 24px;
|
||
background: var(--color-bg-2);
|
||
min-height: 100vh;
|
||
|
||
&__header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
&__title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin: 0;
|
||
}
|
||
|
||
&__content {
|
||
display: flex;
|
||
gap: 24px;
|
||
}
|
||
|
||
&__params {
|
||
width: 340px;
|
||
flex-shrink: 0;
|
||
|
||
.ant-card {
|
||
position: sticky;
|
||
top: 24px;
|
||
}
|
||
}
|
||
|
||
&__preview {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__slider-box {
|
||
.slider-value {
|
||
text-align: center;
|
||
margin-top: 8px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
|
||
&__scene-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
padding: 16px;
|
||
background: var(--color-bg-3);
|
||
border-radius: 8px;
|
||
|
||
.scene-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
span {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
strong {
|
||
color: #333;
|
||
font-size: 16px;
|
||
|
||
&.text-green {
|
||
color: #52c41a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 场景格子样式
|
||
&__scenes {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
&__scene {
|
||
position: relative;
|
||
width: 120px;
|
||
height: 100px;
|
||
border: 2px dashed #d9d9d9;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #fafafa;
|
||
|
||
&:hover {
|
||
border-color: #1890ff;
|
||
background: #f0f7ff;
|
||
}
|
||
|
||
&--filled {
|
||
border-style: solid;
|
||
border-color: #1890ff;
|
||
background: #fff;
|
||
}
|
||
|
||
.scene-index {
|
||
position: absolute;
|
||
top: 4px;
|
||
left: 4px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
color: #fff;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 2;
|
||
}
|
||
|
||
.scene-thumb {
|
||
width: 100%;
|
||
height: 60px;
|
||
object-fit: cover;
|
||
border-radius: 4px 4px 0 0;
|
||
}
|
||
|
||
.scene-placeholder {
|
||
font-size: 24px;
|
||
color: #bfbfbf;
|
||
|
||
&.filled {
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
|
||
.scene-hint {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.scene-name {
|
||
font-size: 11px;
|
||
color: #333;
|
||
padding: 4px;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
width: 100%;
|
||
}
|
||
|
||
.scene-duration {
|
||
position: absolute;
|
||
bottom: 4px;
|
||
right: 4px;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
color: #fff;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.scene-remove {
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 2px;
|
||
z-index: 3;
|
||
}
|
||
}
|
||
|
||
// 素材库格子样式
|
||
&__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
&__item {
|
||
position: relative;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: 2px solid transparent;
|
||
background: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
border-color: #1890ff;
|
||
}
|
||
|
||
&--used {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
&__thumb {
|
||
aspect-ratio: 16 / 9;
|
||
background: #f0f0f0;
|
||
overflow: hidden;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
|
||
&__placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #bfbfbf;
|
||
font-size: 24px;
|
||
}
|
||
|
||
&__used-badge {
|
||
position: absolute;
|
||
top: 4px;
|
||
right: 4px;
|
||
background: #1890ff;
|
||
color: #fff;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
&__name {
|
||
padding: 8px;
|
||
font-size: 12px;
|
||
color: #333;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
// 选择器弹窗样式
|
||
&__selector-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
&__selector-item {
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
border: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
border-color: #1890ff;
|
||
}
|
||
|
||
.selector-thumb {
|
||
aspect-ratio: 16 / 9;
|
||
background: #f0f0f0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #bfbfbf;
|
||
font-size: 24px;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
|
||
.selector-name {
|
||
padding: 6px;
|
||
font-size: 11px;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
}
|
||
}
|
||
</style>
|