| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030 |
- <template>
- <el-dialog
- custom-class="component-background"
- :width="dialogWidth"
- :close-on-click-modal="false"
- :visible="visible"
- :before-close="handleClose"
- >
- <div class="background-title">组件背景设置</div>
- <div class="set-background">
- <div
- ref="backgroundImg"
- class="background-img"
- :style="{ width: `${maxWidth}px`, height: `${maxHeight}px` }"
- :class="{ 'is-crop-mode': cropMode }"
- @mousedown="onBackgroundMouseDown"
- >
- <div v-if="file_url" class="img-set" :style="{ top: imgTop, left: imgLeft, position: 'relative' }">
- <div class="dot top-left" @mousedown="dragStart($event, 'nwse-resize', 'top-left')"></div>
- <div class="horizontal-line" @mousedown="dragStart($event, 'ns-resize', 'top')"></div>
- <div class="dot top-right" @mousedown="dragStart($event, 'nesw-resize', 'top-right')"></div>
- <div class="vertical-line" @mousedown="dragStart($event, 'ew-resize', 'left')"></div>
- <img
- ref="img"
- :src="file_url"
- draggable="false"
- :style="{ width: imgWidth, height: imgHeight }"
- @load="handleImageLoad"
- @mousedown="dragStart($event, 'move', 'move')"
- />
- <div class="vertical-line" @mousedown="dragStart($event, 'ew-resize', 'right')"></div>
- <div class="dot bottom-left" @mousedown="dragStart($event, 'nesw-resize', 'bottom-left')"></div>
- <div class="horizontal-line" @mousedown="dragStart($event, 'ns-resize', 'bottom')"></div>
- <div class="dot bottom-right" @mousedown="dragStart($event, 'nwse-resize', 'bottom-right')"></div>
- </div>
- <div v-if="file_url && cropMode" class="crop-tip">在图片上拖拽框选裁切区域</div>
- <div v-if="file_url && cropMode && hasCropSelection" class="crop-selection" :style="cropSelectionStyle"></div>
- </div>
- <div class="setup">
- <div class="setup-item">
- <div class="setup-top">
- <el-radio v-model="background.mode" label="image">图片</el-radio>
- <SvgIcon v-if="file_url" icon-class="delete" style="cursor: pointer" size="14" @click="file_url = ''" />
- </div>
- <div class="setup-content">
- <div class="image-set">
- <el-upload
- ref="upload"
- class="file-uploader"
- action="no"
- accept="image/*"
- :show-file-list="false"
- :limit="1"
- :http-request="uploadFile"
- >
- <span class="select-button">选择图片</span>
- </el-upload>
- <div>
- <span class="opacity-icon"></span>
- <el-input v-model="background.image_opacity" size="mini" :min="0" :max="100" style="width: 55px" />%
- </div>
- </div>
- <div class="mode-list">
- <span
- v-for="mode in imageModeList"
- :key="mode.value"
- :class="{ active: mode.value === background.imageMode }"
- :style="{ fontSize: mode.value === imageModeList[3].value ? '12px' : '' }"
- @click="background.imageMode = mode.value"
- >
- {{ mode.label }}
- </span>
- </div>
- <div v-if="file_url" class="crop-actions">
- <el-button size="mini" plain @click="startRectCrop">裁切</el-button>
- <el-button v-if="cropMode" size="mini" @click="cancelRectCrop">取消</el-button>
- <el-button v-if="cropMode" size="mini" type="primary" @click="applyRectCrop">应用</el-button>
- </div>
- <div v-if="background.imageMode === imageModeList[3].value">
- <el-checkbox v-model="lockScale">锁定比例</el-checkbox>
- </div>
- </div>
- </div>
- <div class="setup-item">
- <div class="setup-top">
- <el-radio v-model="background.mode" label="color">颜色</el-radio>
- </div>
- <div class="setup-content">
- <el-color-picker v-model="background.color" show-alpha />
- </div>
- </div>
- <div class="setup-item">
- <div class="setup-top">
- <el-checkbox v-model="background.enable_border" label="边框" />
- </div>
- <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">
- <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">
- <el-option label="实线" value="solid" />
- <el-option label="虚线" value="dashed" />
- <el-option label="点线" value="dotted" />
- </el-select>
- </div>
- </div>
- </div>
- <div class="setup-item">
- <div class="setup-top">
- <el-checkbox v-model="background.enable_radius" label="圆角" />
- </div>
- <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" />
- </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" />
- </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" />
- </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" />
- </div>
- </div>
- </div>
- </div>
- </div>
- <div slot="footer">
- <el-button @click="handleClose">取 消</el-button>
- <el-button type="primary" @click="confirm">确 定</el-button>
- </div>
- </el-dialog>
- </template>
- <script>
- import { fileUpload } from '@/api/app';
- export default {
- name: 'SetComponentBackground',
- props: {
- visible: {
- type: Boolean,
- required: true,
- },
- url: {
- type: String,
- default: '',
- },
- position: {
- type: Object,
- default: () => ({ width: 0, height: 0, top: 0, left: 0 }),
- },
- backgroundData: {
- type: Object,
- default: () => ({}),
- },
- componentData: {
- type: Object,
- default: () => ({ courseware_id: '', component_id: '' }),
- },
- },
- data() {
- return {
- maxWidth: 500,
- maxHeight: 450,
- imgData: {
- width: 0,
- height: 0,
- top: 0,
- left: 0,
- },
- drag: {
- dragging: false,
- startX: 0,
- startY: 0,
- type: '',
- aspectRatio: 1, // 锁定比例时的宽高比
- },
- cropMode: false,
- crop: {
- drawing: false,
- startX: 0,
- startY: 0,
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- },
- imageDisplay: {
- top: 0,
- left: 0,
- width: 0,
- 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, // 右下圆角
- },
- imageModeList: [
- { label: '适应', value: 'adapt' },
- { label: '拉伸', value: 'stretch' },
- { label: '平铺', value: 'fill' },
- { label: '自定义', value: 'auto' },
- ],
- lockScale: false, // 是否锁定比例(仅自定义模式)
- };
- },
- computed: {
- imgTop() {
- return `${this.imgData.top - 9}px`;
- },
- imgLeft() {
- return `${this.imgData.left}px`;
- },
- imgWidth() {
- return `${this.imgData.width}px`;
- },
- imgHeight() {
- return `${this.imgData.height}px`;
- },
- hasCropSelection() {
- return this.crop.width > 1 && this.crop.height > 1;
- },
- cropSelectionStyle() {
- return {
- top: `${this.imageDisplay.top + this.crop.y}px`,
- left: `${this.imageDisplay.left + this.crop.x}px`,
- width: `${this.crop.width}px`,
- height: `${this.crop.height}px`,
- };
- },
- dialogWidth() {
- return `${this.maxWidth + 250}px`;
- },
- },
- watch: {
- visible(newVal) {
- if (!newVal) {
- this.revertData();
- return;
- }
- const component = document.querySelector(`div.${this.componentData.component_id}`);
- if (component) {
- const rect = component.getBoundingClientRect();
- const componentRatio = rect.width / rect.height;
- const imgRatio = this.maxWidth / this.maxHeight;
- if (componentRatio > imgRatio) {
- this.maxHeight = this.maxWidth / componentRatio;
- } else {
- this.maxWidth = this.maxHeight * componentRatio;
- }
- }
- this.file_url = this.url;
- this.imgData = {
- width: (this.position.width * this.maxWidth) / 100,
- height: (this.position.height * this.maxHeight) / 100,
- top: (this.position.top * this.maxHeight) / 100,
- left: (this.position.left * this.maxWidth) / 100,
- };
- if (this.backgroundData && Object.keys(this.backgroundData).length) {
- this.background = { ...this.backgroundData };
- }
- this.cropMode = false;
- this.resetCropRect();
- this.$nextTick(() => {
- document.querySelector('.background-img').addEventListener('mousemove', this.mouseMove);
- this.syncImageDisplayRect();
- });
- },
- },
- mounted() {
- document.body.addEventListener('mouseup', this.mouseUp);
- },
- beforeDestroy() {
- document.querySelector('.background-img')?.removeEventListener('mousemove', this.mouseMove);
- document.body.removeEventListener('mouseup', this.mouseUp);
- },
- methods: {
- handleClose() {
- this.$emit('update:visible', false);
- },
- /**
- * 还原数据
- */
- revertData() {
- this.cropMode = false;
- this.resetCropRect();
- this.maxWidth = 500;
- this.maxHeight = 450;
- this.imgData = {
- width: 0,
- height: 0,
- top: 0,
- left: 0,
- };
- this.crop = {
- drawing: false,
- startX: 0,
- startY: 0,
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- };
- this.imageDisplay = {
- top: 0,
- left: 0,
- width: 0,
- 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, // 右下圆角
- };
- },
- /**
- * 拖拽开始
- * @param {MouseEvent} event
- * @param {string} cursor
- * @param {string} type
- */
- dragStart(event, cursor, type) {
- if (this.cropMode) return;
- const { clientX, clientY } = event;
- const aspectRatio = this.imgData.height ? this.imgData.width / this.imgData.height : 1;
- this.drag = {
- dragging: true,
- startX: clientX,
- startY: clientY,
- type,
- aspectRatio,
- };
- document.querySelector('.el-dialog__wrapper').style.cursor = cursor;
- },
- /**
- * 鼠标移动
- * @param {MouseEvent} event
- */
- mouseMove(event) {
- if (this.cropMode) {
- if (!this.crop.drawing) return;
- const point = this.getPointInImage(event);
- if (!point) return;
- const x = Math.min(this.crop.startX, point.x);
- const y = Math.min(this.crop.startY, point.y);
- const width = Math.abs(point.x - this.crop.startX);
- const height = Math.abs(point.y - this.crop.startY);
- this.crop = {
- ...this.crop,
- x,
- y,
- width,
- height,
- };
- return;
- }
- 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;
- 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));
- 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));
- } else if (type === 'top-right') {
- this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
- this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
- 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));
- } else if (type === 'bottom-left') {
- this.imgData.width = Math.min(this.maxWidth, Math.max(0, this.imgData.width - widthDiff));
- this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
- this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
- this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
- } else if (type === 'bottom-right') {
- this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
- this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
- this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
- this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left));
- }
- if (type === 'top') {
- this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
- this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
- } else if (type === 'bottom') {
- this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
- this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
- } else if (type === 'left') {
- this.imgData.width = Math.min(this.maxWidth, this.imgData.width - widthDiff);
- this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
- } else if (type === 'right') {
- this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
- this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left));
- }
- if (type === 'move') {
- 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();
- },
- /**
- * 判断在当前拖动类型下是否应该锁定比例调整尺寸
- * @param {string} type 拖动类型
- * @returns {boolean} 是否应该锁定比例调整尺寸
- */
- shouldLockScale(type) {
- return this.background.imageMode === 'auto' && this.lockScale && type !== 'move';
- },
- /**
- * 在锁定比例的情况下调整尺寸,确保宽高比保持不变,并且不会超出边界
- * @param {string} type 拖动类型
- * @param {object} prevImgData 拖动前的图片数据,用于计算边界限制
- */
- applyLockedScaleResize(type, prevImgData) {
- const ratio = this.drag.aspectRatio || 1;
- if (!ratio) return;
- let width = type === 'top' || type === 'bottom' ? this.imgData.height * ratio : this.imgData.width;
- let height = width / ratio;
- // 根据拖动方向计算允许的最大宽高,确保在锁定比例时不会超出边界
- const widthLimit = type.includes('left')
- ? prevImgData.left + prevImgData.width
- : this.maxWidth - prevImgData.left;
- const heightLimit = type.includes('top')
- ? prevImgData.top + prevImgData.height
- : this.maxHeight - prevImgData.top;
- const maxWidthByHeight = heightLimit * ratio;
- const safeWidth = Math.max(1, Math.min(width, widthLimit, maxWidthByHeight));
- width = safeWidth;
- height = width / ratio;
- // 根据拖动方向计算新的 left 和 top,确保在锁定比例时图片位置调整合理
- const left = type.includes('left') ? prevImgData.left + prevImgData.width - width : prevImgData.left;
- const top = type.includes('top') ? prevImgData.top + prevImgData.height - height : prevImgData.top;
- this.imgData.width = width;
- this.imgData.height = height;
- this.imgData.left = Math.min(this.maxWidth - width, Math.max(0, left));
- this.imgData.top = Math.min(this.maxHeight - height, Math.max(0, top));
- },
- /**
- * 鼠标抬起
- */
- mouseUp() {
- this.crop.drawing = false;
- this.drag.dragging = false;
- document.querySelector('.el-dialog__wrapper').style.cursor = 'auto';
- },
- onBackgroundMouseDown(event) {
- if (!this.cropMode || !this.file_url) return;
- const point = this.getPointInImage(event);
- if (!point) return;
- this.crop = {
- ...this.crop,
- drawing: true,
- startX: point.x,
- startY: point.y,
- x: point.x,
- y: point.y,
- width: 0,
- height: 0,
- };
- event.preventDefault();
- },
- getPointInImage(event) {
- const bg = this.$refs.backgroundImg;
- if (!bg || this.imageDisplay.width <= 0 || this.imageDisplay.height <= 0) return null;
- const bgRect = bg.getBoundingClientRect();
- const rawX = event.clientX - bgRect.left - this.imageDisplay.left;
- const rawY = event.clientY - bgRect.top - this.imageDisplay.top;
- if (rawX < 0 || rawY < 0 || rawX > this.imageDisplay.width || rawY > this.imageDisplay.height) {
- return null;
- }
- return {
- x: Math.min(this.imageDisplay.width, Math.max(0, rawX)),
- y: Math.min(this.imageDisplay.height, Math.max(0, rawY)),
- };
- },
- /**
- * 同步图片显示区域位置和尺寸信息
- */
- syncImageDisplayRect() {
- this.$nextTick(() => {
- const bg = this.$refs.backgroundImg;
- const img = this.$refs.img;
- if (!bg || !img) {
- this.imageDisplay = {
- top: 0,
- left: 0,
- width: 0,
- height: 0,
- };
- return;
- }
- const bgRect = bg.getBoundingClientRect();
- const imgRect = img.getBoundingClientRect();
- this.imageDisplay = {
- top: imgRect.top - bgRect.top,
- left: imgRect.left - bgRect.left,
- width: imgRect.width,
- height: imgRect.height,
- };
- });
- },
- resetCropRect() {
- this.crop = {
- drawing: false,
- startX: 0,
- startY: 0,
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- };
- },
- startRectCrop() {
- if (!this.file_url) return;
- this.cropMode = true;
- this.syncImageDisplayRect();
- this.$nextTick(() => {
- const { width, height } = this.imageDisplay;
- if (!width || !height) return;
- this.crop = {
- drawing: false,
- startX: 0,
- startY: 0,
- x: width * 0.25,
- y: height * 0.25,
- width: width * 0.5,
- height: height * 0.5,
- };
- });
- },
- cancelRectCrop() {
- this.cropMode = false;
- this.resetCropRect();
- },
- async applyRectCrop() {
- if (!this.hasCropSelection) {
- this.$message.warning('请先框选裁切区域');
- return;
- }
- if (!this.file_url || !this.imageDisplay.width || !this.imageDisplay.height) return;
- let sourceImg = null;
- let revokeObjectURL = null;
- try {
- const cropSource = await this.getCropSourceImage();
- sourceImg = cropSource.img;
- revokeObjectURL = cropSource.revokeObjectURL;
- } catch (error) {
- this.$message.error('当前图片不支持裁切,请检查图片服务跨域配置');
- return;
- }
- const naturalWidth = sourceImg.naturalWidth;
- const naturalHeight = sourceImg.naturalHeight;
- const displayWidth = this.imageDisplay.width;
- const displayHeight = this.imageDisplay.height;
- const sx = Math.max(0, Math.round((this.crop.x / displayWidth) * naturalWidth));
- const sy = Math.max(0, Math.round((this.crop.y / displayHeight) * naturalHeight));
- const sw = Math.max(1, Math.round((this.crop.width / displayWidth) * naturalWidth));
- const sh = Math.max(1, Math.round((this.crop.height / displayHeight) * naturalHeight));
- const safeWidth = Math.max(1, Math.min(sw, naturalWidth - sx));
- const safeHeight = Math.max(1, Math.min(sh, naturalHeight - sy));
- try {
- const canvas = document.createElement('canvas');
- canvas.width = safeWidth;
- canvas.height = safeHeight;
- const ctx = canvas.getContext('2d');
- if (!ctx) throw new Error('canvas context unavailable');
- ctx.drawImage(sourceImg, sx, sy, safeWidth, safeHeight, 0, 0, safeWidth, safeHeight);
- const blob = await new Promise((resolve, reject) => {
- canvas.toBlob((result) => {
- if (result) {
- resolve(result);
- return;
- }
- reject(new Error('toBlob failed'));
- }, 'image/png');
- });
- const cropFile = new File([blob], `background-crop-${Date.now()}.png`, { type: 'image/png' });
- const uploadPayload = {
- filename: 'file',
- file: cropFile,
- };
- const { file_info_list } = await fileUpload('Mid', uploadPayload, { isGlobalprogress: true });
- if (!file_info_list || !file_info_list.length) {
- throw new Error('upload failed');
- }
- this.file_url = file_info_list[0].file_url;
- this.normalComputed(safeWidth, safeHeight);
- this.cancelRectCrop();
- this.$message.success('裁切成功');
- } catch (error) {
- this.$message.error('裁切失败');
- console.error('Rect crop error:', error);
- } finally {
- revokeObjectURL?.();
- }
- },
- async getCropSourceImage() {
- try {
- const img = await this.loadImage(this.file_url, 'anonymous');
- return {
- img,
- revokeObjectURL: null,
- };
- } catch (crossOriginError) {
- const blob = await this.fetchImageBlob(this.file_url);
- const objectUrl = URL.createObjectURL(blob);
- try {
- const img = await this.loadImage(objectUrl);
- return {
- img,
- revokeObjectURL: () => URL.revokeObjectURL(objectUrl),
- };
- } catch (loadBlobError) {
- URL.revokeObjectURL(objectUrl);
- throw loadBlobError;
- }
- }
- },
- loadImage(src, crossOrigin = '') {
- return new Promise((resolve, reject) => {
- const img = new Image();
- if (crossOrigin) {
- img.crossOrigin = crossOrigin;
- }
- img.onload = () => resolve(img);
- img.onerror = () => reject(new Error('image load failed'));
- img.src = src;
- });
- },
- async fetchImageBlob(url) {
- const response = await fetch(url, {
- credentials: 'include',
- });
- if (!response.ok) {
- throw new Error('fetch image blob failed');
- }
- return response.blob();
- },
- handleImageLoad() {
- this.syncImageDisplayRect();
- },
- /**
- * 计算圆角样式
- * @param {string} position 位置,top/bottom
- * @param {string} direction 方向,left/right
- * @returns {Object} 圆角样式对象
- */
- computedBorderRadius(position, direction) {
- const radius = this.background[`${position}_${direction}_radius`];
- let borderWidth = {};
- if (position === 'top') {
- borderWidth['border-bottom-width'] = 0;
- } else {
- borderWidth['border-top-width'] = 0;
- }
- if (direction === 'left') {
- borderWidth['border-right-width'] = 0;
- } else {
- borderWidth['border-left-width'] = 0;
- }
- return {
- [`border-${position}-${direction}-radius`]: `${radius}px`,
- ...borderWidth,
- };
- },
- uploadFile(file) {
- fileUpload('Mid', file, { isGlobalprogress: true })
- .then(({ file_info_list }) => {
- this.$refs.upload.clearFiles();
- 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;
- };
- }
- })
- .catch(() => {
- this.$message.error('上传失败');
- });
- },
- /**
- * 正常填充
- * @param {number} width 图片宽度
- * @param {number} height 图片高度
- */
- normalComputed(width, height) {
- if (width > this.maxWidth || height > this.maxHeight) {
- const wScale = width / this.maxWidth;
- const hScale = height / this.maxHeight;
- const scale = wScale > hScale ? this.maxWidth / 2 / width : this.maxHeight / 2 / height;
- this.imgData = {
- width: width * scale,
- height: height * scale,
- top: 0,
- left: 0,
- };
- } else {
- this.imgData = {
- width,
- height,
- top: 0,
- left: 0,
- };
- }
- },
- confirm() {
- this.$emit(
- 'setComponentBackground',
- this.file_url,
- {
- width: (this.imgData.width / this.maxWidth) * 100,
- 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,
- },
- this.background,
- );
- this.handleClose();
- },
- },
- };
- </script>
- <style lang="scss" scoped>
- .background-title {
- margin-bottom: 12px;
- font-size: 16px;
- font-weight: 500;
- color: $font-light-color;
- }
- .set-background {
- display: flex;
- column-gap: 8px;
- .background-img {
- position: relative;
- border: 1px dashed rgba(0, 0, 0, 8%);
- &.is-crop-mode {
- cursor: crosshair;
- }
- .crop-tip {
- position: absolute;
- top: 8px;
- left: 8px;
- z-index: 3;
- padding: 2px 8px;
- font-size: 12px;
- color: #fff;
- pointer-events: none;
- background: rgba(0, 0, 0, 45%);
- border-radius: 10px;
- }
- .crop-selection {
- position: absolute;
- z-index: 2;
- pointer-events: none;
- border: 1px dashed #fff;
- box-shadow: 0 0 0 9999px rgba(0, 0, 0, 35%);
- }
- .img-set {
- display: inline-grid;
- grid-template:
- ' . . . ' 2px
- ' . img . ' auto
- ' . . . ' 2px
- / 2px auto 2px;
- img {
- object-fit: cover;
- }
- .horizontal-line,
- .vertical-line {
- background-color: $main-color;
- }
- .horizontal-line {
- width: 100%;
- height: 2px;
- cursor: ns-resize;
- }
- .vertical-line {
- width: 2px;
- height: 100%;
- cursor: ew-resize;
- }
- .dot {
- z-index: 1;
- width: 6px;
- height: 6px;
- background-color: $main-color;
- &.top-left {
- top: -2px;
- left: -2px;
- }
- &.top-right {
- top: -2px;
- right: 2px;
- }
- &.bottom-left {
- bottom: 2px;
- left: -2px;
- }
- &.bottom-right {
- right: 2px;
- bottom: 2px;
- }
- &.top-left,
- &.bottom-right {
- position: relative;
- cursor: nwse-resize;
- }
- &.top-right,
- &.bottom-left {
- position: relative;
- cursor: nesw-resize;
- }
- }
- }
- }
- .setup {
- display: flex;
- flex-direction: column;
- row-gap: 12px;
- width: 200px;
- &-item {
- display: flex;
- flex-direction: column;
- column-gap: 8px;
- .setup-top {
- margin-bottom: 6px;
- }
- .setup-content {
- display: flex;
- flex-direction: column;
- row-gap: 8px;
- .image-set {
- display: flex;
- align-items: center;
- justify-content: space-around;
- height: 32px;
- color: $font-light-color;
- background-color: #f2f3f5;
- border: 1px solid #d9d9d9;
- border-radius: 4px;
- .opacity-icon {
- display: inline-block;
- width: 20px;
- height: 20px;
- vertical-align: middle;
- background: url('@/assets/icon/opacity.png');
- }
- .select-button {
- color: #165dff;
- cursor: pointer;
- background-color: #fff;
- box-shadow: 0 0 0 3px #fff;
- }
- }
- .mode-list {
- display: flex;
- padding: 2px 3px;
- background-color: #f2f3f5;
- border: 1px solid #d9d9d9;
- border-radius: 4px;
- span {
- flex: 1;
- padding: 2px 6px;
- line-height: 21px;
- text-align: center;
- cursor: pointer;
- &.active {
- color: $main-color;
- background-color: #fff;
- }
- }
- }
- .crop-actions {
- z-index: 3001;
- display: flex;
- }
- &.border-radius {
- display: grid;
- grid-template-rows: 1fr 1fr;
- grid-template-columns: 1fr 1fr;
- gap: 8px 8px;
- .radius-item {
- display: flex;
- flex: 1;
- column-gap: 4px;
- align-items: center;
- height: 32px;
- padding: 2px 6px;
- background-color: #f2f3f5;
- .span-radius {
- display: inline-block;
- width: 16px;
- height: 16px;
- border-color: #000;
- border-style: solid;
- border-width: 2px;
- }
- }
- }
- }
- }
- }
- }
- </style>
- <style lang="scss">
- .el-dialog.component-background {
- .el-dialog__header {
- display: none;
- }
- .el-dialog__body {
- padding: 8px 16px;
- }
- }
- </style>
|