Commit 96e60a33 authored by 黄同智's avatar 黄同智

快速创作的首页内容

parent 9c61bde6
......@@ -232,4 +232,8 @@ $spacing-values: ();
.el-checkbox__label {
display: none !important;
}
}
.hide-scroll::-webkit-scrollbar {
display: none;
}
\ No newline at end of file
......@@ -81,7 +81,7 @@ const menus = [
},
{
name: '快速创作',
path: '/',
path: '/quickCreation',
icon: 'add',
},
{
......@@ -106,7 +106,7 @@ const menus = [
},
{
name: '画布',
path: '/',
path: '/vueFlow',
icon: 'redraw',
},
{
......
......@@ -9,6 +9,7 @@ import VideoOutMp4 from '@/views/vue-flow/components/VideoOutMp4.vue'
import ThreeD from '@/views/vue-flow/3d.vue'
import Layout from '@/layout/index.vue'
import Task from '@/views/task/index.vue'
import QuickCreation from '@/views/quickCreation/index.vue'
const routes = [
{
......@@ -42,6 +43,18 @@ const routes = [
]
},
{
path: '/quickCreation',
component: Layout,
children: [
{
path: '',
name: 'QuickCreation',
component: QuickCreation,
meta: { title: '快速创作', icon: 'add', affix: true }
}
]
},
{
path: '/zhibo',
component: Layout,
children: [
......
<template>
<div>
<section class="creation-box bg-fff round-24">
<div class="composer-input p-24">
<div class="flex-items-center gap-12 mb-8">
<el-button v-if="currentScene" class="scene-tag flex-center px-10 gap-6"
@click="emit('clearScene')">
<el-icon>
<component :is="sceneIcons[currentScene.key]" />
</el-icon>
<span>{{ currentScene.label }}</span>
<el-icon>
<Close />
</el-icon>
</el-button>
</div>
<el-input v-model="prompt" class="prompt-input" type="textarea" resize="none" :rows="8"
:placeholder="currentScene ? '补充创作要求,例如画面风格、卖点、人群或投放场景' : '参考已上传的图片/视频,生成相关的产品素材。'" />
</div>
<div class="relative flex-between gap-16 px-24 py-10">
<div class="flex-items-center gap-10 flex-wrap">
<template v-if="currentScene">
<el-button v-for="action in currentScene.uploads" :key="action"
class="tool-btn flex-center gap-6 h-34 px-14" plain>
<el-icon>
<Upload />
</el-icon>
<span>{{ action }}</span>
</el-button>
<div v-if="currentScene.key === 'product'"
class="target-toggle flex-items-center h-34 px-8 round-6 gap-4">
<span class=" color-888 mr-10">输出目标:</span>
<el-button v-for="item in outputOptions" :key="item.value"
class="target-btn flex-center gap-6 h-28 px-12 round-4"
:class="{ active: outputTarget === item.value }" text
@click="outputTarget = item.value">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</el-button>
</div>
</template>
<template v-else>
<el-button class="tool-btn flex-center gap-6 h-34 px-14" plain>
<el-icon>
<Link />
</el-icon>
<span>商品 URL/ID</span>
</el-button>
<el-button class="tool-btn flex-center gap-6 h-34 px-14" plain @click="mediaVisible = true">
<el-icon>
<Picture />
</el-icon>
<span>媒体素材</span>
</el-button>
</template>
<el-popover v-model:visible="settingVisible" trigger="click" placement="bottom-start" width="334"
popper-class="quick-creation-popover" :teleported="false" :show-arrow="false" :hide-after="0">
<template #reference>
<el-button class="tool-btn flex-center gap-6 h-34 px-14" :class="{ active: settingVisible }"
plain>
<el-icon>
<Operation />
</el-icon>
<span>设置</span>
<el-icon>
<ArrowDown />
</el-icon>
</el-button>
</template>
<div class="popover-card bg-fff round-18 p-16">
<div class="flex-between">
<h3 class="lh-20 color-000">视频输出参数设置</h3>
<span class="soft-pill h-22 lh-22 px-10 round-12 color-888">画质与格式</span>
</div>
<div class="mt-18">
<div class=" lh-20 color-000">1. 视频长度 (20 - 120 秒)</div>
<div class="flex-items-center gap-10 mt-10">
<el-button v-for="item in durationOptions" :key="item"
class="flex-center h-30 px-20"
:class="{ active: duration === item }" plain @click="duration = item">
{{ item }}s
</el-button>
</div>
<el-slider v-model="duration" class="duration-slider mt-14" :min="20" :max="120"
:step="10" :show-tooltip="false" />
<p class="mt-4 color-888">当前: <strong class="color-main">{{ duration }} 秒</strong></p>
</div>
<div class="mt-18">
<div class=" lh-20 color-000">2. 视频画面比例</div>
<div class="flex-items-center gap-10 mt-10">
<el-button class="ratio-btn flex-center h-34 px-20"
:class="{ active: ratio === 'portrait' }" plain @click="ratio = 'portrait'">
<span class="phone-icon w-8 h-16 mr-4"></span>
<span>9:16 竖屏</span>
</el-button>
<el-button class="flex-center h-34 px-20"
:class="{ active: ratio === 'landscape' }" plain @click="ratio = 'landscape'">
<span class="screen-icon w-16 h-10 mr-4"></span>
<span>16:9 横屏</span>
</el-button>
</div>
</div>
<div class="setting-row flex-between mt-16 pt-14">
<span class=" color-000">3. 是否自动去水印并补足</span>
<el-switch v-model="autoFix" active-color="#9022fd" />
</div>
</div>
</el-popover>
<el-popover v-model:visible="modeVisible" trigger="click" placement="bottom-start" width="334"
popper-class="quick-creation-popover" :teleported="false" :show-arrow="false" :hide-after="0">
<template #reference>
<el-button class="tool-btn flex-center gap-6 h-34 px-14 round-6 pointer"
:class="{ active: modeVisible }" plain>
<el-icon>
<MagicStick />
</el-icon>
<span>{{ activeMode.label }}</span>
<el-icon>
<ArrowDown />
</el-icon>
</el-button>
</template>
<div class="popover-card bg-fff round-18 p-16">
<div class="flex-between">
<h3 class="flex-items-center gap-6 color-000">
<el-icon class="color-main">
<MagicStick />
</el-icon>
<span>AI大模型模式选择</span>
</h3>
<span class="soft-pill h-22 lh-22 px-10 round-12 color-main ">运算算力</span>
</div>
<el-button v-for="item in modeOptions" :key="item.value"
class="mode-option relative w-full mt-10 p-12 round-6 text-left pointer"
:class="{ active: activeMode.value === item.value }" plain @click="activeMode = item">
<div class="w-full">
<div class="lh-20 color-000">
{{ item.label }}
<em class="ml-8 ">{{ item.badge }}</em>
</div>
<div class="mt-4 color-888">{{ item.desc }}</div>
</div>
<span v-if="activeMode.value === item.value"
class="mode-dot absolute wh-8 round"></span>
</el-button>
</div>
</el-popover>
</div>
<el-button class="generate-btn flex-center border-none pointer" size="large" circle
:icon="StarFilled" />
</div>
</section>
<div class="flex-center gap-14 flex-wrap mt-18">
<el-button v-for="item in scenes" :key="item.key"
class="scene-btn flex-center gap-8 h-38 px-20 round-20 bg-fff pointer"
:class="{ active: activeScene === item.key }" plain @click="emit('selectScene', item.key)">
<el-icon :style="{ color: item.color }">
<component :is="sceneIcons[item.key]" />
</el-icon>
<span>{{ item.label }}</span>
</el-button>
</div>
<el-dialog v-model="mediaVisible" width="672" align-center append-to-body class="quick-media-dialog">
<div class="media-dialog hidden round-12">
<div class="flex-between px-18 py-16">
<div class="flex-items-center gap-8">
<el-icon class="color-main fs-18">
<Picture />
</el-icon>
<h3 class="fs-16 lh-24 color-000">选择参考素材</h3>
<span class="soft-pill h-22 lh-22 px-10 round-12 color-888">最多选择 5 项</span>
</div>
<el-button class="close-btn wh-28 p-0 border-none bg-none color-888 pointer" circle
@click="mediaVisible = false">
<el-icon>
<Close />
</el-icon>
</el-button>
</div>
<div class="flex-between px-14 pb-14">
<div class="media-tabs flex-items-center h-32 p-4 round-8">
<el-button v-for="item in mediaTabs" :key="item"
class="h-26 px-16 round-6 border-none bg-none pointer"
:class="{ active: mediaTab === item }" text @click="mediaTab = item">
{{ item }}
</el-button>
</div>
<div class="flex-items-center gap-8">
<div class="search-mini flex-items-center h-32 w-194 px-10 round-8 bg-fff">
<el-icon class="color-888">
<Search />
</el-icon>
<el-input v-model="mediaKeyword" class="media-search flex-1 minw-0 ml-8 "
placeholder="搜索资产..." />
</div>
<el-button
class="upload-local flex-center gap-6 h-32 px-14 round-8 border-none color-fff pointer">
<el-icon>
<Upload />
</el-icon>
<span>本地上传</span>
</el-button>
</div>
</div>
<div class="media-grid flex flex-wrap gap-12 px-14 pt-14 minh-300">
<el-card v-for="item in filteredMaterials" :key="item.id"
class="media-card relative w-150 h-152 round-8 hidden border-none pointer text-left"
:class="{ selected: selectedMaterials.includes(item.id) }" shadow="never"
@click="toggleMaterial(item.id)">
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full cover" />
<span v-else class="video-placeholder flex-center w-full h-full color-888">VIDEO</span>
<span v-if="item.kind === '视频'"
class="video-tag absolute top-8 right-8 h-18 lh-18 px-6 round-4 color-fff fs-10 ">VIDEO</span>
<span class="media-mask absolute bottom-0 left-0 w-full px-8 py-8 color-fff">
<span class="media-name lh-18 ellipsis">{{ item.name }}</span>
<span class="media-size fs-10">{{ item.size }}</span>
</span>
</el-card>
</div>
<div class="media-footer flex-between px-18 py-16 mt-18">
<span class=" color-888">已选择 <strong class="color-main">{{ selectedMaterials.length }}</strong>
/ 5
个素材</span>
<div class="flex-items-center gap-10">
<el-button class="footer-btn h-36 px-20 round-12 bg-fff pointer"
@click="mediaVisible = false">取消</el-button>
<el-button class="footer-btn confirm h-36 px-20 round-12 border-none pointer" type="primary"
@click="mediaVisible = false">确认选择</el-button>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { Component } from 'vue';
import {
ArrowDown,
Box,
Close,
Link,
StarFilled,
Monitor,
Operation,
Picture,
Search,
Upload,
User,
VideoCamera,
} from '@element-plus/icons-vue';
import productImg from '@/static/image/huaban.png';
import peopleImg from '@/static/image/meinv.png';
import detailImg from '@/static/image/long.png';
import type { SceneKey } from '../types';
type SceneItem = {
key: SceneKey;
label: string;
desc: string;
color: string;
uploads: string[];
};
type ModeOption = {
value: 'light' | 'standard' | 'expert';
label: string;
badge: string;
desc: string;
};
const props = defineProps<{
activeScene?: SceneKey;
}>();
const emit = defineEmits<{
selectScene: [value: SceneKey];
clearScene: [];
}>();
const defaultDesc = '参考已上传的图片/视频,生成相关的产品素材。';
const prompt = ref('');
const mediaVisible = ref(false);
const settingVisible = ref(false);
const modeVisible = ref(false);
const duration = ref(30);
const ratio = ref<'portrait' | 'landscape'>('portrait');
const autoFix = ref(true);
const outputTarget = ref<'image' | 'video'>('video');
const mediaTab = ref('全部');
const mediaKeyword = ref('');
const selectedMaterials = ref<string[]>([]);
const scenes: SceneItem[] = [
{ key: 'product', label: '产品素材', desc: '参考已上传的图片/视频,生成相关的产品素材。', color: '#1877ff', uploads: ['上传图片/视频素材'] },
{ key: 'pain', label: '痛点对比', desc: '参考已上传的图片,生成痛点对比素材。', color: '#ff3b55', uploads: ['上传痛点素材', '上传解决痛点素材'] },
{ key: 'process', label: '使用过程', desc: '参考已上传的图片/视频,生成使用过程视频。', color: '#10c887', uploads: ['上传商品图片', '上传使用过程视频'] },
{ key: 'koc', label: 'koc口播', desc: '输入产品卖点与人群场景,生成达人口播素材。', color: '#8b1cff', uploads: ['上传商品图片', '上传口播参考'] },
{ key: 'story', label: '剧情素材', desc: '参考商品素材和目标人群,生成剧情化短视频素材。', color: '#ff8200', uploads: ['上传商品素材', '上传剧情参考'] },
];
const sceneIcons: Record<SceneKey, Component> = {
product: Box,
pain: Monitor,
process: VideoCamera,
koc: User,
story: VideoCamera,
};
const outputOptions = [
{ value: 'image' as const, label: '图片', icon: Picture },
{ value: 'video' as const, label: '视频', icon: VideoCamera },
];
const durationOptions = [20, 30, 60, 120];
const modeOptions: ModeOption[] = [
{ value: 'light', label: '轻量模式', badge: '省积分', desc: '响应极快,节省50%算力积分(推荐)' },
{ value: 'standard', label: '标准模式', badge: '均衡', desc: '高清顺滑,兼顾生成速度与画质' },
{ value: 'expert', label: '专家高清模式', badge: '极高画质', desc: '极清画质与细腻质感,适合商业展示' },
];
const activeMode = ref<ModeOption>(modeOptions[0]);
const materials = [
{ id: 'm1', name: '雅诗兰黛精华空瓶主图主体.png', kind: '图片', size: '1.2 MB', image: productImg },
{ id: 'm2', name: '智能蓝牙音箱白色高对比展示.jpg', kind: '图片', size: '980 KB', image: detailImg },
{ id: 'm3', name: '模特红裙走秀素材_原画.mp4', kind: '视频', size: '18.4 MB', image: '' },
{ id: 'm4', name: '七夕美妆礼盒_烫礼场景成片.mp4', kind: '视频', size: '42.6 MB', image: '' },
{ id: 'm5', name: 'AI生成 精华液晨间护肤场景.jpg', kind: '图片', size: '2.3 MB', image: peopleImg },
];
const mediaTabs = ['全部', '图片', '视频'];
const currentScene = computed(() => scenes.find((item) => item.key === props.activeScene));
const filteredMaterials = computed(() => {
const keyword = mediaKeyword.value.trim().toLowerCase();
return materials.filter((item) => {
const matchTab = mediaTab.value === '全部' || item.kind === mediaTab.value;
const matchKeyword = !keyword || item.name.toLowerCase().includes(keyword);
return matchTab && matchKeyword;
});
});
const toggleMaterial = (id: string) => {
if (selectedMaterials.value.includes(id)) {
selectedMaterials.value = selectedMaterials.value.filter((item) => item !== id);
return;
}
if (selectedMaterials.value.length < 5) {
selectedMaterials.value = [...selectedMaterials.value, id];
}
};
</script>
<style lang="scss" scoped>
.creation-box {
border: 1px solid #e3eaf3;
box-shadow: 0 2px 8px rgba(15, 35, 70, .08);
}
:deep(.el-button + .el-button) {
margin-left: 0;
}
:deep(.prompt-input .el-textarea__inner) {
min-height: 72px !important;
padding: 0;
border: 0;
box-shadow: none;
color: #183151;
background: transparent;
resize: none;
}
:deep(.prompt-input .el-textarea__inner::placeholder) {
color: #7890ae;
}
:deep(.quick-creation-popover) {
padding: 0;
border: 0;
border-radius: 18px;
background: transparent;
box-shadow: none;
}
:deep(.media-search .el-input__wrapper) {
padding: 0;
box-shadow: none;
background: transparent;
}
:deep(.media-search .el-input__inner) {
height: 30px;
font-size: 12px;
}
:deep(.media-card .el-card__body) {
width: 100%;
height: 100%;
padding: 0;
}
.tool-btn,
.scene-btn,
.duration-btn,
.ratio-btn,
.footer-btn {
border: 1px solid #dfe7f2;
color: #183151;
box-shadow: 0 2px 6px rgba(21, 44, 76, .06);
}
.tool-btn.active,
.tool-btn:hover,
.scene-btn.active,
.scene-btn:hover {
border-color: #cf9dff;
color: rgba(var(--main-color), 1);
background: #fbf4ff;
}
.upload-btn {
border: 1px solid #e2bcff;
background: #fbebff;
}
.target-toggle,
.soft-pill,
.media-tabs {
background: #f3f7fc;
}
.target-btn {
color: #516986;
background: transparent;
}
.target-btn.active {
color: rgba(var(--main-color), 1);
background: #fff;
box-shadow: 0 0 0 1px #e4c4ff;
}
.generate-btn {
color: #4c42ff;
background: #eff1ff;
box-shadow: 0 2px 6px rgba(77, 66, 255, .5);
}
.popover-card {
border: 1px solid #dfe7f2;
box-shadow: 0 14px 36px rgba(20, 38, 70, .14);
}
.duration-btn,
.ratio-btn {
background: #f8fbff;
}
.duration-btn.active,
.ratio-btn.active,
.mode-option.active {
border-color: rgba(var(--main-color), 1);
color: rgba(var(--main-color), 1);
background: #fbf4ff;
}
.duration-slider {
accent-color: rgba(var(--main-color), 1);
}
.phone-icon {
border: 2px solid currentColor;
}
.screen-icon {
border: 2px solid currentColor;
}
.setting-row {
border-top: 1px solid #edf2f7;
}
.mode-option {
height: auto;
justify-content: flex-start;
white-space: normal;
border: 1px solid #dfe7f2;
background: #fbfdff;
}
:deep(.mode-option > span) {
width: 100%;
}
.mode-option em {
color: #7894b4;
font-style: normal;
}
.mode-dot {
top: 24px;
right: 12px;
background: rgba(var(--main-color), 1);
}
.search-mini {
border: 1px solid #dfe7f2;
}
.media-tabs button.active {
color: #183151;
background: #fff;
box-shadow: 0 1px 4px rgba(17, 36, 62, .08);
}
.upload-local,
.footer-btn.confirm {
color: #fff;
background: rgba(var(--main-color), 1);
}
.media-card {
background: #eef3f8;
}
.media-card.selected {
box-shadow: 0 0 0 2px rgba(var(--main-color), 1);
}
.video-placeholder {
background: linear-gradient(180deg, #f4f7fb 0%, #dce5ee 100%);
}
.video-tag {
background: rgba(34, 44, 58, .72);
}
.media-mask {
background: linear-gradient(180deg, transparent 0%, rgba(18, 25, 38, .72) 35%, rgba(18, 25, 38, .9) 100%);
}
.media-name,
.media-size {
display: block;
}
.media-footer {
border-top: 1px solid #edf2f7;
}
.footer-btn {
border: 1px solid #dfe7f2;
}
</style>
<style lang="scss">
.quick-media-dialog {
padding: 0;
border-radius: 12px;
overflow: hidden;
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0;
}
}
</style>
<template>
<section>
<div class="flex-between mb-16">
<h2 class="fs-18 lh-28 fw-700">电商爆款灵感画廊</h2>
<div class="category-tabs flex-items-center gap-4 h-34 p-4 round-10 bg-fff">
<el-button
v-for="item in categories"
:key="item"
class="tab-btn h-26 px-12 round-8 border-none fs-12 fw-700"
:class="{ active: activeCategory === item }"
text
@click="activeCategory = item"
>
{{ item }}
</el-button>
</div>
</div>
<div class="gallery-list flex flex-wrap gap-20">
<el-card
v-for="(item, index) in filteredList"
:key="item.title"
class="gallery-card flex-item round-14 hidden pointer"
shadow="never"
@click="openPreview(index)"
>
<div class="image-wrap relative h-240 hidden">
<img :src="item.cover" :alt="item.title" class="w-full h-full cover" />
<span class="play-btn absolute flex-center wh-50 round color-fff fs-24">
<el-icon><VideoPlay /></el-icon>
</span>
<span class="copy-btn absolute flex-center gap-6 h-30 px-12 round-8 color-fff fs-12 fw-700">
<el-icon><MagicStick /></el-icon>
<span>一键同款</span>
</span>
<span class="duration absolute h-20 lh-20 px-6 round-4 color-fff fs-10 fw-700">{{ item.duration }}</span>
</div>
<div class="p-16">
<h3 class="fs-14 lh-22 fw-700 ellipsis">{{ item.title }}</h3>
<div class="flex-between mt-12">
<div class="flex-items-center minw-0">
<span class="avatar flex-center wh-18 round mr-6 color-fff fs-10">{{ item.author.slice(0, 1) }}</span>
<span class="fs-12 color-789 ellipsis">{{ item.author }}</span>
</div>
<div class="flex-items-center gap-10 fs-12 color-789">
<span class="flex-items-center gap-4">
<el-icon class="like-icon"><Star /></el-icon>
{{ item.likes }}
</span>
<span class="flex-items-center gap-4">
<el-icon><View /></el-icon>
{{ item.views }}
</span>
</div>
</div>
</div>
</el-card>
</div>
<InspirationPreviewDialog v-model="previewVisible" :items="filteredList" :index="previewIndex" />
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { MagicStick, Star, VideoPlay, View } from '@element-plus/icons-vue';
import productImg from '@/static/image/huaban.png';
import peopleImg from '@/static/image/meinv.png';
import detailImg from '@/static/image/long.png';
import InspirationPreviewDialog from './InspirationPreviewDialog.vue';
type GalleryCategory = '全部爆款' | '日化美妆' | '数码科技' | '鞋履服饰';
type GalleryItem = {
title: string;
author: string;
category: GalleryCategory;
cover: string;
duration: string;
likes: number;
views: number;
heat: number;
prompt: string;
tags: string[];
};
const categories: GalleryCategory[] = ['全部爆款', '日化美妆', '数码科技', '鞋履服饰'];
const activeCategory = ref<GalleryCategory>('全部爆款');
const previewVisible = ref(false);
const previewIndex = ref(0);
const galleryList: GalleryItem[] = [
{
title: '极简日系高奢美妆主视觉视频',
author: 'Mika_Design',
category: '日化美妆',
cover: productImg,
duration: '12s',
likes: 1240,
views: 8900,
heat: 2630,
prompt: 'A bottle of luxury cosmetic serum on a minimalist beige sand surface, soft studio lighting, organic shadows, high-end editorial product video.',
tags: ['美妆', '日化', '极简高奢', '大理石晨光'],
},
{
title: '时尚轻奢真丝吊带裙模特视频',
author: '电商爆款制造机',
category: '鞋履服饰',
cover: peopleImg,
duration: '10s',
likes: 852,
views: 12400,
heat: 2280,
prompt: 'Fashion model wearing a silk dress in a modern corridor, cinematic walking shot, soft contrast, premium apparel campaign.',
tags: ['服饰', '模特', '轻奢', '街拍'],
},
{
title: '多功能不粘锅户外场景画质增强',
author: 'Chef_Creative',
category: '日化美妆',
cover: detailImg,
duration: '15s',
likes: 412,
views: 3100,
heat: 1860,
prompt: 'Outdoor cooking scene with non-stick pan, natural daylight, joyful lifestyle composition, clean product close-up.',
tags: ['厨具', '户外', '画质增强', '生活方式'],
},
{
title: '智能运动手表多机位动态展示视频',
author: 'Tech_Reviewer',
category: '数码科技',
cover: productImg,
duration: '8s',
likes: 981,
views: 6540,
heat: 1988,
prompt: 'Smart sports watch rotating on a clean gray surface, multi-angle product showcase, premium tech lighting.',
tags: ['数码', '手表', '动态展示', '科技感'],
},
{
title: '法式复古高帮帆布鞋户外场景视频',
author: 'SoleMate',
category: '鞋履服饰',
cover: peopleImg,
duration: '15s',
likes: 1540,
views: 11000,
heat: 2960,
prompt: 'Red canvas sneaker on vivid red background, dynamic product hero shot, energetic streetwear commercial.',
tags: ['鞋履', '红色视觉', '户外', '潮流'],
},
{
title: '暮华金箔高脚杯澄澈饮品倾倒视频',
author: 'Mika_Design',
category: '日化美妆',
cover: detailImg,
duration: '10s',
likes: 672,
views: 4200,
heat: 1740,
prompt: 'Refreshing drink poured into a glass with mint and lime, dark moody background, crisp bubbles, summer beverage video.',
tags: ['饮品', '清爽', '慢动作', '暗调'],
},
{
title: '蓝牙耳机通勤痛点对比短视频',
author: 'Tech_Reviewer',
category: '数码科技',
cover: productImg,
duration: '11s',
likes: 736,
views: 7820,
heat: 2050,
prompt: 'Wireless earbuds commute pain point comparison, split scene, clean tech product lighting, fast-paced social video.',
tags: ['数码科技', '痛点对比', '通勤', '蓝牙耳机'],
},
{
title: '夏季防晒衣户外试穿种草视频',
author: 'SoleMate',
category: '鞋履服饰',
cover: peopleImg,
duration: '13s',
likes: 1188,
views: 9340,
heat: 2410,
prompt: 'Summer sun-protection jacket try-on in outdoor daylight, lightweight fabric detail, natural model movement.',
tags: ['服饰', '防晒衣', '试穿', '户外'],
},
];
const filteredList = computed(() => {
if (activeCategory.value === '全部爆款') {
return galleryList;
}
return galleryList.filter((item) => item.category === activeCategory.value);
});
const openPreview = (index: number) => {
previewIndex.value = index;
previewVisible.value = true;
};
</script>
<style lang="scss" scoped>
.category-tabs,
.gallery-card {
border: 1px solid #dfe7f2;
box-shadow: 0 2px 6px rgba(21, 44, 76, .05);
}
:deep(.el-button + .el-button) {
margin-left: 0;
}
.tab-btn {
color: #4c6583;
}
.tab-btn.active {
color: #001a3d;
background: #fff;
box-shadow: 0 1px 4px rgba(17, 36, 62, .1);
}
.gallery-card {
flex: 0 0 calc((100% - 60px) / 4);
background: #fff;
transition: border-color .24s, box-shadow .24s, transform .24s;
}
:deep(.gallery-card .el-card__body) {
padding: 0;
}
.image-wrap img {
transition: transform .42s ease, filter .42s ease;
}
.gallery-card:hover {
border-color: rgba(var(--main-color), .55);
box-shadow: 0 14px 30px rgba(28, 46, 76, .14);
transform: translateY(-3px);
}
.gallery-card:hover .image-wrap img {
filter: brightness(.88);
transform: scale(1.08);
}
.play-btn {
top: 50%;
left: 50%;
background: rgba(var(--main-color), 1);
box-shadow: 0 10px 24px rgba(var(--main-color), .34);
transform: translate(-50%, -50%);
transition: transform .24s, box-shadow .24s;
}
.gallery-card:hover .play-btn {
box-shadow: 0 14px 30px rgba(var(--main-color), .46);
transform: translate(-50%, -50%) scale(1.12);
}
.copy-btn {
left: 14px;
bottom: 12px;
background: rgba(var(--main-color), 1);
box-shadow: 0 6px 16px rgba(var(--main-color), .3);
}
.duration {
right: 10px;
bottom: 12px;
background: rgba(9, 16, 28, .72);
}
.avatar {
flex: 0 0 auto;
background: linear-gradient(135deg, #9d19ff, #ff8a00);
}
h3 {
color: #001a3d;
}
.color-789 {
color: #7890ae;
}
.like-icon {
color: #ff4ca0;
}
@media (max-width: 1280px) {
.gallery-card {
flex-basis: calc((100% - 40px) / 3);
}
}
@media (max-width: 900px) {
.gallery-card {
flex-basis: calc((100% - 20px) / 2);
}
}
</style>
<template>
<Dialog v-model="dialogVisible" bg-color="#fff" width="900">
<div v-if="currentItem" class="preview-dialog flex bg-fff hidden">
<aside class="preview-aside flex-col items-center justify-between py-18">
<div class="nav-btn wh-24 flex-center pointer" @click="prevItem">
<el-icon>
<ArrowUp />
</el-icon>
</div>
<div class="thumb-list flex-col gap-10">
<div v-for="(item, index) in items" :key="item.title"
class="thumb-btn w-46 h-58 p-0 round-8 hidden pointer"
:class="{ active: index === activeIndex }" text @click="activeIndex = index">
<img :src="item.cover" :alt="item.title" class="w-full h-full cover" />
</div>
</div>
<div class="nav-btn wh-24 flex-center pointer" @click="nextItem">
<el-icon>
<ArrowDown />
</el-icon>
</div>
</aside>
<section class="preview-stage relative flex-1 hidden">
<img :src="currentItem.cover" :alt="currentItem.title" class="stage-cover w-full h-full cover" />
<div class="stage-shade absolute top-0 left-0 w-full h-full"></div>
<span class="video-badge absolute top-14 left-14 h-22 lh-22 px-8 round-6 color-fff fs-12">
{{ currentItem.duration }} VIDEO
</span>
<span class="play-large absolute flex-center wh-64 round color-fff fs-30">
<el-icon>
<VideoPlay />
</el-icon>
</span>
<!-- <div class="fake-controls absolute left-16 right-16 bottom-18">
<div class="flex-between color-fff mb-10">
<div class="flex-items-center gap-18">
<el-icon class="fs-20">
<VideoPlay />
</el-icon>
<span class="fs-16">0:00</span>
</div>
<div class="flex-items-center gap-20 fs-18">
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="progress-line h-4 round-4"></div>
</div> -->
</section>
<section class="preview-info flex-col p-20" style="max-width: 380px;min-width: 380px;">
<div class="flex-1 over-y-auto hide-scroll">
<div class="flex-items-center gap-10 fs-12 color-789">
<span>创意详情</span>
<span>|</span>
<span class="heat flex-items-center gap-4">
<span>🔥</span>
<span>热度</span>
<strong>{{ currentItem.heat }}</strong>
</span>
</div>
<h2 class="mt-10 fs-16 lh-30 color-001">{{ currentItem.title }}</h2>
<div class="flex-items-center gap-8 mt-6">
<span class="avatar flex-center wh-22 round color-fff fs-12">{{ currentItem.author.slice(0, 1)
}}</span>
<span class="fs-12 color-334">{{ currentItem.author }}</span>
<span class="dot wh-4 round"></span>
<span class="type-tag h-22 lh-22 px-10 round-6 fs-12">视频</span>
</div>
<div class="flex-between mt-10">
<span class="fs-12 color-789">视频提示词</span>
<el-button class="copy-text h-24 p-0 border-none fs-12 color-main" text>
<el-icon>
<CopyDocument />
</el-icon>
<span>复制</span>
</el-button>
</div>
<div class="prompt-box mt-4 p-10 round-6 fs-12 lh-22 color-334">
{{ currentItem.prompt }}
</div>
<div class="mt-20">
<div class="fs-12 color-789 mb-10">视频参考图</div>
<div class="reference-img relative w-96 h-96 round-12 hidden">
<img :src="currentItem.cover" :alt="currentItem.title" class="w-full h-full cover" />
<span
class="source-tag absolute top-8 left-8 h-20 lh-20 px-8 round-6 color-fff fs-10">参考</span>
</div>
</div>
</div>
<div class="mt-20">
<div class="fs-12 color-789 mb-12">视频标签</div>
<div class="flex-items-center flex-wrap gap-10">
<span v-for="tag in currentItem.tags" :key="tag"
class="tag-pill h-28 lh-28 px-12 round-14 fs-12 color-main">
{{ tag }}
</span>
</div>
</div>
<div class="dialog-footer flex-items-center mt-20 pt-16">
<el-button class="footer-icon wh-32 p-0" plain>
<el-icon>
<Star />
</el-icon>
</el-button>
<el-button class="footer-icon wh-32 p-0" plain>
<el-icon>
<Share />
</el-icon>
</el-button>
<el-button type="primary" class="same-btn flex-1">
<el-icon>
<MagicStick />
</el-icon>
<span>一键同款</span>
</el-button>
</div>
</section>
</div>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { ArrowDown, ArrowUp, CopyDocument, MagicStick, Share, Star, VideoPlay } from '@element-plus/icons-vue';
export type InspirationPreviewItem = {
title: string;
author: string;
cover: string;
duration: string;
heat: number;
prompt: string;
tags: string[];
};
const props = defineProps<{
modelValue: boolean;
items: InspirationPreviewItem[];
index: number;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const activeIndex = ref(0);
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const currentItem = computed(() => props.items[activeIndex.value]);
watch(
() => props.index,
(value) => {
activeIndex.value = Math.max(0, Math.min(value, props.items.length - 1));
},
{ immediate: true },
);
const prevItem = () => {
if (!props.items.length) return;
activeIndex.value = activeIndex.value === 0 ? props.items.length - 1 : activeIndex.value - 1;
};
const nextItem = () => {
if (!props.items.length) return;
activeIndex.value = activeIndex.value === props.items.length - 1 ? 0 : activeIndex.value + 1;
};
</script>
<style lang="scss" scoped>
.preview-dialog {
height: 572px;
border-radius: 10px;
}
.preview-aside {
flex: 0 0 74px;
background: #02091d;
}
.nav-btn {
&:hover {
background-color: #ffffff13;
border-radius: 6px;
}
}
.thumb-list {
max-height: 390px;
overflow: hidden;
}
.thumb-btn {
border: 2px solid transparent !important;
opacity: .58;
}
.thumb-btn.active {
border-color: rgba(var(--main-color), 1) !important;
opacity: 1;
}
:deep(.thumb-btn > span) {
width: 100%;
height: 100%;
}
.preview-stage {
background: #2f2f2f;
}
.stage-cover {
opacity: .32;
filter: grayscale(.08);
}
.stage-shade {
background: rgba(0, 0, 0, .36);
}
.video-badge,
.source-tag {
background: rgba(9, 13, 22, .76);
}
.play-large {
top: 50%;
left: 50%;
background: rgba(var(--main-color), 1);
transform: translate(-50%, -50%);
}
.fake-controls {
z-index: 1;
}
.progress-line {
background: rgba(255, 255, 255, .36);
}
.preview-info {
background: #fff;
}
.heat {
color: #ff8200;
}
.avatar {
background: linear-gradient(135deg, #1877ff, #9d19ff);
}
.dot {
background: #d4deeb;
}
.type-tag {
color: #526f91;
background: #edf3fb;
}
.prompt-box {
background: #f8fbff;
border: 1px solid #e6edf5;
}
.reference-img {
border: 1px solid #e6edf5;
}
.tag-pill {
background: #fbf0ff;
border: 1px solid #ead0ff;
}
.dialog-footer {
border-top: 1px solid #edf2f7;
}
.footer-icon {
color: #526f91;
border-color: #dfe7f2;
}
.same-btn {
background: rgba(var(--main-color), 1);
border-color: rgba(var(--main-color), 1);
}
.color-001 {
color: #001a3d;
}
.color-334 {
color: #334b68;
}
.color-789 {
color: #7890ae;
}
</style>
<template>
<section>
<div class="fs-18 lh-28 fw-700 mb-16">电商核心创意工具组</div>
<div class="tool-grid flex gap-16">
<div v-for="item in tools" :key="item.title"
class="core-card flex-col justify-between flex-1 minh-160 p-16 round-14 bg-fff text-left pointer"
:style="{ '--accent': item.color, '--soft': item.bgColor }">
<div>
<span class="core-icon flex-center wh-36 round-10 mb-12 fs-20">
<el-icon>
<component :is="item.icon" />
</el-icon>
</span>
<div class="lh-22 fw-700">{{ item.title }}</div>
<p class="fs-12 lh-20">{{ item.desc }}</p>
</div>
<div class="card-action flex-items-center gap-4 fs-12 fw-700">
{{ item.action }}
<el-icon>
<ArrowRight />
</el-icon>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import type { Component } from 'vue';
import { ArrowRight, Edit, Picture, Scissor, User, ZoomIn } from '@element-plus/icons-vue';
type ToolItem = {
title: string;
desc: string;
action: string;
color: string;
bgColor: string;
icon: Component;
};
const tools: ToolItem[] = [
{
title: '视频去水印',
desc: '智能擦除视频水印与遮挡元素',
action: '立即擦除',
color: '#ff1f82',
bgColor: '#fff0f7',
icon: Edit,
},
{
title: '字幕擦除',
desc: '一键去除视频字幕与画面文字',
action: '立即清除',
color: '#1877ff',
bgColor: '#edf5ff',
icon: Scissor,
},
{
title: '画质增强',
desc: '提升视频清晰度与画作多倍质感',
action: '立即增强',
color: '#ff9900',
bgColor: '#fff8e8',
icon: ZoomIn,
},
{
title: '数字人分身',
desc: '高保真克隆音容姿态,输入文本即产出主播视频',
action: '极速制作',
color: '#f2187d',
bgColor: '#fff0f7',
icon: User,
},
{
title: '模特换衣',
desc: '上传服装图搭配不同肤色AI模特和逼真场景',
action: 'AI试穿',
color: '#1877ff',
bgColor: '#edf5ff',
icon: Picture,
},
];
</script>
<style lang="scss" scoped>
.core-card {
height: auto;
white-space: normal;
border: 1px solid #e4ebf4;
transition: border-color .2s, box-shadow .2s, transform .2s;
}
:deep(.core-card > span) {
display: flex;
flex: 1;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
width: 100%;
height: 100%;
}
.core-card p,
.card-action {
color: #86a0bd;
}
.core-icon {
color: var(--accent);
background: var(--soft);
}
.core-card:hover {
border-color: var(--accent);
box-shadow: 0 10px 24px rgba(22, 37, 67, .1);
transform: translateY(-2px);
}
.core-card:hover h3,
.core-card:hover .card-action {
color: var(--accent);
}
@media (max-width: 1100px) {
.tool-grid {
flex-wrap: wrap;
}
.core-card {
flex: 0 0 calc((100% - 16px) / 2);
}
}
</style>
<template>
<div class="quick-page h-full over-y-auto py-40">
<div class="card-box">
<header class="quick-header">
<div class="premium-chip items-center gap-6 h-26 px-12 round-14 fs-12 fw-700">
<el-icon>
<StarFilled />
</el-icon>
<span>PREMIUM PLATFORM</span>
</div>
<h1 class="mt-8 fs-24 lh-44 fw-700">
<span>梦畅AIGC,</span>
<span class="gradient-title">为电商而生的 AIGC 平台</span>
</h1>
</header>
<CreativeComposer class="mt-30" :active-scene="activeScene" @select-scene="activeScene = $event"
@clear-scene="activeScene = undefined" />
</div>
<div class="content">
<ToolGroup class="mt-86" />
<InspirationGallery class="mt-36" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { StarFilled } from '@element-plus/icons-vue';
import CreativeComposer from './components/CreativeComposer.vue';
import ToolGroup from './components/ToolGroup.vue';
import InspirationGallery from './components/InspirationGallery.vue';
import type { SceneKey } from './types';
const activeScene = ref<SceneKey>();
</script>
<style scoped lang="scss">
.card-box {
width: 1400px;
margin: auto;
}
.content {
width: 1600px;
margin: auto;
}
.quick-page {
background: #f6f9fc;
}
.quick-header h1 {
color: #001a3d;
letter-spacing: 0;
}
.premium-chip {
display: inline-flex;
color: rgba(var(--main-color), 1);
background: #fbf1ff;
border: 1px solid #efd6ff;
letter-spacing: 1.4px;
}
.gradient-title {
background: linear-gradient(90deg, #9d19ff 0%, #e01892 48%, #ff8a00 100%);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
</style>
export type SceneKey = 'product' | 'pain' | 'process' | 'koc' | 'story';
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