场景和功能
有一个用户自定义衣服、手机壳、书包等等东西的图案logo,如下面这个小程序的业务
这个小程序就是可以定制衣服上的logo然后下单制作你自定义图案的衣服
核心就是有一个自定义的定制区域,在区域内可以自己设计图片和文本
目前demo实现的功能:
- 可添加文本和图片素材
- 素材支持:单指移动、两指缩放和旋转、使用操作按钮使素材:旋转、缩放、删除、编辑
- 图片素材可支持内置图片和用户上传的自定义图片
- 文本素材支持大多数文本的样式:颜色、字号、粗细、倾斜、下划线、删除线、行高、间距、对齐方式等等
效果如下图
代码
<template>
<div class="page-box">
<!-- 编辑区域 -->
<div
class="design-box"
@click="
addPopup = false;
currentIndex = -1;
"
>
<div
class="design-box-item"
v-for="(item, index) in list"
:key="index"
:style="[
{
left: item.positionX + 'px',
top: item.positionY + 'px',
transform: `rotate(${item.rotate || 0}deg) scale(${item.scale || 1})`,
},
]"
@click.stop="currentIndex = index"
>
<text
v-if="item.type == 'text'"
:style="[
item.style,
{
display: 'block',
'white-space': 'pre-wrap',
'word-wrap': 'break-word',
'word-break': 'break-all',
'unicode-bidi': 'bidi-override',
fontSize: item.style.fontSize + 'px',
'letter-spacing': item.style.letterSpacing + 'px',
width: 'fit-content',
transform: item.style.transform,
},
]"
>
{{ item.text }}
</text>
<image
v-else
:src="item.url"
mode="scaleToFill"
:style="[
item.style,
{
width: item.style.width + 'px',
height: item.style.height + 'px',
},
]"
/>
</div>
</div>
<!-- 素材操作元素 -->
<div
class="controls-box"
:style="[
{
width: controlInfo.width + 'px',
height: controlInfo.height + 'px',
top: controlInfo.top + 'px',
left: controlInfo.left + 'px',
},
]"
v-if="currentIndex > -1 && controlInfo"
@touchstart="handleControlsTouchStart($event, currentIndex)"
@touchmove="handleControlsTouchMove($event)"
@touchend="handleControlsTouchEnd"
>
<div
class="controls-scale"
@touchstart.stop="handleScaleStart"
@touchmove.stop="handleScaleMove"
@touchend.stop="handleScaleEnd"
>缩</div
>
<div class="controls-edit" @click.stop="handleEdit">编</div>
<div class="controls-delete" @click.stop="handleDelete">删</div>
<div
class="controls-rotate"
@touchstart.stop="handleRotateStart"
@touchmove.stop="handleRotateMove"
@touchend.stop="handleRotateEnd"
>旋</div
>
</div>
<!-- 添加素材 -->
<div class="operate-box" v-if="!addPopup">
<div class="operate-btn operate-text" @click="addMaterial('text')">添加文本素材</div>
<div class="operate-btn operate-image" @click="addMaterial('image')">添加图片素材</div>
</div>
</div>
<!-- 添加/编辑 素材弹窗 -->
<my-popup v-model:show="addPopup" position="bottom" radius="10" :isMask="false">
<div class="add-popup-box" v-if="currentIndex > -1">
<div class="title">添加{{ addType == 'text' ? '文本' : '图片' }}素材</div>
<button @click="addPopup = false">关闭</button>
<div class="add-text-box" v-if="addType == 'text'">
<div class="text-textarea">
<textarea v-model="list[currentIndex].text" placeholder="请输入文本" auto-height />
</div>
<div class="text-operate-box">
<div
class="text-operate-item"
v-for="(item, index) in textOperateList"
:key="index"
@click="handleTextOperate(item)"
>
{{ item.name }}
</div>
</div>
<div class="text-color-box">
<div
class="text-color-item"
v-for="(item, index) in textColorList"
:key="index"
:style="{
backgroundColor: item,
border: item == '#fff' ? '1rpx solid #000' : 'none',
}"
@click="handleTextColor(item)"
>
</div>
</div>
<div class="text-font-box">
<div
class="text-font-item"
v-for="(item, index) in textFontList"
:key="index"
:style="{
fontFamily: item,
}"
@click="handleTextFont(item)"
>
{{ item }}
</div>
</div>
</div>
<div class="add-image-box" v-else>
<div
class="add-image-item"
v-for="(item, index) in imageList"
:key="index"
@click="handleAddImage(item)"
>
<image
:src="item.url"
mode="scaleToFill"
:style="{ width: item.width + 'px', height: item.height + 'px' }"
></image>
</div>
<div class="cursor-pointer" @click="customAddImage('custom')"> 自选图片</div>
</div>
</div>
</my-popup>
</template>
<script setup>
import { ref, onMounted, computed, watchEffect, getCurrentInstance, watch, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { proxy } = getCurrentInstance();
onMounted(() => {
getOperateBoxInfo();
});
onLoad((res) => {});
// 获取编辑区域的信息:宽高/中心点坐标
let designBoxInfo = {};
function getOperateBoxInfo() {
const query = uni.createSelectorQuery().in(proxy);
query
.select('.design-box')
.boundingClientRect((data) => {
data.centerX = data.width / 2;
data.centerY = data.height / 2;
designBoxInfo = data;
})
.exec();
}
// 素材列表
const list = ref([]);
// 当前操作的素材索引
const currentIndex = ref(-1);
import myPopup from './myPopup.vue';
const addPopup = ref(false);
watch(addPopup, (newVal) => {
if (!newVal) {
list.value = list.value.filter((item) => {
if (item.type === 'image') {
return item.url !== '';
}
return item.text !== '';
});
if (list.value.length === 0) {
currentIndex.value = -1;
} else {
currentIndex.value = list.value.length - 1;
}
}
});
// 添加素材类型
const addType = ref('');
// 添加素材
function addMaterial(type) {
addType.value = type;
addPopup.value = true;
if (type == 'text') {
addText();
} else {
addImage();
}
}
// 添加文本素材
function addText() {
currentIndex.value = list.value.length;
textOperateList.value[0].value = false;
textOperateList.value[1].value = false;
textOperateList.value[2].value = false;
textOperateList.value[3].value = false;
let data = {};
data.type = 'text';
data.text = '默认文本';
data.positionX = designBoxInfo.centerX;
data.positionY = designBoxInfo.centerY;
data.style = {
// 加粗
fontWeight: 'normal',
// 斜体
fontStyle: 'normal',
// 下划线 删除线
textDecoration: 'none',
// 字体大小
fontSize: 16,
// 字体颜色
color: '#000',
// 字体
fontFamily: 'Arial',
// 文本对齐方式
textAlign: 'left',
// 行高
lineHeight: 1.2,
// 文本间距
letterSpacing: 0,
// 文本水平方向
direction: 'ltr',
// 文本垂直方向
writingMode: '',
// 层级
zIndex: list.value.length ? list.value[list.value.length - 1].style.zIndex + 1 : 1,
transform: '',
};
data.rotate = 0;
data.scale = 1;
list.value.push(data);
}
// 文本操作列表
const textOperateList = ref([
{
name: '是否加粗',
key: 'is-weight',
value: false,
},
{
name: '是否斜体',
key: 'is-italic',
value: false,
},
{
name: '是否下划线',
key: 'is-underline',
value: false,
},
{
name: '是否删除线',
key: 'is-line-through',
value: false,
},
{
name: '字体大小增加',
key: 'font-size-add',
value: 2,
},
{
name: '字体大小减少',
key: 'font-size-reduce',
value: 2,
},
{
name: '文本左对齐',
key: 'text-align-left',
value: 'left',
},
{
name: '文本右对齐',
key: 'text-align-right',
value: 'right',
},
{
name: '文本居中对齐',
key: 'text-align-center',
value: 'center',
},
{
name: '文本行高增加',
value: 0.2,
key: 'text-line-height-add',
},
{
name: '文本行高减少',
value: 0.2,
key: 'text-line-height-reduce',
},
{
name: '文本间距增加',
value: 2,
key: 'text-spacing-add',
},
{
name: '文本间距减少',
value: 2,
key: 'text-spacing-reduce',
},
{
name: '文本方向:水平左到右',
value: 'ltr',
key: 'text-direction-left-to-right',
},
{
name: '文本方向:水平右到左',
value: 'rtl',
key: 'text-direction-right-to-left',
},
{
name: '文本方向:垂直左到右',
value: 'vertical-lr',
key: 'text-writingMode-top-to-bottom',
},
{
name: '文本方向:垂直右到左',
value: 'vertical-rl',
key: 'text-writingMode-bottom-to-top',
},
{
name: '水平翻转',
key: 'text-flip-horizontal',
value: false,
},
{
name: '垂直翻转',
key: 'text-flip-vertical',
value: false,
},
]);
// 文本操作
function handleTextOperate(item) {
let currentItem = list.value[currentIndex.value];
switch (item.key) {
case 'is-weight':
item.value = !item.value;
currentItem.style.fontWeight = item.value ? 'bold' : 'normal';
break;
case 'is-italic':
item.value = !item.value;
currentItem.style.fontStyle = item.value ? 'italic' : 'normal';
break;
case 'is-underline':
item.value = !item.value;
toggleTextDecoration(currentItem, 'underline', item.value);
break;
case 'is-line-through':
item.value = !item.value;
toggleTextDecoration(currentItem, 'line-through', item.value);
break;
case 'font-size-add':
currentItem.style.fontSize += item.value;
break;
case 'font-size-reduce':
currentItem.style.fontSize -= item.value;
break;
case 'text-align-left':
case 'text-align-right':
case 'text-align-center':
currentItem.style.textAlign = item.value;
break;
case 'text-line-height-add':
currentItem.style.lineHeight += item.value;
break;
case 'text-line-height-reduce':
currentItem.style.lineHeight -= item.value;
break;
case 'text-spacing-add':
currentItem.style.letterSpacing += item.value;
break;
case 'text-spacing-reduce':
currentItem.style.letterSpacing -= item.value;
break;
case 'text-direction-left-to-right':
case 'text-direction-right-to-left':
currentItem.style.direction = item.value;
// 清除垂直方向设置
currentItem.style.writingMode = '';
break;
case 'text-writingMode-top-to-bottom':
case 'text-writingMode-bottom-to-top':
currentItem.style.writingMode = item.value;
// 清除水平方向设置
currentItem.style.direction = 'ltr';
break;
case 'text-flip-horizontal':
item.value = !item.value;
updateTransform(currentItem, 'rotateX', item.value ? '180deg' : '0deg');
break;
case 'text-flip-vertical':
item.value = !item.value;
updateTransform(currentItem, 'rotateY', item.value ? '180deg' : '0deg');
break;
}
function toggleTextDecoration(element, style, isActive) {
let decorations = element.style.textDecoration.split(' ').filter((d) => d && d !== 'none');
if (isActive) {
if (!decorations.includes(style)) {
decorations.push(style);
}
} else {
decorations = decorations.filter((d) => d !== style);
}
element.style.textDecoration = decorations.length ? decorations.join(' ') : 'none';
}
function updateTransform(element, property, value) {
let transforms = element.style.transform ? element.style.transform.split(' ') : [];
let hasProperty = false;
transforms = transforms.map((t) => {
if (t.includes(property)) {
hasProperty = true;
return `${property}(${value})`;
}
return t;
});
if (!hasProperty) {
transforms.push(`${property}(${value})`);
}
element.style.transform = transforms.join(' ');
}
}
// 文本颜色列表(测试用,正式使用时请通过接口获取)
const textColorList = ref([
'#000',
'#fff',
'#ccc',
'#f00',
'#0f0',
'#00f',
'#f0f',
'#0ff',
'#ff0',
]);
// 改变文本颜色
function handleTextColor(item) {
list.value[currentIndex.value].style.color = item;
}
// 文本字体列表(测试用,正式使用时请通过接口获取)
const textFontList = ref([
'Arial',
'Helvetica',
'Times New Roman',
'Georgia',
'Garamond',
'Courier New',
'Verdana',
'Tahoma',
'Trebuchet MS',
'Source Han Sans SC',
]);
// 改变文本字体
function handleTextFont(item) {
list.value[currentIndex.value].style.fontFamily = item;
}
// 添加图片素材
function addImage() {
currentIndex.value = list.value.length;
let data = {};
data.type = 'image';
data.url = '';
data.positionX = designBoxInfo.centerX;
data.positionY = designBoxInfo.centerY;
data.style = {
width: 50,
height: 50,
zIndex: list.value.length ? list.value[list.value.length - 1].style.zIndex + 1 : 1,
};
data.rotate = 0;
data.scale = 1;
list.value.push(data);
}
// 图片列表(测试用,正式使用时请通过接口获取)
const imageList = ref([
{
name: '图片1',
url: 'https://pic.20988.xyz/2024-08-29/1724898997-618191-preview.jpg',
},
{
name: '图片2',
url: 'https://pic.20988.xyz/2024-08-29/1724899028-533968-preview.jpg',
},
{
name: '图片3',
url: 'https://pic.20988.xyz/2024-08-29/1724899165-67654-preview.jpg',
},
]);
// 给图片素材添加图片
function handleAddImage(item) {
list.value[currentIndex.value].url = item.url;
}
// 给图片素材添加自选图片
function customAddImage() {
const platform = uni.getSystemInfoSync().uniPlatform;
switch (platform) {
case 'web':
uni.chooseImage({
count: 1,
success: (res) => {
let arr = res.tempFiles.map((item) => {
return {
type: 'image',
url: item.path,
size: item.size,
name: item.name || '',
};
});
// 正式使用时解开
// uploadFile(arr[0]);
// 测试用的
list.value[currentIndex.value].url = arr[0].url;
},
fail: () => {},
});
break;
case 'mp-weixin':
uni.chooseImage({
count: 1,
success: (res) => {
let arr = res.tempFiles.map((item) => {
return {
type: 'image',
url: item.path,
size: item.size,
name: item.name || '',
};
});
// 正式使用时解开
// uploadFile(arr[0]);
// 测试用的
list.value[currentIndex.value].url = arr[0].url;
},
fail: () => {},
});
break;
case 'app':
uni.chooseImage({
count: 1,
success: (res) => {
let arr = res.tempFiles.map((item) => {
return {
type: 'image',
url: item.path,
size: item.size,
name: item.name || '',
};
});
// 正式使用时解开
// uploadFile(arr[0]);
// 测试用的
list.value[currentIndex.value].url = arr[0].url;
},
fail: () => {},
});
break;
}
}
async function uploadFile(file) {
try {
// 开始上传
let res = await upload(file.url);
return res.fullurl;
} catch (error) {
console.log(error);
}
}
function upload(url) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: '改成你的后端接口',
filePath: url,
name: 'file',
formData: {},
success: (res) => {
const data = JSON.parse(res.data);
// console.log('后端接口返回结果', data);
if (data.code === 200) {
resolve(data.data);
} else {
reject(data);
uni.showToast({
icon: 'error',
title: data.msg,
});
}
},
fail: (e) => {
uni.showToast({
icon: 'error',
title: '上传失败',
});
reject(e);
},
});
});
}
// 素材操作元素的信息
const controlInfo = ref('');
watch(
[() => list.value, () => currentIndex.value],
async () => {
if (currentIndex.value > -1) {
await nextTick();
addType.value = list.value[currentIndex.value].type;
uni
.createSelectorQuery()
.in(proxy)
.selectAll('.design-box-item')
.boundingClientRect((arr) => {
if (arr.length) {
let data = arr[currentIndex.value];
let space = 20;
controlInfo.value = {
width: data.width + space,
height: data.height + space,
left: data.left - space / 2,
top: data.top - space / 2,
};
} else {
controlInfo.value = '';
}
})
.exec();
} else {
controlInfo.value = '';
}
},
{
deep: true,
}
);
// 编辑素材
function handleEdit() {
if (addType.value == 'text') {
let data = list.value[currentIndex.value];
textOperateList.value[0].value = data.style.fontWeight == 'bold';
textOperateList.value[1].value = data.style.fontStyle == 'italic';
textOperateList.value[2].value = data.style.textDecoration.includes('underline');
textOperateList.value[3].value = data.style.textDecoration.includes('line-through');
}
addPopup.value = true;
}
// 删除素材
function handleDelete() {
list.value.splice(currentIndex.value, 1);
currentIndex.value = -1;
}
// 添加触摸和旋转相关的状态
const isDragging = ref(false);
const isRotating = ref(false);
const isScaling = ref(false);
const startX = ref(0);
const startY = ref(0);
const startPositionX = ref(0);
const startPositionY = ref(0);
const startAngle = ref(0);
const startDistance = ref(0);
const startScale = ref(1);
const centerPoint = ref({ x: 0, y: 0 });
// 触摸开始
function handleTouchStart(event, index) {
// 如果是双指触摸,不触发拖动
if (event.touches.length === 2) {
return;
}
currentIndex.value = index;
isDragging.value = true;
// 记录起始触摸点
const touch = event.touches[0];
startX.value = touch.clientX;
startY.value = touch.clientY;
// 记录元素初始位置
startPositionX.value = list.value[index].positionX;
startPositionY.value = list.value[index].positionY;
}
// 触摸移动
function handleTouchMove(event) {
// 如果是双指触摸,不触发拖动
if (event.touches.length === 2) {
return;
}
if (!isDragging.value) return;
// 阻止页面滚动
event.preventDefault();
const touch = event.touches[0];
const deltaX = touch.clientX - startX.value;
const deltaY = touch.clientY - startY.value;
// 更新元素位置
list.value[currentIndex.value].positionX = startPositionX.value + deltaX;
list.value[currentIndex.value].positionY = startPositionY.value + deltaY;
}
// 触摸结束
function handleTouchEnd() {
isDragging.value = false;
}
// 获取两点之间的角度
function getAngle(center, point) {
const x = point.x - center.x;
const y = point.y - center.y;
return (Math.atan2(y, x) * 180) / Math.PI;
}
// 获取两点之间的距离
function getDistance(center, point) {
const x = point.x - center.x;
const y = point.y - center.y;
return Math.sqrt(x * x + y * y);
}
// 缩放开始
function handleScaleStart(event) {
event.preventDefault();
isScaling.value = true;
const touch = event.touches[0];
// 使用 uni-app 的 API 获取元素信息
const query = uni.createSelectorQuery().in(proxy);
query
.selectAll('.design-box-item')
.boundingClientRect((data) => {
if (!data || !data[currentIndex.value]) return;
const currentItem = data[currentIndex.value];
centerPoint.value = {
x: currentItem.left + currentItem.width / 2,
y: currentItem.top + currentItem.height / 2,
};
// 记录起始距离和缩放值
startDistance.value = getDistance(centerPoint.value, {
x: touch.clientX,
y: touch.clientY,
});
startScale.value = list.value[currentIndex.value].scale || 1;
})
.exec();
}
// 缩放移动
function handleScaleMove(event) {
if (!isScaling.value || !centerPoint.value.x) return;
event.preventDefault();
const touch = event.touches[0];
const currentDistance = getDistance(centerPoint.value, {
x: touch.clientX,
y: touch.clientY,
});
// 计算缩放比例
const scaleChange = currentDistance / startDistance.value;
const newScale = startScale.value * scaleChange;
// 限制缩放范围
const minScale = 0.5;
const maxScale = 3;
list.value[currentIndex.value].scale = Math.min(Math.max(newScale, minScale), maxScale);
}
// 缩放结束
function handleScaleEnd() {
isScaling.value = false;
}
// 旋转开始
function handleRotateStart(event) {
event.preventDefault();
isRotating.value = true;
const touch = event.touches[0];
const query = uni.createSelectorQuery().in(proxy);
query
.selectAll('.design-box-item')
.boundingClientRect((data) => {
if (!data || !data[currentIndex.value]) return;
const currentItem = data[currentIndex.value];
centerPoint.value = {
x: currentItem.left + currentItem.width / 2,
y: currentItem.top + currentItem.height / 2,
};
// 只计算起始角度
startAngle.value =
getAngle(centerPoint.value, {
x: touch.clientX,
y: touch.clientY,
}) - (list.value[currentIndex.value].rotate || 0);
})
.exec();
}
// 旋转移动时只处理旋转
function handleRotateMove(event) {
if (!isRotating.value || !centerPoint.value.x) return;
event.preventDefault();
const touch = event.touches[0];
const currentAngle = getAngle(centerPoint.value, {
x: touch.clientX,
y: touch.clientY,
});
// 只更新旋转角度
list.value[currentIndex.value].rotate = currentAngle - startAngle.value;
}
// 旋转结束
function handleRotateEnd() {
isRotating.value = false;
}
// 双指缩放相关参数
const isPinching = ref(false);
const initialPinchDistance = ref(0);
const initialScale = ref(1);
// 双指旋转相关参数
const initialRotation = ref(0);
const initialElementRotation = ref(0);
// 双指触摸开始处理函数
function handleControlsTouchStart(event, index) {
if (event.touches.length === 2) {
event.preventDefault();
event.stopPropagation();
isPinching.value = true;
isDragging.value = false;
const touch1 = event.touches[0];
const touch2 = event.touches[1];
// 计算初始距离(用于缩放)
initialPinchDistance.value = getPinchDistance(touch1, touch2);
initialScale.value = list.value[currentIndex.value].scale || 1;
// 计算初始角度(用于旋转)
initialRotation.value = getTwoFingerAngle(touch1, touch2);
initialElementRotation.value = list.value[currentIndex.value].rotate || 0;
// 记录中心点
centerPoint.value = {
x: (touch1.clientX + touch2.clientX) / 2,
y: (touch1.clientY + touch2.clientY) / 2,
};
} else if (event.touches.length === 1) {
handleTouchStart(event, index);
}
}
// 双指触摸移动处理函数
function handleControlsTouchMove(event) {
if (isPinching.value && event.touches.length === 2) {
event.preventDefault();
event.stopPropagation();
const touch1 = event.touches[0];
const touch2 = event.touches[1];
// 处理缩放
const currentDistance = getPinchDistance(touch1, touch2);
const scaleChange = currentDistance / initialPinchDistance.value;
const newScale = initialScale.value * scaleChange;
// 限制缩放范围
const minScale = 0.5;
const maxScale = 3;
list.value[currentIndex.value].scale = Math.min(Math.max(newScale, minScale), maxScale);
// 处理旋转
const currentRotation = getTwoFingerAngle(touch1, touch2);
const rotationDiff = currentRotation - initialRotation.value;
list.value[currentIndex.value].rotate = initialElementRotation.value + rotationDiff;
} else if (event.touches.length === 1 && isDragging.value) {
handleTouchMove(event);
}
}
// 计算两个触摸点之间角度的函数
function getTwoFingerAngle(touch1, touch2) {
return (
(Math.atan2(touch2.clientY - touch1.clientY, touch2.clientX - touch1.clientX) * 180) / Math.PI
);
}
// 双指缩放的触摸处理函数 结束
function handleControlsTouchEnd(event) {
if (isPinching.value) {
isPinching.value = false;
}
if (isDragging.value) {
handleTouchEnd();
}
}
// 计算两个触摸点之间的距离
function getPinchDistance(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
</script>
<style lang="scss" scoped>
.page-box {
background: #ccc;
min-height: 100vh;
padding: 30rpx;
position: relative;
}
.design-box {
background: #fff;
width: 60%;
height: 600rpx;
margin: 0 auto;
position: relative;
overflow: hidden;
}
.design-box-item {
position: absolute;
touch-action: manipulation;
user-select: none;
width: fit-content;
}
.controls-box {
position: absolute;
border: 1rpx dashed #19dbe9;
touch-action: manipulation;
.controls-scale {
position: absolute;
top: 0;
left: 0;
transform: translate(-50%, -50%);
}
.controls-edit {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
}
.controls-delete {
position: absolute;
bottom: 0;
left: 0;
transform: translate(-50%, 50%);
}
.controls-rotate {
position: absolute;
bottom: 0;
right: 0;
transform: translate(50%, 50%);
}
}
.operate-box {
margin-top: 40rpx;
display: flex;
justify-content: center;
align-items: center;
.operate-btn {
padding: 10rpx 20rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10rpx;
font-size: 26rpx;
margin: 0 20rpx;
}
.operate-text {
background: #4ceba8;
color: #fff;
}
.operate-image {
background: #279ae7;
color: #fff;
}
}
.add-popup-box {
max-height: 40vh;
overflow: auto;
padding: 30rpx;
padding-bottom: 60rpx;
.title {
display: flex;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
color: #000;
margin-bottom: 20rpx;
}
.add-text-box {
.text-textarea {
margin-bottom: 20rpx;
}
.text-operate-box {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
.text-operate-item {
display: flex;
justify-content: center;
align-items: center;
border: 1rpx solid #ccc;
border-radius: 10rpx;
padding: 10rpx;
font-size: 20rpx;
color: #ccc;
}
}
.text-color-box {
margin-top: 20rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
.text-color-item {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
margin: 0 10rpx;
}
}
.text-font-box {
margin-top: 20rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
.text-font-item {
border: 1rpx solid #ccc;
border-radius: 10rpx;
padding: 10rpx;
font-size: 20rpx;
color: #ccc;
display: flex;
justify-content: center;
align-items: center;
margin: 0 10rpx;
}
}
}
.add-image-box {
display: flex;
flex-wrap: wrap;
.add-image-item {
width: 100rpx;
height: 100rpx;
border-radius: 10rpx;
margin: 0 10rpx;
image {
width: 100%;
height: 100%;
}
}
.cursor-pointer {
width: 100rpx;
height: 100rpx;
border-radius: 10rpx;
margin: 0 10rpx;
display: flex;
justify-content: center;
align-items: center;
border: 1rpx dashed #ccc;
}
}
}
</style>
注意点
里面的myPopup是我另一个弹出层组件:juejin.cn/post/744678… 可根据项目情况自行更换
确保设计页面不可滚动,也就是需要一屏展示完页面内容,不然移动素材的时候会出现错位问题
当前支持平台
- 微信小程序
- H5
- App(安卓)
- App(苹果)未测试