Commit 310cf38d authored by 黄同智's avatar 黄同智

完成商品中的选择商品和商品地址查询的弹窗

parent a1f3aea0
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<div class="common-table-wrapper"> <div class="common-table-wrapper">
<!-- 1. 表格主体 --> <!-- 1. 表格主体 -->
<el-table :data="tableData" style="width: 100%" :border="border" :size="size" :stripe="stripe" <el-table :data="tableData" style="width: 100%" :border="border" :size="size" :stripe="stripe"
:row-key="rowKey || undefined" v-loading="loading" @selection-change="handleSelectionChange"> :height="height || undefined" :row-key="rowKey || undefined" v-loading="loading"
@selection-change="handleSelectionChange">
<!-- 默认插槽:让父组件自定义列 --> <!-- 默认插槽:让父组件自定义列 -->
<slot></slot> <slot></slot>
...@@ -15,8 +16,8 @@ ...@@ -15,8 +16,8 @@
<!-- 2. 分页组件 --> <!-- 2. 分页组件 -->
<div v-if="showPagination" class="pagination-container"> <div v-if="showPagination" class="pagination-container">
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :size="size" <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :size="psize"
:page-sizes="[10, 20, 50, 100]" :background="true" layout="total, sizes, prev, pager, next, jumper" :page-sizes="pageSizes" :background="true" layout="total, sizes, prev, pager, next, jumper"
:total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div> </div>
</div> </div>
...@@ -59,6 +60,10 @@ const props = defineProps({ ...@@ -59,6 +60,10 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
psize: {
type: String,
default: ''
},
showPagination: { showPagination: {
type: Boolean, type: Boolean,
default: true, default: true,
...@@ -66,7 +71,15 @@ const props = defineProps({ ...@@ -66,7 +71,15 @@ const props = defineProps({
rowKey: { rowKey: {
type: [String, Function], type: [String, Function],
default: '', default: '',
} },
height: {
type: [String, Number],
default: '',
},
pageSizes: {
type: Array,
default: () => [10, 20, 50, 100],
},
}); });
// 2. 定义 Emits(向父组件传递事件) // 2. 定义 Emits(向父组件传递事件)
......
<template>
<Dialog v-model="dialogVisible" bg-color="#fff" width="1152">
<div class="flex-col bg-fff round-8 hidden">
<header class="dialog-header flex-items-center h-56 px-20">
<h3 class="fs-14 fw-700 color-000">添加商品图</h3>
</header>
<section class="dialog-body flex-1 flex-col hidden px-20 py-14">
<div class="tab-row flex-items-center mb-20">
<div v-for="item in tabs" :key="item.value" class="tab-item h-34 lh-34 px-16 fs-12 fw-700 pointer"
:class="{ active: activeTab === item.value }" @click="activeTab = item.value">
{{ item.label }}
</div>
</div>
<div v-if="activeTab === 'library'" class="library-panel flex-1 flex-col hidden">
<div class="filter-row flex-between gap-8 mb-12">
<div class="flex-items-center flex-wrap gap-8 flex-1">
<el-select v-model="firstCategory" class="filter-select" placeholder="全部一级分类">
<el-option v-for="item in firstCategories" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="secondCategory" class="filter-select" placeholder="全部二级分类">
<el-option v-for="item in secondCategories" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="tagValue" class="filter-select" placeholder="全部标签">
<el-option v-for="item in tags" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="statusValue" class="filter-select" placeholder="全部状态">
<el-option v-for="item in statuses" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="uploaderValue" class="uploader-select" placeholder="全部上传人">
<el-option v-for="item in uploaders" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-model="searchKeyword" class="search-input flex-1" placeholder="搜索图片名称或 ID">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-checkbox v-model="onlyMine" class="mine-check">仅看我的</el-checkbox>
</div>
</div>
<SeTable class="material-table flex-1" :border="false" :data-list="currentPageImageRows" :total="imageRows.length"
:default-page-size="pageSize" :page-sizes="[6, 10, 20, 50]" :height="tableHeight" row-key="id" psize="small"
@update:page="handleImagePageChange" @selection-change="handleImageSelectionChange">
<el-table-column type="selection" reserve-selection width="46" />
<el-table-column label="图片缩略图" width="122">
<template #default="{ row }">
<img :src="row.image" :alt="row.name" class="thumb-img cover round-4" />
</template>
</el-table-column>
<el-table-column label="文件名称 / ID" min-width="240">
<template #default="{ row }">
<div class="flex-col">
<span class="fs-12 fw-700 color-333 ellipsis">{{ row.name }}</span>
<span class="fs-10 color-999 mt-4">{{ row.id }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="118">
<template #default="{ row }">
<span class="status-tag h-22 lh-22 px-10 round-4 fs-10">{{ row.status }}</span>
</template>
</el-table-column>
<el-table-column label="分类 / 标签" min-width="190">
<template #default="{ row }">
<div class="flex-col">
<span class="fs-12 fw-700 color-333 ellipsis">{{ row.category }}</span>
<span class="fs-10 color-999 mt-4 ellipsis">{{ row.tags }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="uploader" label="上传人" width="112" />
<el-table-column prop="resolution" label="分辨率" width="118" />
<el-table-column prop="size" label="大小" width="72" />
</SeTable>
</div>
<div v-else class="flex-1 round-8">
<UploadFile v-model="localFiles" list-type="drap" :multiple="true" :limit="6">
<template #trigger>
<div>
<el-icon size="20">
<Upload />
</el-icon>
</div>
<p class="fs-12 color-888 mt-6">点击选择本地图片</p>
</template>
</UploadFile>
<div class="color-888 fs-12 text-center">支持上传本地图片文件,图片格式:jpeg、 png、 webp、 bmp、 tiff、 gif,单张图片大小≤30MB。</div>
<div class="color-888 fs-12 text-center">图片宽高比需在 (0.4, 2.5) 之间,宽高像素需在 (300px, 6000px) 之间。</div>
<div class="color-888 fs-12 text-center">仅支持上传非人脸图,请仔细查看要求并确保上传素材为您原创或已取得合法授权。</div>
</div>
</section>
<footer class="dialog-footer flex-between px-20 py-12">
<div class="fs-12 color-666">
已选 <span class="color-main fw-700">{{ selectedImageIds.length }}</span> 张,还可添加
{{ restCount }} 张
</div>
<div class="flex-items-center gap-8">
<el-button class="h-34 px-18" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" class="confirm-btn h-34 px-20 border-none" @click="confirmImages">
确认选择
</el-button>
</div>
</footer>
</div>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { UploadUserFile } from 'element-plus';
import { Search, Upload } from '@element-plus/icons-vue';
import Dialog from '@/components/Dialog/index.vue';
import UploadFile from '@/components/Upload/index.vue';
import productImg from '@/static/image/huaban.png';
import peopleImg from '@/static/image/meinv.png';
type TabValue = 'library' | 'local';
type ImageRow = {
id: string;
name: string;
image: string;
status: string;
category: string;
tags: string;
uploader: string;
resolution: string;
size: string;
};
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
confirm: [ids: string[]];
}>();
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const tabs: { label: string; value: TabValue }[] = [
{ label: '图片管理', value: 'library' },
{ label: '本地上传', value: 'local' },
];
const firstCategories = ['全部一级分类', '美妆护肤', '服饰内衣', '日用百货', '食品饮料'];
const secondCategories = ['全部二级分类', '商品主图', '模特展示', '效果对比', '资质证明'];
const tags = ['全部标签', '产品实拍', '成分党', '清凉感', '对比实测'];
const statuses = ['全部状态', '审核通过', '待审核', '未审核'];
const uploaders = ['全部上传人', '致上互娱', '汤小真', '徐振', '美妆设计组'];
const activeTab = ref<TabValue>('library');
const firstCategory = ref('全部一级分类');
const secondCategory = ref('全部二级分类');
const tagValue = ref('全部标签');
const statusValue = ref('全部状态');
const uploaderValue = ref('全部上传人');
const searchKeyword = ref('');
const onlyMine = ref(false);
const selectedImageIds = ref<string[]>([]);
const localFiles = ref<UploadUserFile[]>([]);
const currentPage = ref(1);
const pageSize = ref(10);
const tableHeight = 500;
const imageRows: ImageRow[] = [
{
id: 'img-1',
name: '防晒植物提取精华液展图.jpg',
image: peopleImg,
status: '审核通过',
category: '美妆护肤 / 商品主图',
tags: '产品实拍、成分党',
uploader: '致上互娱',
resolution: '1080x1440',
size: '2.4 MB',
},
{
id: 'img-2',
name: '无痕防晒冰丝丝袜场景模特图.png',
image: productImg,
status: '待审核',
category: '服饰内衣 / 模特展示',
tags: '模特出镜、清凉冰丝',
uploader: '汤小真',
resolution: '800x1200',
size: '1.8 MB',
},
{
id: 'img-3',
name: '夏日爆款产品盒装主图.jpg',
image: productImg,
status: '审核通过',
category: '日用百货 / 宣发主图',
tags: '爆款、活动促销',
uploader: '致上互娱',
resolution: '1920x1080',
size: '4.5 MB',
},
{
id: 'img-4',
name: '草本护肤成分拆解对比展图.png',
image: peopleImg,
status: '审核通过',
category: '美妆护肤 / 成分展示',
tags: '对比实测',
uploader: '汤小真',
resolution: '1080x1080',
size: '3.1 MB',
},
{
id: 'img-5',
name: '防晒霜SPF50权威检测图.jpg',
image: productImg,
status: '未审核',
category: '美妆护肤 / 资质证明',
tags: '成分党、检测报告',
uploader: '致上互娱',
resolution: '1080x1920',
size: '1.2 MB',
},
{
id: 'img-6',
name: '补水面膜水分提升对比实验图.jpg',
image: peopleImg,
status: '审核通过',
category: '美妆护肤 / 效果对比',
tags: '对比实测',
uploader: '徐振',
resolution: '1080x1440',
size: '2.9 MB',
},
{
id: 'img-7',
name: '夏日清凉草本展示图.jpg',
image: productImg,
status: '待审核',
category: '食品饮料 / 商品主图',
tags: '清凉夏日、产品实拍',
uploader: '美妆设计组',
resolution: '2000x2000',
size: '5.4 MB',
},
{
id: 'img-8',
name: '高奢护肤瓶身渲染特写.jpg',
image: peopleImg,
status: '审核通过',
category: '美妆护肤 / 商品主图',
tags: '高端质感、3D渲染',
uploader: '汤小真',
resolution: '1440x1920',
size: '3.8 MB',
},
{
id: 'img-9',
name: '高奢护肤瓶身渲染特写.jpg',
image: peopleImg,
status: '审核通过',
category: '美妆护肤 / 商品主图',
tags: '高端质感、3D渲染',
uploader: '汤小真',
resolution: '1440x1920',
size: '3.8 MB',
},
{
id: 'img-10',
name: '高奢护肤瓶身渲染特写.jpg',
image: peopleImg,
status: '审核通过',
category: '美妆护肤 / 商品主图',
tags: '高端质感、3D渲染',
uploader: '汤小真',
resolution: '1440x1920',
size: '3.8 MB',
},
{
id: 'img-11',
name: '高奢护肤瓶身渲染特写.jpg',
image: peopleImg,
status: '审核通过',
category: '美妆护肤 / 商品主图',
tags: '高端质感、3D渲染',
uploader: '汤小真',
resolution: '1440x1920',
size: '3.8 MB',
},
];
const restCount = computed(() => Math.max(0, 6 - selectedImageIds.value.length));
const currentPageImageRows = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
return imageRows.slice(start, start + pageSize.value);
});
const handleImagePageChange = (page: number, size: number) => {
currentPage.value = page;
pageSize.value = size;
};
const handleImageSelectionChange = (selection: ImageRow[]) => {
selectedImageIds.value = selection.map((item) => item.id);
};
const confirmImages = () => {
emit('confirm', selectedImageIds.value);
dialogVisible.value = false;
};
</script>
<style scoped lang="scss">
.dialog-header {
flex: 0 0 56px;
border-bottom: 1px solid #edf2f7;
}
.dialog-body,
.library-panel,
.material-table {
min-height: 0;
}
.dialog-footer {
flex: 0 0 58px;
border-top: 1px solid #edf2f7;
background: #f8fafc;
}
.tab-row {
border-bottom: 1px solid #dfe7f2;
}
.tab-item {
color: #536883;
border-bottom: 2px solid transparent;
&.active {
color: #7d28ff;
border-bottom-color: #7d28ff;
}
}
.filter-select {
width: 130px;
}
.uploader-select {
width: 124px;
}
.search-input {
min-width: 220px;
}
.mine-check {
white-space: nowrap;
}
.thumb-img {
width: 48px;
height: 48px;
}
.status-tag {
color: #61738c;
background: #eef3f8;
}
.confirm-btn {
background: #7d28ff;
}
:deep(.el-select__wrapper),
:deep(.el-input__wrapper) {
border-radius: 4px;
box-shadow: 0 0 0 1px #dce6f2 inset;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
border-color: #7d28ff;
background: #7d28ff;
}
</style>
<template>
<Dialog v-model="dialogVisible" bg-color="#fff" width="768">
<div class="product-info-dialog bg-fff round-8 hidden">
<header class="dialog-header flex-items-center h-56 px-20">
<h3 class="fs-14 fw-700 color-000">输入商品信息</h3>
</header>
<section class="dialog-body px-20 py-20">
<div class="fs-12 fw-700 color-333 mb-8">抖音商品链接或商品 ID</div>
<div class="flex gap-10">
<el-input v-model="productKeyword" class="flex-1" size="small" placeholder="粘贴抖音商品链接或输入商品 ID" />
<el-button :disabled="productKeyword === ''" type="primary" class="search-btn fs-12 h-34 px-16">搜索</el-button>
</div>
<div v-for="item in productOptions" :key="item.id"
class="product-card flex-items-center gap-12 mt-16 p-12 round-6 pointer"
:class="{ active: selectedProductId === item.id }" @click="selectedProductId = item.id">
<img :src="item.image" :alt="item.name" class="product-cover wh-60 round-4 cover" />
<div class="flex-1">
<div class="fs-14 fw-700 color-000 lh-22">{{ item.name }}</div>
<div class="fs-12 color-666 lh-20">商品 ID:{{ item.id }}</div>
</div>
<span v-if="selectedProductId === item.id" class="checked-icon flex-center wh-18 round">
<el-icon class="fs-12">
<Check />
</el-icon>
</span>
</div>
</section>
<footer class="dialog-footer flex justify-end gap-8 px-20 py-12">
<el-button class="px-18" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" class="confirm-btn px-18 border-none" @click="confirmProductInfo">
确认
</el-button>
</footer>
</div>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Check } from '@element-plus/icons-vue';
import Dialog from '@/components/Dialog/index.vue';
import productImg from '@/static/image/huaban.png';
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
confirm: [value: string];
}>();
const productKeyword = ref('');
const selectedProductId = ref('');
const productOptions = [
{
id: '353829104771',
name: 'Bestore 负离子高速吹风机',
image: productImg,
},
{
id: '353829104772',
name: '负离子高速吹风机卡卡的撒',
image: productImg,
},
];
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const confirmProductInfo = () => {
emit('confirm', selectedProductId.value || productKeyword.value);
dialogVisible.value = false;
};
</script>
<style scoped lang="scss">
.dialog-header,
.dialog-footer {
border-bottom: 1px solid #edf2f7;
}
.dialog-footer {
border-top: 1px solid #edf2f7;
border-bottom: 0;
background: #f8fafc;
}
:deep(.el-input__wrapper) {
// height: 40px;
// border-radius: 6px;
box-shadow: 0 0 0 1px #dce6f2 inset;
}
:deep(.el-input__inner) {
font-size: 12px;
}
.product-card {
border: 1px solid #dce6f2;
background: #fff;
transition: border-color .2s, background .2s;
&.active {
border-color: #9b6cff;
background: #f4f0ff;
}
}
.product-cover {
background: #f3f6fb;
}
.checked-icon {
color: #7d28ff;
border: 1px solid #7d28ff;
}
</style>
<template>
<div class="flex-items-center flex-wrap">
<el-popover v-model:visible="productPopoverVisible" placement="bottom" :width="150" trigger="hover" :hide-after="0">
<template #reference>
<el-button class="m-2 h-30 border-none px-6">
<el-icon>
<component :is="Box" />
</el-icon>
<span>商品</span>
</el-button>
</template>
<div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover" @click="openProductInfoDialog">
<el-icon>
<component :is="Link" />
</el-icon>
<span>输入商品信息</span>
</div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover" @click="openProductImageDialog">
<el-icon>
<component :is="Link" />
</el-icon>
<span>添加商品图</span>
</div>
</div>
</el-popover>
<el-popover placement="bottom" :width="150" trigger="hover" :hide-after="0">
<template #reference>
<el-button class="m-2 h-30 border-none px-6">
<el-icon>
<component :is="VideoCamera" />
</el-icon>
<span>参考</span>
</el-button>
</template>
<div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Clock" />
</el-icon>
<span>历史投放素材</span>
</div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Upload" />
</el-icon>
<span>本地上传</span>
</div>
</div>
</el-popover>
<el-popover placement="bottom" :width="120" trigger="hover" popper-class="agent-prompt-tools-popper" :hide-after="0">
<template #reference>
<el-button class="m-2 h-30 border-none px-6">
<el-icon>
<component :is="Document" />
</el-icon>
<span>脚本/原料</span>
</el-button>
</template>
<div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Document" />
</el-icon>
<span>添加脚本</span>
</div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Film" />
</el-icon>
<span>添加原料</span>
</div>
</div>
</el-popover>
<el-popover placement="bottom" :width="378" trigger="click" popper-class="agent-param-popper" :hide-after="0">
<template #reference>
<el-button class="flex-center gap-6 h-30 border-none px-6">
<el-icon>
<component :is="Setting" />
</el-icon>
<span class="mr-6">生成参数</span>
<el-icon size="12">
<component :is="ArrowDownBold" />
</el-icon>
</el-button>
</template>
<div class="param-panel p-12 round-4 bg-fff">
<div class="switch-row flex h-36 p-4 round-4 mb-8">
<el-button v-for="item in mediaTypes" :key="item.value"
class="switch-btn flex-1 h-28 round-4 border-none fs-14"
:class="{ active: mediaType === item.value }" text @click="setMediaType(item.value)">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
</el-button>
</div>
<div class="switch-row flex h-36 p-4 round-4">
<el-button v-for="item in paramTabs" :key="item.value"
class="switch-btn flex-1 h-28 round-4 border-none fs-14"
:class="{ active: paramTab === item.value }" text @click="setParamTab(item.value)">
{{ item.label }}
</el-button>
</div>
<template v-if="mediaType === 'image' && paramTab === 'frame'">
<div class="flex-between mt-14 mb-10">
<span class="fs-12 color-888">比例</span>
<span class="fs-12 color-000">{{ imageRatio }}</span>
</div>
<div class="option-list flex flex-wrap gap-6">
<div v-for="item in imageRatios" :key="item.value"
class="option-btn flex-center gap-6 h-34 px-12 round-6 pointer fs-12"
:class="{ active: imageRatio === item.value }" text @click="imageRatio = item.value">
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span v-else class="ratio-icon round-2" :class="item.iconClass"></span>
<span>{{ item.label }}</span>
</div>
</div>
<el-button class="custom-size flex-center gap-8 w-full h-34 mt-8 round-6 bg-fff fs-12">
<el-icon>
<FullScreen />
</el-icon>
<span>自定义像素尺寸</span>
</el-button>
</template>
<template v-if="mediaType === 'image' && paramTab === 'output'">
<div class="fs-12 color-888 mt-14 mb-10">画质</div>
<div class="option-list flex gap-6">
<div v-for="item in qualities" :key="item"
class="option-btn flex-center flex-1 fs-12 pointer h-34 round-6 border-none"
:class="{ active: imageQuality === item }" text @click="imageQuality = item">
{{ item }}
</div>
</div>
<div class="fs-12 color-888 mt-14 mb-10">数量</div>
<div class="option-list flex gap-6">
<div v-for="item in quantities" :key="item"
class="option-btn flex-1 h-34 round-6 flex-center fs-12 pointer"
:class="{ active: imageQuantity === item }" text @click="imageQuantity = item">
{{ item }}
</div>
</div>
</template>
<template v-if="mediaType === 'video' && paramTab === 'frame'">
<div class="fs-12 color-888 mt-14 mb-10">参考方式</div>
<div class="option-list flex gap-6">
<div v-for="item in referenceModes" :key="item"
class="option-btn flex-1 h-34 round-6 flex-center pointer fs-12"
:class="{ active: referenceMode === item }" text @click="referenceMode = item">
{{ item }}
</div>
</div>
<div class="flex-between mt-14 mb-10">
<span class="fs-12 color-888">比例</span>
<span class="fs-12 color-888">智能</span>
</div>
<div class="option-list flex flex-wrap gap-6">
<div v-for="item in videoRatios" :key="item.value"
class="option-btn flex-center gap-6 h-34 px-12 round-6 flex-center pointer fs-12"
:class="{ active: videoRatio === item.value }" text @click="videoRatio = item.value">
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span v-else class="ratio-icon round-2" :class="item.iconClass"></span>
<span>{{ item.label }}</span>
</div>
</div>
<el-button class="custom-size flex-center gap-8 w-full h-34 mt-8 round-10 bg-fff">
<el-icon>
<FullScreen />
</el-icon>
<span>自定义像素尺寸</span>
</el-button>
</template>
<template v-if="mediaType === 'video' && paramTab === 'output'">
<div class="fs-12 color-888 mt-14 mb-10">清晰度</div>
<div class="option-list flex gap-6">
<div v-for="item in resolutions" :key="item"
class="option-btn flex-1 h-32 round-6 flex-center pointer fs-12"
:class="{ active: resolution === item }" text @click="resolution = item">
{{ item }}
</div>
</div>
<div class="custom-input flex-between h-32 px-10 mt-6 round-6">
<el-input v-model="customResolution" class="param-input flex-1"
placeholder="自定义,例如 2160 或 4K" />
<span class="fs-12 color-888">建议值可直接选择</span>
</div>
<div class="fs-12 color-888 mt-14 mb-10">数量</div>
<div class="option-list flex gap-6">
<div v-for="item in quantities" :key="item"
class="option-btn flex-1 h-32 round-4 flex-center pointer fs-12"
:class="{ active: videoQuantity === item }" text @click="videoQuantity = item">
{{ item }}
</div>
<el-input v-model="customDuration" class="w-96" placeholder="自定义" />
</div>
<div class="fs-12 color-888 mt-14 mb-10">时长</div>
<div class="option-list flex gap-6">
<div v-for="item in durations" :key="item"
class="option-btn flex-1 h-32 round-6 flex-center pointer fs-12"
:class="{ active: duration === item }" text @click="duration = item">
{{ item }}
</div>
<div class="duration-input flex-items-center h-32 round-6 pl-10">
<el-input v-model="customDuration" class="param-input w-96" placeholder="自定义" />
<span class="unit flex-center h-full px-10 fs-12"></span>
</div>
</div>
<div class="switch-setting flex-between h-44 px-14 mt-16 round-6">
<span class="fs-12 color-888">生成声音</span>
<el-switch v-model="withVoice" />
<span class="fs-12 color-888 ml-18">添加水印</span>
<el-switch v-model="withWatermark" />
</div>
</template>
</div>
</el-popover>
<el-popover placement="bottom" :width="232" trigger="click" popper-class="agent-style-popper" :hide-after="0">
<template #reference>
<el-button class="flex-center gap-6 h-30 border-none px-6">
<el-icon>
<component :is="Position" />
</el-icon>
<span class="mr-6">风格</span>
<el-icon size="12">
<component :is="ArrowDownBold" />
</el-icon>
</el-button>
</template>
<div class="flex bg-fff hidden">
<div class="style-tabs w-100 py-8">
<div v-for="item in styleTabs" :key="item.value" class="flex-between h-38 px-12 fs-14 pointer"
:class="{ active: activeStyleTab === item.value }" @click="activeStyleTab = item.value">
<span>{{ item.label }}-{{ item.count }}</span>
<el-icon v-if="activeStyleTab === item.value" size="12">
<ArrowRightBold />
</el-icon>
</div>
</div>
<div class="style-options flex-1 py-8 over-y-auto">
<el-checkbox-group v-model="currentStyleChecked" class="flex-col">
<el-checkbox v-for="item in currentStyleOptions" :key="item.value" :value="item.value"
class="style-check flex-items-center h-42 px-16">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</el-popover>
<AgentProductInfoDialog v-model="productInfoVisible" />
<AgentProductImageDialog v-model="productImageVisible" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, type Component } from 'vue';
import {
ArrowDownBold,
ArrowRightBold,
Box,
Clock,
Document,
Film,
FullScreen,
Link,
MagicStick,
Picture,
Position,
Setting,
Upload,
VideoCamera,
} from '@element-plus/icons-vue';
import AgentProductImageDialog from './AgentProductImageDialog.vue';
import AgentProductInfoDialog from './AgentProductInfoDialog.vue';
type MediaType = 'image' | 'video';
type ParamTab = 'frame' | 'output';
type StyleTab = 'style' | 'element';
type SwitchOption<T extends string = string> = {
label: string;
value: T;
icon?: Component;
};
type IconSwitchOption<T extends string = string> = SwitchOption<T> & {
icon: Component;
};
type RatioOption = SwitchOption & {
iconClass?: string;
};
const mediaType = ref<MediaType>('image');
const paramTab = ref<ParamTab>('frame');
const imageRatio = ref('smart');
const videoRatio = ref('smart');
const imageQuality = ref('智能');
const imageQuantity = ref('1份');
const videoQuantity = ref('1份');
const referenceMode = ref('智能参考');
const resolution = ref('智能');
const duration = ref('5秒');
const customResolution = ref('');
const customDuration = ref('');
const withVoice = ref(true);
const withWatermark = ref(false);
const productPopoverVisible = ref(false);
const productInfoVisible = ref(false);
const productImageVisible = ref(false);
const activeStyleTab = ref<StyleTab>('style');
const checkedStyleValues = ref(['english', 'revenge']);
const checkedElementValues = ref(['contrast']);
const setMediaType = (value: MediaType) => {
mediaType.value = value;
};
const setParamTab = (value: ParamTab) => {
paramTab.value = value;
};
const openProductInfoDialog = () => {
productPopoverVisible.value = false;
productInfoVisible.value = true;
};
const openProductImageDialog = () => {
productPopoverVisible.value = false;
productImageVisible.value = true;
};
const mediaTypes: IconSwitchOption<MediaType>[] = [
{ label: '图片', value: 'image', icon: Picture },
{ label: '视频', value: 'video', icon: VideoCamera },
];
const paramTabs: SwitchOption<ParamTab>[] = [
{ label: '画面', value: 'frame' },
{ label: '输出', value: 'output' },
];
const imageRatios: RatioOption[] = [
{ label: '智能', value: 'smart', icon: MagicStick },
{ label: '1:1', value: '1:1', iconClass: 'square' },
{ label: '16:9', value: '16:9', iconClass: 'wide' },
{ label: '4:3', value: '4:3', iconClass: 'standard' },
{ label: '3:2', value: '3:2', iconClass: 'wide' },
{ label: '2:3', value: '2:3', iconClass: 'portrait' },
{ label: '3:4', value: '3:4', iconClass: 'portrait' },
{ label: '9:16', value: '9:16', iconClass: 'phone' },
];
const videoRatios: RatioOption[] = [
{ label: '智能', value: 'smart', icon: MagicStick },
{ label: '21:9', value: '21:9', iconClass: 'cinema' },
{ label: '16:9', value: '16:9', iconClass: 'wide' },
{ label: '4:3', value: '4:3', iconClass: 'standard' },
{ label: '1:1', value: '1:1', iconClass: 'square' },
{ label: '3:4', value: '3:4', iconClass: 'portrait' },
{ label: '9:16', value: '9:16', iconClass: 'phone' },
];
const qualities = ['智能', '高', '中', '低'];
const quantities = ['1份', '2份', '3份', '4份'];
const referenceModes = ['智能参考', '首帧', '首尾帧'];
const resolutions = ['智能', '480P', '720P', '1080P'];
const durations = ['5s', '10s'];
const styleOptions = [
{ label: '英文虐渣', value: 'english' },
{ label: '打脸复仇', value: 'revenge' },
{ label: '强势碾压', value: 'power' },
{ label: '沙雕搞笑', value: 'funny' },
{ label: '幽默风趣', value: 'humor' },
{ label: '玩梗搞笑', value: 'meme' },
];
const elementOptions = [
{ label: '痛点对比', value: 'contrast' },
{ label: '使用过程', value: 'process' },
{ label: '产品展示', value: 'product' },
];
const styleTabs = computed(() => [
{ label: '风格', value: 'style' as const, count: checkedStyleValues.value.length },
{ label: '元素', value: 'element' as const, count: checkedElementValues.value.length },
]);
const currentStyleOptions = computed(() => activeStyleTab.value === 'style' ? styleOptions : elementOptions);
const currentStyleChecked = computed({
get: () => activeStyleTab.value === 'style' ? checkedStyleValues.value : checkedElementValues.value,
set: (value: string[]) => {
if (activeStyleTab.value === 'style') {
checkedStyleValues.value = value;
return;
}
checkedElementValues.value = value;
},
});
</script>
<style scoped lang="scss">
.hover {
&:hover {
color: rgba(var(--main-color), 0.8);
}
}
.param-panel {
box-shadow: 0 8px 24px rgba(23, 38, 64, .12);
}
.switch-row,
.option-btn,
.switch-setting {
background: #f1f3f5;
}
.switch-btn {
color: #1f2f45;
}
.switch-btn.active {
color: #1f2f45;
background: #fff;
box-shadow: 0 1px 6px rgba(12, 24, 44, .14);
}
.option-btn {
color: #1f2f45;
}
.option-btn.active {
color: #1f2f45;
background: #e8f0f4;
}
.option-list .option-btn {
flex: 1 0 calc((100% - 24px) / 5);
}
.custom-size {
border: 1px dashed #d9e4f2;
color: #1f2f45;
}
.ratio-icon {
display: inline-block;
border: 1px solid currentColor;
}
.ratio-icon.square {
width: 12px;
height: 12px;
}
.ratio-icon.wide {
width: 16px;
height: 9px;
}
.ratio-icon.standard {
width: 14px;
height: 10px;
}
.ratio-icon.portrait {
width: 9px;
height: 14px;
}
.ratio-icon.phone {
width: 8px;
height: 16px;
}
.ratio-icon.cinema {
width: 18px;
height: 8px;
}
.custom-input,
.duration-input {
border: 1px solid #dfe7f2;
background: #fff;
}
:deep(.param-input .el-input__wrapper) {
padding: 0;
box-shadow: none;
background: transparent;
}
:deep(.param-input .el-input__inner) {
height: 30px;
font-size: 12px;
}
.unit {
border-left: 1px solid #dfe7f2;
color: #1f2f45;
}
.style-tabs {
border-right: 1px solid #eee;
}
.style-options {
max-height: 268px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #6a6a6a;
border-radius: 6px;
}
}
.style-check {
margin-right: 0;
&:hover {
background: #eee;
}
}
:deep(.style-check .el-checkbox__label) {
font-size: 14px;
}
</style>
<style>
.agent-prompt-tools-popper {
min-width: 120px !important;
}
.agent-param-popper {
padding: 0 !important;
border: 0 !important;
border-radius: 6px !important;
overflow: hidden;
}
.agent-style-popper {
padding: 0 !important;
/* border: 0 !important; */
border-radius: 6px !important;
overflow: hidden;
background: transparent !important;
}
</style>
<template> <template>
<div class="h-full bgmr relative hidden"> <div class="h-full bgmr relative hidden">
<section v-if="hasMessages"> <div v-if="hasMessages" class="fixed top-20 pl-20 flex-items-center gap-6 pointer" @click="hasMessages = false">
<el-icon class="fs-16">
<Back />
</el-icon>
<span class="fs-12 fw-600">返回</span>
</div>
<section v-if="!hasMessages">
<div class="mt-60 flex-col items-center"> <div class="mt-60 flex-col items-center">
<div class="flex-center wh-44 round-8 color-fff bg-jb"> <div class="flex-center wh-44 round-8 color-fff bg-jb">
<el-icon class="fs-24"> <el-icon class="fs-24">
...@@ -12,93 +18,8 @@ ...@@ -12,93 +18,8 @@
<div class="flex-col bg-fff round-16 model2 p-20 boxshaw"> <div class="flex-col bg-fff round-16 model2 p-20 boxshaw">
<el-input v-model="prompt" :rows="10" type="textarea" resize="none" placeholder="发送给 Agent" /> <el-input v-model="prompt" :rows="10" type="textarea" resize="none" placeholder="发送给 Agent" />
<div class="flex-between pt-16"> <div class="flex-between pt-16">
<div class="flex-items-center flex-wrap"> <AgentPromptTools />
<el-popover placement="top-start" :width="150" trigger="hover"> <div class="border-eee flex bg-fff round-100 p-6 gap-10 pl-16 minw-110 pointer hoverbth" @click="hasMessages = true">
<template #reference>
<el-button class="m-2 h-30 border-none px-6">
<el-icon>
<component :is="Box" />
</el-icon>
<span>商品</span>
</el-button>
</template>
<div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Link" />
</el-icon>
<span>输入商品信息</span>
</div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Link" />
</el-icon>
<span>添加商品图</span>
</div>
</div>
</el-popover>
<el-popover placement="top-start" :width="150" trigger="hover">
<template #reference>
<el-button class="m-2 h-30 border-none px-6">
<el-icon>
<component :is="VideoCamera" />
</el-icon>
<span>参考</span>
</el-button>
</template>
<div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Clock" />
</el-icon>
<span>历史投放素材</span>
</div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Upload" />
</el-icon>
<span>本地上传</span>
</div>
</div>
</el-popover>
<el-popover placement="top-start" :width="120" trigger="hover" popper-class="xxx">
<template #reference>
<el-button class="m-2 h-30 border-none px-6">
<el-icon>
<component :is="Document" />
</el-icon>
<span>脚本/原料</span>
</el-button>
</template>
<div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Document" />
</el-icon>
<span>添加脚本</span>
</div>
<div class="flex-items-center gap-6 h-30 border-none pointer hover">
<el-icon>
<component :is="Film" />
</el-icon>
<span>添加原料</span>
</div>
</div>
</el-popover>
<el-button class="flex-center gap-6 h-30 border-none px-6">
<el-icon>
<component :is="Setting" />
</el-icon>
<span>45秒 · 9:16</span>
</el-button>
<el-button class="flex-center gap-6 h-30 border-none px-6">
<el-icon>
<component :is="Position" />
</el-icon>
<span>风格</span>
</el-button>
</div>
<div class="border-eee flex bg-fff round-100 p-6 gap-10 pl-16 minw-110 pointer hoverbth">
开始创作 开始创作
<div class="wh-24 color-fff round flex-center bg-jb"> <div class="wh-24 color-fff round flex-center bg-jb">
<el-icon> <el-icon>
...@@ -161,25 +82,17 @@ ...@@ -161,25 +82,17 @@
<span>回到底部</span> <span>回到底部</span>
</el-button> </el-button>
</div> </div>
<div class="composer-box flex-col bg-fff round-16 border-eee boxshaw"> <div ref="composerBoxRef" class="composer-box flex-col bg-fff round-16 border-eee boxshaw"
@focusin="handleComposerActive" @pointerdown="handleComposerActive">
<div class="input-row flex gap-20 px-14 pt-14"> <div class="input-row flex gap-20 px-14 pt-14">
<UploadFile :width="isComposerCompact ? 40 : 44" /> <UploadFile :width="isComposerCompact ? 40 : 44" />
<el-input v-model="prompt" class="flex-1 boxno" type="textarea" resize="none" :rows="3" <el-input v-model="prompt" class="flex-1 boxno" type="textarea" resize="none" :rows="3"
:placeholder="isComposerCompact ? '输入你的创作想法' : '输入你的创作想法、脚本或画面要求'" :placeholder="isComposerCompact ? '输入你的创作想法' : '输入你的创作想法、脚本或画面要求'" @focus="handleInputFocus"
@focus="handleInputFocus" @blur="handleInputBlur" /> @blur="handleInputBlur" />
</div> </div>
<div class="composer-actions flex-between px-14"> <div class="composer-actions flex-between px-14">
<div class="flex-items-center flex-wrap"> <div class="flex-items-center flex-wrap">
<el-button v-for="item in agentControls" :key="item.label" <AgentPromptTools />
class="flex-center gap-6 h-32 px-12 fs-12 round-6">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.label }}</span>
<el-icon v-if="item.hasArrow">
<ArrowDown />
</el-icon>
</el-button>
</div> </div>
<div class="send-btn flex-center wh-40 p-0 color-fff pointer border-none round fs-20 mb-10 bg-jb" <div class="send-btn flex-center wh-40 p-0 color-fff pointer border-none round fs-20 mb-10 bg-jb"
@click="sendMessage"> @click="sendMessage">
...@@ -195,10 +108,11 @@ ...@@ -195,10 +108,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import type { Component } from 'vue'; import type { Component } from 'vue';
import { ArrowDown, Box, Coin, CopyDocument, Document, Link, MagicStick, Position, Setting, Top, VideoCamera, Clock, Upload, Film } from '@element-plus/icons-vue'; import { ArrowDown, Back, Box, Coin, CopyDocument, Link, MagicStick, Position, Top } from '@element-plus/icons-vue';
import UploadFile from '@/components/Upload/index.vue' import UploadFile from '@/components/Upload/index.vue'
import AgentPromptTools from './components/AgentPromptTools.vue';
type ToolItem = { type ToolItem = {
label: string; label: string;
...@@ -209,11 +123,13 @@ type ToolItem = { ...@@ -209,11 +123,13 @@ type ToolItem = {
const prompt = ref(''); const prompt = ref('');
const submittedTopic = ref('美颜'); const submittedTopic = ref('美颜');
const messagesCount = ref(0); const messagesCount = ref(0);
const hasMessages = computed(() => messagesCount.value > 0); const hasMessages = ref(false)
const chatScrollRef = ref<HTMLElement>(); const chatScrollRef = ref<HTMLElement>();
const composerBoxRef = ref<HTMLElement>();
const isInputFocused = ref(false); const isInputFocused = ref(false);
const isComposerActive = ref(false);
const isNearBottom = ref(true); const isNearBottom = ref(true);
const isComposerCompact = computed(() => !isNearBottom.value && !isInputFocused.value); const isComposerCompact = computed(() => !isNearBottom.value && !isInputFocused.value && !isComposerActive.value);
const agentControls: ToolItem[] = [ const agentControls: ToolItem[] = [
{ label: 'Agent 模式', icon: Position, hasArrow: true }, { label: 'Agent 模式', icon: Position, hasArrow: true },
...@@ -268,9 +184,27 @@ const handleInputBlur = () => { ...@@ -268,9 +184,27 @@ const handleInputBlur = () => {
nextTick(handleChatScroll); nextTick(handleChatScroll);
}; };
const handleComposerActive = () => {
isComposerActive.value = true;
};
const handleDocumentPointerDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof Node)) {
return;
}
isComposerActive.value = Boolean(composerBoxRef.value?.contains(target));
nextTick(handleChatScroll);
};
onMounted(() => { onMounted(() => {
document.addEventListener('pointerdown', handleDocumentPointerDown);
nextTick(handleChatScroll); nextTick(handleChatScroll);
}); });
onBeforeUnmount(() => {
document.removeEventListener('pointerdown', handleDocumentPointerDown);
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
...@@ -289,12 +223,6 @@ onMounted(() => { ...@@ -289,12 +223,6 @@ onMounted(() => {
} }
} }
.hover {
&:hover {
color: rgba(var(--main-color), 0.8);
}
}
.boxshaw { .boxshaw {
box-shadow: 0 0 10px #00000009; box-shadow: 0 0 10px #00000009;
...@@ -384,7 +312,7 @@ onMounted(() => { ...@@ -384,7 +312,7 @@ onMounted(() => {
padding: 0 8px 0 0; padding: 0 8px 0 0;
} }
.chat-composer-wrap.is-compact .composer-actions > .flex-items-center:first-child { .chat-composer-wrap.is-compact .composer-actions>.flex-items-center:first-child {
display: none; display: none;
} }
...@@ -406,13 +334,9 @@ onMounted(() => { ...@@ -406,13 +334,9 @@ onMounted(() => {
.send-btn { .send-btn {
opacity: 0.8; opacity: 0.8;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
} }
</style> </style>
<style>
.xxx {
min-width: 120px !important;
}
</style>
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<div v-if="!isCheck" class="absolute top-0 left-0 bg-main w-full h-full video-main"> <div v-if="!isCheck" class="absolute top-0 left-0 bg-main w-full h-full video-main">
<video src="../../../../../public/aaa.mp4" height="100%" controls autoplay disablePictureInPicture></video> <video src="/aaa.mp4" height="100%" controls autoplay disablePictureInPicture></video>
<div class="absolute top-10 right-10 flex gap-4"> <div class="absolute top-10 right-10 flex gap-4">
<div class="bg-333 w-24 h-24 round-6 pointer"></div> <div class="bg-333 w-24 h-24 round-6 pointer"></div>
<div class="bg-333 w-24 h-24 round-6 pointer"></div> <div class="bg-333 w-24 h-24 round-6 pointer"></div>
...@@ -79,4 +79,4 @@ const props = defineProps({ ...@@ -79,4 +79,4 @@ const props = defineProps({
.round-bl { .round-bl {
border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;
} }
</style> </style>
\ No newline at end of file
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
"lib": ["ES2020", "DOM"], "lib": ["ES2020", "DOM"],
"skipLibCheck": true, "skipLibCheck": true,
"baseUrl": ".", "baseUrl": ".",
"ignoreDeprecations": "6.0", "ignoreDeprecations": "5.0",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
......
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