Commit 626b653c authored by 黄同智's avatar 黄同智

工具类页面

parent 96e60a33
......@@ -51,6 +51,24 @@ const routes = [
name: 'QuickCreation',
component: QuickCreation,
meta: { title: '快速创作', icon: 'add', affix: true }
},
{
path: 'remove-watermark',
name: 'RemoveWatermark',
component: () => import('@/views/quickCreation/remove-watermark.vue'),
meta: { title: '视频去水印', hidden: true }
},
{
path: 'remove-subtitles',
name: 'RemoveSubtitles',
component: () => import('@/views/quickCreation/remove-subtitles.vue'),
meta: { title: '视频去字幕', hidden: true }
},
{
path: 'remove-quality',
name: 'RemoveQuality',
component: () => import('@/views/quickCreation/remove-quality.vue'),
meta: { title: '视频画质增强', hidden: true }
}
]
},
......
......@@ -80,8 +80,8 @@
<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">
class="flex-center h-30 px-20" :class="{ active: duration === item }" plain
@click="duration = item">
{{ item }}s
</el-button>
</div>
......@@ -98,8 +98,8 @@
<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'">
<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>
......@@ -169,78 +169,7 @@
</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>
<MediaMaterialDialog v-model="mediaVisible" v-model:selected="selectedMaterials" />
</div>
</template>
......@@ -256,15 +185,12 @@ import {
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';
import MediaMaterialDialog from './MediaMaterialDialog.vue';
import type { MaterialItem, SceneKey } from '../types';
type SceneItem = {
key: SceneKey;
......@@ -290,7 +216,6 @@ const emit = defineEmits<{
clearScene: [];
}>();
const defaultDesc = '参考已上传的图片/视频,生成相关的产品素材。';
const prompt = ref('');
const mediaVisible = ref(false);
const settingVisible = ref(false);
......@@ -299,9 +224,7 @@ 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 selectedMaterials = ref<MaterialItem[]>([]);
const scenes: SceneItem[] = [
{ key: 'product', label: '产品素材', desc: '参考已上传的图片/视频,生成相关的产品素材。', color: '#1877ff', uploads: ['上传图片/视频素材'] },
......@@ -334,36 +257,7 @@ const modeOptions: ModeOption[] = [
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>
......@@ -398,28 +292,10 @@ const toggleMaterial = (id: string) => {
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 {
.ratio-btn {
border: 1px solid #dfe7f2;
color: #183151;
box-shadow: 0 2px 6px rgba(21, 44, 76, .06);
......@@ -434,14 +310,8 @@ const toggleMaterial = (id: string) => {
background: #fbf4ff;
}
.upload-btn {
border: 1px solid #e2bcff;
background: #fbebff;
}
.target-toggle,
.soft-pill,
.media-tabs {
.soft-pill {
background: #f3f7fc;
}
......@@ -518,69 +388,4 @@ const toggleMaterial = (id: string) => {
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>
<Dialog v-model="dialogVisible" bg-color="#fff" width="1000">
<div class="bg-fff hidden round-10">
<header class="dialog-header flex-items-center gap-12 px-24 py-18">
<span class="header-icon flex-center wh-32 round-6">
<el-icon class="fs-20">
<User />
</el-icon>
</span>
<div>
<div class="flex-items-center gap-8">
<h3 class="fs-16 lh-22 color-000">AI 智能数字人分身工作台</h3>
</div>
<p class="fs-12 lh-20 color-888">高保真音容笑貌克隆,无需实景拍摄,分钟级输出口播带货视频</p>
</div>
</header>
<section class="dialog-body flex gap-24 px-24 py-24">
<div class="left-column flex-col gap-24 flex-1">
<div>
<div class="lh-22 mb-14">1 · 选择主播模特分身</div>
<div class="anchor-list flex flex-wrap gap-12 w-full">
<div v-for="item in anchors" :key="item.id"
class="anchor-option pointer"
:class="{ active: selectedAnchor === item.id }"
@click="selectAnchor(item.id)">
<div class="anchor-card flex-items-center gap-12 p-14 round-12 bg-fff">
<span class="avatar flex-center wh-42 round hidden"
:style="{ background: item.avatarBg }">
<img v-if="item.avatar" :src="item.avatar" :alt="item.name"
class="w-full h-full cover" />
<span v-else class="color-fff">{{ item.name.slice(0, 1) }}</span>
</span>
<span class="anchor-copy flex-col text-left">
<span class="anchor-name lh-20 color-000">{{ item.name }}</span>
<span class="anchor-desc fs-12 lh-20 color-888">{{ item.desc }}</span>
</span>
</div>
</div>
</div>
</div>
<div>
<div class="lh-22 mb-10">2 · 调节声音音色及音速</div>
<div class="voice-panel p-16 round-14">
<div class="fs-12 lh-20 color-888 mb-8">推荐音色标签</div>
<el-select v-model="voiceStyle" class="w-full" size="large">
<el-option label="热情高亢(带货爆款)" value="hot" />
<el-option label="温柔亲和(护肤种草)" value="soft" />
<el-option label="专业可信(知识讲解)" value="pro" />
</el-select>
<div class="flex-between mt-14">
<span class="fs-12 lh-20 color-888">口播语速倍率</span>
<span class="fs-12 lh-20 speed-value">{{ speed.toFixed(2) }}x</span>
</div>
<el-slider v-model="speed" :min="0.8" :max="1.3" :step="0.05" :show-tooltip="false" />
</div>
</div>
</div>
<div class="right-column flex-col gap-24 flex-1">
<div>
<div class="lh-22 mb-10">3 · 设定数字人播报文案</div>
<el-input v-model="scriptText" class="script-input" type="textarea" resize="none" :rows="7"
maxlength="300" show-word-limit />
<div class="flex-between mt-12 fs-12 color-888">
<span>建议文案字数控制在 300 字以内</span>
<span>当前: {{ scriptText.length }}</span>
</div>
</div>
<div class="cost-card flex-col gap-12 p-18 round-14 mt-auto">
<div class="flex-between">
<div class=" lh-22 color-888">所需积分消耗</div>
<div class=" lh-22 cost-value">40 点算力</div>
</div>
<el-button type="primary" class="submit-btn h-40 round-10 border-none">
<el-icon>
<User />
</el-icon>
<span>一键合成数字人推流视频</span>
</el-button>
</div>
</div>
</section>
</div>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { User } from '@element-plus/icons-vue';
import peopleImg from '@/static/image/meinv.png';
import productImg from '@/static/image/huaban.png';
type AnchorItem = {
id: string;
name: string;
desc: string;
avatar?: string;
avatarBg: string;
};
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const anchors: AnchorItem[] = [
{ id: 'xiaomei', name: '主播小美', desc: '甜美知性 · 亲和力极佳', avatar: peopleImg, avatarBg: '#edf3fb' },
{ id: 'daliu', name: '主播大刘', desc: '专业严谨 · 沉稳有说服力', avatarBg: 'linear-gradient(135deg, #ba7a43, #16324f)' },
{ id: 'luoxi', name: '主播露西', desc: '外籍超模 · 时尚气息浓郁', avatar: productImg, avatarBg: '#dff4fb' },
{ id: 'wang', name: '主播王总', desc: '中年精英 · 行业大咖气质', avatarBg: 'linear-gradient(135deg, #d9c4ae, #2e3b32)' },
];
const selectedAnchor = ref('xiaomei');
const voiceStyle = ref('hot');
const speed = ref(1.05);
const scriptText = ref('哈罗各位宝宝们!今天给大家带来一款超级好用的黑科技香水。轻轻一喷,香味持久一整天,而且前中后调都特别有层次感。现在下单还享受买一送一哦,心动的宝宝赶紧点击下方链接抢购吧!');
const selectAnchor = (id: string) => {
selectedAnchor.value = id;
};
</script>
<style scoped lang="scss">
.dialog-header {
border-bottom: 1px solid #edf2f7;
}
.header-icon {
color: #f23594;
background: #fff0f7;
}
.anchor-list {
align-items: stretch;
}
.anchor-option {
flex: 0 0 calc((100% - 12px) / 2);
border: 1px solid #dfe7f2;
border-radius: 14px;
transition: border-color .2s, box-shadow .2s;
}
.anchor-option.active {
border-color: #f23594;
box-shadow: 0 0 0 1px #f23594;
}
.anchor-option:hover {
border-color: #f23594;
}
.anchor-card {
min-height: 66px;
}
.voice-panel,
.script-input :deep(.el-textarea__inner),
.cost-card {
border: 1px solid #dfe7f2;
background: #fbfdff;
}
:deep(.el-select__wrapper) {
border-radius: 10px;
box-shadow: 0 0 0 1px #dfe7f2 inset;
background: #fff;
}
:deep(.el-slider__bar),
:deep(.el-slider__button) {
background: #f23594;
}
:deep(.el-slider__button) {
border-color: #f23594;
}
.speed-value,
.cost-value {
color: #f23594;
}
.script-input :deep(.el-textarea__inner) {
min-height: 174px !important;
padding: 16px;
border-radius: 14px;
box-shadow: none;
color: #183151;
line-height: 24px;
}
.script-input :deep(.el-input__count) {
display: none;
}
.submit-btn {
width: 100%;
background: #f23594;
box-shadow: 0 8px 18px rgba(242, 53, 148, .24);
}
.right-column {
min-width: 0;
}
</style>
......@@ -224,7 +224,7 @@ const openPreview = (index: number) => {
}
.gallery-card {
flex: 0 0 calc((100% - 60px) / 4);
flex: 0 0 calc((100% - 80px) / 5);
background: #fff;
transition: border-color .24s, box-shadow .24s, transform .24s;
}
......@@ -294,7 +294,7 @@ h3 {
@media (max-width: 1280px) {
.gallery-card {
flex-basis: calc((100% - 40px) / 3);
flex-basis: calc((100% - 60px) / 4);
}
}
......
<template>
<el-dialog v-model="dialogVisible" 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>
</div>
<el-button class="close-btn wh-28 p-0 border-none bg-none color-888 pointer" circle
@click="dialogVisible = 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-4">
<el-button v-for="item in mediaTabs" :key="item" class="h-26 px-16 round-4 border-none bg-none"
: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-4 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">
<el-icon>
<Upload />
</el-icon>
<span>本地上传</span>
</el-button>
</div>
</div>
<el-radio-group v-model="selectedId" class="material-list flex flex-wrap gap-12 px-14 pt-14 minh-300">
<el-radio v-for="item in filteredMaterials" :key="item.id"
class="media-card relative round-8 hidden border-none pointer text-left h-150"
:class="{ selected: selectedId === item.id }" :value="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">
<div class="lh-18 ellipsis fs-12 fw-400">{{ item.name }}</div>
<div class="fs-10 fw-400">{{ item.size }}</div>
</span>
</el-radio>
</el-radio-group>
<div class="media-footer flex justify-end px-18 py-16 mt-18">
<div class="flex-items-center">
<el-button class="h-32 px-16 fs-12" @click="dialogVisible = false">取消</el-button>
<el-button class="h-32 px-16 fs-12" type="primary" @click="handleConfirm">
确认选择
</el-button>
</div>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { Close, Picture, Search, Upload } 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 { MaterialItem } from '../types';
const props = defineProps<{
modelValue: boolean;
selected: MaterialItem[];
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'update:selected': [value: MaterialItem[]];
confirm: [value: MaterialItem[]];
}>();
const mediaTabs = ['全部', '图片', '视频'];
const mediaTab = ref('全部');
const mediaKeyword = ref('');
const selectedId = ref(props.selected[0]?.id ?? '');
const materials: MaterialItem[] = [
{ 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 dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const selectedMaterials = computed(() => {
const current = materials.find((item) => item.id === selectedId.value);
return current ? [current] : [];
});
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 handleConfirm = () => {
emit('confirm', selectedMaterials.value);
dialogVisible.value = false;
};
watch(
() => props.selected,
(value) => {
const id = value[0]?.id ?? '';
if (id !== selectedId.value) {
selectedId.value = id;
}
},
{ deep: true }
);
watch(selectedId, () => {
const propId = props.selected[0]?.id ?? '';
if (selectedId.value !== propId) {
emit('update:selected', selectedMaterials.value);
}
});
</script>
<style lang="scss" scoped>
: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(.material-list .el-radio__input) {
position: absolute;
top: 8px;
left: 8px;
z-index: 1;
}
:deep(.material-list .el-radio__label) {
display: block;
width: 100%;
height: 100%;
padding-left: 0;
}
.soft-pill,
.media-tabs {
background: #f3f7fc;
}
.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
{
color: #fff;
background: rgba(var(--main-color), 1);
}
.material-list {
align-items: stretch;
}
.media-card {
flex: 0 0 calc((100% - 36px) / 4);
aspect-ratio: 1 / 1;
margin: 0;
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-footer {
border-top: 1px solid #edf2f7;
}
</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>
<Dialog v-model="dialogVisible" bg-color="#fff" width="1000">
<div class="tryon-dialog bg-fff hidden round-10">
<header class="dialog-header flex-items-center gap-12 px-24 py-18">
<span class="header-icon flex-center wh-32 round-6">
<el-icon class="fs-20">
<MagicStick />
</el-icon>
</span>
<div>
<div class="flex-items-center gap-8">
<h3 class="fs-16 lh-22 color-000">模特换衣</h3>
<span class="batch-tag h-22 lh-22 px-10 round-6 fs-10 fw-700">PRO BATCH</span>
</div>
<p class="fs-12 lh-20 color-888">将您的服装智能融合到数字模特与写实商用场景中,极速批量生成商用主图</p>
</div>
</header>
<section class="dialog-body px-24 py-24 over-y-auto">
<div class="step-row flex-between mb-14">
<div class="step-title flex-items-center gap-8">
<span class="step-index flex-center w-20 h-20 round-6 fs-12">1</span>
<span class="lh-22 fw-700 color-000">选择或上传模特原型(MODEL PROTOTYPE)</span>
</div>
<div class="switch-tabs flex-items-center h-34 p-4 round-8">
<el-button v-for="item in modelSources" :key="item.value"
class="h-26 px-12 round-6 border-none fs-12" :class="{ active: modelSource === item.value }"
text @click="modelSource = item.value">
{{ item.label }}
</el-button>
</div>
</div>
<div v-if="modelSource === 'library'" class="model-list flex gap-16">
<div v-for="item in models" :key="item.id" class="model-card flex-1 p-6 round-14 pointer"
:class="{ active: selectedModel === item.id }" @click="selectedModel = item.id">
<div class="model-cover relative h-202 hidden round-10">
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full cover" />
<div v-else class="fake-portrait w-full h-full" :style="{ background: item.bg }"></div>
<span class="gender-tag absolute right-8 bottom-8 h-20 lh-20 px-8 round-6 fs-10 color-fff">
{{ item.gender }}
</span>
</div>
<div class="text-center mt-8">
<div class="fs-12 lh-20 fw-700 color-000">{{ item.name }}</div>
<div class="fs-12 lh-18 color-888">{{ item.desc }}</div>
</div>
</div>
</div>
<UploadFile v-else listType="drap" />
<div class="section-line my-24"></div>
<div class="step-row flex-between mb-12">
<div class="step-title flex-items-center gap-8">
<span class="step-index flex-center w-20 h-20 round-6 fs-12">2</span>
<span class="lh-22 fw-700 color-000">调整模特身材类型(MODEL FIGURE TYPE)</span>
</div>
<div class="switch-tabs flex-items-center h-34 p-4 round-8">
<el-button v-for="item in bodyModes" :key="item.value"
class="h-26 px-12 round-6 border-none fs-12" :class="{ active: bodyMode === item.value }"
text @click="bodyMode = item.value">
{{ item.label }}
</el-button>
</div>
</div>
<div v-if="bodyMode === 'origin'" class="tip-box flex-items-center gap-8 h-44 px-16 round-10 fs-12 color-888">
<el-icon class="tip-icon">
<Opportunity />
</el-icon>
<span>智能引擎将保持上传模特照片中的原本身材形态、比例与线条,仅完成衣物及高级换脸的智能融合。</span>
</div>
<div v-else class="figure-list flex flex-wrap gap-12">
<div v-for="item in figureTemplates" :key="item.id"
class="figure-card flex-col justify-center h-48 px-14 round-10 pointer"
:class="{ active: selectedFigure === item.id }" @click="selectedFigure = item.id">
<div class="fs-12 lh-20 fw-700 color-000">{{ item.name }}</div>
<div class="fs-10 lh-18 color-888">{{ item.desc }}</div>
</div>
</div>
<div class="section-line my-24"></div>
<div class="step-row flex-between mb-12">
<div class="step-title flex-items-center gap-8">
<span class="step-index flex-center w-20 h-20 round-6 fs-12">3</span>
<span class="lh-22 fw-700 color-000">高级智能换脸 / 面孔重塑</span>
</div>
<div class="flex-items-center gap-8">
<el-switch v-model="faceEnabled" size="small" />
<span class="fs-12 fw-700 color-000">{{ faceEnabled ? '换脸已开启' : '换脸未开启' }}</span>
</div>
</div>
<div v-if="!faceEnabled" class="tip-box flex-center h-38 px-16 round-20 fs-12 color-888">
<el-icon class="tip-icon mr-6">
<Opportunity />
</el-icon>
<span>关闭换脸重塑后,模特将自动保持原型人面孔,或应用内置数字模特默认五官。</span>
</div>
<div v-else class="face-panel p-18 round-16">
<div class="face-tabs flex-items-center gap-16 mb-14">
<el-button v-for="item in faceSources" :key="item.value"
class="h-32 px-16 round-8 border-none fs-12"
:class="{ active: faceSource === item.value }" text @click="faceSource = item.value">
{{ item.label }}
</el-button>
</div>
<div class="face-list flex gap-12">
<div v-for="item in faceModels" :key="item.id"
class="face-card flex-1 flex-col flex-center h-92 round-10 pointer"
:class="{ active: selectedFace === item.id }" @click="selectedFace = item.id">
<span class="face-avatar flex-center wh-40 round hidden mb-6"
:style="{ background: item.avatarBg }">
<img v-if="item.avatar" :src="item.avatar" :alt="item.name"
class="w-full h-full cover" />
<span v-else class="color-fff fs-12">{{ item.name.slice(0, 1) }}</span>
</span>
<div class="fs-12 lh-20 fw-700 color-000">{{ item.name }}</div>
<div class="fs-10 lh-18 color-888">{{ item.desc }}</div>
</div>
</div>
</div>
<div class="section-line my-24"></div>
<div class="step-row flex-between mb-14">
<div class="step-title flex-items-center gap-8">
<span class="step-index flex-center w-20 h-20 round-6 fs-12">4</span>
<span class="lh-22 fw-700 color-000">选择服装(一次支持上传 / 添加多张批量生成)</span>
</div>
<div class="switch-tabs flex-items-center h-34 p-4 round-8">
<el-button v-for="item in clothSources" :key="item.value"
class="h-26 px-12 round-6 border-none fs-12"
:class="{ active: clothSource === item.value }" text @click="clothSource = item.value">
{{ item.label }}
</el-button>
</div>
</div>
<div class="cloth-list flex gap-14">
<div v-for="item in clothes" :key="item.id" class="cloth-card relative h-182 round-10 hidden">
<img :src="item.image" :alt="item.name" class="w-full h-full cover" />
<span class="cloth-tag absolute left-8 bottom-8 h-22 lh-22 px-8 round-6 fs-10 color-fff">
{{ item.name }}
</span>
</div>
<div class="cloth-card add-cloth flex-col flex-center h-182 round-10 pointer">
<el-icon class="fs-24 mb-8">
<Plus />
</el-icon>
<span class="fs-12 color-888">添加上传衣服</span>
</div>
</div>
<p class="flex-items-center mt-12 fs-12 lh-20 color-888">
<el-icon class="tip-icon mr-6">
<Opportunity />
</el-icon>
一次决定多张衣服照片进行上传,系统将自动对每件衣服与模特、场景进行真实穿戴试衣 Lookbook 生成。
</p>
<div class="section-line my-24"></div>
<div class="step-row flex-between mb-14">
<div class="step-title flex-items-center gap-8">
<span class="step-index flex-center w-20 h-20 round-6 fs-12">5</span>
<span class="lh-22 fw-700 color-000">选择逼真商用融合场景(REQUIRED SCENE)</span>
</div>
<span class="fs-12 fw-700 required-text">* 必须选择场景</span>
</div>
<div class="scene-list flex gap-16">
<div v-for="item in scenes" :key="item.id" class="scene-card flex-1 p-4 round-12 pointer"
:class="{ active: selectedScene === item.id }" @click="selectedScene = item.id">
<div class="scene-cover relative h-112 hidden round-8">
<img :src="item.image" :alt="item.name" class="w-full h-full cover" />
<span v-if="selectedScene === item.id"
class="check-dot absolute flex-center wh-22 round color-fff">
<el-icon>
<Check />
</el-icon>
</span>
</div>
<div class="px-8 py-8">
<div class="fs-12 lh-20 fw-700 color-000">{{ item.name }}</div>
<div class="fs-10 lh-18 color-888">{{ item.desc }}</div>
</div>
</div>
</div>
</section>
<footer class="dialog-footer flex-between px-24 py-14">
<div class="fs-14 color-888">
算力点数扣除
<strong class="color-main fs-16 mx-4">50</strong>
点数
<span class="fs-12 ml-6">(每件生成消耗 25 点)</span>
</div>
<div class="flex-items-center">
<el-button class="h-32 px-22" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" class="submit-btn h-32 px-28 border-none">
<el-icon>
<MagicStick />
</el-icon>
<span>一键启动 AI 模特换衣(2件批量生成)</span>
</el-button>
</div>
</footer>
</div>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Check, MagicStick, Opportunity, Plus } from '@element-plus/icons-vue';
import peopleImg from '@/static/image/meinv.png';
import productImg from '@/static/image/huaban.png';
import UploadFile from '@/components/Upload/index.vue'
type OptionItem = {
label: string;
value: string;
};
type ModelItem = {
id: string;
name: string;
desc: string;
gender: string;
image?: string;
bg?: string;
};
type ImageCardItem = {
id: string;
name: string;
desc?: string;
image: string;
};
type FigureTemplate = {
id: string;
name: string;
desc: string;
};
type FaceItem = {
id: string;
name: string;
desc: string;
avatar?: string;
avatarBg: string;
};
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const modelSources: OptionItem[] = [
{ label: '数字人库', value: 'library' },
{ label: '本地自主上传', value: 'local' },
];
const bodyModes: OptionItem[] = [
{ label: '默认图片身材', value: 'origin' },
{ label: '指定身材模板', value: 'template' },
];
const faceSources: OptionItem[] = [
{ label: '选择数字人预设面容', value: 'library' },
{ label: '用户本地上传人脸', value: 'local' },
];
const clothSources: OptionItem[] = [
{ label: '多图本地上传', value: 'multi' },
{ label: '商品链接/ID抓取', value: 'link' },
];
const models: ModelItem[] = [
{ id: 'shiyu', name: '诗雨', desc: '古典东方优雅', gender: '女性', image: peopleImg },
{ id: 'rose', name: 'Rose', desc: '欧美金发气场', gender: '女性', bg: 'linear-gradient(135deg, #d7b29c, #1d1f28)' },
{ id: 'jiale', name: '凯特', desc: '都市白领知性', gender: '女性', image: productImg },
{ id: 'tuozai', name: '拓哉', desc: '日韩轻熟形象', gender: '男性', bg: 'linear-gradient(135deg, #101014, #5a4236)' },
{ id: 'david', name: 'David', desc: '欧美时尚头', gender: '男性', bg: 'linear-gradient(135deg, #c4d2df, #34445c)' },
];
const clothes: ImageCardItem[] = [
{ id: 'cloth1', name: '衣服 #1', image: peopleImg },
{ id: 'cloth2', name: '衣服 #2', image: productImg },
];
const figureTemplates: FigureTemplate[] = [
{ id: 'slim', name: '欧美背部超模', desc: '高挑比例/肩背挺拔' },
{ id: 'sweet', name: '日系甜美少女', desc: '娇小匀称/元气少女' },
{ id: 'athletic', name: '国潮复古型男', desc: '肩宽腰窄/硬朗有型' },
{ id: 'urban', name: '资深都市丽人', desc: '优雅腰线/黄金比例' },
{ id: 'sport', name: '阳光运动型男', desc: '阳刚轮廓/腿侧线条' },
{ id: 'curve', name: '微胖丰满身材', desc: '丰满肉感/曲线柔和' },
];
const faceModels: FaceItem[] = [
{ id: 'face-xiaomei', name: '时尚小美', desc: '甜美东方', avatar: peopleImg, avatarBg: '#edf3fb' },
{ id: 'face-jia', name: '千练佳丽', desc: '知性丽人', avatar: productImg, avatarBg: '#fff0f7' },
{ id: 'face-shuai', name: '阳光小帅', desc: '活力潮男', avatarBg: 'linear-gradient(135deg, #262b36, #b47c5b)' },
{ id: 'face-han', name: '韩系欧巴', desc: '极简潮男', avatarBg: 'linear-gradient(135deg, #a9bfdb, #273449)' },
];
const scenes: ImageCardItem[] = [
{ id: 'studio', name: '高端室内摄影棚', desc: '影棚/静谧高光', image: peopleImg },
{ id: 'street', name: '巴黎春季时尚街头', desc: '户外/自然漫射', image: productImg },
{ id: 'villa', name: '奢华居家阳光露台', desc: '居家/落日氛围', image: peopleImg },
{ id: 'beach', name: '夏日热带落日海滩', desc: '海滩/海风阳光', image: productImg },
];
const modelSource = ref('library');
const bodyMode = ref('origin');
const clothSource = ref('multi');
const faceEnabled = ref(false);
const selectedModel = ref('shiyu');
const selectedScene = ref('studio');
const selectedFigure = ref('slim');
const faceSource = ref('library');
const selectedFace = ref('face-xiaomei');
</script>
<style scoped lang="scss">
.tryon-dialog {
max-height: 92vh;
}
.dialog-header,
.dialog-footer {
border-bottom: 1px solid #edf2f7;
}
.dialog-footer {
border-top: 1px solid #edf2f7;
border-bottom: 0;
}
.dialog-body {
max-height: calc(92vh - 148px);
}
.header-icon {
color: #1b6cff;
background: #edf5ff;
}
.batch-tag,
.step-index {
color: #1b6cff;
background: #edf5ff;
}
.switch-tabs {
background: #f3f7fc;
border: 1px solid #dfe7f2;
}
.switch-tabs button.active {
color: #1b6cff;
background: #fff;
box-shadow: 0 1px 4px rgba(17, 36, 62, .1);
}
.model-card,
.scene-card,
.figure-card,
.face-card {
border: 1px solid #dfe7f2;
background: #fff;
transition: border-color .2s, box-shadow .2s;
}
.model-card.active,
.scene-card.active,
.figure-card.active,
.face-card.active {
border-color: #1b6cff;
box-shadow: 0 0 0 1px #1b6cff;
}
.model-card:hover,
.scene-card:hover,
.figure-card:hover,
.face-card:hover {
border-color: #1b6cff;
}
.model-cover {
background: #eef3f8;
}
.fake-portrait {
position: relative;
}
.fake-portrait::after {
position: absolute;
inset: 34px 28px 0;
border-radius: 80px 80px 16px 16px;
background: rgba(255, 255, 255, .28);
content: '';
}
.gender-tag,
.cloth-tag {
background: rgba(9, 16, 28, .78);
}
.section-line {
height: 1px;
background: #edf2f7;
}
.tip-box,
.add-cloth,
.face-panel {
border: 1px solid #dfe7f2;
background: #fbfdff;
}
.figure-card {
flex: 0 0 calc((100% - 24px) / 3);
}
.face-tabs .el-button.active {
color: #1b6cff;
background: #fff;
box-shadow: 0 1px 4px rgba(17, 36, 62, .1);
}
.face-card {
background: #fff;
}
.tip-icon {
color: #ffbd2f;
}
.cloth-card {
flex: 0 0 182px;
border: 1px dashed #d9e4f2;
background: #fbfdff;
}
.add-cloth {
color: #8aa0bf;
}
.required-text {
color: #ff2d55;
}
.check-dot {
top: 50%;
left: 50%;
background: #1b6cff;
transform: translate(-50%, -50%);
}
.submit-btn {
background: #1f67ff;
box-shadow: 0 8px 18px rgba(31, 103, 255, .24);
}
</style>
<template>
<div class="flex items-start gap-24">
<div class="left-panel flex-col gap-20">
<el-card class="panel-card round-10" shadow="never">
<template #header>
<div class=" lh-22 fw-700 color-000">选择待处理视频</div>
</template>
<div class="upload-area w-full pointer" @click="mediaVisible = true">
<div class="flex-col flex-center h-150">
<span class="upload-icon flex-center wh-40 round mb-10">
<el-icon class="fs-22">
<UploadFilled />
</el-icon>
</span>
<div class="lh-22 fs-12">选择待擦除水印视频</div>
<p class="fs-12 color-999">智能分析格式,支持最高4K源文件输入</p>
</div>
</div>
</el-card>
<el-card class="panel-card round-10" shadow="never">
<template #header>
<div class="flex-between">
<div class=" lh-22 fw-700 color-000">标定水印区域 (1)</div>
<el-button class="h-28 px-8 fs-12 color-main" text>
<el-icon>
<Plus />
</el-icon>
<span>添加选区</span>
</el-button>
</div>
</template>
<div class="flex-items-center flex-wrap gap-10">
<el-tag class="watermark-tag h-30 lh-30 px-12 round-6 color-main" closable effect="plain">
水印区 1:78,4 / 96,14
</el-tag>
</div>
</el-card>
<el-card class="guide-card round-10 pt-16" shadow="never">
<div class="flex-items-center gap-6 mb-10">
<el-icon class="color-main fs-16">
<QuestionFilled />
</el-icon>
<span class=" lh-22 fw-700 color-333">智能擦除操作指引</span>
</div>
<div class="fs-12 lh-26 color-888">
<p>1. 上传视频素材,并在右侧视频静止帧中直接拖动/画框。</p>
<p>2. 如果水印在视频的不同位置,可多次点击「添加选区」进行联合擦除。</p>
<p>3. 点击执行,系统会提取水印周围相邻多帧的纹理流,智能重构无瑕背景。</p>
</div>
</el-card>
<el-button type="primary" class="run-btn h-40 round-6 border-none">
立即执行智能重绘擦除 (预计 2.50)
</el-button>
</div>
<el-card class="preview-card flex-1 round-10" shadow="never">
<template #header>
<div class="flex-between">
<div class=" lh-22 fw-700 color-000">画面预览与划定框</div>
<span class="fs-12 color-999">模拟视频帧排布</span>
</div>
</template>
<div class="preview-frame relative hidden round-12">
<img :src="previewImage" alt="待处理视频预览" class="w-full h-full cover" />
<div class="preview-mask absolute top-0 left-0 w-full h-full"></div>
<div class="selection-box absolute top-16 right-26 w-108 h-34">
<el-tag class="selection-label m-6 h-22 lh-22 px-8 round-6 color-fff fs-12 border-none">选区
1</el-tag>
<el-button class="selection-close absolute top-20 right-6 wh-14 p-0 border-none color-fff"
type="danger" circle>
<el-icon class="fs-10">
<Close />
</el-icon>
</el-button>
</div>
<div class="empty-state absolute flex-col flex-center text-center">
<span class="video-icon flex-center wh-42 round mb-10">
<el-icon class="fs-22">
<VideoCamera />
</el-icon>
</span>
<div class="lh-24">尚未加载工作视频</div>
<p class="fs-12 lh-20 color-999">请在左侧点击上传视频,以便框选水印或字幕边界</p>
</div>
</div>
<div class="status-bar flex-between h-38 px-12 mt-16 round-6 fs-12 color-999">
<span>状态:GPU像素缓存区待命</span>
<span>分辨率:1920 × 1080px</span>
</div>
</el-card>
<MediaMaterialDialog v-model="mediaVisible" v-model:selected="selectedMaterials" />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Close, Plus, QuestionFilled, UploadFilled, VideoCamera } from '@element-plus/icons-vue';
import previewImage from '@/static/image/meinv.png';
import MediaMaterialDialog from './MediaMaterialDialog.vue';
import type { MaterialItem, SceneKey } from '../types';
const mediaVisible = ref(false);
const selectedMaterials = ref<MaterialItem[]>([]);
</script>
<style scoped lang="scss">
.left-panel {
flex: 0 0 40%;
}
.panel-card,
.preview-card,
.guide-card {
border: 1px solid #dfe7f2;
}
:deep(.el-card__header) {
padding: 16px 16px 10px;
border-bottom: 0;
}
:deep(.el-card__body) {
padding: 0 20px 20px;
}
.upload-area {
width: 100%;
border: 1px dashed #ddd;
border-radius: 10px;
}
:deep(.upload-area .el-upload-dragger) {
padding: 0;
border-color: #d9e4f2;
border-style: dashed;
background: #fbfdff;
}
.upload-icon,
.video-icon {
color: #708aaf;
background: #edf3fb;
}
.watermark-tag {
border-color: rgba(var(--main-color), .1);
background: rgba(var(--main-color), .06);
}
.guide-card {
background: #f8fbff;
}
.run-btn {
background: linear-gradient(90deg, #9d19ff 0%, #f23594 100%);
}
.preview-frame {
height: 348px;
background: #eef3f8;
}
.preview-mask {
background: rgba(248, 250, 252, .68);
backdrop-filter: blur(1px);
}
.selection-box {
border: 2px dashed #c77dff;
background: rgba(199, 125, 255, .08);
}
.selection-label {
background: #b45cff;
}
.selection-close {
background: #ff6b8b;
}
.empty-state {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.video-icon {
color: rgba(var(--main-color), 1);
background: #fff;
}
.status-bar {
border: 1px solid #dfe7f2;
background: #f8fbff;
}
</style>
\ No newline at end of file
......@@ -2,16 +2,16 @@
<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"
<el-card 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 }">
:style="{ '--accent': item.color, '--soft': item.bgColor }" shadow="never" @click="handleToolClick(item)">
<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>
<div class="core-title 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">
......@@ -20,14 +20,19 @@
<ArrowRight />
</el-icon>
</div>
</div>
</el-card>
</div>
<DigitalHumanDialog v-model="digitalHumanVisible" />
<ModelTryOnDialog v-model="modelTryOnVisible" />
</section>
</template>
<script setup lang="ts">
import type { Component } from 'vue';
import { ref, type Component } from 'vue';
import { useRouter } from 'vue-router';
import { ArrowRight, Edit, Picture, Scissor, User, ZoomIn } from '@element-plus/icons-vue';
import DigitalHumanDialog from './DigitalHumanDialog.vue';
import ModelTryOnDialog from './ModelTryOnDialog.vue';
type ToolItem = {
title: string;
......@@ -36,8 +41,14 @@ type ToolItem = {
color: string;
bgColor: string;
icon: Component;
path?: string;
dialog?: 'digitalHuman' | 'modelTryOn';
};
const router = useRouter();
const digitalHumanVisible = ref(false);
const modelTryOnVisible = ref(false);
const tools: ToolItem[] = [
{
title: '视频去水印',
......@@ -46,6 +57,7 @@ const tools: ToolItem[] = [
color: '#ff1f82',
bgColor: '#fff0f7',
icon: Edit,
path: '/quickCreation/remove-watermark',
},
{
title: '字幕擦除',
......@@ -54,6 +66,7 @@ const tools: ToolItem[] = [
color: '#1877ff',
bgColor: '#edf5ff',
icon: Scissor,
path: '/quickCreation/remove-subtitles'
},
{
title: '画质增强',
......@@ -62,6 +75,7 @@ const tools: ToolItem[] = [
color: '#ff9900',
bgColor: '#fff8e8',
icon: ZoomIn,
path: '/quickCreation/remove-quality',
},
{
title: '数字人分身',
......@@ -70,6 +84,7 @@ const tools: ToolItem[] = [
color: '#f2187d',
bgColor: '#fff0f7',
icon: User,
dialog: 'digitalHuman',
},
{
title: '模特换衣',
......@@ -78,8 +93,23 @@ const tools: ToolItem[] = [
color: '#1877ff',
bgColor: '#edf5ff',
icon: Picture,
dialog: 'modelTryOn',
},
];
const handleToolClick = (item: ToolItem) => {
if (item.dialog === 'digitalHuman') {
digitalHumanVisible.value = true;
return;
}
if (item.dialog === 'modelTryOn') {
modelTryOnVisible.value = true;
return;
}
if (item.path) {
router.push(item.path);
}
};
</script>
<style lang="scss" scoped>
......@@ -90,7 +120,7 @@ const tools: ToolItem[] = [
transition: border-color .2s, box-shadow .2s, transform .2s;
}
:deep(.core-card > span) {
:deep(.core-card .el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
......@@ -98,6 +128,7 @@ const tools: ToolItem[] = [
justify-content: space-between;
width: 100%;
height: 100%;
padding: 0;
}
.core-card p,
......@@ -105,6 +136,10 @@ const tools: ToolItem[] = [
color: #86a0bd;
}
.core-title {
color: #001a3d;
}
.core-icon {
color: var(--accent);
background: var(--soft);
......@@ -116,7 +151,7 @@ const tools: ToolItem[] = [
transform: translateY(-2px);
}
.core-card:hover h3,
.core-card:hover .core-title,
.core-card:hover .card-action {
color: var(--accent);
}
......
<template>
<div class="h-full over-y-auto p-20 bgmr">
<div class="remove-content">
<div class=" mb-20">
<div class="fw-600 fs-16 flex-items-center color-000 gap-10">视频画质增强<span
class="color-main fs-10 round-4 px-6 py-2 tagssss">SUPERSAMPLING AI</span></div>
<div class="fs-12 color-888 mt-6">智能重构低清素材、还原奢华微距材质、平滑运动插帧,挽救失焦素材</div>
</div>
<div class="quality-main flex items-start gap-24">
<div class="left-panel flex-col gap-20">
<el-card class="panel-card round-10" shadow="never">
<template #header>
<div class="lh-22 fw-700 color-000">选择带货视频</div>
</template>
<div class="upload-area w-full pointer" @click="mediaVisible = true">
<div class="flex-col flex-center h-150">
<span class="upload-icon flex-center wh-40 round mb-10">
<el-icon class="fs-22">
<UploadFilled />
</el-icon>
</span>
<div class="lh-22 fs-12 color-000">选择素材库中的电商视频</div>
<p class="fs-12 color-999">支持选择 5 分钟以内的 mp4 / mov / avi 素材</p>
</div>
</div>
</el-card>
<el-card class="panel-card round-10" shadow="never">
<template #header>
<div class="lh-22 fw-700 color-000">画质重构设置</div>
</template>
<div class="quality-form flex-col gap-20">
<div class="flex gap-16">
<div class="flex-1">
<div class="form-label fs-12 lh-20 mb-8 color-888">输出分辨率 / RESOLUTION</div>
<el-select v-model="resolution" class="w-full" size="large">
<el-option label="1080P 超高清极速渲染" value="1080p" />
<el-option label="2K 商业展示画质" value="2k" />
<el-option label="4K 品质重写" value="4k" />
</el-select>
</div>
<div class="flex-1">
<div class="form-label fs-12 lh-20 color-888 mb-8">目标帧率 / MOTION FRAME</div>
<el-select v-model="frameRate" class="w-full" size="large">
<el-option label="AI 智能插帧 60fps (超流畅)" value="60fps" />
<el-option label="保持原帧率" value="source" />
<el-option label="电影感 24fps" value="24fps" />
</el-select>
</div>
</div>
<div>
<div class="form-label fs-12 lh-20 color-888 mb-8">超分辨率计算引擎 / AI ENGINE</div>
<el-select v-model="engine" class="w-full" size="large">
<el-option label="UltraEnhance-v3.0 (高细节材质深度还原)" value="ultra" />
<el-option label="FastRestore-v2.5 (极速清晰增强)" value="fast" />
<el-option label="PortraitClean-v1.8 (人像肤质优化)" value="portrait" />
</el-select>
</div>
</div>
</el-card>
<el-card class="guide-card round-10 pt-16" shadow="never">
<div class="flex-items-center gap-6 mb-10">
<el-icon class="color-main fs-16">
<QuestionFilled />
</el-icon>
<span class="lh-22 fw-700 color-main">超分增强操作指引</span>
</div>
<div class="fs-12 lh-26 color-888">
<p>• 本算法针对珠宝类纹理、服装皮质、毛发质感有深度重构增强作用。</p>
<p>• 超分渲染时间受视频源时长及分辨率选项影响,一般在 1-3 分钟内完成。</p>
<p>• 开始增强预计将消耗 5.00 积分点。</p>
</div>
</el-card>
<el-button type="primary" class="run-btn h-40 round-6 border-none">
一键激发 4K 画质重构 (预计 5.00)
</el-button>
</div>
<el-card class="preview-card flex-1 round-10" shadow="never">
<template #header>
<div class="flex-between">
<div class="lh-22 fw-700 color-000">算法效果对比(实时微距模拟)</div>
<span class="fs-12 color-999">左右拖拽中线看清超高清细节</span>
</div>
</template>
<div ref="compareRef" class="compare-stage relative hidden round-10 pointer"
@pointerdown="handlePointerMove">
<img :src="previewImage" alt="画质增强后预览" class="compare-img w-full h-full cover" />
<div class="before-layer absolute top-0 left-0 w-full h-full hidden"
:style="{ clipPath: `inset(0 ${100 - comparePercent}% 0 0)` }">
<img :src="previewImage" alt="画质增强前预览" class="before-img w-full h-full cover" />
</div>
<el-tag class="compare-tag before-tag absolute top-14 left-14 h-20 lh-20 px-10 round-10 fs-10"
effect="dark">
BEFORE / 480P 压缩源图
</el-tag>
<el-tag class="compare-tag after-tag absolute top-14 right-14 h-20 lh-20 px-10 round-10 fs-10"
effect="dark">
AFTER / 4K 质感重写
</el-tag>
<div class="compare-line absolute top-0 h-full" :style="{ left: `${comparePercent}%` }">
<span class="handle-dot absolute flex-center wh-28 round">
<span class="handle-bar h-12"></span>
<span class="handle-bar h-12"></span>
</span>
</div>
</div>
<div class="effect-list flex gap-12 mt-24">
<div class="effect-card flex-1 flex-col flex-center h-78 round-10">
<el-icon class="color-main fs-18 mb-6">
<MagicStick />
</el-icon>
<div class="fs-12 lh-20 fw-700 color-000">GPU 硬件加速</div>
<p class="fs-12 lh-20 color-999">双核 Tensor A100 并行解算</p>
</div>
<div class="effect-card flex-1 flex-col flex-center h-78 round-10">
<el-icon class="quality-pink fs-18 mb-6">
<Picture />
</el-icon>
<div class="fs-12 lh-20 fw-700 color-000">质感智能补强</div>
<p class="fs-12 lh-20 color-999">还原奢侈品反射与纯面高光</p>
</div>
</div>
</el-card>
</div>
<MediaMaterialDialog v-model="mediaVisible" v-model:selected="selectedMaterials" />
</div>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { BrushFilled, MagicStick, Picture, QuestionFilled, UploadFilled } from '@element-plus/icons-vue';
import previewImage from '@/static/image/meinv.png';
import MediaMaterialDialog from './components/MediaMaterialDialog.vue';
import type { MaterialItem } from './types';
const mediaVisible = ref(false);
const selectedMaterials = ref<MaterialItem[]>([]);
const resolution = ref('1080p');
const frameRate = ref('60fps');
const engine = ref('ultra');
const compareRef = ref<HTMLElement>();
const comparePercent = ref(48);
const updateComparePercent = (event: PointerEvent) => {
if (!compareRef.value) {
return;
}
const rect = compareRef.value.getBoundingClientRect();
const percent = ((event.clientX - rect.left) / rect.width) * 100;
comparePercent.value = Math.min(88, Math.max(12, Math.round(percent)));
};
const stopDrag = () => {
window.removeEventListener('pointermove', updateComparePercent);
window.removeEventListener('pointerup', stopDrag);
};
const handlePointerMove = (event: PointerEvent) => {
updateComparePercent(event);
window.addEventListener('pointermove', updateComparePercent);
window.addEventListener('pointerup', stopDrag);
};
onBeforeUnmount(stopDrag);
</script>
<style scoped lang="scss">
.remove-content {
max-width: 1600px;
margin: auto;
}
.tagssss {
background-color: rgba(var(--main-color), .04);
box-shadow: 0 0 0 1px rgba(var(--main-color), .1);
}
.left-panel {
flex: 0 0 49%;
}
.panel-card,
.preview-card,
.guide-card {
border: 1px solid #dfe7f2;
}
:deep(.el-card__header) {
padding: 16px 16px 10px;
border-bottom: 0;
}
:deep(.el-card__body) {
padding: 0 20px 20px;
}
.upload-area {
border: 1px dashed #d9e4f2;
border-radius: 10px;
background: #fbfdff;
}
.upload-icon {
color: #708aaf;
background: #edf3fb;
}
.guide-card {
background: #f8fbff;
}
.run-btn {
background: linear-gradient(90deg, #9d19ff 0%, #f23594 100%);
}
.preview-card {
min-width: 0;
}
.compare-stage {
height: 292px;
background: #eef3f8;
touch-action: none;
user-select: none;
}
.compare-img {
filter: saturate(1.12) contrast(1.08);
}
.before-layer {
filter: blur(2px) saturate(.72) contrast(.78);
}
.compare-tag {
border: 0;
font-weight: 700;
}
.before-tag {
background: #ff4d5f;
}
.after-tag {
background: #26c486;
}
.compare-line {
width: 2px;
background: rgba(255, 255, 255, .9);
transform: translateX(-1px);
}
.handle-dot {
top: 50%;
left: 50%;
gap: 3px;
color: rgba(var(--main-color), 1);
background: #fff;
border: 2px solid rgba(var(--main-color), 1);
box-shadow: 0 4px 14px rgba(var(--main-color), .2);
transform: translate(-50%, -50%);
}
.handle-bar {
width: 2px;
background: currentColor;
}
.effect-card {
border: 1px solid #dfe7f2;
background: #fbfdff;
}
.quality-pink {
color: #f23594;
}
@media (max-width: 1180px) {
.quality-main {
flex-wrap: wrap;
}
.left-panel,
.preview-card {
flex: 0 0 100%;
}
}
</style>
\ No newline at end of file
<template>
<div class="h-full over-y-auto p-20 bgmr">
<div class="remove-content">
<div class="flex-between mb-20">
<div>
<div class="fw-600 fs-16 flex-items-center color-000 gap-10">视频字幕擦除<span
class="color-main fs-10 round-4 px-6 py-2 tagssss">SUBTITLE ERASE</span></div>
<div class="fs-12 color-888 mt-6">一键分析字幕所在底条,智能无痕还原带货视频底部像素</div>
</div>
<el-button class="h-28 px-8 fs-12">
<el-icon>
<BrushFilled />
</el-icon>
<span>新建擦除</span>
</el-button>
</div>
<RemoveVideo />
</div>
</div>
</template>
<script setup lang="ts">
import { BrushFilled } from '@element-plus/icons-vue';
import RemoveVideo from './components/Remove-video.vue';
</script>
<style scoped lang="scss">
.remove-content {
max-width: 1600px;
margin: auto;
}
.tagssss {
background-color: rgba(var(--main-color), .04);
box-shadow: 0 0 0 1px rgba(var(--main-color), .1);
}
.left-panel {
flex: 0 0 40%;
}
.panel-card,
.preview-card,
.guide-card {
border: 1px solid #dfe7f2;
}
:deep(.el-card__header) {
padding: 16px 16px 10px;
border-bottom: 0;
}
:deep(.el-card__body) {
padding: 0 20px 20px;
}
:deep(.upload-area .el-upload) {
width: 100%;
}
:deep(.upload-area .el-upload-dragger) {
padding: 0;
border-color: #d9e4f2;
border-style: dashed;
background: #fbfdff;
}
.upload-icon,
.video-icon {
color: #708aaf;
background: #edf3fb;
}
.watermark-tag {
border-color: rgba(var(--main-color), .1);
background: rgba(var(--main-color), .06);
}
.guide-card {
background: #f8fbff;
}
.run-btn {
background: linear-gradient(90deg, #9d19ff 0%, #f23594 100%);
}
.preview-frame {
height: 348px;
background: #eef3f8;
}
.preview-mask {
background: rgba(248, 250, 252, .68);
backdrop-filter: blur(1px);
}
.selection-box {
border: 2px dashed #c77dff;
background: rgba(199, 125, 255, .08);
}
.selection-label {
background: #b45cff;
}
.selection-close {
background: #ff6b8b;
}
.empty-state {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.video-icon {
color: rgba(var(--main-color), 1);
background: #fff;
}
.status-bar {
border: 1px solid #dfe7f2;
background: #f8fbff;
}
</style>
<template>
<div class="h-full over-y-auto p-20 bgmr">
<div class="remove-content">
<div class="flex-between mb-20">
<div>
<div class="fw-600 fs-16 flex-items-center color-000 gap-10">视频去水印<span
class="color-main fs-10 round-4 px-6 py-2 tagssss">WATERMARK REMOVAL</span></div>
<div class="fs-12 color-888 mt-6">采用多维光流时间轴补全算法,干净擦除顽固角标,动态品牌浮窗与遮挡物</div>
</div>
<el-button class="h-28 px-8 fs-12">
<el-icon>
<BrushFilled />
</el-icon>
<span>新建擦除</span>
</el-button>
</div>
<RemoveVideo />
</div>
</div>
</template>
<script setup lang="ts">
import { BrushFilled } from '@element-plus/icons-vue';
import RemoveVideo from './components/Remove-video.vue';
</script>
<style scoped lang="scss">
.remove-content {
max-width: 1600px;
margin: auto;
}
.tagssss {
background-color: rgba(var(--main-color), .04);
box-shadow: 0 0 0 1px rgba(var(--main-color), .1);
}
</style>
export type SceneKey = 'product' | 'pain' | 'process' | 'koc' | 'story';
export type MaterialItem = {
id: string;
name: string;
kind: '图片' | '视频';
size: string;
image: string;
};
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