|
|
@@ -58,7 +58,13 @@
|
|
|
</el-upload>
|
|
|
<div>
|
|
|
<span class="opacity-icon"></span>
|
|
|
- <el-input v-model="background.image_opacity" size="mini" :min="0" :max="100" style="width: 55px" />%
|
|
|
+ <el-input
|
|
|
+ v-model.number="background.image_opacity"
|
|
|
+ size="mini"
|
|
|
+ :min="0"
|
|
|
+ :max="100"
|
|
|
+ style="width: 55px"
|
|
|
+ />%
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="mode-list">
|
|
|
@@ -97,7 +103,7 @@
|
|
|
<div class="setup-content">
|
|
|
<el-color-picker v-model="background.border_color" show-alpha />
|
|
|
<div>
|
|
|
- <el-input v-model="background.border_width" style="width: 70px; margin-right: 10px">
|
|
|
+ <el-input v-model.number="background.border_width" style="width: 70px; margin-right: 10px">
|
|
|
<i slot="prefix" class="el-icon-s-fold" style="line-height: 32px"></i>
|
|
|
</el-input>
|
|
|
<el-select v-model="background.border_style" placeholder="边框样式" style="width: 120px">
|
|
|
@@ -115,19 +121,19 @@
|
|
|
<div class="setup-content border-radius">
|
|
|
<div class="radius-item">
|
|
|
<span class="span-radius" :style="computedBorderRadius('top', 'left')"></span>
|
|
|
- <el-input v-model="background.top_left_radius" :min="0" type="number" />
|
|
|
+ <el-input v-model.number="background.top_left_radius" :min="0" type="number" />
|
|
|
</div>
|
|
|
<div class="radius-item">
|
|
|
<span class="span-radius" :style="computedBorderRadius('top', 'right')"></span>
|
|
|
- <el-input v-model="background.top_right_radius" :min="0" type="number" />
|
|
|
+ <el-input v-model.number="background.top_right_radius" :min="0" type="number" />
|
|
|
</div>
|
|
|
<div class="radius-item">
|
|
|
<span class="span-radius" :style="computedBorderRadius('bottom', 'left')"></span>
|
|
|
- <el-input v-model="background.bottom_left_radius" :min="0" type="number" />
|
|
|
+ <el-input v-model.number="background.bottom_left_radius" :min="0" type="number" />
|
|
|
</div>
|
|
|
<div class="radius-item">
|
|
|
<span class="span-radius" :style="computedBorderRadius('bottom', 'right')"></span>
|
|
|
- <el-input v-model="background.bottom_right_radius" :min="0" type="number" />
|
|
|
+ <el-input v-model.number="background.bottom_right_radius" :min="0" type="number" />
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -146,6 +152,22 @@ import ColorPicker from '@/components/ColorPicker.vue';
|
|
|
|
|
|
import { fileUpload } from '@/api/app';
|
|
|
|
|
|
+const DEFAULT_BACKGROUND = {
|
|
|
+ mode: 'image',
|
|
|
+ imageMode: 'auto',
|
|
|
+ image_opacity: 100,
|
|
|
+ color: 'rgba(255, 255, 255, 1)',
|
|
|
+ enable_border: false,
|
|
|
+ border_color: 'rgba(0, 0, 0, 1)',
|
|
|
+ border_width: 1,
|
|
|
+ border_style: 'solid',
|
|
|
+ enable_radius: false,
|
|
|
+ top_left_radius: 4,
|
|
|
+ top_right_radius: 4,
|
|
|
+ bottom_left_radius: 4,
|
|
|
+ bottom_right_radius: 4,
|
|
|
+};
|
|
|
+
|
|
|
export default {
|
|
|
name: 'SetComponentBackground',
|
|
|
components: {
|
|
|
@@ -208,21 +230,7 @@ export default {
|
|
|
height: 0,
|
|
|
},
|
|
|
file_url: '', // 背景图片地址
|
|
|
- background: {
|
|
|
- mode: 'image', // 背景模式
|
|
|
- imageMode: 'auto', // 图片背景模式
|
|
|
- image_opacity: 100, // 背景图片透明度
|
|
|
- color: 'rgba(255, 255, 255, 1)', // 背景颜色
|
|
|
- enable_border: false, // 是否启用边框
|
|
|
- border_color: 'rgba(0, 0, 0, 1)', // 边框颜色
|
|
|
- border_width: 1, // 边框宽度
|
|
|
- border_style: 'solid', // 边框样式
|
|
|
- enable_radius: false, // 是否启用圆角
|
|
|
- top_left_radius: 4, // 左上圆角
|
|
|
- top_right_radius: 4, // 右上圆角
|
|
|
- bottom_left_radius: 4, // 左下圆角
|
|
|
- bottom_right_radius: 4, // 右下圆角
|
|
|
- },
|
|
|
+ background: { ...DEFAULT_BACKGROUND },
|
|
|
imageModeList: [
|
|
|
{ label: '适应', value: 'adapt' },
|
|
|
{ label: '拉伸', value: 'stretch' },
|
|
|
@@ -230,9 +238,15 @@ export default {
|
|
|
{ label: '自定义', value: 'auto' },
|
|
|
],
|
|
|
lockScale: false, // 是否锁定比例(仅自定义模式)
|
|
|
+ dialogWrapper: null, // 当前弹窗 wrapper 引用,避免全局 querySelector 冲突
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
|
+ /**
|
|
|
+ * 图片容器 top 偏移(px)。
|
|
|
+ * -9 补偿了网格布局的顶部行高(2px)与拖拽手柄 dot 的 relative 偏移量之和,
|
|
|
+ * 使图片定位与用户拖拽感知一致。
|
|
|
+ */
|
|
|
imgTop() {
|
|
|
return `${this.imgData.top - 9}px`;
|
|
|
},
|
|
|
@@ -263,6 +277,7 @@ export default {
|
|
|
watch: {
|
|
|
visible(newVal) {
|
|
|
if (!newVal) {
|
|
|
+ this.dialogWrapper = null;
|
|
|
this.revertData();
|
|
|
return;
|
|
|
}
|
|
|
@@ -287,22 +302,26 @@ export default {
|
|
|
left: (this.position.left * this.maxWidth) / 100,
|
|
|
};
|
|
|
if (this.backgroundData && Object.keys(this.backgroundData).length) {
|
|
|
- this.background = { ...this.backgroundData };
|
|
|
+ this.background = { ...DEFAULT_BACKGROUND, ...this.backgroundData };
|
|
|
}
|
|
|
this.cropMode = false;
|
|
|
this.resetCropRect();
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
- document.querySelector('.background-img').addEventListener('mousemove', this.mouseMove);
|
|
|
+ const bg = this.$refs.backgroundImg;
|
|
|
+ if (bg) {
|
|
|
+ this.dialogWrapper = bg.closest('.el-dialog__wrapper');
|
|
|
+ }
|
|
|
this.syncImageDisplayRect();
|
|
|
});
|
|
|
},
|
|
|
},
|
|
|
mounted() {
|
|
|
+ document.addEventListener('mousemove', this.mouseMove);
|
|
|
document.body.addEventListener('mouseup', this.mouseUp);
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- document.querySelector('.background-img')?.removeEventListener('mousemove', this.mouseMove);
|
|
|
+ document.removeEventListener('mousemove', this.mouseMove);
|
|
|
document.body.removeEventListener('mouseup', this.mouseUp);
|
|
|
},
|
|
|
methods: {
|
|
|
@@ -339,21 +358,7 @@ export default {
|
|
|
height: 0,
|
|
|
};
|
|
|
this.file_url = ''; // 背景图片地址
|
|
|
- this.background = {
|
|
|
- mode: 'image', // 背景模式
|
|
|
- imageMode: 'auto', // 图片背景模式
|
|
|
- image_opacity: 100, // 背景图片透明度
|
|
|
- color: 'rgba(255, 255, 255, 1)', // 背景颜色
|
|
|
- enable_border: false, // 是否启用边框
|
|
|
- border_color: 'rgba(0, 0, 0, 1)', // 边框颜色
|
|
|
- border_width: 1, // 边框宽度
|
|
|
- border_style: 'solid', // 边框样式
|
|
|
- enable_radius: false, // 是否启用圆角
|
|
|
- top_left_radius: 4, // 左上圆角
|
|
|
- top_right_radius: 4, // 右上圆角
|
|
|
- bottom_left_radius: 4, // 左下圆角
|
|
|
- bottom_right_radius: 4, // 右下圆角
|
|
|
- };
|
|
|
+ this.background = { ...DEFAULT_BACKGROUND };
|
|
|
},
|
|
|
/**
|
|
|
* 拖拽开始
|
|
|
@@ -373,7 +378,9 @@ export default {
|
|
|
aspectRatio,
|
|
|
};
|
|
|
|
|
|
- document.querySelector('.el-dialog__wrapper').style.cursor = cursor;
|
|
|
+ if (this.dialogWrapper) {
|
|
|
+ this.dialogWrapper.style.cursor = cursor;
|
|
|
+ }
|
|
|
},
|
|
|
/**
|
|
|
* 鼠标移动
|
|
|
@@ -402,11 +409,30 @@ export default {
|
|
|
if (!this.drag.dragging) return;
|
|
|
const { clientX, clientY } = event;
|
|
|
const { startX, startY, type } = this.drag;
|
|
|
- const prevImgData = { ...this.imgData };
|
|
|
|
|
|
- const widthDiff = clientX - startX;
|
|
|
- const heightDiff = clientY - startY;
|
|
|
+ // 锁定比例时跳过常规 resize 计算,直接用 applyLockedScaleResize 处理
|
|
|
+ if (this.shouldLockScale(type)) {
|
|
|
+ const prevImgData = { ...this.imgData };
|
|
|
+ this.applyLockedScaleResize(type, prevImgData);
|
|
|
+ this.drag.startX = clientX;
|
|
|
+ this.drag.startY = clientY;
|
|
|
+ this.syncImageDisplayRect();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.applyDragResize(type, clientX - startX, clientY - startY);
|
|
|
|
|
|
+ this.drag.startX = clientX;
|
|
|
+ this.drag.startY = clientY;
|
|
|
+ this.syncImageDisplayRect();
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 非锁定比例下的拖拽 resize / move,直接修改 imgData。
|
|
|
+ * @param {string} type 拖拽类型
|
|
|
+ * @param {number} widthDiff 水平位移量
|
|
|
+ * @param {number} heightDiff 垂直位移量
|
|
|
+ */
|
|
|
+ applyDragResize(type, widthDiff, heightDiff) {
|
|
|
if (type === 'top-left') {
|
|
|
this.imgData.width = Math.min(this.maxWidth, Math.max(0, this.imgData.width - widthDiff));
|
|
|
this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
|
|
|
@@ -447,14 +473,6 @@ export default {
|
|
|
this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
|
|
|
this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
|
|
|
}
|
|
|
-
|
|
|
- if (this.shouldLockScale(type)) {
|
|
|
- this.applyLockedScaleResize(type, prevImgData);
|
|
|
- }
|
|
|
-
|
|
|
- this.drag.startX = clientX;
|
|
|
- this.drag.startY = clientY;
|
|
|
- this.syncImageDisplayRect();
|
|
|
},
|
|
|
/**
|
|
|
* 判断在当前拖动类型下是否应该锁定比例调整尺寸
|
|
|
@@ -502,9 +520,12 @@ export default {
|
|
|
* 鼠标抬起
|
|
|
*/
|
|
|
mouseUp() {
|
|
|
+ if (!this.drag.dragging && !this.crop.drawing) return;
|
|
|
this.crop.drawing = false;
|
|
|
this.drag.dragging = false;
|
|
|
- document.querySelector('.el-dialog__wrapper').style.cursor = 'auto';
|
|
|
+ if (this.dialogWrapper) {
|
|
|
+ this.dialogWrapper.style.cursor = 'auto';
|
|
|
+ }
|
|
|
},
|
|
|
onBackgroundMouseDown(event) {
|
|
|
if (!this.cropMode || !this.file_url) return;
|
|
|
@@ -662,6 +683,7 @@ export default {
|
|
|
|
|
|
this.file_url = file_info_list[0].file_url;
|
|
|
this.normalComputed(safeWidth, safeHeight);
|
|
|
+ this.syncImageDisplayRect();
|
|
|
this.cancelRectCrop();
|
|
|
this.$message.success('裁切成功');
|
|
|
} catch (error) {
|
|
|
@@ -750,12 +772,12 @@ export default {
|
|
|
if (file_info_list.length > 0) {
|
|
|
const fileUrl = file_info_list[0].file_url_open;
|
|
|
const img = new Image();
|
|
|
- img.src = fileUrl;
|
|
|
img.onload = () => {
|
|
|
const { width, height } = img;
|
|
|
this.normalComputed(width, height);
|
|
|
this.file_url = fileUrl;
|
|
|
};
|
|
|
+ img.src = fileUrl;
|
|
|
}
|
|
|
})
|
|
|
.catch(() => {
|
|
|
@@ -763,9 +785,11 @@ export default {
|
|
|
});
|
|
|
},
|
|
|
/**
|
|
|
- * 正常填充
|
|
|
- * @param {number} width 图片宽度
|
|
|
- * @param {number} height 图片高度
|
|
|
+ * 正常填充(等比例缩放使图片适配容器)
|
|
|
+ * 大图初始缩放为容器的一半大小,方便用户后续拖拽到合适位置。
|
|
|
+ * 小图(未超出容器)则保持原始尺寸。
|
|
|
+ * @param {number} width 图片原始宽度
|
|
|
+ * @param {number} height 图片原始高度
|
|
|
*/
|
|
|
normalComputed(width, height) {
|
|
|
if (width > this.maxWidth || height > this.maxHeight) {
|
|
|
@@ -789,6 +813,10 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
confirm() {
|
|
|
+ // 防止除零:图片尺寸恰好等于容器大小时,剩余空间为 0
|
|
|
+ const remainingWidth = Math.max(1, this.maxWidth - this.imgData.width);
|
|
|
+ const remainingHeight = Math.max(1, this.maxHeight - this.imgData.height);
|
|
|
+
|
|
|
this.$emit(
|
|
|
'setComponentBackground',
|
|
|
this.file_url,
|
|
|
@@ -797,8 +825,8 @@ export default {
|
|
|
height: (this.imgData.height / this.maxHeight) * 100,
|
|
|
top: (this.imgData.top / this.maxHeight) * 100,
|
|
|
left: (this.imgData.left / this.maxWidth) * 100,
|
|
|
- imgX: (this.imgData.left / (this.maxWidth - this.imgData.width)) * 100,
|
|
|
- imgY: (this.imgData.top / (this.maxHeight - this.imgData.height)) * 100,
|
|
|
+ imgX: (this.imgData.left / remainingWidth) * 100,
|
|
|
+ imgY: (this.imgData.top / remainingHeight) * 100,
|
|
|
},
|
|
|
this.background,
|
|
|
);
|