Commit 5d11284c authored by 黄同智's avatar 黄同智

爆款复刻的70%页面

parent 048729dc
<template>
<div>
<el-upload v-if="listType === 'drap'" class="upload-demo" drag
<el-upload v-if="listType === 'drap'" drag :class="bgClass ?? 'upload-demo'"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15" :multiple="multiple" :limit="limit"
:accept="accept" :showFileList="showFileList">
<slot v-if="!hasFile" name="trigger">
......@@ -38,6 +38,10 @@ import type { UploadProps, UploadUserFile, UploadFile } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
bgClass: {
type: String,
default: 'upload-demo'
},
action: {
type: String,
default: '',
......@@ -193,7 +197,7 @@ defineExpose({
<style scoped lang="scss">
:deep(.upload-demo) {
.el-upload-dragger {
background-color: rgba(var(--main-color), 0.04);
background-color: #eee;
}
}
.file-upload-wrapper {
......
......@@ -91,7 +91,7 @@ const menus = [
},
{
name: '爆款复刻',
path: '/',
path: '/burst',
icon: 'caijian',
},
{
......
......@@ -11,6 +11,7 @@ import Layout from '@/layout/index.vue'
import Task from '@/views/task/index.vue'
import QuickCreation from '@/views/quickCreation/index.vue'
import Agent from '@/views/agent/index.vue'
import Burst from '@/views/burst/index.vue'
const routes = [
{
......@@ -92,6 +93,24 @@ const routes = [
]
},
{
path: '/burst',
component: Layout,
children: [
{
path: '',
name: 'Burst',
component: Burst,
meta: { title: '爆款复刻', icon: 'caijian', affix: true }
}
]
},
{
path: '/burst/replica',
name: 'BurstReplica',
component: () => import('@/views/burst/replica.vue'),
meta: { title: '爆款复刻工作台', hidden: true }
},
{
path: '/zhibo',
component: Layout,
children: [
......
<template>
<section class="step-one maxw-1200 mx-auto">
<div class="notice-box flex-items-center gap-10 h-48 px-18 round-8 mb-26">
<el-icon class="color-main fs-16">
<InfoFilled />
</el-icon>
<span class="fs-12 color-000">
说明:确认剧集视频后,系统将对原视频进行资产解析,提取主要的人物、场景、道具资产,用于后续替换
</span>
</div>
<div class="flex-between mb-26">
<h2 class="fs-20 fw-700 color-000">原片剧集</h2>
</div>
<div class="flex-between mb-14">
<div class="flex-items-center gap-16 fs-12 color-536">
<span class="flex-items-center gap-4">
<el-icon>
<VideoCamera />
</el-icon>
全部剧集<span class="color-000 fw-700">{{ episodes.length }}</span>
</span>
<span>已上传<span class="color-main fw-700">{{ uploadedCount }}</span></span>
<span>上传中<span class="color-main fw-700">{{ uploadingCount }}</span></span>
<span>上传失败<span class="danger-text fw-700">{{ failedCount }}</span></span>
</div>
<el-button class="h-32 px-18 fs-12" @click="addEpisode">
<el-icon>
<Plus />
</el-icon>
<span>新增视频</span>
</el-button>
</div>
<el-table :data="episodes" class="episode-table w-full" row-key="id">
<el-table-column label="剧集信息" min-width="420">
<template #default="{ row }">
<div class="flex-items-center gap-12 py-8">
<img :src="row.cover" :alt="row.name" class="episode-cover w-48 h-32 round-4 cover" />
<div class="flex-col">
<span class="fs-14 fw-700 color-000 lh-22">{{ row.name }}</span>
<span class="fs-12 color-666 lh-20">{{ row.filename }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="duration" label="时长" width="150" />
<el-table-column prop="size" label="大小" width="170" />
<el-table-column label="状态" width="170">
<template #default="{ row }">
<span class="flex-items-center gap-6 fs-12 color-333">
<span class="status-dot wh-8 round" :class="row.status"></span>
{{ row.statusText }}*
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="190">
<template #default>
<el-button type="primary" link class="fs-12">重新上传</el-button>
<el-button type="danger" link class="fs-12">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="fixed bottom-10 left-0 flex-center w-full">
<el-button type="primary" class="start-btn h-44 round-140 border-none mt-24" @click="goNext">
<el-icon>
<MagicStick />
</el-icon>
<span>进入下一步</span>
<el-icon>
<ArrowRight />
</el-icon>
</el-button>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { ArrowRight, InfoFilled, MagicStick, Plus, VideoCamera } from '@element-plus/icons-vue';
import coverImg from '@/static/image/huaban.png';
type EpisodeStatus = 'done' | 'uploading' | 'failed';
type EpisodeItem = {
id: string;
name: string;
filename: string;
duration: string;
size: string;
status: EpisodeStatus;
statusText: string;
cover: string;
};
const emit = defineEmits<{
(e: 'next'): void;
}>();
const episodes = ref<EpisodeItem[]>([
{
id: 'episode-1',
name: '第 1 集',
filename: 'r_cde1602bd05a40f9b3d3606dc531df01.mp4',
duration: '00:15',
size: '19.44 MB',
status: 'done',
statusText: '已完成',
cover: coverImg,
},
]);
const uploadedCount = computed(() => episodes.value.filter((item) => item.status === 'done').length);
const uploadingCount = computed(() => episodes.value.filter((item) => item.status === 'uploading').length);
const failedCount = computed(() => episodes.value.filter((item) => item.status === 'failed').length);
const addEpisode = () => {
episodes.value.push({
id: `episode-${Date.now()}`,
name: `第 ${episodes.value.length + 1} 集`,
filename: 'new_replica_source.mp4',
duration: '00:00',
size: '0 MB',
status: 'uploading',
statusText: '上传中',
cover: coverImg,
});
};
const goNext = () => {
emit('next');
};
</script>
<style scoped lang="scss">
.step-one {
width: 1200px;
margin: 0 auto;
}
.notice-box {
border: 1px solid #cfd8ff;
background: #eef1ff;
}
.color-536 {
color: #536883;
}
.danger-text {
color: #f5222d;
}
.episode-table {
border: 1px solid #dce6f2;
border-bottom: 0;
}
.episode-cover {
flex: 0 0 48px;
background: #f3f6fb;
}
.status-dot.done {
background: #18b66a;
}
.status-dot.uploading {
background: rgba(var(--main-color), 1);
}
.status-dot.failed {
background: #f5222d;
}
:deep(.episode-table .el-table__header th) {
height: 40px;
font-size: 12px;
color: #536883;
font-weight: 500;
background: #f8fafc;
}
:deep(.episode-table .el-table__cell) {
border-color: #dce6f2;
}
:deep(.episode-table .el-table__row) {
height: 62px;
}
</style>
<template>
<div class="step-two-page h-full hidden flex" :class="{ 'detail-open': selectedAsset }">
<div class="step-left h-full relative" :class="selectedAsset ? 'flex-1' : 'w-full'">
<div class="h-full w-full">
<section class="step-two h-full px-24 relative flex-col">
<div v-if="!isParsed" class="parse-panel flex-col flex-center text-center">
<div class="parse-illus relative mb-16">
<el-icon class="document-icon">
<Document />
</el-icon>
<span class="loading-badge flex-center wh-24 round">
<el-icon class="loading-icon">
<Loading />
</el-icon>
</span>
</div>
<div class="fs-18 fw-700 color-000 lh-30">剧集资产解析中</div>
<div class="fs-14 color-536 lh-24">剧集资产解析中,请耐心等待...</div>
<div class="parse-card flex-between gap-24 mt-52 px-52 py-22 round-14">
<div v-for="item in parseTasks" :key="item.key" class="parse-item flex-col flex-center">
<el-progress type="circle" :percentage="item.progress" :width="48" :stroke-width="5"
:status="item.progress === 100 ? 'success' : undefined" :color="progressColor" />
<div class="fs-14 color-536 lh-24 mt-12">{{ item.label }}</div>
</div>
</div>
</div>
<div v-else class="parsed-panel h-full flex-col">
<div class="result-notice flex-between h-48 px-16 round-8 mt-14 mb-20">
<div class="flex-items-center gap-10 fs-12 color-000">
<el-icon class="color-main ">
<InfoFilled />
</el-icon>
<span>请完成新旧角色、场景、和道具映射关系;确认替换关系后,系统将自动对原视频进行智能切片并提取关键词对应,用于后续分镜生成。</span>
</div>
<el-icon class="color-536 pointer">
<Close />
</el-icon>
</div>
<div class="flex-between mb-28">
<div class="asset-tabs flex-items-center gap-28">
<div v-for="tab in assetTabs" :key="tab.key"
class="asset-tab flex-items-center gap-6 pointer"
:class="{ active: activeTab === tab.key }" @click="changeTab(tab.key)">
<span>{{ tab.label }}</span>
<span class="tab-count flex-center h-16 px-6 round-8 color-fff fs-12">{{ tab.count
}}</span>
</div>
</div>
<div class="asset-actions flex-items-center gap-16 fs-12 color-536">
<span>形象总计 <span class="color-000">3</span></span>
<span>已解析 <span class="color-main">0</span></span>
<span>失败<span class="danger-text">0</span></span>
<span class="divider"></span>
<el-button link class="fs-12 color-000">
<el-icon>
<Plus />
</el-icon>
添加角色
</el-button>
<span class="divider"></span>
<el-button link class="fs-12 color-000">
<el-icon>
<List />
</el-icon>
批量生成
</el-button>
<span class="divider"></span>
<el-icon class="fs-18 color-536 pointer">
<MoreFilled />
</el-icon>
</div>
</div>
<div class="asset-scroll flex-1 over-y-auto hide-scroll pb-80">
<div v-if="currentAssets.length" class="asset-list flex flex-wrap gap-20">
<div v-for="item in currentAssets" :key="item.id"
class="asset-card bg-fff round-8 p-12 pointer"
:class="{ active: selectedAsset?.id === item.id }" @click="selectAsset(item)">
<div class="asset-compare flex gap-8">
<div class="asset-side flex-col flex-1">
<div class="asset-media relative round-6 hidden">
<span class="corner-tag old flex-center color-fff fs-12"></span>
<img :src="item.origin" :alt="item.oldName"
class="w-full h-full cover" />
</div>
<div class="fs-14 color-000 lh-24 mt-8">{{ item.oldName }}</div>
</div>
<div class="asset-side flex-col flex-1">
<div class="asset-media empty-media relative flex-center round-6">
<span class="corner-tag new flex-center color-fff fs-12"></span>
<div class="flex-col flex-center color-999 fs-12">
<el-icon class="fs-22 mb-6">
<Picture />
</el-icon>
<span>暂未配置设定图</span>
</div>
</div>
<div class="fs-14 color-000 lh-24 mt-8">{{ item.newName }}</div>
</div>
</div>
<div class="flex-between mt-8">
<el-button class="h-32 px-14 fs-12" @click.stop="openVoiceDialog(item)">
<el-icon>
<Microphone />
</el-icon>
配置音色
</el-button>
<el-button class="more-btn wh-32 p-0" plain>
<el-icon>
<MoreFilled />
</el-icon>
</el-button>
</div>
</div>
</div>
<div v-else class="empty-assets flex-center h-220 bg-fff round-8 color-999 fs-14">
暂无道具资产
</div>
</div>
</div>
</section>
</div>
<div class="absolute bottom-10 w-full color-000 flex-center">
<el-button type="primary" class="start-btn h-44 round-140 border-none mt-24" @click="goNext">
<el-icon>
<MagicStick />
</el-icon>
<span>分镜解析</span>
<el-icon>
<ArrowRight />
</el-icon>
</el-button>
</div>
</div>
<div class="py-14 pr-14 flex-1">
<aside v-if="selectedAsset" class="asset-detail bg-fff h-full p-20 flex-col round-14">
<div class="detail-header flex-between mb-24">
<div class=" fw-700 color-000">{{ selectedAsset.newName }}</div>
<div class="flex-items-center gap-16">
<el-icon class=" color-333 pointer">
<Edit />
</el-icon>
<el-button link class="fs-12 color-000">
<el-icon>
<Plus />
</el-icon>
添加变装
</el-button>
<span class="divider"></span>
<el-icon class=" color-333 pointer">
<Star />
</el-icon>
<el-icon class=" color-333 pointer">
<Download />
</el-icon>
<span class="divider"></span>
<el-button type="primary" class="h-32 px-18 fs-12 border-none">确认角色</el-button>
<el-icon class=" color-999 pointer" @click="clearSelectedAsset">
<Close />
</el-icon>
</div>
</div>
<div class="detail-preview-wrap flex-1 flex gap-12 hidden">
<div class="detail-preview flex-1 flex-center round-6">
<div class="flex-col flex-center color-999 fs-14">
<el-icon class="fs-28 mb-8">
<Picture />
</el-icon>
<span>暂未配置设定图</span>
</div>
</div>
<div class="thumb-column flex-col items-center gap-10">
<div class="fs-12 color-333 lh-20">当前</div>
<div class="current-thumb wh-48 round-6 hidden pointer">
<img :src="selectedAsset.origin" :alt="selectedAsset.oldName" class="w-full h-full cover" />
</div>
</div>
</div>
<div class="agent-panel mt-14 round-14 hidden bg-fff mr-70">
<div class="agent-title flex-between h-48 px-12">
<div class="flex-items-center gap-8 fs-14 fw-700 color-000">
<el-icon>
<MagicStick />
</el-icon>
Agent 生成形象
</div>
<div class="flex-items-center gap-16 fs-12 color-333">
<span class="flex-items-center gap-4 pointer">
<el-icon>
<FolderOpened />
</el-icon>
从角色库选择
</span>
<span class="flex-items-center gap-4 pointer">
<el-icon>
<Upload />
</el-icon>
本地上传
</span>
</div>
</div>
<div class="agent-content m-12 p-12 round-10">
<el-button class="wh-36 p-0 mb-10" plain>
<el-icon>
<Plus />
</el-icon>
</el-button>
<div class="fs-12 color-000 lh-22">
- 任务:完成角色 {{ selectedAsset.newName }}
的上半身正面平视特写和该角色的全身三视图,左边是角色的上半身正面平视特写,右边是该角色的全身三视图。三视图不可以有分割线,比例是
16:9,用于展示面部、发型、表情、眼神、上半身服装和配饰细节。
</div>
<div class="flex-items-center flex-wrap gap-6 mt-12">
<el-tag effect="plain" round>Doubao-Seedream-5.0-Pro</el-tag>
<el-tag effect="plain" round>1张</el-tag>
<el-tag effect="plain" round>1k</el-tag>
<el-tag effect="plain" round>16:9</el-tag>
<el-tag effect="plain" round>现代都市通用</el-tag>
</div>
<div class="flex justify-end mt-10">
<el-button type="primary" class="h-32 px-18 border-none">
<el-icon>
<MagicStick />
</el-icon>
30
</el-button>
</div>
</div>
</div>
</aside>
</div>
<VoiceConfigDialog v-model="voiceDialogVisible" @confirm="confirmVoice" />
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import {
ArrowRight,
Close,
Document,
Download,
Edit,
FolderOpened,
InfoFilled,
List,
Loading,
MagicStick,
Microphone,
MoreFilled,
Picture,
Plus,
Star,
Upload,
} from '@element-plus/icons-vue';
import coverImg from '@/static/image/huaban.png';
import roleImg from '@/static/image/meinv.png';
import VoiceConfigDialog from './VoiceConfigDialog.vue';
type AssetTabKey = 'role' | 'scene' | 'prop';
type ParseTask = {
key: string;
label: string;
progress: number;
};
type AssetItem = {
id: string;
oldName: string;
newName: string;
origin: string;
};
const isParsed = ref(false);
const activeTab = ref<AssetTabKey>('role');
const selectedAsset = ref<AssetItem>();
const voiceDialogVisible = ref(false);
const voiceAsset = ref<AssetItem>();
const emit = defineEmits<{
(e: 'next'): void;
}>();
const parseTasks = ref<ParseTask[]>([
{ key: 'summary', label: '剧情概要解析', progress: 0 },
{ key: 'role', label: '角色解析', progress: 0 },
{ key: 'scene', label: '场景解析', progress: 0 },
{ key: 'prop', label: '道具解析', progress: 0 },
]);
const assetTabs = [
{ key: 'role', label: '角色', count: 3 },
{ key: 'scene', label: '场景', count: 1 },
{ key: 'prop', label: '道具', count: 0 },
] as const;
const assetMap: Record<AssetTabKey, AssetItem[]> = {
role: [
{ id: 'role-1', oldName: '万秋', newName: '万秋', origin: roleImg },
{ id: 'role-2', oldName: '村支书', newName: '村支书', origin: coverImg },
{ id: 'role-3', oldName: '[群像]村民', newName: '[群像]村民', origin: roleImg },
],
scene: [
{ id: 'scene-1', oldName: '村口院落', newName: '村口院落', origin: coverImg },
],
prop: [],
};
let timer: number | undefined;
const progressColor = 'rgba(var(--main-color), 1)';
const currentAssets = computed(() => assetMap[activeTab.value]);
const changeTab = (key: AssetTabKey) => {
activeTab.value = key;
selectedAsset.value = undefined;
};
const selectAsset = (item: AssetItem) => {
selectedAsset.value = item;
};
const clearSelectedAsset = () => {
selectedAsset.value = undefined;
};
const openVoiceDialog = (item: AssetItem) => {
voiceAsset.value = item;
voiceDialogVisible.value = true;
};
const confirmVoice = (value: { tab: string; voiceId?: string; prompt?: string }) => {
console.log('confirm voice', voiceAsset.value, value);
};
const goNext = () => {
emit('next');
};
const runParsing = () => {
let times = 0;
timer = window.setInterval(() => {
times += 1;
parseTasks.value = parseTasks.value.map((item, index) => {
const fastComplete = index !== 1 && times >= 4;
const nextProgress = fastComplete ? 100 : Math.min(100, item.progress + 18 + index * 4);
return {
...item,
progress: nextProgress,
};
});
if (parseTasks.value.every((item) => item.progress === 100)) {
window.clearInterval(timer);
timer = undefined;
window.setTimeout(() => {
isParsed.value = true;
}, 500);
}
}, 420);
};
onMounted(() => {
runParsing();
});
onBeforeUnmount(() => {
if (timer) {
window.clearInterval(timer);
}
});
</script>
<style scoped lang="scss">
.step-two-page {
background: #f8fafc;
}
.step-left,
.asset-detail {
min-width: 0;
}
.asset-detail {
box-shadow: 0 0 8px #eee;
}
.step-two {
box-sizing: border-box;
}
.parsed-panel,
.asset-scroll {
min-height: 0;
}
.parse-panel {
min-height: 520px;
}
.parse-illus {
width: 72px;
height: 72px;
}
.document-icon {
font-size: 72px;
color: #e4e8ef;
}
.loading-badge {
position: absolute;
right: 2px;
bottom: 8px;
color: #fff;
background: rgba(var(--main-color), 1);
box-shadow: 0 2px 8px rgba(var(--main-color), .4);
}
.loading-icon {
animation: spin 1s linear infinite;
}
.parse-card {
width: 560px;
border: 1px solid #e6ebf5;
background: #fbfcff;
}
.parse-item {
flex: 1;
}
.color-536 {
color: #536883;
}
.result-notice {
border: 1px solid #cfd8ff;
background: #eef1ff;
}
.asset-tab {
color: #536883;
&.active {
color: rgba(var(--main-color), 1);
}
}
.tab-count {
min-width: 16px;
background: #f04455;
}
.divider {
width: 1px;
height: 14px;
background: #dce6f2;
}
.danger-text {
color: #f5222d;
}
.asset-card {
flex: 0 0 calc((100% - 60px) / 4);
border: 1px solid transparent;
box-shadow: 0 6px 20px rgba(22, 36, 58, .04);
&.active {
border-color: rgba(var(--main-color), 1);
}
}
.detail-open {
.asset-card {
flex: 0 0 calc((100% - 20px) / 2);
}
.asset-media {
height: 352px;
}
}
.asset-media {
height: 376px;
background: #f4f6fa;
}
.empty-media {
border: 1px solid #eef1f6;
}
.corner-tag {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 18px;
height: 18px;
border-radius: 4px 0 4px 0;
}
.corner-tag.old {
background: #6b7b93;
}
.corner-tag.new {
background: rgba(var(--main-color), 1);
}
.more-btn {
background: #f8fafc;
}
.asset-detail {
border-left: 1px solid #edf2f7;
}
.detail-preview-wrap {
min-height: 360px;
}
.detail-preview {
background: #f4f6fa;
}
.thumb-column {
flex: 0 0 58px;
}
.current-thumb {
border: 2px solid rgba(var(--main-color), 1);
}
.agent-panel {
border: 1px solid #edf2f7;
}
.agent-title {
background: #f8fafc;
}
.agent-content {
border: 1px solid #edf2f7;
background: #fff;
}
:deep(.el-progress-circle__track) {
stroke: #dfe5f6;
}
:deep(.el-progress__text) {
font-size: 12px !important;
font-weight: 700;
color: #000;
}
:deep(.el-progress.is-success .el-progress__text) {
color: #18b66a;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
<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 48px;
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>
<template>
<div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
</style>
\ No newline at end of file
<template>
<Dialog v-model="dialogVisible" bg-color="#fff" width="720">
<div class="voice-dialog bg-fff round-10 hidden flex-col">
<header class="dialog-header flex-items-center gap-12 px-28 pt-28 pb-22">
<span class="header-icon flex-center wh-48 round-10">
<el-icon class="fs-24 color-536">
<Microphone />
</el-icon>
</span>
<h3 class="fs-18 fw-700 color-000">配置音色</h3>
</header>
<section class="dialog-body flex-col px-32">
<div class="tab-row flex-items-center h-44 p-4 round-6 mb-20">
<div v-for="item in tabs" :key="item.value" class="tab-item flex-center h-36 fs-14 pointer"
:class="{ active: activeTab === item.value }" @click="activeTab = item.value">
{{ item.label }}
</div>
</div>
<div v-if="activeTab === 'design'" class="design-panel flex-1 flex hidden">
<div class="design-form flex-1 pr-20">
<div class="field-label flex-items-center gap-8 mb-8">
<span class="index-dot flex-center wh-18 round fs-12">1</span>
<span class="fs-14 color-666">提示词</span>
</div>
<el-input v-model="voicePrompt" class="voice-textarea mb-18" type="textarea" resize="none"
:rows="7" placeholder="请输入音色提示词" />
<div class="field-label flex-items-center gap-8 mb-8">
<span class="index-dot flex-center wh-18 round fs-12">2</span>
<span class="fs-14 color-666">试听文本(可选)</span>
</div>
<el-input v-model="auditionText" class="voice-textarea" type="textarea" resize="none" :rows="3"
placeholder="请输入试听文本" />
<div class="flex-center mt-42">
<el-button type="primary" class="generate-btn w-260 h-36 border-none fs-14"
:disabled="!voicePrompt.trim()">
<el-icon>
<MagicStick />
</el-icon>
<span>100</span>
</el-button>
</div>
</div>
<div class="history-panel flex-1 pl-20">
<div class="fs-14 color-666 mb-10">历史</div>
<div class="history-empty flex-col flex-center h-full color-aaa fs-14">
<el-icon class="fs-28 mb-8">
<Microphone />
</el-icon>
<span>暂未生成角色音频</span>
</div>
</div>
</div>
<div v-else-if="activeTab === 'library'" class="library-panel flex-1 flex-col hidden">
<div class="filter-row flex-items-center gap-12 mb-20">
<el-input v-model="searchKeyword" class="search-input" placeholder="搜索">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-select v-model="genderValue" class="filter-select" placeholder="全部性别">
<el-option v-for="item in genderOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="ageValue" class="filter-select" placeholder="全部年龄">
<el-option v-for="item in ageOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="languageValue" class="filter-select" placeholder="全部语言">
<el-option v-for="item in languageOptions" :key="item" :label="item" :value="item" />
</el-select>
</div>
<div class="voice-list flex flex-wrap over-y-auto hide-scroll">
<div v-for="item in voiceList" :key="item.id" class="voice-item flex-items-center gap-10 p-10 round-8 pointer"
:class="{ active: selectedVoiceId === item.id }" @click="selectedVoiceId = item.id">
<div class="voice-avatar relative wh-42 round hidden">
<img :src="item.avatar" :alt="item.name" class="w-full h-full cover" />
<span class="play-dot absolute flex-center wh-14 round color-fff">
<el-icon class="fs-10">
<VideoPlay />
</el-icon>
</span>
</div>
<div class="flex-1 hidden">
<div class="fs-14 fw-700 color-000 lh-22">{{ item.name }}</div>
<div class="fs-12 color-aaa lh-20 ellipsis">{{ item.desc }}</div>
</div>
</div>
</div>
</div>
<div v-else-if="activeTab === 'local'" class="local-panel flex-1">
<UploadFile v-model="localFiles" list-type="drap" accept=".mp3,.wav" :max-size="10"
:show-file-list="true">
<template #trigger>
<div class="upload-trigger flex-col flex-center">
<el-icon class="fs-28 color-536 mb-14">
<Microphone />
</el-icon>
<div class="fs-16 fw-700 color-333 lh-26">点击或拖拽音频文件到此处上传</div>
<div class="fs-12 color-888 lh-22">支持 MP3、WAV,单个文件不超过 10MB,超过 5 秒将自动裁剪</div>
</div>
</template>
</UploadFile>
</div>
<div v-else class="audio-panel flex-1 flex-col">
<div class="flex-between mb-12">
<el-select v-model="teamValue" class="team-select">
<el-option v-for="item in teamOptions" :key="item" :label="item" :value="item" />
</el-select>
<div class="audio-tabs flex-items-center gap-24 fs-14">
<span class="audio-tab active pointer">所有音频</span>
<span class="audio-tab color-666 pointer">文件夹</span>
</div>
</div>
<el-input v-model="audioKeyword" class="mb-20" placeholder="搜索">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<div class="audio-empty flex-1 flex-col flex-center color-aaa fs-14">
<span class="empty-icon flex-center wh-48 round-10 mb-14">
<el-icon class="fs-24">
<Microphone />
</el-icon>
</span>
<span>暂无音频内容</span>
</div>
</div>
</section>
<footer class="dialog-footer flex justify-end gap-12 px-24 py-20">
<el-button class="w-96 h-36 fs-14" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" class="confirm-btn w-96 h-36 fs-14 border-none" @click="confirmVoice">
确认
</el-button>
</footer>
</div>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { UploadUserFile } from 'element-plus';
import { MagicStick, Microphone, Search, VideoPlay } from '@element-plus/icons-vue';
import Dialog from '@/components/Dialog/index.vue';
import UploadFile from '@/components/Upload/index.vue';
import peopleImg from '@/static/image/meinv.png';
import productImg from '@/static/image/huaban.png';
type VoiceTab = 'design' | 'library' | 'local' | 'audio';
type VoiceItem = {
id: string;
name: string;
desc: string;
avatar: string;
};
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
confirm: [value: { tab: VoiceTab; voiceId?: string; prompt?: string }];
}>();
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const tabs: { label: string; value: VoiceTab }[] = [
{ label: '智能音色设计', value: 'design' },
{ label: '音色库选择', value: 'library' },
{ label: '本地上传', value: 'local' },
{ label: '音频库', value: 'audio' },
];
const genderOptions = ['全部性别', '男声', '女声'];
const ageOptions = ['全部年龄', '青年', '中年', '老年'];
const languageOptions = ['全部语言', '中文', '英文', '粤语'];
const teamOptions = ['团队资产', '我的资产'];
const voiceList: VoiceItem[] = [
{ id: 'voice-1', name: '婆婆', desc: '语调舒缓、声线慈祥,自带岁月感的长辈音色', avatar: peopleImg },
{ id: 'voice-2', name: '幽默大爷', desc: '墨迹沧桑的乐观爷爷,通透豁达又从容', avatar: productImg },
{ id: 'voice-3', name: '和蔼奶奶', desc: '慈祥的老奶奶,耐心亲切,散发着岁月沉淀', avatar: peopleImg },
{ id: 'voice-4', name: '武则天', desc: '声线威严、气场拉满,自带帝王霸气的御姐音', avatar: productImg },
{ id: 'voice-5', name: '邻居阿姨', desc: '温暖成熟的中年阿姨,兼具知性气质', avatar: peopleImg },
{ id: 'voice-6', name: '女雷神', desc: '声线雄浑、气场拉满,充满力量感的御姐音', avatar: productImg },
{ id: 'voice-7', name: '温柔妈妈', desc: '语调舒缓、咬字温润,自带母性柔光的治愈音', avatar: peopleImg },
{ id: 'voice-8', name: '胡子叔叔', desc: '历经风雨后变得沉稳的大叔,果敢让人信赖', avatar: productImg },
{ id: 'voice-9', name: '油腻大叔', desc: '沧桑的大叔,性格张扬,十分自信爱吹牛', avatar: peopleImg },
{ id: 'voice-10', name: '诡异神秘', desc: '诡异神秘的大叔,气质沧桑,自带压迫感', avatar: productImg },
];
const activeTab = ref<VoiceTab>('design');
const voicePrompt = ref('女声,青年音色,音调偏高,质感清透柔亮,气息绵长,语速轻快,情绪底色亲切活泼');
const auditionText = ref('你好,我的声音,为你演尽喜怒哀乐。');
const searchKeyword = ref('');
const genderValue = ref('全部性别');
const ageValue = ref('全部年龄');
const languageValue = ref('全部语言');
const selectedVoiceId = ref('voice-1');
const localFiles = ref<UploadUserFile[]>([]);
const teamValue = ref('团队资产');
const audioKeyword = ref('');
const confirmVoice = () => {
emit('confirm', {
tab: activeTab.value,
voiceId: selectedVoiceId.value,
prompt: voicePrompt.value,
});
dialogVisible.value = false;
};
</script>
<style scoped lang="scss">
.voice-dialog {
height: 660px;
}
.dialog-header {
flex: 0 0 98px;
}
.dialog-body {
flex: 1;
min-height: 0;
}
.dialog-footer {
flex: 0 0 76px;
}
.header-icon {
box-shadow: 0 2px 8px rgba(22, 36, 58, .12);
}
.color-536 {
color: #536883;
}
.tab-row {
background: #f5f7fa;
}
.tab-item {
flex: 1;
color: #000;
border: 1px solid transparent;
border-radius: 6px;
&.active {
background: #fff;
border-color: #d5ddeb;
box-shadow: 0 1px 4px rgba(22, 36, 58, .1);
}
}
.design-panel,
.library-panel,
.local-panel,
.audio-panel {
min-height: 0;
}
.design-form {
border-right: 1px solid #e7edf5;
}
.index-dot {
color: #8aa0bf;
border: 1px solid #ccd7e6;
}
.voice-textarea {
:deep(.el-textarea__inner) {
color: #000;
line-height: 24px;
border-radius: 4px;
box-shadow: 0 0 0 1px #d5ddeb inset;
}
}
.generate-btn {
background: linear-gradient(90deg, #2382ff 0%, #b947ee 100%);
}
.history-empty {
min-height: 300px;
}
.search-input {
flex: 1;
}
.filter-select {
width: 112px;
}
.voice-list {
flex: 1;
gap: 6px 24px;
}
.voice-item {
flex: 0 0 calc((100% - 24px) / 2);
border: 1px solid transparent;
&.active,
&:hover {
border-color: rgba(var(--main-color), 1);
background: rgba(var(--main-color), .04);
}
}
.play-dot {
right: 0;
bottom: 0;
background: #6b7b93;
}
.local-panel {
:deep(.el-upload),
:deep(.el-upload-dragger) {
width: 100%;
}
:deep(.el-upload-dragger) {
height: 318px;
display: flex;
align-items: center;
justify-content: center;
border-color: #d9e2f2;
background: #fbfcff;
}
}
.audio-tab {
position: relative;
height: 34px;
line-height: 34px;
&.active {
color: #000;
&::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: rgba(var(--main-color), 1);
content: '';
}
}
}
.team-select {
width: 136px;
}
.audio-empty {
min-height: 280px;
}
.empty-icon {
box-shadow: 0 2px 8px rgba(22, 36, 58, .1);
}
.confirm-btn {
background: #a5adf7;
}
:deep(.el-input__wrapper),
:deep(.el-select__wrapper) {
border-radius: 4px;
box-shadow: 0 0 0 1px #d5ddeb inset;
}
</style>
<template>
<div class="vh100 hidden flex-col">
<div class="flex-center bg-fff py-10">
<div class="flex-center gap-10 mr-100 bg-eee round-100 p-4">
<div v-for="(item, index) in steps" :key="index" class="flex-items-center gap-10 default"
:class="{ finish: item.stepn < currentStep, active: item.stepn === currentStep, select: selectStep === item.stepn }"
@click="lookStep(item)">
<div class="flex-items-center px-10 py-2 gap-6 round-100 fs-12 pointer color-888 name">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.name }}</span>
</div>
<div v-if="index < steps.length - 1" class="w-30 h-2 line"></div>
</div>
</div>
</div>
<div class="flex-1 p-20 hidden">
<Step1 v-if="selectStep === 1" />
<!-- <div class="bg-fff h-full over-y-auto round-12">
<div v-for="value in 100">发的发生大是</div>
</div> -->
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Edit } from '@element-plus/icons-vue';
import Step1 from './components/Step-1.vue';
import Step2 from './components/Step-2.vue';
import Step3 from './components/Step-3.vue';
import Step4 from './components/Step-4.vue';
const title = ref('商品分析信息')
const currentStep = ref(4)
const selectStep = ref(1)
const steps = ref([
{ name: '原片', icon: Edit, stepn: 1, desc: '商品分析信息' },
{ name: '设定', icon: Edit, stepn: 2, desc: '创意与分镜' },
{ name: '分镜', icon: Edit, stepn: 3, desc: '视频预览' },
{ name: '视频', icon: Edit, stepn: 4, desc: '视频成片' },
])
const lookStep = (item: any) => {
if (currentStep.value < item.stepn) return
selectStep.value = item.stepn
title.value = item.desc
}
</script>
<style scoped lang="scss">
.default {
.name {
&:hover {
background-color: #eee;
}
}
.line {
background: #ddd;
}
&.finish {
.name {
color: #01b95d;
}
}
&.active {
.name {
color: rgba(var(--main-color), 1);
}
}
&.select {
.name {
color: #fff;
background-color: rgba(var(--main-color), 1);
}
}
}
</style>
\ No newline at end of file
<template>
<div class="vh100" style="padding: 0 20%">
<div class="mb-30" style="margin-top: 20%;">
<div class="text-center fs-18 fw-600">创意复刻,让创意焕发新生</div>
<div class="text-center color-888 fs-12 mt-20">上传您需要转换风格、语言或人物角色的视频片断,通过 AI 大模型极速复刻整个故事片断。</div>
</div>
<div class="w-full">
<div class="bg-fff p-20 round-12">
<UploadFile listType="drap" bgClass="xxxx">
<template #trigger>
<div class="flex-center flex-col">
<div class="w-40 h-40 round bg-eee flex-center color-888">
<el-icon size="20">
<Upload />
</el-icon>
</div>
<div class="color-333 fs-12 fw-600 mt-10">点击上传 或 拖拽视频文件到此区域</div>
<div class="color-888 fs-12 mt-10">视频格式支持mp4/mov,大小不超过500M,时长不超过3分钟</div>
</div>
</template>
</UploadFile>
</div>
</div>
<div class="setting-card w-full flex-between gap-18 px-20 py-14 mt-24 round-14 bg-fff">
<div class="setting-left flex-items-center">
<el-select v-model="language" class="language-select" size="large">
<template #prefix>
<el-icon>
<Monitor />
</el-icon>
<span class="fs-12 color-666 ml-4">目标语种</span>
</template>
<el-option v-for="item in languageOptions" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</div>
<div class="setting-right flex-items-center gap-8">
<el-popover v-model:visible="ratioVisible" placement="bottom" :width="158" trigger="click"
popper-class="burst-setting-popper" :hide-after="0">
<template #reference>
<div class="setting-trigger flex-center gap-8 h-34 px-12 round-8 pointer"
:class="{ active: ratioVisible }">
<span class="ratio-icon" :class="selectedRatio.iconClass"></span>
<span class="fs-12 fw-700 color-000">{{ selectedRatio.value }}</span>
<el-icon class="fs-12 color-666">
<ArrowUp v-if="ratioVisible" />
<ArrowDown v-else />
</el-icon>
</div>
</template>
<div class="select-panel p-6">
<div class="fs-12 color-888 px-8 py-6">比例</div>
<div v-for="item in ratioOptions" :key="item.value"
class="select-option flex-items-center gap-10 h-34 px-8 round-4 pointer"
:class="{ active: ratio === item.value }" @click="selectRatio(item.value)">
<span class="ratio-icon" :class="item.iconClass"></span>
<span class="fs-12 fw-700 color-000">{{ item.value }}</span>
</div>
</div>
</el-popover>
<el-popover v-model:visible="resolutionVisible" placement="bottom" :width="158" trigger="click"
popper-class="burst-setting-popper" :hide-after="0">
<template #reference>
<div class="setting-trigger flex-between gap-12 h-34 px-12 round-8 pointer w-80"
:class="{ active: resolutionVisible }">
<span class="fs-12 fw-700 color-000">{{ resolution }}</span>
<el-icon class="fs-12 color-666">
<ArrowUp v-if="resolutionVisible" />
<ArrowDown v-else />
</el-icon>
</div>
</template>
<div class="select-panel p-6">
<div class="fs-12 color-888 px-8 py-6">分辨率</div>
<div v-for="item in resolutionOptions" :key="item.value"
class="select-option h-34 lh-34 px-8 round-4 fs-12 fw-700 pointer"
:class="{ active: resolution === item.value }" @click="selectResolution(item.value)">
{{ item.value }}
</div>
</div>
</el-popover>
<el-popover v-model:visible="styleVisible" placement="bottom-end" :width="600" trigger="click"
popper-class="burst-style-popper" :hide-after="0">
<template #reference>
<div class="style-trigger flex-items-center gap-8 h-34 px-8 round-8 pointer"
:class="{ active: styleVisible }">
<img :src="selectedStyle.image" :alt="selectedStyle.name"
class="style-thumb wh-28 round-6 cover" />
<span class="fs-12 fw-700 color-000">{{ selectedStyle.name }}</span>
<el-icon class="fs-12 color-666">
<ArrowUp v-if="styleVisible" />
<ArrowDown v-else />
</el-icon>
</div>
</template>
<div class="style-panel p-24 hide-scroll">
<div class="fs-16 fw-700 color-000 mb-18">风格库</div>
<div class="flex-between mb-20">
<div class="style-tabs flex-items-center h-34 p-4 round-8">
<span v-for="item in styleTabs" :key="item.value"
class="style-tab h-26 lh-26 px-14 round-6 fs-12 pointer"
:class="{ active: activeStyleTab === item.value }"
@click="activeStyleTab = item.value">
{{ item.label }}
<span class="color-888">{{ item.count }}</span>
</span>
</div>
<el-input v-model="styleKeyword" class="style-search" placeholder="搜索">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
<div class="style-list flex flex-wrap gap-10">
<div class="style-card custom-style flex-col flex-center round-12 pointer"
:class="{ active: style === 'custom' }" @click="selectStyle('custom')">
<el-icon class="fs-24 color-666 mb-10">
<Picture />
</el-icon>
<span class="fs-12 color-666">自定义风格</span>
</div>
<div v-for="item in filteredStyles" :key="item.value"
class="style-card relative round-12 hidden pointer"
:class="{ active: style === item.value }" @click="selectStyle(item.value)">
<img :src="item.image" :alt="item.name" class="w-full h-full cover" />
<div
class="style-name absolute left-0 right-0 bottom-0 h-32 lh-32 text-center fs-12 fw-700 color-fff">
{{ item.name }}
</div>
<span v-if="style === item.value"
class="style-check absolute flex-center wh-18 round color-fff">
<el-icon class="fs-12">
<Check />
</el-icon>
</span>
</div>
</div>
</div>
</el-popover>
</div>
</div>
<div class="flex-center">
<el-button type="primary" class="start-btn h-48 px-88 round-14 border-none fs-16 fw-700 mt-24"
@click="goReplica">
<el-icon>
<MagicStick />
</el-icon>
<span>开始智能复刻</span>
<el-icon>
<ArrowRight />
</el-icon>
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import {
ArrowDown,
ArrowRight,
ArrowUp,
Check,
MagicStick,
Monitor,
Picture,
Search,
Upload,
} from '@element-plus/icons-vue';
import UploadFile from '@/components/Upload/index.vue'
import productImg from '@/static/image/huaban.png';
import peopleImg from '@/static/image/meinv.png';
const router = useRouter();
const language = ref('zh');
const ratio = ref('16:9');
const resolution = ref('4k');
const style = ref('anime');
const ratioVisible = ref(false);
const resolutionVisible = ref(false);
const styleVisible = ref(false);
const activeStyleTab = ref('all');
const styleKeyword = ref('');
const languageOptions = [
{ label: '中文', value: 'zh' },
{ label: 'English', value: 'en' },
];
const ratioOptions = [
{ value: '16:9', iconClass: 'wide' },
{ value: '4:3', iconClass: 'wide' },
{ value: '3:4', iconClass: 'vertical' },
{ value: '9:16', iconClass: 'vertical' },
{ value: '21:9', iconClass: 'cinema' },
];
const resolutionOptions = [
{ value: '480p' },
{ value: '720p' },
{ value: '1080p' },
{ value: '4k' },
];
const styleTabs = [
{ label: '全部', value: 'all', count: 21 },
{ label: '真人', value: 'real', count: 10 },
{ label: '3D', value: '3d', count: 5 },
{ label: '2D', value: '2d', count: 6 },
{ label: '自定义', value: 'custom', count: 0 },
];
const styleOptions = [
{ value: 'modern', name: '现代都市通用', type: 'real', image: peopleImg },
{ value: 'film', name: '电影感', type: 'real', image: productImg },
{ value: 'anime', name: '2D日漫', type: '2d', image: peopleImg },
{ value: 'korean-city', name: '2D韩漫都市', type: '2d', image: productImg },
{ value: 'chinese', name: '3D国风', type: '3d', image: peopleImg },
{ value: 'xianxia', name: '3D仙侠', type: '3d', image: productImg },
{ value: 'korean', name: '2D韩漫', type: '2d', image: peopleImg },
{ value: 'girl', name: '2D乙女', type: '2d', image: productImg },
{ value: 'dream', name: '2D国漫', type: '2d', image: peopleImg },
{ value: 'cg', name: 'CG风格', type: '3d', image: productImg },
{ value: 'cartoon', name: '3D卡通', type: '3d', image: peopleImg },
{ value: 'dark', name: 'CG赛博朋克', type: '3d', image: productImg },
{ value: 'ink', name: '工笔画', type: '2d', image: peopleImg },
{ value: 'ancient', name: '古韵写实雅致风', type: 'real', image: productImg },
];
const selectedRatio = computed(() => ratioOptions.find((item) => item.value === ratio.value) || ratioOptions[0]);
const selectedStyle = computed(() => styleOptions.find((item) => item.value === style.value) || styleOptions[2]);
const filteredStyles = computed(() => {
const keyword = styleKeyword.value.trim();
return styleOptions.filter((item) => {
const isMatchedTab = activeStyleTab.value === 'all' || item.type === activeStyleTab.value;
const isMatchedKeyword = !keyword || item.name.includes(keyword);
return isMatchedTab && isMatchedKeyword;
});
});
const selectRatio = (value: string) => {
ratio.value = value;
ratioVisible.value = false;
};
const selectResolution = (value: string) => {
resolution.value = value;
resolutionVisible.value = false;
};
const selectStyle = (value: string) => {
style.value = value;
if (value === 'custom') {
return;
}
styleVisible.value = false;
};
const goReplica = () => {
router.push({ name: 'BurstReplica' });
};
</script>
<style scoped lang="scss">
.setting-card {
box-shadow: 0 8px 22px rgba(20, 38, 70, .08);
}
.language-select {
width: 160px;
}
.setting-trigger,
.style-trigger {
border: 1px solid #dce6f2;
background: #f5f7fb;
&.active {
border-color: rgba(var(--main-color), 1);
box-shadow: 0 0 0 1px rgba(var(--main-color), .45);
}
}
.ratio-icon {
display: inline-block;
width: 18px;
height: 12px;
border: 2px solid #56657a;
border-radius: 2px;
&.vertical {
width: 12px;
height: 18px;
}
&.cinema {
width: 22px;
height: 8px;
}
}
.start-btn {
min-width: 400px;
background: linear-gradient(135deg, #b212ff 0%, #573cff 100%);
box-shadow: 0 14px 28px rgba(125, 40, 255, .28);
}
:deep(.el-select__wrapper) {
min-height: 34px;
border-radius: 10px;
background: #f5f7fb;
box-shadow: 0 0 0 1px #edf2f7 inset;
}
.select-option {
&:hover,
&.active {
background: #f5f7fb;
}
}
.style-panel {
max-height: 500px;
overflow-y: auto;
}
.style-tabs {
background: #f5f7fb;
}
.style-tab {
color: #334b68;
&.active {
background: #fff;
color: #111827;
box-shadow: 0 2px 8px rgba(20, 38, 70, .06);
}
}
.style-search {
width: 156px;
}
.style-card {
width: 102px;
height: 102px;
border: 2px solid transparent;
background: #f5f7fb;
&.active {
border-color: rgba(var(--main-color), 1);
}
}
.custom-style {
border: 1px solid #dce6f2;
}
.style-name {
background: linear-gradient(to top, rgba(0, 0, 0, .72), rgba(0, 0, 0, 0));
}
.style-check {
top: 6px;
right: 6px;
background: rgba(var(--main-color), 1);
border: 2px solid #fff;
}
</style>
<style lang="scss">
.burst-setting-popper,
.burst-style-popper {
padding: 0 !important;
border-radius: 12px !important;
}
</style>
<template>
<div class="replica-page vh100 flex-col bgmr hidden">
<header class="replica-header flex-between bg-fff px-28">
<div class="project-info flex-items-center gap-14">
<el-icon class="fs-18 color-333 pointer" @click="goBack">
<Back />
</el-icon>
<div>
<div class="flex-items-center gap-8">
<span class="fs-14 fw-700 color-000">新项目</span>
<el-icon class="fs-14 color-888 pointer">
<Edit />
</el-icon>
</div>
<div class="flex-items-center gap-12 fs-12 color-666">
<span class="flex-items-center gap-4">
<span class="ratio-icon wide"></span>
16:9
</span>
<span class="divider"></span>
<span>720p</span>
<span class="divider"></span>
<span class="flex-items-center gap-4">
<img :src="styleImg" alt="现代都市通用" class="style-thumb wh-16 round-4 cover" />
现代都市通用
</span>
<span class="divider"></span>
<span>目标语言:中文</span>
</div>
</div>
</div>
<div class="step-nav flex-1 flex-center">
<template v-for="(item, index) in steps" :key="item.stepn">
<div class="step-item flex-col flex-center pointer" :class="{
active: item.stepn === selectStep,
disabled: item.stepn > currentStep,
}" @click="lookStep(item)">
<div class="step-icon flex-center h-34 w-60 round-100">
<el-icon class="fs-16">
<component :is="item.icon" />
</el-icon>
<span class="fs-12 lh-20 fw-700">{{ item.name }}</span>
</div>
</div>
<div v-if="index < steps.length - 1" class="step-line w-28"></div>
</template>
</div>
<div class="point-box flex-items-center gap-12 h-34 px-14 round-8">
<span class="flex-items-center gap-4 color-main fs-12 fw-700">
<el-icon>
<MagicStick />
</el-icon>
5,010
</span>
<span class="flex-items-center gap-4 fs-12 color-666">
<el-icon>
<Clock />
</el-icon>
265小时13分9秒
</span>
<el-icon class="fs-12 color-888">
<InfoFilled />
</el-icon>
</div>
</header>
<main class="flex-1 over-y-auto hide-scroll">
<Step1 v-if="selectStep === 1" @next="goStep(2)" />
<Step2 v-else-if="selectStep === 2" @next="goStep(3)" />
<Step3 v-else-if="selectStep === 3" />
<Step4 v-else />
</main>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Back, Clock, Document, Edit, Files, InfoFilled, MagicStick, Setting, VideoCamera } from '@element-plus/icons-vue';
import Step1 from './components/Step-1.vue';
import Step2 from './components/Step-2.vue';
import Step3 from './components/Step-3.vue';
import Step4 from './components/Step-4.vue';
import styleImg from '@/static/image/meinv.png';
type StepItem = {
name: string;
icon: typeof VideoCamera;
stepn: number;
};
const router = useRouter();
const currentStep = ref(1);
const selectStep = ref(1);
const steps: StepItem[] = [
{ name: '原片', icon: VideoCamera, stepn: 1 },
{ name: '设定', icon: Setting, stepn: 2 },
{ name: '分镜', icon: Files, stepn: 3 },
{ name: '视频', icon: Document, stepn: 4 },
];
const goBack = () => {
router.push('/burst');
};
const lookStep = (item: StepItem) => {
if (currentStep.value < item.stepn) {
return;
}
selectStep.value = item.stepn;
};
const goStep = (step: number) => {
currentStep.value = Math.max(currentStep.value, step);
selectStep.value = step;
};
</script>
<style scoped lang="scss">
.replica-header {
flex: 0 0 60px;
border-bottom: 1px solid #dce6f2;
}
.divider {
width: 1px;
height: 12px;
background: #dce6f2;
}
.ratio-icon {
display: inline-block;
width: 14px;
height: 8px;
border: 1px solid #536883;
border-radius: 2px;
}
.step-item {
color: #b8c1d2;
&.active {
color: rgba(var(--main-color), 1);
.step-icon {
border-color: rgba(var(--main-color), 1);
background: rgba(var(--main-color), .06);
}
}
&.disabled {
cursor: not-allowed;
}
}
.step-icon {
border: 1px solid #edf2f7;
background: #f8fafc;
}
.step-line {
height: 1px;
margin: 0 8px 0px;
background: #dce6f2;
}
.point-box {
justify-content: flex-end;
background: #f8fafc;
}
</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