视频贴片相关
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<div class="patch-template-editor">
|
||||
<el-divider content-position="left">贴片模板</el-divider>
|
||||
|
||||
<div v-for="(template, index) in modelValue" :key="index" class="template-card">
|
||||
<div class="template-card-header">
|
||||
<span class="template-card-title">模板 #{{ index + 1 }}</span>
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="removeTemplate(index)" />
|
||||
</div>
|
||||
|
||||
<div class="template-card-body">
|
||||
<!-- 模板文件上传 -->
|
||||
<div class="template-field-row">
|
||||
<span class="field-label">模板文件</span>
|
||||
<div class="field-control">
|
||||
<el-upload
|
||||
:key="`upload-${index}`"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="(file: any) => handleTemplateUpload(index, file)"
|
||||
>
|
||||
<el-button size="small" type="primary" :disabled="!!template.url">
|
||||
<el-icon><Upload /></el-icon> 上传模板
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button size="small" @click="handleDownload">
|
||||
<el-icon><Download /></el-icon> 下载参考模板
|
||||
</el-button>
|
||||
<el-tag v-if="template.url" type="success" size="small" class="uploaded-tag">已上传</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 起始/结束位置 -->
|
||||
<div class="template-field-row">
|
||||
<span class="field-label">开始结束时间</span>
|
||||
<div class="field-control field-control-inline">
|
||||
<el-input-number v-model="template.start" :min="0" :max="99999" size="small" controls-position="right" class="inline-number" />
|
||||
<span class="inline-separator">~</span>
|
||||
<el-input-number v-model="template.end" :min="0" :max="99999" size="small" controls-position="right" class="inline-number" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<div class="template-field-row">
|
||||
<span class="field-label">标题</span>
|
||||
<div class="field-control">
|
||||
<el-input v-model="template.data.title" placeholder="可选填,如果没有内容将会没有贴片信息" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 产品图片(动态追加) -->
|
||||
<div class="template-field-row template-field-row-vertical">
|
||||
<span class="field-label">图片</span>
|
||||
<div class="field-control field-control-images">
|
||||
<div v-for="(img, imgIdx) in getProductImages(template)" :key="imgIdx" class="image-item">
|
||||
<span class="image-index">#{{ imgIdx + 1 }}</span>
|
||||
<div v-if="img.url" class="image-preview-wrapper">
|
||||
<img :src="img.url" class="image-preview" @click="previewImage(img.url)" />
|
||||
<el-button class="image-remove-btn" type="danger" :icon="Close" circle size="small" @click="removeProductImage(template, imgIdx)" />
|
||||
</div>
|
||||
<el-upload
|
||||
v-else
|
||||
:key="`img-${index}-${imgIdx}`"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:accept="'image/*'"
|
||||
:on-change="(file: any) => handleImageUpload(index, imgIdx, file)"
|
||||
>
|
||||
<el-button size="small" type="primary" class="image-upload-btn">
|
||||
<el-icon><Plus /></el-icon> 上传图片
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<el-button size="small" class="add-image-btn" @click="addProductImage(template)">
|
||||
<el-icon><Plus /></el-icon> 添加图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 产品描述 -->
|
||||
<div class="template-field-row template-field-row-vertical">
|
||||
<span class="field-label">产品描述</span>
|
||||
<div class="field-control">
|
||||
<el-input
|
||||
v-model="template.data.product_desc"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="可选填,如果没有内容将会没有贴片信息"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 产品信息 -->
|
||||
<div class="template-field-row template-field-row-vertical">
|
||||
<span class="field-label">产品信息</span>
|
||||
<div class="field-control">
|
||||
<el-input
|
||||
v-model="template.data.product_info"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="可选填,如果没有内容将会没有贴片信息"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 免责声明 -->
|
||||
<div class="template-field-row template-field-row-vertical">
|
||||
<span class="field-label">免责声明</span>
|
||||
<div class="field-control">
|
||||
<el-input v-model="template.data.disclaimer" type="textarea" :rows="2" placeholder="可选填,如果没有内容将会没有贴片信息" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加模板按钮 -->
|
||||
<el-button type="primary" :icon="Plus" class="add-template-btn" @click="addTemplate"> 添加模板 </el-button>
|
||||
|
||||
<!-- 图片预览弹窗 -->
|
||||
<el-image-viewer v-if="previewVisible" :url-list="[previewUrl]" @close="previewVisible = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Plus, Delete, Close, Upload, Download } from '@element-plus/icons-vue';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export interface PatchTemplate {
|
||||
url: string;
|
||||
start: number;
|
||||
end: number;
|
||||
data: {
|
||||
title: string;
|
||||
product_desc: string;
|
||||
product_info: string;
|
||||
disclaimer: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取模板中所有产品图片(按 product_image_N 顺序,含已添加的空槽位) */
|
||||
function getProductImages(template: PatchTemplate): { key: string; url: string }[] {
|
||||
const images: { key: string; url: string }[] = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const key = `product_image_${i}`;
|
||||
if (key in template.data) {
|
||||
images.push({ key, url: template.data[key] || '' });
|
||||
}
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
/** 获取下一个可用的 product_image_N key */
|
||||
function nextImageKey(template: PatchTemplate): string | null {
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const key = `product_image_${i}`;
|
||||
if (!template.data[key]) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: PatchTemplate[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: PatchTemplate[]): void;
|
||||
}>();
|
||||
|
||||
// 图片预览
|
||||
const previewVisible = ref(false);
|
||||
const previewUrl = ref('');
|
||||
|
||||
const previewImage = (url: string) => {
|
||||
previewUrl.value = url;
|
||||
previewVisible.value = true;
|
||||
};
|
||||
|
||||
const updateTemplates = (newTemplates: PatchTemplate[]) => {
|
||||
emit('update:modelValue', [...newTemplates]);
|
||||
};
|
||||
|
||||
const addTemplate = () => {
|
||||
const newTemplate: PatchTemplate = {
|
||||
url: '',
|
||||
start: 0,
|
||||
end: 0,
|
||||
data: {
|
||||
title: '',
|
||||
product_image_1: '',
|
||||
product_desc: '',
|
||||
product_info: '',
|
||||
disclaimer: '',
|
||||
},
|
||||
};
|
||||
updateTemplates([...props.modelValue, newTemplate]);
|
||||
};
|
||||
|
||||
const removeTemplate = (index: number) => {
|
||||
const newList = props.modelValue.filter((_, i) => i !== index);
|
||||
updateTemplates(newList);
|
||||
};
|
||||
|
||||
const addProductImage = (template: PatchTemplate) => {
|
||||
const key = nextImageKey(template);
|
||||
if (!key) {
|
||||
ElMessage.warning('产品图片最多 20 张');
|
||||
return;
|
||||
}
|
||||
template.data[key] = '';
|
||||
updateTemplates([...props.modelValue]);
|
||||
};
|
||||
|
||||
const removeProductImage = (template: PatchTemplate, imgIdx: number) => {
|
||||
// 删除该图片,并把后面的图片 key 前移
|
||||
const images = getProductImages(template);
|
||||
if (imgIdx >= images.length) return;
|
||||
|
||||
const removedKey = images[imgIdx].key;
|
||||
delete template.data[removedKey];
|
||||
|
||||
// 将后面的图片往前挪
|
||||
for (let i = imgIdx + 1; i < images.length; i++) {
|
||||
const oldKey = images[i].key;
|
||||
const newKey = `product_image_${i}`;
|
||||
if (oldKey in template.data) {
|
||||
template.data[newKey] = template.data[oldKey];
|
||||
if (newKey !== oldKey) {
|
||||
delete template.data[oldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTemplates([...props.modelValue]);
|
||||
};
|
||||
|
||||
const handleTemplateUpload = async (index: number, file: any) => {
|
||||
const raw = file.raw;
|
||||
if (!raw) return;
|
||||
|
||||
try {
|
||||
const uploadRes = await uploadFile(raw, { timeout: 0 });
|
||||
if (!uploadRes?.data?.fileURL) throw new Error('上传失败:未返回文件URL');
|
||||
|
||||
const fileUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
|
||||
|
||||
const newList = [...props.modelValue];
|
||||
newList[index].url = fileUrl;
|
||||
updateTemplates(newList);
|
||||
|
||||
ElMessage.success('模板上传成功');
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '模板上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageUpload = async (templateIndex: number, imgIdx: number, file: any) => {
|
||||
const raw = file.raw;
|
||||
if (!raw) return;
|
||||
|
||||
try {
|
||||
const uploadRes = await uploadFile(raw, { timeout: 0 });
|
||||
if (!uploadRes?.data?.fileURL) throw new Error('上传失败:未返回文件URL');
|
||||
|
||||
const fileUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
|
||||
|
||||
const newList = [...props.modelValue];
|
||||
const images = getProductImages(newList[templateIndex]);
|
||||
if (imgIdx < images.length) {
|
||||
newList[templateIndex].data[images[imgIdx].key] = fileUrl;
|
||||
}
|
||||
updateTemplates(newList);
|
||||
|
||||
ElMessage.success('图片上传成功');
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '图片上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 下载参考模板文件
|
||||
const handleDownload = (_e?: Event) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = '/template_example.html';
|
||||
a.download = 'template_example.html';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.patch-template-editor {
|
||||
width: 100%;
|
||||
|
||||
.template-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.template-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f1f5f9;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
.template-card-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
}
|
||||
}
|
||||
|
||||
.template-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.template-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
gap: 8px;
|
||||
|
||||
&.template-field-row-vertical {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
min-width: 70px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.field-control {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.field-control-inline {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.inline-number {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.inline-separator {
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.uploaded-tag {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.field-control-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.image-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.image-index {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.image-preview-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.image-preview {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.image-remove-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-upload-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-image-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-template-btn {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user