Commit f44c8668 authored by 黄同智's avatar 黄同智

重新冲冲冲

parent 5d11284c
<template>
<section class="step-three h-full">
<div v-if="!isReady" class="planning-panel h-full flex-col flex-center text-center">
<div class="fs-18 fw-700 color-000 lh-30">分镜策划进行中</div>
<div class="fs-14 color-536 lh-24 mt-8">Agent正在进行分镜策划,请耐心等待...</div>
<div class="progress-meta flex-between w-400 mt-36 fs-14 color-536">
<span>当前剧集进度:</span>
<span>第1集/共1集</span>
</div>
<div class="planning-card w-400 p-12 mt-14 round-10 bg-fff">
<div class="fs-14 fw-700 color-000 lh-24 text-left">第1集 策划解析进度: {{ planningProgress }}%</div>
<el-progress class="mt-14" :percentage="planningProgress" :show-text="false" :stroke-width="6" />
</div>
</div>
<div v-else class="storyboard-workspace h-full flex hidden">
<aside class="episode-rail flex-col items-center pt-12">
<div class="fs-12 color-536 lh-22 mb-8">集数</div>
<div class="episode-no flex-center w-36 h-36 round-8 color-fff fw-700">1</div>
</aside>
<aside class="asset-sidebar bg-fff flex-col px-12 py-12 hidden">
<div class="flex-items-center gap-8 fs-16 fw-700 color-000 lh-24 mb-16">
<el-icon class="color-aaa">
<Document />
</el-icon>
<span>第 1 集 丝袜产品展示</span>
<el-icon class="fs-14 color-536">
<Notebook />
</el-icon>
<span class="fs-12 color-536">剧本</span>
</div>
<div class="asset-filter flex-items-center h-34 p-4 round-8 mb-14">
<el-button v-for="item in filters" :key="item.value" class="filter-btn flex-1 h-26 fs-12 border-none"
:class="{ active: activeFilter === item.value }" text @click="activeFilter = item.value">
{{ item.label }}
</el-button>
</div>
<div class="asset-scroll flex-1 over-y-auto hide-scroll">
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>角色</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon>
</div>
<div class="role-card mb-18">
<img :src="peopleImg" alt="苏晓" class="role-img w-118 h-118 round-8 cover" />
<div class="fs-14 color-000 lh-22 mt-6">苏晓</div>
<div class="fs-12 color-536 lh-20">主形象</div>
<el-button class="w-118 h-32 mt-8 fs-12">未配置音色</el-button>
</div>
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>场景</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon>
</div>
<div class="empty-card flex-col flex-center h-186 round-8 mb-18">
<el-icon class="fs-26 color-ccc mb-8">
<Picture />
</el-icon>
<span class="fs-12 color-aaa">暂未选择场景</span>
</div>
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>道具</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon>
</div>
<div class="empty-card flex-col flex-center h-80 round-8">
<el-icon class="fs-26 color-ccc mb-8">
<Star />
</el-icon>
<span class="fs-12 color-aaa">暂未选择道具</span>
</div>
</div>
</aside>
<main class="story-main flex-1 flex-col hidden">
<header class="toolbar flex-between px-12 py-10">
<div></div>
<div class="flex-items-center gap-12">
<el-button class="h-32 px-16 fs-12">
<el-icon>
<Refresh />
</el-icon>
重新解析
</el-button>
<el-button class="h-32 px-16 fs-12">
<el-icon>
<List />
</el-icon>
批量生视频
</el-button>
</div>
</header>
<section class="story-content flex-1 flex gap-12 px-12 hidden">
<div class="script-panel flex-1 bg-fff round-12 p-12 flex-col">
<div class="flex-items-center gap-12 mb-8">
<div class="fs-16 fw-700 color-000">分镜1</div>
<el-tag size="small" effect="plain">ID</el-tag>
<el-button class="h-32 px-12 fs-12">
<el-icon>
<Aim />
</el-icon>
3D导演台
<span class="beta fs-10 ml-4">Beta</span>
</el-button>
</div>
<div class="flex-items-center gap-8 mb-8">
<el-button class="h-30 px-12 fs-12">
<el-icon>
<FolderOpened />
</el-icon>
项目素材库
</el-button>
<el-button class="h-30 px-12 fs-12">
<el-icon>
<Folder />
</el-icon>
资产库
</el-button>
</div>
<div class="prompt-row flex-items-center gap-8 h-34 mb-10">
<el-button class="wh-34 p-0" plain>
<el-icon>
<Plus />
</el-icon>
</el-button>
<el-button class="wh-34 p-0" plain>
<el-icon>
<CopyDocument />
</el-icon>
</el-button>
<span class="fs-14 color-aaa">使用 @ 引用角色、场景、道具、音色及参考素材,编辑更灵活,分镜更精准</span>
</div>
<div class="script-scroll flex-1 over-y-auto hide-scroll pr-8">
<p class="fs-14 color-000 lh-28 mb-18">
画风: 写实风格, 细节刻画逼真,参考院线电影,真人电影风格,影视大片,真实透视比例,细节清晰不过度锐化
</p>
<p class="fs-14 color-000 lh-28 mb-18">
分镜具体动作描述:<br />
整体视觉基调:明亮柔和的自然光影,画面干净,色调温暖,营造出舒适真实的居家氛围。
</p>
<p v-for="item in shots" :key="item.id" class="fs-14 color-000 lh-28 mb-18">
镜头{{ item.id }}{{ item.duration }}秒):【场景】居家客厅,室内,白天【机位】{{ item.camera }}【站位】苏晓 {{ item.position }}【动作】{{ item.action }}(画外音,苏晓解说):“{{ item.speech }}
</p>
</div>
<div class="script-footer flex-between pt-8">
<div class="flex-items-center gap-6">
<el-tag effect="plain">Doubao-Seedance-2.5</el-tag>
<el-tag effect="plain">27s | 720p | 1个 | mov</el-tag>
<el-tag effect="plain">@</el-tag>
</div>
<div class="flex-items-center gap-8">
<el-button class="wh-32 p-0">
<el-icon>
<Pointer />
</el-icon>
</el-button>
<el-button type="primary" class="h-32 px-18 border-none">
<el-icon>
<MagicStick />
</el-icon>
6,129
</el-button>
</div>
</div>
</div>
<div class="preview-panel flex-1 bg-fff round-12 p-12 flex-col">
<div class="preview-tabs flex justify-end gap-26 fs-14 mb-8">
<span class="preview-tab pointer" :class="{ active: previewMode === 'generated' }"
@click="previewMode = 'generated'">分镜生成</span>
<span class="preview-tab pointer" :class="{ active: previewMode === 'origin' }"
@click="previewMode = 'origin'">原视频</span>
</div>
<div v-if="previewMode === 'generated'" class="preview-empty flex-1 flex-col flex-center round-8">
<el-icon class="fs-36 color-ccc mb-8">
<VideoCamera />
</el-icon>
<span class="fs-12 color-aaa">请先完成分镜视频生成</span>
</div>
<div v-else class="origin-preview flex-1 flex-center round-8 hidden relative">
<img :src="peopleImg" alt="原视频" class="origin-img h-full cover" />
<span class="play-btn absolute flex-center wh-64 round pointer">
<el-icon class="fs-40 color-fff">
<VideoPlay />
</el-icon>
</span>
</div>
</div>
</section>
<footer class="shot-strip bg-fff round-10 mx-12 my-12 px-14 py-14">
<div class="fs-14 color-000 lh-24 mb-10">1 分镜</div>
<div class="shot-card flex-col items-center pointer">
<div class="shot-thumb flex-center w-130 h-90 round-6">
<el-icon class="fs-28 color-ccc">
<VideoCamera />
</el-icon>
</div>
<div class="fs-12 color-536 lh-22 mt-6">分镜 01 · 待生成</div>
</div>
</footer>
</main>
</div>
</section>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import {
Aim,
CopyDocument,
Document,
Folder,
FolderOpened,
List,
MagicStick,
Notebook,
Picture,
Plus,
Pointer,
Refresh,
Star,
VideoCamera,
VideoPlay,
} from '@element-plus/icons-vue';
import peopleImg from '@/static/image/meinv.png';
type FilterValue = 'all' | 'role' | 'scene' | 'prop';
type PreviewMode = 'generated' | 'origin';
const filters: { label: string; value: FilterValue }[] = [
{ label: '全部', value: 'all' },
{ label: '角色', value: 'role' },
{ label: '场景', value: 'scene' },
{ label: '道具', value: 'prop' },
];
const shots = [
{
id: 1,
duration: '2.0',
camera: '位于苏晓正前方约1.5米,与她盘坐坐姿平齐的高度,固定不动。',
position: '盘腿坐在地面上,位于画面中央,面向镜头。',
action: '双手捧着一个白色包装盒,面带微笑地看向镜头。',
speech: '今天新到手了来蜜的热销款0.01,真的很清透。',
},
{
id: 2,
duration: '1.3',
camera: '位于苏晓正前方,略微俯拍她的手部和腿部,固定不动。',
position: '保持盘坐姿势,身体位于画面外,仅展示手部和膝盖区域。',
action: '打开包装盒,从里面拿出一团雾好的黑色丝袜。',
speech: '让我看看是怎么个超薄丝滑法。',
},
{
id: 3,
duration: '2.2',
camera: '与镜头2机位相同,固定不动。',
position: '保持盘坐姿势,身体位于画面外,仅展示手部和膝盖区域。',
action: '双手捏住黑色丝袜的两端,将其缓缓拉伸展开。',
speech: '颜色均匀通透。',
},
{
id: 4,
duration: '2.5',
camera: '位于苏晓右侧,平拍她的手部,固定不动。',
position: '苏晓的手臂举起,将丝袜置于镜头与窗帘之间。',
action: '双手撑开黑色丝袜的一部分,对着光亮处展示其通透质感。',
speech: '真的很清透。',
},
{
id: 5,
duration: '1.8',
camera: '机位位于苏晓正前方低位,略微仰拍,固定不动。',
position: '双腿交叉站立,身体位于画面中央。',
action: '穿着黑色丝袜和黑色高跟鞋,保持站姿,展示腿部穿着效果。',
speech: '是那种高级的黑丝质感。',
},
{
id: 6,
duration: '2.0',
camera: '机位在苏晓右前方,平拍,固定不动。',
position: '坐在黑色沙发上,跷着二郎腿,身体朝向镜头左前方约45度。',
action: '右手轻轻地从膝盖处向下抚摸穿着丝袜的小腿,面带微笑。',
speech: '穿上真的超舒适。',
},
{
id: 7,
duration: '1.2',
camera: '机位贴近苏晓的腿部,平拍特写,固定不动。',
position: '腿部占据画面大部分。',
action: '一只手在穿着黑色丝袜的小腿上缓慢地抚摸,展示丝袜的光泽和丝滑感。',
speech: '它的面料很亲肤。',
},
{
id: 8,
duration: '4.0',
camera: '机位在苏晓正前方,平拍,固定不动。',
position: '位于镜头前,身体被丝袜遮挡,仅手臂可见。',
action: '双手分别抓住黑色丝袜的腰部两端,用力向两侧反复拉伸。',
speech: '丝袜细腻,弹性也很大,各种身材的姐妹都能穿。',
},
{
id: 9,
duration: '1.8',
camera: '机位在苏晓右前方低位,略微仰拍她的腿部,固定不动。',
position: '穿着丝袜和高跟鞋,侧身坐在一个黑色吧台凳上,双腿交叠。',
action: '镜头停留在腿部线条,展示自然的光泽。',
speech: '显瘦又高级。',
},
];
const isReady = ref(false);
const planningProgress = ref(0);
const activeFilter = ref<FilterValue>('all');
const previewMode = ref<PreviewMode>('generated');
let timer: number | undefined;
const runPlanning = () => {
timer = window.setInterval(() => {
planningProgress.value = Math.min(100, planningProgress.value + 20);
if (planningProgress.value >= 100) {
window.clearInterval(timer);
timer = undefined;
window.setTimeout(() => {
isReady.value = true;
}, 400);
}
}, 420);
};
onMounted(() => {
runPlanning();
});
onBeforeUnmount(() => {
if (timer) {
window.clearInterval(timer);
}
});
</script>
<style scoped lang="scss">
.step-three {
background: #f8fafc;
}
.planning-panel {
min-height: calc(100vh - 60px);
}
.color-536 {
color: #536883;
}
.planning-card {
border: 1px solid #e2e8f3;
}
.episode-rail {
flex: 0 0 60px;
background: #eef3ff;
}
.episode-no {
background: rgba(var(--main-color), 1);
}
.asset-sidebar {
flex: 0 0 282px;
min-height: 0;
}
.asset-filter {
background: #f4f7fb;
}
.filter-btn.active {
background: #fff;
box-shadow: 0 1px 4px rgba(17, 36, 62, .12);
}
.asset-scroll,
.script-scroll,
.story-main {
min-height: 0;
}
.role-img,
.empty-card,
.preview-empty,
.shot-thumb {
background: #f3f5f9;
}
.story-content {
min-height: 0;
}
.toolbar {
flex: 0 0 54px;
border-bottom: 1px solid #dce6f2;
}
.script-panel,
.preview-panel,
.shot-strip {
border: 1px solid #e2e8f3;
}
.script-scroll {
border-top: 1px solid #edf2f7;
border-bottom: 1px solid #edf2f7;
}
.beta {
color: #6a44ff;
}
.preview-panel {
min-width: 420px;
}
.preview-tab {
position: relative;
height: 28px;
line-height: 28px;
color: #536883;
&.active {
color: #000;
&::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: rgba(var(--main-color), 1);
content: '';
}
}
}
.origin-preview {
background: #000;
}
.origin-img {
max-width: 74%;
}
.play-btn {
top: 50%;
left: 50%;
background: rgba(0, 0, 0, .38);
transform: translate(-50%, -50%);
}
.shot-strip {
flex: 0 0 164px;
}
.shot-card {
width: 150px;
}
.shot-thumb {
border: 2px solid rgba(var(--main-color), 1);
}
:deep(.planning-card .el-progress-bar__outer) {
background: #eef1f8;
}
:deep(.planning-card .el-progress-bar__inner) {
background: rgba(var(--main-color), 1);
}
</style>
...@@ -13,76 +13,23 @@ ...@@ -13,76 +13,23 @@
</div> </div>
</div> </div>
<div v-else class="storyboard-workspace h-full flex hidden"> <div v-else class="storyboard-workspace h-full flex gap-12 hidden pr-12">
<aside class="episode-rail flex-col items-center pt-12"> <aside class="episode-rail flex-col items-center pt-12">
<div class="fs-12 color-536 lh-22 mb-8">集数</div> <div class="fs-12 color-536 lh-22 mb-8">集数</div>
<div class="episode-no flex-center w-36 h-36 round-8 color-fff fw-700">1</div> <div class="episode-no flex-center w-36 h-36 round-8 color-fff fw-700">1</div>
</aside> </aside>
<div class="flex-1 flex-col">
<aside class="asset-sidebar bg-fff flex-col px-12 py-12 hidden"> <div class="toolbar flex-between">
<div class="flex-items-center gap-8 fs-16 fw-700 color-000 lh-24 mb-16"> <div class="flex-items-center gap-8 fw-700 color-000">
<el-icon class="color-aaa"> <el-icon class="color-aaa">
<Document /> <Document />
</el-icon>
<span>第 1 集 丝袜产品展示</span>
<el-icon class="fs-14 color-536">
<Notebook />
</el-icon>
<span class="fs-12 color-536">剧本</span>
</div>
<div class="asset-filter flex-items-center h-34 p-4 round-8 mb-14">
<el-button v-for="item in filters" :key="item.value" class="filter-btn flex-1 h-26 fs-12 border-none"
:class="{ active: activeFilter === item.value }" text @click="activeFilter = item.value">
{{ item.label }}
</el-button>
</div>
<div class="asset-scroll flex-1 over-y-auto hide-scroll">
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>角色</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon> </el-icon>
</div> <span>第 1 集 丝袜产品展示</span>
<div class="role-card mb-18"> <el-icon class="fs-14 color-536">
<img :src="peopleImg" alt="苏晓" class="role-img w-118 h-118 round-8 cover" /> <Notebook />
<div class="fs-14 color-000 lh-22 mt-6">苏晓</div>
<div class="fs-12 color-536 lh-20">主形象</div>
<el-button class="w-118 h-32 mt-8 fs-12">未配置音色</el-button>
</div>
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>场景</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon> </el-icon>
<span class="fs-12 color-536">剧本</span>
</div> </div>
<div class="empty-card flex-col flex-center h-186 round-8 mb-18">
<el-icon class="fs-26 color-ccc mb-8">
<Picture />
</el-icon>
<span class="fs-12 color-aaa">暂未选择场景</span>
</div>
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>道具</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon>
</div>
<div class="empty-card flex-col flex-center h-80 round-8">
<el-icon class="fs-26 color-ccc mb-8">
<Star />
</el-icon>
<span class="fs-12 color-aaa">暂未选择道具</span>
</div>
</div>
</aside>
<main class="story-main flex-1 flex-col hidden">
<header class="toolbar flex-between px-12 py-10">
<div></div>
<div class="flex-items-center gap-12"> <div class="flex-items-center gap-12">
<el-button class="h-32 px-16 fs-12"> <el-button class="h-32 px-16 fs-12">
<el-icon> <el-icon>
...@@ -97,131 +44,298 @@ ...@@ -97,131 +44,298 @@
批量生视频 批量生视频
</el-button> </el-button>
</div> </div>
</header> </div>
<div class="flex-1 flex pb-12 h-full hidden gap-12">
<section class="story-content flex-1 flex gap-12 px-12 hidden"> <div class="flex-col round-10 bg-fff asset-sidebar">
<div class="script-panel flex-1 bg-fff round-12 p-12 flex-col"> <div class="px-30 py-10">
<div class="flex-items-center gap-12 mb-8"> <div class="asset-filter flex-items-center h-34 p-4 round-8">
<div class="fs-16 fw-700 color-000">分镜1</div> <el-button v-for="item in filters" :key="item.value"
<el-tag size="small" effect="plain">ID</el-tag> class="filter-btn flex-1 h-26 fs-12 border-none"
<el-button class="h-32 px-12 fs-12"> :class="{ active: activeFilter === item.value }" text
<el-icon> @click="activeFilter = item.value">
<Aim /> {{ item.label }}
</el-icon>
3D导演台
<span class="beta fs-10 ml-4">Beta</span>
</el-button>
</div>
<div class="flex-items-center gap-8 mb-8">
<el-button class="h-30 px-12 fs-12">
<el-icon>
<FolderOpened />
</el-icon>
项目素材库
</el-button>
<el-button class="h-30 px-12 fs-12">
<el-icon>
<Folder />
</el-icon>
资产库
</el-button>
</div>
<div class="prompt-row flex-items-center gap-8 h-34 mb-10">
<el-button class="wh-34 p-0" plain>
<el-icon>
<Plus />
</el-icon>
</el-button>
<el-button class="wh-34 p-0" plain>
<el-icon>
<CopyDocument />
</el-icon>
</el-button>
<span class="fs-14 color-aaa">使用 @ 引用角色、场景、道具、音色及参考素材,编辑更灵活,分镜更精准</span>
</div>
<div class="script-scroll flex-1 over-y-auto hide-scroll pr-8">
<p class="fs-14 color-000 lh-28 mb-18">
画风: 写实风格, 细节刻画逼真,参考院线电影,真人电影风格,影视大片,真实透视比例,细节清晰不过度锐化
</p>
<p class="fs-14 color-000 lh-28 mb-18">
分镜具体动作描述:<br />
整体视觉基调:明亮柔和的自然光影,画面干净,色调温暖,营造出舒适真实的居家氛围。
</p>
<p v-for="item in shots" :key="item.id" class="fs-14 color-000 lh-28 mb-18">
镜头{{ item.id }}{{ item.duration }}秒):【场景】居家客厅,室内,白天【机位】{{ item.camera }}【站位】苏晓 {{ item.position }}【动作】{{ item.action }}(画外音,苏晓解说):“{{ item.speech }}
</p>
</div>
<div class="script-footer flex-between pt-8">
<div class="flex-items-center gap-6">
<el-tag effect="plain">Doubao-Seedance-2.5</el-tag>
<el-tag effect="plain">27s | 720p | 1个 | mov</el-tag>
<el-tag effect="plain">@</el-tag>
</div>
<div class="flex-items-center gap-8">
<el-button class="wh-32 p-0">
<el-icon>
<Pointer />
</el-icon>
</el-button>
<el-button type="primary" class="h-32 px-18 border-none">
<el-icon>
<MagicStick />
</el-icon>
6,129
</el-button> </el-button>
</div> </div>
</div> </div>
</div> <div class="flex-1 over-y-auto hide-scroll px-12">
<div class="asset-scroll flex-1 over-y-auto hide-scroll">
<div class="preview-panel flex-1 bg-fff round-12 p-12 flex-col"> <div v-if="isFilterVisible('role')" class="asset-section">
<div class="preview-tabs flex justify-end gap-26 fs-14 mb-8"> <div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span class="preview-tab pointer" :class="{ active: previewMode === 'generated' }" <span>角色</span>
@click="previewMode = 'generated'">分镜生成</span> <el-icon class="color-536 pointer">
<span class="preview-tab pointer" :class="{ active: previewMode === 'origin' }" <Plus />
@click="previewMode = 'origin'">原视频</span> </el-icon>
</div> </div>
<div v-if="previewMode === 'generated'" class="preview-empty flex-1 flex-col flex-center round-8"> <div v-if="currentShot.assets.roles.length" class="asset-card-list flex flex-wrap gap-10 mb-18">
<el-icon class="fs-36 color-ccc mb-8"> <div v-for="item in currentShot.assets.roles" :key="item.id" class="role-card">
<VideoCamera /> <img :src="item.image" :alt="item.name"
</el-icon> class="role-img w-118 h-118 round-8 cover" />
<span class="fs-12 color-aaa">请先完成分镜视频生成</span> <div class="fs-14 color-000 lh-22 mt-6">{{ item.name }}</div>
</div> <div class="fs-12 color-536 lh-20">{{ item.desc }}</div>
<div v-else class="origin-preview flex-1 flex-center round-8 hidden relative"> <el-button class="w-118 h-32 mt-8 fs-12">
<img :src="peopleImg" alt="原视频" class="origin-img h-full cover" /> <el-icon>
<span class="play-btn absolute flex-center wh-64 round pointer"> <VideoPlay />
<el-icon class="fs-40 color-fff"> </el-icon>
<VideoPlay /> 自定义音色
</el-icon> </el-button>
</span> </div>
</div>
<div v-else class="empty-card flex-col flex-center h-80 round-8 mb-18">
<el-icon class="fs-26 color-ccc mb-8">
<User />
</el-icon>
<span class="fs-12 color-aaa">暂未选择角色</span>
</div>
</div>
<div v-if="isFilterVisible('scene')" class="asset-section">
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>场景</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon>
</div>
<div v-if="currentShot.assets.scenes.length" class="mb-18">
<div v-for="item in currentShot.assets.scenes" :key="item.id" class="scene-card">
<img :src="item.image" :alt="item.name"
class="scene-img w-full h-128 round-8 cover" />
<div class="fs-14 color-000 lh-22 mt-8">{{ item.name }}</div>
<div class="fs-12 color-536 lh-20">{{ item.desc }}</div>
</div>
</div>
<div v-else class="empty-card flex-col flex-center h-186 round-8 mb-18">
<el-icon class="fs-26 color-ccc mb-8">
<Picture />
</el-icon>
<span class="fs-12 color-aaa">暂未选择场景</span>
</div>
</div>
<div v-if="isFilterVisible('prop')" class="asset-section">
<div class="flex-between fs-14 fw-700 color-000 lh-24 mb-10">
<span>道具</span>
<el-icon class="color-536 pointer">
<Plus />
</el-icon>
</div>
<div v-if="currentShot.assets.props.length" class="asset-card-list flex flex-wrap gap-10 mb-18">
<div v-for="item in currentShot.assets.props" :key="item.id" class="role-card">
<img :src="item.image" :alt="item.name"
class="role-img w-118 h-118 round-8 cover" />
<div class="fs-14 color-000 lh-22 mt-6">{{ item.name }}</div>
<div class="fs-12 color-536 lh-20">{{ item.desc }}</div>
</div>
</div>
<div v-else class="empty-card flex-col flex-center h-80 round-8">
<el-icon class="fs-26 color-ccc mb-8">
<Star />
</el-icon>
<span class="fs-12 color-aaa">暂未选择道具</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</section> <div class="story-editor flex-1 flex-col gap-12">
<div class="flex-1 bg-fff round-12 p-10 flex gap-12">
<footer class="shot-strip bg-fff round-10 mx-12 my-12 px-14 py-14"> <div class="flex-col" style="width: 60%;">
<div class="fs-14 color-000 lh-24 mb-10">1 分镜</div> <div class="flex-items-center gap-12 mb-8">
<div class="shot-card flex-col items-center pointer"> <el-icon size="18">
<div class="shot-thumb flex-center w-130 h-90 round-6"> <Film />
<el-icon class="fs-28 color-ccc"> </el-icon>
<VideoCamera /> <div class="fw-700 color-000">分镜{{ currentShotIndex + 1 }}</div>
</el-icon> <span class="bg-main px-4 fs-10 round-6 color-fff">ID</span>
<div class="h-30 px-10 fs-12 bg-eee round-8 flex-items-center">
<el-icon size="16">
<Aim />
</el-icon>
<span class="ml-10">3D导演台</span>
<span class="beta fs-10 ml-4 fw-600">Beta</span>
</div>
</div>
<div class="round-12 border-eee flex-1 flex-col">
<div class="p-10 pt-16">
<div class="mb-10">
<el-button class="h-26 px-12 round-6 color-333" size="small">
<el-icon>
<FolderOpened />
</el-icon>
<span class="ml-4">项目素材库</span>
</el-button>
<el-button class="h-26 px-12 round-6 color-333" size="small">
<el-icon>
<Folder />
</el-icon>
<span class="ml-4">资产库</span>
</el-button>
</div>
<div class="flex-items-center gap-10">
<UploadFile :width="36" />
<div class="h-36 w-36 round-6 bg-f5 flex-center"
style="border: 1px dashed #bbb;"></div>
<span class="fs-12 color-999">使用 @ 引用角色、场景、道具、音色及参考素材,编辑更灵活,分镜更精准</span>
</div>
</div>
<div class="flex-1">
<el-input v-model="currentShotPrompt" type="textarea" resize="none"
placeholder="请输入分镜视频提示词" class="textarea" style="height: 100%;" />
</div>
<div class="h-60 flex-between px-10 py-8">
<div class="flex-items-center gap-10">
<el-select v-model="aiVal" placeholder="Select" style="width: 190px"
class="select">
<el-option v-for="item in aiOptions" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
<el-popover v-model:visible="specVisible" placement="top-start" :width="214"
trigger="click" popper-class="burst-spec-popper" :teleported="false"
:show-arrow="false" :hide-after="0">
<template #reference>
<div class="spec-trigger flex-center gap-6 h-32 px-10 round-8 pointer"
:class="{ active: specVisible }">
<el-icon class="fs-14 color-536">
<Clock />
</el-icon>
<span class="fs-12 color-000">{{ specSummary }}</span>
</div>
</template>
<div class="spec-panel bg-fff">
<div class="spec-section">
<div class="spec-label">视频时长</div>
<div class="duration-row flex-items-center gap-10">
<el-slider v-model="currentVideoDuration"
class="spec-slider flex-1" :min="1" :max="60"
:show-tooltip="false" />
<div class="duration-value flex-center h-34 px-12 round-10">
{{ currentVideoDuration }}s
</div>
</div>
</div>
<div class="spec-section">
<div class="spec-label">视频数量</div>
<div class="spec-segment grid-4">
<button v-for="item in videoCountOptions" :key="item"
type="button" class="spec-option"
:class="{ active: currentVideoCount === item }"
@click="currentVideoCount = item">
{{ item }}个
</button>
</div>
</div>
<div class="spec-section">
<div class="spec-label">视频清晰度</div>
<div class="spec-segment grid-3">
<button v-for="item in resolutionOptions" :key="item"
type="button" class="spec-option"
:class="{ active: currentResolution === item }"
@click="currentResolution = item">
{{ item }}
</button>
</div>
</div>
<div class="spec-section">
<div class="spec-label">视频格式</div>
<div class="spec-segment grid-2">
<button v-for="item in formatOptions" :key="item"
type="button" class="spec-option"
:class="{ active: currentFormat === item }"
@click="currentFormat = item">
{{ item }}
</button>
</div>
</div>
</div>
</el-popover>
<div class="h-32 w-32 flex-center border-eee round-8 pointer"
style="border: 1px solid #dce6f2;">@</div>
</div>
<el-button type="primary" class="h-30 px-18 round-8 border-none">
<el-icon>
<MagicStick />
</el-icon>
{{ currentShot.points.toLocaleString() }}
</el-button>
</div>
</div>
</div>
<div class="preview-panel round-12 flex-col" style="width: 40%;">
<div class="preview-tabs flex-center gap-26 fs-14 mb-8">
<span class="preview-tab pointer" :class="{ active: previewMode === 'generated' }"
@click="previewMode = 'generated'">分镜生成</span>
<span class="preview-tab pointer" :class="{ active: previewMode === 'origin' }"
@click="previewMode = 'origin'">原视频</span>
</div>
<div v-if="previewMode === 'generated' && currentShot.status === 'pending'"
class="preview-empty flex-1 flex-col flex-center round-8">
<el-icon class="fs-36 color-ccc mb-8">
<VideoCamera />
</el-icon>
<span class="fs-12 color-aaa">请先完成分镜视频生成</span>
</div>
<div v-else class="origin-preview flex-1 flex-center round-8 hidden relative">
<img :src="currentShot.cover || peopleImg" :alt="`分镜${currentShotIndex + 1}`"
class="origin-img h-full cover" />
<span class="play-btn absolute flex-center wh-64 round pointer">
<el-icon class="fs-40 color-fff">
<VideoPlay />
</el-icon>
</span>
</div>
</div>
</div> </div>
<div class="fs-12 color-536 lh-22 mt-6">分镜 01 · 待生成</div> <footer class="shot-strip bg-fff round-12 px-14 py-14">
<div class="color-000 lh-24 mb-10">{{ shotList.length }} 分镜</div>
<div class="shot-list flex-items-center over-x-auto hide-scroll">
<template v-for="(shot, index) in shotList" :key="shot.id">
<div class="shot-card flex-col items-center pointer"
:class="{ active: activeShotId === shot.id }" @click="selectShot(shot.id)">
<div class="shot-thumb relative flex-center w-130 h-90 round-6 hidden">
<img v-if="shot.cover" :src="shot.cover" :alt="`分镜${index + 1}`"
class="w-full h-full cover" />
<el-icon v-else class="fs-28 color-ccc">
<VideoCamera />
</el-icon>
<button v-if="shotList.length > 1" type="button"
class="shot-delete absolute flex-center wh-24 round-6 pointer"
@click.stop="deleteShot(shot.id)">
<el-icon>
<Delete />
</el-icon>
</button>
</div>
<div class="fs-12 color-536 lh-22 mt-6">
分镜 {{ padShotNo(index + 1) }} · {{ shot.status === 'generated' ? `${shot.duration}s` : '待生成' }}
</div>
</div>
<el-tooltip v-if="index < shotList.length - 1" content="新增分镜" placement="top"
:show-after="120">
<button type="button" class="shot-insert flex-center pointer"
@click.stop="insertShotAfter(index)">
<span class="insert-line"></span>
<span class="insert-dot flex-center">+</span>
</button>
</el-tooltip>
</template>
</div>
</footer>
</div> </div>
</footer> </div>
</main> </div>
</div> </div>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { import {
Aim, Aim,
CopyDocument, Clock,
Document, Document,
Folder, Folder,
FolderOpened, FolderOpened,
...@@ -230,16 +344,47 @@ import { ...@@ -230,16 +344,47 @@ import {
Notebook, Notebook,
Picture, Picture,
Plus, Plus,
Pointer,
Refresh, Refresh,
Star, Star,
User,
Delete,
VideoCamera, VideoCamera,
VideoPlay, VideoPlay,
Film,
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
import peopleImg from '@/static/image/meinv.png'; import peopleImg from '@/static/image/meinv.png';
import UploadFile from '@/components/Upload/index.vue';
type FilterValue = 'all' | 'role' | 'scene' | 'prop'; type FilterValue = 'all' | 'role' | 'scene' | 'prop';
type AssetFilterValue = Exclude<FilterValue, 'all'>;
type PreviewMode = 'generated' | 'origin'; type PreviewMode = 'generated' | 'origin';
type ResolutionValue = '480p' | '720p' | '1080p';
type FormatValue = 'mp4' | 'mov';
type ShotStatus = 'generated' | 'pending';
type ShotAsset = {
id: string;
name: string;
desc: string;
image: string;
};
type ShotItem = {
id: string;
status: ShotStatus;
duration: number;
count: number;
resolution: ResolutionValue;
format: FormatValue;
points: number;
prompt: string;
cover?: string;
assets: {
roles: ShotAsset[];
scenes: ShotAsset[];
props: ShotAsset[];
};
};
const filters: { label: string; value: FilterValue }[] = [ const filters: { label: string; value: FilterValue }[] = [
{ label: '全部', value: 'all' }, { label: '全部', value: 'all' },
...@@ -247,87 +392,148 @@ const filters: { label: string; value: FilterValue }[] = [ ...@@ -247,87 +392,148 @@ const filters: { label: string; value: FilterValue }[] = [
{ label: '场景', value: 'scene' }, { label: '场景', value: 'scene' },
{ label: '道具', value: 'prop' }, { label: '道具', value: 'prop' },
]; ];
const shots = [ const aiVal = ref('1')
{ const aiOptions: any = [
id: 1, { label: 'Doubao-Seedance-2.5', value: '1' },
duration: '2.0', { label: 'Doubao-Seedance-2-0', value: '2' },
camera: '位于苏晓正前方约1.5米,与她盘坐坐姿平齐的高度,固定不动。', ];
position: '盘腿坐在地面上,位于画面中央,面向镜头。', const filledShotPrompt = `画风: 写实风格, 细节刻画逼真,参考院线电影,真人电影风格,影视大片,真实透视比例,细节清晰不过度锐化
action: '双手捧着一个白色包装盒,面带微笑地看向镜头。',
speech: '今天新到手了来蜜的热销款0.01,真的很清透。', 分镜场景设定在:现代白天·轻奢简约风·居家客厅,室内,白天。
},
{ 分镜具体动作描述:
id: 2, 整体视觉基调:画面明亮,色彩饱和,质感细腻,景深较浅以突出人物和产品。
duration: '1.3',
camera: '位于苏晓正前方,略微俯拍她的手部和腿部,固定不动。', 镜头1(0-2秒):【现代白天·轻奢简约风·居家客厅,室内,白天】
position: '保持盘坐姿势,身体位于画面外,仅展示手部和膝盖区域。', 【机位】机位与美妆博主胸部齐平,平拍朝向美妆博主,固定不动,美妆博主坐在客厅沙发上。
action: '打开包装盒,从里面拿出一团雾好的黑色丝袜。', 【动作】中近景,美妆博主面部神态鲜活自然,眉眼灵动,带有自然小幅度肢体微晃,右手持产品,左手对准产品瓶盖用手指将产品瓶盖弹飞,随后左手拿出紫色手套涂抹工具,嘴角上扬,眼神明亮自信。`;
speech: '让我看看是怎么个超薄丝滑法。',
}, let nextShotId = 7;
{
id: 3, const createPendingShot = (id = `shot-${nextShotId++}`): ShotItem => ({
duration: '2.2', id,
camera: '与镜头2机位相同,固定不动。', status: 'pending',
position: '保持盘坐姿势,身体位于画面外,仅展示手部和膝盖区域。', duration: 5,
action: '双手捏住黑色丝袜的两端,将其缓缓拉伸展开。', count: 1,
speech: '颜色均匀通透。', resolution: '720p',
}, format: 'mov',
{ points: 1135,
id: 4, prompt: '',
duration: '2.5', assets: {
camera: '位于苏晓右侧,平拍她的手部,固定不动。', roles: [],
position: '苏晓的手臂举起,将丝袜置于镜头与窗帘之间。', scenes: [],
action: '双手撑开黑色丝袜的一部分,对着光亮处展示其通透质感。', props: [],
speech: '真的很清透。',
},
{
id: 5,
duration: '1.8',
camera: '机位位于苏晓正前方低位,略微仰拍,固定不动。',
position: '双腿交叉站立,身体位于画面中央。',
action: '穿着黑色丝袜和黑色高跟鞋,保持站姿,展示腿部穿着效果。',
speech: '是那种高级的黑丝质感。',
},
{
id: 6,
duration: '2.0',
camera: '机位在苏晓右前方,平拍,固定不动。',
position: '坐在黑色沙发上,跷着二郎腿,身体朝向镜头左前方约45度。',
action: '右手轻轻地从膝盖处向下抚摸穿着丝袜的小腿,面带微笑。',
speech: '穿上真的超舒适。',
},
{
id: 7,
duration: '1.2',
camera: '机位贴近苏晓的腿部,平拍特写,固定不动。',
position: '腿部占据画面大部分。',
action: '一只手在穿着黑色丝袜的小腿上缓慢地抚摸,展示丝袜的光泽和丝滑感。',
speech: '它的面料很亲肤。',
},
{
id: 8,
duration: '4.0',
camera: '机位在苏晓正前方,平拍,固定不动。',
position: '位于镜头前,身体被丝袜遮挡,仅手臂可见。',
action: '双手分别抓住黑色丝袜的腰部两端,用力向两侧反复拉伸。',
speech: '丝袜细腻,弹性也很大,各种身材的姐妹都能穿。',
}, },
});
const shotList = ref<ShotItem[]>([
{ {
id: 9, id: 'shot-1',
duration: '1.8', status: 'generated',
camera: '机位在苏晓右前方低位,略微仰拍她的腿部,固定不动。', duration: 15,
position: '穿着丝袜和高跟鞋,侧身坐在一个黑色吧台凳上,双腿交叠。', count: 1,
action: '镜头停留在腿部线条,展示自然的光泽。', resolution: '720p',
speech: '显瘦又高级。', format: 'mp4',
points: 3405,
prompt: filledShotPrompt,
cover: peopleImg,
assets: {
roles: [
{ id: 'role-1', name: '美妆博主', desc: '主形象', image: peopleImg },
{ id: 'role-2', name: '路人甲', desc: '变装1', image: peopleImg },
{ id: 'role-3', name: '路人乙', desc: '变装2', image: peopleImg },
],
scenes: [
{ id: 'scene-1', name: 'Modern Living Room Streaming Area', desc: '主形象', image: peopleImg },
],
props: [],
},
}, },
]; createPendingShot('shot-2'),
createPendingShot('shot-3'),
createPendingShot('shot-4'),
createPendingShot('shot-5'),
createPendingShot('shot-6'),
]);
const isReady = ref(false); const isReady = ref(false);
const planningProgress = ref(0); const planningProgress = ref(0);
const activeFilter = ref<FilterValue>('all'); const activeFilter = ref<FilterValue>('all');
const previewMode = ref<PreviewMode>('generated'); const previewMode = ref<PreviewMode>('generated');
const specVisible = ref(false);
const activeShotId = ref(shotList.value[0].id);
const videoCountOptions = [1, 2, 3, 4];
const resolutionOptions: ResolutionValue[] = ['480p', '720p', '1080p'];
const formatOptions: FormatValue[] = ['mp4', 'mov'];
const currentShotIndex = computed(() => Math.max(0, shotList.value.findIndex((item) => item.id === activeShotId.value)));
const currentShot = computed(() => shotList.value[currentShotIndex.value] || shotList.value[0]);
const currentShotPrompt = computed({
get: () => currentShot.value.prompt,
set: (value: string) => {
currentShot.value.prompt = value;
},
});
const currentVideoDuration = computed({
get: () => currentShot.value.duration,
set: (value: number) => {
currentShot.value.duration = value;
},
});
const currentVideoCount = computed({
get: () => currentShot.value.count,
set: (value: number) => {
currentShot.value.count = value;
},
});
const currentResolution = computed({
get: () => currentShot.value.resolution,
set: (value: ResolutionValue) => {
currentShot.value.resolution = value;
},
});
const currentFormat = computed({
get: () => currentShot.value.format,
set: (value: FormatValue) => {
currentShot.value.format = value;
},
});
const specSummary = computed(() => `${currentShot.value.duration}s | ${currentShot.value.resolution} | ${currentShot.value.count}个 | ${currentShot.value.format}`);
let timer: number | undefined; let timer: number | undefined;
const isFilterVisible = (value: AssetFilterValue) => {
return activeFilter.value === 'all' || activeFilter.value === value;
};
const padShotNo = (value: number) => String(value).padStart(2, '0');
const selectShot = (id: string) => {
activeShotId.value = id;
specVisible.value = false;
};
const insertShotAfter = (index: number) => {
const shot = createPendingShot();
shotList.value.splice(index + 1, 0, shot);
selectShot(shot.id);
};
const deleteShot = (id: string) => {
if (shotList.value.length <= 1) {
return;
}
const index = shotList.value.findIndex((item) => item.id === id);
if (index === -1) {
return;
}
shotList.value.splice(index, 1);
if (activeShotId.value === id) {
const nextIndex = Math.min(index, shotList.value.length - 1);
selectShot(shotList.value[nextIndex].id);
}
};
const runPlanning = () => { const runPlanning = () => {
timer = window.setInterval(() => { timer = window.setInterval(() => {
planningProgress.value = Math.min(100, planningProgress.value + 20); planningProgress.value = Math.min(100, planningProgress.value + 20);
...@@ -355,7 +561,7 @@ onBeforeUnmount(() => { ...@@ -355,7 +561,7 @@ onBeforeUnmount(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.step-three { .step-three {
background: #f8fafc; background: #f8f9fa;
} }
.planning-panel { .planning-panel {
...@@ -371,8 +577,8 @@ onBeforeUnmount(() => { ...@@ -371,8 +577,8 @@ onBeforeUnmount(() => {
} }
.episode-rail { .episode-rail {
flex: 0 0 48px; flex: 0 0 60px;
background: #eef3ff; background: #f3f4fa;
} }
.episode-no { .episode-no {
...@@ -380,7 +586,7 @@ onBeforeUnmount(() => { ...@@ -380,7 +586,7 @@ onBeforeUnmount(() => {
} }
.asset-sidebar { .asset-sidebar {
flex: 0 0 282px; flex: 0 0 270px;
min-height: 0; min-height: 0;
} }
...@@ -400,25 +606,31 @@ onBeforeUnmount(() => { ...@@ -400,25 +606,31 @@ onBeforeUnmount(() => {
} }
.role-img, .role-img,
.scene-img,
.empty-card, .empty-card,
.preview-empty, .preview-empty,
.shot-thumb { .shot-thumb {
background: #f3f5f9; background: #f3f5f9;
} }
.role-card {
width: 118px;
}
.scene-card {
width: 100%;
}
.story-content { .story-content {
min-height: 0; min-height: 0;
} }
.toolbar { .story-editor {
flex: 0 0 54px; min-width: 0;
border-bottom: 1px solid #dce6f2;
} }
.script-panel, .toolbar {
.preview-panel, flex: 0 0 54px;
.shot-strip {
border: 1px solid #e2e8f3;
} }
.script-scroll { .script-scroll {
...@@ -472,14 +684,91 @@ onBeforeUnmount(() => { ...@@ -472,14 +684,91 @@ onBeforeUnmount(() => {
.shot-strip { .shot-strip {
flex: 0 0 164px; flex: 0 0 164px;
min-width: 0;
overflow: hidden;
border: 1px solid #edf2f7;
}
.shot-list {
width: 100%;
min-width: 0;
overflow-y: hidden;
gap: 2px;
} }
.shot-card { .shot-card {
width: 150px; flex: 0 0 130px;
&.active {
.shot-thumb {
border-color: #4c45ff;
}
}
} }
.shot-thumb { .shot-thumb {
border: 2px solid rgba(var(--main-color), 1); border: 2px solid transparent;
transition: border-color .18s ease;
}
.shot-delete {
top: 6px;
right: 6px;
border: 0;
color: #fff;
background: rgba(85, 92, 106, .72);
opacity: 0;
transition: opacity .18s ease;
}
.shot-card:hover {
.shot-delete {
opacity: 1;
}
}
.shot-insert {
position: relative;
flex: 0 0 16px;
align-self: flex-start;
width: 16px;
height: 90px;
margin-top: 0;
border: 0;
background: transparent;
.insert-line {
width: 2px;
height: 90px;
border-radius: 2px;
background: transparent;
}
.insert-dot {
position: absolute;
top: 39px;
left: 50%;
width: 18px;
height: 18px;
border-radius: 50%;
color: #fff;
background: #4c45ff;
font-size: 14px;
line-height: 18px;
opacity: 0;
transform: translateX(-50%);
transition: opacity .18s ease;
}
&:hover {
.insert-line {
background: #4c45ff;
}
.insert-dot {
opacity: 1;
}
}
} }
:deep(.planning-card .el-progress-bar__outer) { :deep(.planning-card .el-progress-bar__outer) {
...@@ -489,4 +778,127 @@ onBeforeUnmount(() => { ...@@ -489,4 +778,127 @@ onBeforeUnmount(() => {
:deep(.planning-card .el-progress-bar__inner) { :deep(.planning-card .el-progress-bar__inner) {
background: rgba(var(--main-color), 1); background: rgba(var(--main-color), 1);
} }
.textarea {
:deep(.el-textarea__inner) {
height: 100%;
box-shadow: none;
}
}
.select {
:deep(.el-select__wrapper) {
border-radius: 8px;
}
}
:deep(.burst-spec-popper) {
padding: 0 !important;
border: 0 !important;
border-radius: 14px !important;
box-shadow: 0 12px 34px rgba(19, 35, 64, .14) !important;
}
.spec-trigger {
min-width: 168px;
border: 1px solid #dce6f2;
background: #fff;
white-space: nowrap;
&.active,
&:hover {
border-color: #cdd7e6;
background: #f8fafc;
}
}
.spec-panel {
padding: 14px 12px 10px;
}
.spec-section+.spec-section {
margin-top: 12px;
}
.spec-label {
margin-bottom: 8px;
color: #8b97aa;
font-size: 12px;
line-height: 18px;
}
.duration-row {
height: 34px;
}
.duration-value {
min-width: 44px;
border: 1px solid #dbe4f0;
background: #fff;
color: #111827;
font-size: 12px;
font-weight: 700;
box-shadow: 0 4px 12px rgba(37, 56, 88, .08);
}
.spec-slider {
:deep(.el-slider__runway) {
height: 2px;
margin: 16px 0;
background: #121212;
}
:deep(.el-slider__bar) {
height: 2px;
background: #121212;
}
:deep(.el-slider__button) {
width: 13px;
height: 13px;
border: 1px solid #d7dfeb;
background: #fff;
box-shadow: 0 2px 7px rgba(25, 43, 72, .2);
}
}
.spec-segment {
display: grid;
height: 34px;
padding: 3px;
border-radius: 10px;
background: #f1f3f8;
gap: 2px;
}
.grid-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.spec-option {
min-width: 0;
border: 0;
border-radius: 9px;
background: transparent;
color: #68758a;
font-size: 12px;
font-weight: 600;
line-height: 28px;
cursor: pointer;
&.active {
background: #fff;
color: #111827;
font-weight: 800;
box-shadow: 0 3px 10px rgba(31, 46, 76, .1);
}
}
</style> </style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment