This commit is contained in:
2026-05-31 12:09:14 +08:00
commit e4f9eedb80
1121 changed files with 209380 additions and 0 deletions

View File

@@ -0,0 +1,779 @@
<template>
<a-page-header title="分销海报设置" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '24px' }">
<div class="poster-container">
<!-- 说明信息 -->
<a-alert
message="分销商海报设置说明"
type="info"
show-icon
class="poster-alert"
>
<template #description>
<div>
<p>1. 可根据需要二维码昵称等动态位置</p>
<p>2. 保存后将海报生成新图片这里一般不变更清除图片缓存</p>
</div>
</template>
</a-alert>
<div class="poster-content">
<!-- 左侧海报预览 -->
<div class="poster-preview">
<div class="poster-canvas" ref="posterCanvasRef">
<img
:src="posterConfig.backgroundImage || defaultBackground"
alt="海报背景"
class="poster-bg"
@load="onBackgroundLoad"
/>
<!-- 动态元素层 -->
<div class="poster-elements">
<!-- 头像 -->
<div
class="poster-avatar draggable-element"
:style="getElementStyle('avatar')"
v-if="posterConfig.showAvatar"
@mousedown="startDrag($event, 'avatar')"
>
<img
:src="posterConfig.avatarUrl || defaultAvatar"
alt="头像"
/>
<div class="element-handle">拖拽</div>
</div>
<!-- 昵称 -->
<div
class="poster-nickname draggable-element"
:style="getElementStyle('nickname')"
v-if="posterConfig.showNickname"
@mousedown="startDrag($event, 'nickname')"
>
{{ posterConfig.nickname || '这里是昵称' }}
<div class="element-handle">拖拽</div>
</div>
<!-- 二维码 -->
<div
class="poster-qrcode draggable-element"
:style="getElementStyle('qrcode')"
v-if="posterConfig.showQrcode"
@mousedown="startDrag($event, 'qrcode')"
>
<img
:src="posterConfig.qrcodeUrl || defaultQrcode"
alt="二维码"
/>
<div class="element-handle">拖拽</div>
</div>
</div>
</div>
</div>
<!-- 右侧设置面板 -->
<div class="poster-settings">
<!-- 预设模板 -->
<div class="setting-section">
<h4>预设模板</h4>
<div class="template-grid">
<div
v-for="template in templates"
:key="template.id"
class="template-item"
:class="{ active: currentTemplate === template.id }"
@click="applyTemplate(template)"
>
<img :src="template.preview" :alt="template.name" />
<span>{{ template.name }}</span>
</div>
</div>
</div>
<!-- 背景图片设置 -->
<div class="setting-section">
<h4>背景图片</h4>
<div class="background-preview">
<img
:src="posterConfig.backgroundImage || defaultBackground"
alt="背景预览"
/>
</div>
<a-upload
:file-list="backgroundFileList"
list-type="picture"
:max-count="1"
@change="handleBackgroundChange"
:before-upload="beforeUpload"
>
<a-button> <UploadOutlined /> 选择背景图片 </a-button>
</a-upload>
<div class="setting-desc">
图片尺寸宽750像素 高1200<br />
请务必按尺寸上传否则生成的海报会变形
</div>
</div>
<!-- 头像设置 -->
<div class="setting-section">
<h4>头像设置</h4>
<div class="setting-row">
<span>头像宽度</span>
<a-input-number
v-model:value="posterConfig.avatarWidth"
:min="20"
:max="200"
@change="updatePreview"
/>
</div>
<div class="setting-row">
<span>头像样式</span>
<a-radio-group
v-model:value="posterConfig.avatarShape"
@change="updatePreview"
>
<a-radio value="circle">圆形</a-radio>
<a-radio value="square">方形</a-radio>
</a-radio-group>
</div>
</div>
<!-- 昵称设置 -->
<div class="setting-section">
<h4>昵称字体大小</h4>
<a-input-number
v-model:value="posterConfig.nicknameFontSize"
:min="12"
:max="48"
@change="updatePreview"
/>
</div>
<!-- 昵称颜色设置 -->
<div class="setting-section">
<h4>昵称字体颜色</h4>
<div class="color-picker">
<input
type="color"
v-model="posterConfig.nicknameColor"
@change="updatePreview"
/>
<span>{{ posterConfig.nicknameColor }}</span>
</div>
</div>
<!-- 二维码设置 -->
<div class="setting-section">
<h4>二维码宽度</h4>
<a-input-number
v-model:value="posterConfig.qrcodeWidth"
:min="50"
:max="200"
@change="updatePreview"
/>
</div>
<!-- 保存按钮 -->
<div class="setting-footer">
<a-button
type="primary"
size="large"
@click="savePosterConfigData"
:loading="saving"
>
保存
</a-button>
</div>
</div>
</div>
</div>
</a-card>
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import {
getCurrentPosterConfig,
savePosterConfig,
uploadPosterBackground
} from '@/api/shop/shopDealerPoster';
import type { PosterConfig } from '@/api/shop/shopDealerPoster/model';
// 海报配置
const posterConfig = reactive({
backgroundImage:
'https://pro2.yiovo.com/assets/store/img/dealer/backdrop.png',
showAvatar: true,
avatarUrl: 'https://pro2.yiovo.com/assets/store/img/dealer/avatar.png',
avatarWidth: 50,
avatarShape: 'circle',
showNickname: true,
nickname: '这里是昵称',
nicknameFontSize: 12,
nicknameColor: '#000000',
showQrcode: true,
qrcodeUrl: '',
qrcodeWidth: 66,
// 元素位置配置
elements: {
avatar: { x: 50, y: 50 },
nickname: { x: 120, y: 65 },
qrcode: { x: 300, y: 500 }
}
});
// 默认资源 - 使用在线图片作为占位符
const defaultBackground =
'https://via.placeholder.com/750x1200/ff6b35/ffffff?text=分享赚钱';
const defaultAvatar =
'https://via.placeholder.com/100x100/4CAF50/ffffff?text=头像';
const defaultQrcode =
'https://via.placeholder.com/100x100/2196F3/ffffff?text=二维码';
// 状态
const saving = ref(false);
const backgroundFileList = ref([]);
const posterCanvasRef = ref();
// 拖拽状态
const dragging = ref(false);
const dragElement = ref('');
const dragStart = ref({ x: 0, y: 0 });
const elementStart = ref({ x: 0, y: 0 });
// 当前模板
const currentTemplate = ref(1);
// 预设模板
const templates = ref([
{
id: 1,
name: '经典模板',
preview: 'https://via.placeholder.com/120x180/ff6b35/ffffff?text=经典',
config: {
backgroundImage:
'https://via.placeholder.com/750x1200/ff6b35/ffffff?text=分享赚钱',
elements: {
avatar: { x: 50, y: 50 },
nickname: { x: 120, y: 65 },
qrcode: { x: 300, y: 500 }
},
avatarWidth: 50,
nicknameFontSize: 16,
nicknameColor: '#ffffff',
qrcodeWidth: 100
}
},
{
id: 2,
name: '简约模板',
preview: 'https://via.placeholder.com/120x180/2196F3/ffffff?text=简约',
config: {
backgroundImage:
'https://via.placeholder.com/750x1200/2196F3/ffffff?text=简约风格',
elements: {
avatar: { x: 325, y: 100 },
nickname: { x: 300, y: 200 },
qrcode: { x: 325, y: 800 }
},
avatarWidth: 80,
nicknameFontSize: 20,
nicknameColor: '#ffffff',
qrcodeWidth: 120
}
},
{
id: 3,
name: '活力模板',
preview: 'https://via.placeholder.com/120x180/4CAF50/ffffff?text=活力',
config: {
backgroundImage:
'https://via.placeholder.com/750x1200/4CAF50/ffffff?text=活力四射',
elements: {
avatar: { x: 100, y: 300 },
nickname: { x: 200, y: 320 },
qrcode: { x: 500, y: 300 }
},
avatarWidth: 60,
nicknameFontSize: 18,
nicknameColor: '#ffffff',
qrcodeWidth: 80
}
}
]);
/* 获取元素样式 */
const getElementStyle = (elementType: string) => {
const element = posterConfig.elements[elementType];
const baseStyle = {
position: 'absolute',
left: `${element.x}px`,
top: `${element.y}px`
};
switch (elementType) {
case 'avatar':
return {
...baseStyle,
width: `${posterConfig.avatarWidth}px`,
height: `${posterConfig.avatarWidth}px`,
borderRadius: posterConfig.avatarShape === 'circle' ? '50%' : '4px',
overflow: 'hidden'
};
case 'nickname':
return {
...baseStyle,
fontSize: `${posterConfig.nicknameFontSize}px`,
color: posterConfig.nicknameColor,
fontWeight: 'bold'
};
case 'qrcode':
return {
...baseStyle,
width: `${posterConfig.qrcodeWidth}px`,
height: `${posterConfig.qrcodeWidth}px`
};
default:
return baseStyle;
}
};
/* 背景图片加载完成 */
const onBackgroundLoad = () => {
updatePreview();
};
/* 更新预览 */
const updatePreview = () => {
// 这里可以添加实时预览更新逻辑
console.log('更新预览');
};
/* 应用模板 */
const applyTemplate = (template: any) => {
currentTemplate.value = template.id;
// 应用模板配置
posterConfig.backgroundImage = template.config.backgroundImage;
posterConfig.elements = { ...template.config.elements };
posterConfig.avatarWidth = template.config.avatarWidth;
posterConfig.nicknameFontSize = template.config.nicknameFontSize;
posterConfig.nicknameColor = template.config.nicknameColor;
posterConfig.qrcodeWidth = template.config.qrcodeWidth;
updatePreview();
message.success(`已应用${template.name}`);
};
/* 开始拖拽 */
const startDrag = (event: MouseEvent, elementType: string) => {
event.preventDefault();
dragging.value = true;
dragElement.value = elementType;
dragStart.value = {
x: event.clientX,
y: event.clientY
};
const element = posterConfig.elements[elementType];
elementStart.value = {
x: element.x,
y: element.y
};
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
};
/* 拖拽中 */
const onDrag = (event: MouseEvent) => {
if (!dragging.value || !dragElement.value) return;
const deltaX = event.clientX - dragStart.value.x;
const deltaY = event.clientY - dragStart.value.y;
// 计算新位置,考虑缩放比例
const scale = 0.4; // 预览区域的缩放比例
const newX = Math.max(
0,
Math.min(750 - 100, elementStart.value.x + deltaX / scale)
);
const newY = Math.max(
0,
Math.min(1200 - 100, elementStart.value.y + deltaY / scale)
);
posterConfig.elements[dragElement.value] = {
x: Math.round(newX),
y: Math.round(newY)
};
};
/* 停止拖拽 */
const stopDrag = () => {
dragging.value = false;
dragElement.value = '';
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
};
/* 背景图片上传前检查 */
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片大小不能超过 5MB!');
return false;
}
return true;
};
/* 背景图片变更 */
const handleBackgroundChange = async (info: any) => {
backgroundFileList.value = info.fileList;
if (info.file.status === 'uploading') {
return;
}
if (info.file.originFileObj) {
try {
const result = await uploadPosterBackground(info.file.originFileObj);
posterConfig.backgroundImage = result.url;
message.success('背景图片上传成功');
updatePreview();
} catch (error) {
console.error('上传失败:', error);
message.error('背景图片上传失败');
}
}
};
/* 保存海报配置 */
const savePosterConfigData = async () => {
saving.value = true;
try {
const configData: PosterConfig = {
backgroundImage: posterConfig.backgroundImage,
width: 750,
height: 1200,
showAvatar: posterConfig.showAvatar,
avatarUrl: posterConfig.avatarUrl,
avatarWidth: posterConfig.avatarWidth,
avatarShape: posterConfig.avatarShape,
showNickname: posterConfig.showNickname,
nickname: posterConfig.nickname,
nicknameFontSize: posterConfig.nicknameFontSize,
nicknameColor: posterConfig.nicknameColor,
showQrcode: posterConfig.showQrcode,
qrcodeUrl: posterConfig.qrcodeUrl,
qrcodeWidth: posterConfig.qrcodeWidth,
elements: posterConfig.elements
};
await savePosterConfig(configData);
message.success('海报配置保存成功');
} catch (error) {
console.error('保存失败:', error);
message.error('保存失败');
} finally {
saving.value = false;
}
};
/* 加载海报配置 */
const loadPosterConfig = async () => {
try {
const config = await getCurrentPosterConfig();
if (config) {
Object.assign(posterConfig, config);
updatePreview();
}
} catch (error) {
console.error('加载配置失败:', error);
// 使用默认配置,不显示错误信息
}
};
onMounted(() => {
loadPosterConfig();
});
</script>
<script lang="ts">
export default {
name: 'ShopDealerPoster'
};
</script>
<style lang="less" scoped>
.poster-container {
max-width: 1400px;
margin: 0 auto;
}
.poster-alert {
margin-bottom: 24px;
:deep(.ant-alert-description) {
p {
margin: 4px 0;
color: #666;
}
}
}
.poster-content {
display: flex;
gap: 32px;
align-items: flex-start;
}
.poster-preview {
flex: 0 0 400px;
.poster-canvas {
position: relative;
width: 300px;
height: 480px;
border: 2px solid #d9d9d9;
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
margin: 0 auto;
.poster-bg {
width: 100%;
height: 100%;
object-fit: cover;
}
.poster-elements {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
.draggable-element {
pointer-events: auto;
cursor: move;
border: 2px dashed transparent;
transition: border-color 0.2s;
&:hover {
border-color: #1890ff;
.element-handle {
opacity: 1;
}
}
.element-handle {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background: #1890ff;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
white-space: nowrap;
}
}
.poster-avatar {
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.poster-nickname {
white-space: nowrap;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
padding: 4px 8px;
background: rgba(255, 255, 255, 0.8);
border-radius: 4px;
}
.poster-qrcode {
background: white;
border-radius: 4px;
padding: 4px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
}
}
.poster-settings {
flex: 1;
.setting-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
h4 {
margin: 0 0 12px 0;
color: #333;
font-weight: 600;
}
.background-preview {
width: 120px;
height: 80px;
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
margin-bottom: 12px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.setting-desc {
color: #999;
font-size: 12px;
margin-top: 8px;
line-height: 1.4;
}
.template-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
.template-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
border: 2px solid #d9d9d9;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #1890ff;
transform: translateY(-2px);
}
&.active {
border-color: #1890ff;
background: #f0f8ff;
}
img {
width: 60px;
height: 90px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 6px;
}
span {
font-size: 12px;
color: #666;
text-align: center;
}
}
}
.setting-row {
display: flex;
align-items: center;
margin-bottom: 12px;
span {
width: 100px;
color: #666;
}
}
.color-picker {
display: flex;
align-items: center;
gap: 12px;
input[type='color'] {
width: 40px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
}
span {
color: #666;
font-family: monospace;
}
}
}
.setting-footer {
text-align: center;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
}
:deep(.ant-upload-list) {
margin-top: 8px;
}
:deep(.ant-input-number) {
width: 120px;
}
:deep(.ant-radio-group) {
.ant-radio-wrapper {
margin-right: 16px;
}
}
// 响应式设计
@media (max-width: 1200px) {
.poster-content {
flex-direction: column;
align-items: center;
}
.poster-preview {
flex: none;
margin-bottom: 24px;
}
.poster-settings {
width: 100%;
max-width: 600px;
}
}
</style>