SetBackground.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. <template>
  2. <el-dialog
  3. custom-class="background"
  4. :width="dialogWidth"
  5. :close-on-click-modal="false"
  6. :visible="visible"
  7. :before-close="handleClose"
  8. >
  9. <div class="background-title">背景设置</div>
  10. <div class="set-background">
  11. <div
  12. ref="backgroundImg"
  13. class="background-img"
  14. :style="{ width: `${maxWidth}px`, height: `${maxHeight}px` }"
  15. :class="{ 'is-crop-mode': cropMode }"
  16. @mousedown="onBackgroundMouseDown"
  17. >
  18. <div v-if="file_url" class="img-set" :style="{ top: imgTop, left: imgLeft, position: 'relative' }">
  19. <div class="dot top-left" @mousedown="dragStart($event, 'nwse-resize', 'top-left')"></div>
  20. <div class="horizontal-line" @mousedown="dragStart($event, 'ns-resize', 'top')"></div>
  21. <div class="dot top-right" @mousedown="dragStart($event, 'nesw-resize', 'top-right')"></div>
  22. <div class="vertical-line" @mousedown="dragStart($event, 'ew-resize', 'left')"></div>
  23. <img
  24. ref="img"
  25. :src="file_url"
  26. draggable="false"
  27. :style="{ width: imgWidth, height: imgHeight }"
  28. @load="handleImageLoad"
  29. @mousedown="dragStart($event, 'move', 'move')"
  30. />
  31. <div class="vertical-line" @mousedown="dragStart($event, 'ew-resize', 'right')"></div>
  32. <div class="dot bottom-left" @mousedown="dragStart($event, 'nesw-resize', 'bottom-left')"></div>
  33. <div class="horizontal-line" @mousedown="dragStart($event, 'ns-resize', 'bottom')"></div>
  34. <div class="dot bottom-right" @mousedown="dragStart($event, 'nwse-resize', 'bottom-right')"></div>
  35. </div>
  36. <div v-if="file_url && cropMode" class="crop-tip">在图片上拖拽框选裁切区域</div>
  37. <div v-if="file_url && cropMode && hasCropSelection" class="crop-selection" :style="cropSelectionStyle"></div>
  38. </div>
  39. <div class="setup">
  40. <div class="setup-item">
  41. <div class="setup-top">
  42. <el-radio v-model="background.mode" label="image">图片</el-radio>
  43. <SvgIcon v-if="file_url" icon-class="delete" style="cursor: pointer" size="14" @click="file_url = ''" />
  44. </div>
  45. <div class="setup-content">
  46. <div class="image-set">
  47. <el-upload
  48. ref="upload"
  49. class="file-uploader"
  50. action="no"
  51. accept="image/*"
  52. :show-file-list="false"
  53. :limit="1"
  54. :http-request="uploadFile"
  55. >
  56. <span style="cursor: pointer">选择图片</span>
  57. </el-upload>
  58. <div>
  59. <span class="opacity-icon"></span>
  60. <el-input v-model="background.image_opacity" size="mini" :min="0" :max="100" style="width: 55px" />%
  61. </div>
  62. </div>
  63. <div class="mode-list">
  64. <span
  65. v-for="mode in imageModeList"
  66. :key="mode.value"
  67. :class="{ active: mode.value === background.imageMode }"
  68. @click="background.imageMode = mode.value"
  69. >
  70. {{ mode.label }}
  71. </span>
  72. </div>
  73. <div v-if="file_url" class="crop-actions">
  74. <el-button size="mini" plain @click="startRectCrop">裁切</el-button>
  75. <el-button v-if="cropMode" size="mini" @click="cancelRectCrop">取消</el-button>
  76. <el-button v-if="cropMode" size="mini" type="primary" @click="applyRectCrop">应用</el-button>
  77. </div>
  78. </div>
  79. </div>
  80. <div class="setup-item">
  81. <div class="setup-top">
  82. <el-radio v-model="background.mode" label="color">颜色</el-radio>
  83. </div>
  84. <div class="setup-content">
  85. <el-color-picker v-model="background.color" show-alpha />
  86. </div>
  87. </div>
  88. <div class="setup-item">
  89. <div class="setup-top">
  90. <el-checkbox v-model="background.enable_border" label="边框" />
  91. </div>
  92. <div class="setup-content">
  93. <el-color-picker v-model="background.border_color" show-alpha />
  94. <div>
  95. <el-input v-model="background.border_width" style="width: 70px; margin-right: 10px">
  96. <i slot="prefix" class="el-icon-s-fold" style="line-height: 32px"></i>
  97. </el-input>
  98. <el-select v-model="background.border_style" placeholder="边框样式" style="width: 120px">
  99. <el-option label="实线" value="solid" />
  100. <el-option label="虚线" value="dashed" />
  101. <el-option label="点线" value="dotted" />
  102. </el-select>
  103. </div>
  104. </div>
  105. </div>
  106. <div class="setup-item">
  107. <div class="setup-top">
  108. <el-checkbox v-model="background.enable_radius" label="圆角" />
  109. </div>
  110. <div class="setup-content border-radius">
  111. <div class="radius-item">
  112. <span class="span-radius" :style="computedBorderRadius('top', 'left')"></span>
  113. <el-input v-model="background.top_left_radius" :min="0" type="number" />
  114. </div>
  115. <div class="radius-item">
  116. <span class="span-radius" :style="computedBorderRadius('top', 'right')"></span>
  117. <el-input v-model="background.top_right_radius" :min="0" type="number" />
  118. </div>
  119. <div class="radius-item">
  120. <span class="span-radius" :style="computedBorderRadius('bottom', 'left')"></span>
  121. <el-input v-model="background.bottom_left_radius" :min="0" type="number" />
  122. </div>
  123. <div class="radius-item">
  124. <span class="span-radius" :style="computedBorderRadius('bottom', 'right')"></span>
  125. <el-input v-model="background.bottom_right_radius" :min="0" type="number" />
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. </div>
  131. <div slot="footer">
  132. <el-button @click="handleClose">取 消</el-button>
  133. <el-button type="primary" @click="confirm">确 定</el-button>
  134. </div>
  135. </el-dialog>
  136. </template>
  137. <script>
  138. import { fileUpload } from '@/api/app';
  139. export default {
  140. name: 'SetBackground',
  141. props: {
  142. visible: {
  143. type: Boolean,
  144. required: true,
  145. },
  146. url: {
  147. type: String,
  148. default: '',
  149. },
  150. position: {
  151. type: Object,
  152. default: () => ({ width: 0, height: 0, top: 0, left: 0 }),
  153. },
  154. backgroundData: {
  155. type: Object,
  156. default: () => ({}),
  157. },
  158. },
  159. data() {
  160. return {
  161. maxWidth: 500,
  162. maxHeight: 450,
  163. imgData: {
  164. width: 0,
  165. height: 0,
  166. top: 0,
  167. left: 0,
  168. },
  169. drag: {
  170. dragging: false,
  171. startX: 0,
  172. startY: 0,
  173. type: '',
  174. },
  175. cropMode: false,
  176. crop: {
  177. drawing: false,
  178. startX: 0,
  179. startY: 0,
  180. x: 0,
  181. y: 0,
  182. width: 0,
  183. height: 0,
  184. },
  185. imageDisplay: {
  186. top: 0,
  187. left: 0,
  188. width: 0,
  189. height: 0,
  190. },
  191. file_url: '', // 背景图片地址
  192. background: {
  193. mode: 'image', // 背景模式
  194. imageMode: 'auto', // 图片背景模式
  195. image_opacity: 100, // 背景图片透明度
  196. color: 'rgba(255, 255, 255, 1)', // 背景颜色
  197. enable_border: false, // 是否启用边框
  198. border_color: 'rgba(0, 0, 0, 1)', // 边框颜色
  199. border_width: 1, // 边框宽度
  200. border_style: 'solid', // 边框样式
  201. enable_radius: false, // 是否启用圆角
  202. top_left_radius: 4, // 左上圆角
  203. top_right_radius: 4, // 右上圆角
  204. bottom_left_radius: 4, // 左下圆角
  205. bottom_right_radius: 4, // 右下圆角
  206. },
  207. imageModeList: [
  208. { label: '平铺', value: 'fill' },
  209. { label: '拉伸', value: 'stretch' },
  210. { label: '适应', value: 'adapt' },
  211. { label: '自定', value: 'auto' },
  212. ],
  213. };
  214. },
  215. computed: {
  216. imgTop() {
  217. return `${this.imgData.top - 9}px`;
  218. },
  219. imgLeft() {
  220. return `${this.imgData.left}px`;
  221. },
  222. imgWidth() {
  223. return `${this.imgData.width}px`;
  224. },
  225. imgHeight() {
  226. return `${this.imgData.height}px`;
  227. },
  228. hasCropSelection() {
  229. return this.crop.width > 1 && this.crop.height > 1;
  230. },
  231. cropSelectionStyle() {
  232. return {
  233. top: `${this.imageDisplay.top + this.crop.y}px`,
  234. left: `${this.imageDisplay.left + this.crop.x}px`,
  235. width: `${this.crop.width}px`,
  236. height: `${this.crop.height}px`,
  237. };
  238. },
  239. dialogWidth() {
  240. return `${this.maxWidth + 250}px`;
  241. },
  242. },
  243. watch: {
  244. visible(newVal) {
  245. if (newVal) {
  246. const courseware = document.querySelector('div.courserware');
  247. if (courseware) {
  248. const rect = courseware.getBoundingClientRect();
  249. const coursewareRatio = rect.width / rect.height;
  250. const imgRatio = this.maxWidth / this.maxHeight;
  251. if (coursewareRatio > imgRatio) {
  252. this.maxHeight = this.maxWidth / coursewareRatio;
  253. } else {
  254. this.maxWidth = this.maxHeight * coursewareRatio;
  255. }
  256. }
  257. this.file_url = this.url;
  258. this.imgData = {
  259. width: (this.position.width * this.maxWidth) / 100,
  260. height: (this.position.height * this.maxHeight) / 100,
  261. top: (this.position.top * this.maxHeight) / 100,
  262. left: (this.position.left * this.maxWidth) / 100,
  263. };
  264. if (this.backgroundData && Object.keys(this.backgroundData).length) {
  265. this.background = { ...this.backgroundData };
  266. }
  267. this.cropMode = false;
  268. this.resetCropRect();
  269. this.$nextTick(() => {
  270. document.querySelector('.background-img').addEventListener('mousemove', this.mouseMove);
  271. this.syncImageDisplayRect();
  272. });
  273. } else {
  274. document.querySelector('.background-img')?.removeEventListener('mousemove', this.mouseMove);
  275. this.cropMode = false;
  276. this.resetCropRect();
  277. }
  278. },
  279. },
  280. mounted() {
  281. document.body.addEventListener('mouseup', this.mouseUp);
  282. },
  283. beforeDestroy() {
  284. document.querySelector('.background-img')?.removeEventListener('mousemove', this.mouseMove);
  285. document.body.removeEventListener('mouseup', this.mouseUp);
  286. },
  287. methods: {
  288. handleClose() {
  289. this.$emit('update:visible', false);
  290. },
  291. /**
  292. * 拖拽开始
  293. * @param {MouseEvent} event
  294. * @param {string} cursor
  295. * @param {string} type
  296. */
  297. dragStart(event, cursor, type) {
  298. if (this.cropMode) return;
  299. const { clientX, clientY } = event;
  300. this.drag = {
  301. dragging: true,
  302. startX: clientX,
  303. startY: clientY,
  304. type,
  305. };
  306. document.querySelector('.el-dialog__wrapper').style.cursor = cursor;
  307. },
  308. /**
  309. * 鼠标移动
  310. * @param {MouseEvent} event
  311. */
  312. mouseMove(event) {
  313. if (this.cropMode) {
  314. if (!this.crop.drawing) return;
  315. const point = this.getPointInImage(event);
  316. if (!point) return;
  317. const x = Math.min(this.crop.startX, point.x);
  318. const y = Math.min(this.crop.startY, point.y);
  319. const width = Math.abs(point.x - this.crop.startX);
  320. const height = Math.abs(point.y - this.crop.startY);
  321. this.crop = {
  322. ...this.crop,
  323. x,
  324. y,
  325. width,
  326. height,
  327. };
  328. return;
  329. }
  330. if (!this.drag.dragging) return;
  331. const { clientX, clientY } = event;
  332. const { startX, startY, type } = this.drag;
  333. const widthDiff = clientX - startX;
  334. const heightDiff = clientY - startY;
  335. if (type === 'top-left') {
  336. this.imgData.width = Math.min(this.maxWidth, Math.max(0, this.imgData.width - widthDiff));
  337. this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
  338. this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
  339. this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
  340. } else if (type === 'top-right') {
  341. this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
  342. this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
  343. this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
  344. this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left));
  345. } else if (type === 'bottom-left') {
  346. this.imgData.width = Math.min(this.maxWidth, Math.max(0, this.imgData.width - widthDiff));
  347. this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
  348. this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
  349. this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
  350. } else if (type === 'bottom-right') {
  351. this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
  352. this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
  353. this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
  354. this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left));
  355. }
  356. if (type === 'top') {
  357. this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
  358. this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
  359. } else if (type === 'bottom') {
  360. this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
  361. this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
  362. } else if (type === 'left') {
  363. this.imgData.width = Math.min(this.maxWidth, this.imgData.width - widthDiff);
  364. this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
  365. } else if (type === 'right') {
  366. this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
  367. this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left));
  368. }
  369. if (type === 'move') {
  370. this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
  371. this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
  372. }
  373. this.drag.startX = clientX;
  374. this.drag.startY = clientY;
  375. this.syncImageDisplayRect();
  376. },
  377. /**
  378. * 鼠标抬起
  379. */
  380. mouseUp() {
  381. this.crop.drawing = false;
  382. this.drag.dragging = false;
  383. document.querySelector('.el-dialog__wrapper').style.cursor = 'auto';
  384. },
  385. onBackgroundMouseDown(event) {
  386. if (!this.cropMode || !this.file_url) return;
  387. const point = this.getPointInImage(event);
  388. if (!point) return;
  389. this.crop = {
  390. ...this.crop,
  391. drawing: true,
  392. startX: point.x,
  393. startY: point.y,
  394. x: point.x,
  395. y: point.y,
  396. width: 0,
  397. height: 0,
  398. };
  399. event.preventDefault();
  400. },
  401. getPointInImage(event) {
  402. const bg = this.$refs.backgroundImg;
  403. if (!bg || this.imageDisplay.width <= 0 || this.imageDisplay.height <= 0) return null;
  404. const bgRect = bg.getBoundingClientRect();
  405. const rawX = event.clientX - bgRect.left - this.imageDisplay.left;
  406. const rawY = event.clientY - bgRect.top - this.imageDisplay.top;
  407. if (rawX < 0 || rawY < 0 || rawX > this.imageDisplay.width || rawY > this.imageDisplay.height) {
  408. return null;
  409. }
  410. return {
  411. x: Math.min(this.imageDisplay.width, Math.max(0, rawX)),
  412. y: Math.min(this.imageDisplay.height, Math.max(0, rawY)),
  413. };
  414. },
  415. /**
  416. * 同步图片显示区域位置和尺寸信息
  417. */
  418. syncImageDisplayRect() {
  419. this.$nextTick(() => {
  420. const bg = this.$refs.backgroundImg;
  421. const img = this.$refs.img;
  422. if (!bg || !img) {
  423. this.imageDisplay = {
  424. top: 0,
  425. left: 0,
  426. width: 0,
  427. height: 0,
  428. };
  429. return;
  430. }
  431. const bgRect = bg.getBoundingClientRect();
  432. const imgRect = img.getBoundingClientRect();
  433. this.imageDisplay = {
  434. top: imgRect.top - bgRect.top,
  435. left: imgRect.left - bgRect.left,
  436. width: imgRect.width,
  437. height: imgRect.height,
  438. };
  439. });
  440. },
  441. resetCropRect() {
  442. this.crop = {
  443. drawing: false,
  444. startX: 0,
  445. startY: 0,
  446. x: 0,
  447. y: 0,
  448. width: 0,
  449. height: 0,
  450. };
  451. },
  452. startRectCrop() {
  453. if (!this.file_url) return;
  454. this.cropMode = true;
  455. this.syncImageDisplayRect();
  456. this.$nextTick(() => {
  457. const { width, height } = this.imageDisplay;
  458. if (!width || !height) return;
  459. this.crop = {
  460. drawing: false,
  461. startX: 0,
  462. startY: 0,
  463. x: width * 0.25,
  464. y: height * 0.25,
  465. width: width * 0.5,
  466. height: height * 0.5,
  467. };
  468. });
  469. },
  470. cancelRectCrop() {
  471. this.cropMode = false;
  472. this.resetCropRect();
  473. },
  474. async applyRectCrop() {
  475. if (!this.hasCropSelection) {
  476. this.$message.warning('请先框选裁切区域');
  477. return;
  478. }
  479. if (!this.file_url || !this.imageDisplay.width || !this.imageDisplay.height) return;
  480. let sourceImg = null;
  481. let revokeObjectURL = null;
  482. try {
  483. const cropSource = await this.getCropSourceImage();
  484. sourceImg = cropSource.img;
  485. revokeObjectURL = cropSource.revokeObjectURL;
  486. } catch (error) {
  487. this.$message.error('当前图片不支持裁切,请检查图片服务跨域配置');
  488. return;
  489. }
  490. const naturalWidth = sourceImg.naturalWidth;
  491. const naturalHeight = sourceImg.naturalHeight;
  492. const displayWidth = this.imageDisplay.width;
  493. const displayHeight = this.imageDisplay.height;
  494. const sx = Math.max(0, Math.round((this.crop.x / displayWidth) * naturalWidth));
  495. const sy = Math.max(0, Math.round((this.crop.y / displayHeight) * naturalHeight));
  496. const sw = Math.max(1, Math.round((this.crop.width / displayWidth) * naturalWidth));
  497. const sh = Math.max(1, Math.round((this.crop.height / displayHeight) * naturalHeight));
  498. const safeWidth = Math.max(1, Math.min(sw, naturalWidth - sx));
  499. const safeHeight = Math.max(1, Math.min(sh, naturalHeight - sy));
  500. try {
  501. const canvas = document.createElement('canvas');
  502. canvas.width = safeWidth;
  503. canvas.height = safeHeight;
  504. const ctx = canvas.getContext('2d');
  505. if (!ctx) throw new Error('canvas context unavailable');
  506. ctx.drawImage(sourceImg, sx, sy, safeWidth, safeHeight, 0, 0, safeWidth, safeHeight);
  507. const blob = await new Promise((resolve, reject) => {
  508. canvas.toBlob((result) => {
  509. if (result) {
  510. resolve(result);
  511. return;
  512. }
  513. reject(new Error('toBlob failed'));
  514. }, 'image/png');
  515. });
  516. const cropFile = new File([blob], `background-crop-${Date.now()}.png`, { type: 'image/png' });
  517. const uploadPayload = {
  518. filename: 'file',
  519. file: cropFile,
  520. };
  521. const { file_info_list } = await fileUpload('Mid', uploadPayload, { isGlobalprogress: true });
  522. if (!file_info_list || !file_info_list.length) {
  523. throw new Error('upload failed');
  524. }
  525. this.file_url = file_info_list[0].file_url;
  526. this.normalComputed(safeWidth, safeHeight);
  527. this.cancelRectCrop();
  528. this.$message.success('裁切成功');
  529. } catch (error) {
  530. this.$message.error('裁切失败');
  531. console.error('Rect crop error:', error);
  532. } finally {
  533. revokeObjectURL?.();
  534. }
  535. },
  536. async getCropSourceImage() {
  537. try {
  538. const img = await this.loadImage(this.file_url, 'anonymous');
  539. return {
  540. img,
  541. revokeObjectURL: null,
  542. };
  543. } catch (crossOriginError) {
  544. const blob = await this.fetchImageBlob(this.file_url);
  545. const objectUrl = URL.createObjectURL(blob);
  546. try {
  547. const img = await this.loadImage(objectUrl);
  548. return {
  549. img,
  550. revokeObjectURL: () => URL.revokeObjectURL(objectUrl),
  551. };
  552. } catch (loadBlobError) {
  553. URL.revokeObjectURL(objectUrl);
  554. throw loadBlobError;
  555. }
  556. }
  557. },
  558. loadImage(src, crossOrigin = '') {
  559. return new Promise((resolve, reject) => {
  560. const img = new Image();
  561. if (crossOrigin) {
  562. img.crossOrigin = crossOrigin;
  563. }
  564. img.onload = () => resolve(img);
  565. img.onerror = () => reject(new Error('image load failed'));
  566. img.src = src;
  567. });
  568. },
  569. async fetchImageBlob(url) {
  570. const response = await fetch(url, {
  571. credentials: 'include',
  572. });
  573. if (!response.ok) {
  574. throw new Error('fetch image blob failed');
  575. }
  576. return response.blob();
  577. },
  578. handleImageLoad() {
  579. this.syncImageDisplayRect();
  580. },
  581. /**
  582. * 计算圆角样式
  583. * @param {string} position 位置,top/bottom
  584. * @param {string} direction 方向,left/right
  585. * @returns {Object} 圆角样式对象
  586. */
  587. computedBorderRadius(position, direction) {
  588. const radius = this.background[`${position}_${direction}_radius`];
  589. let borderWidth = {};
  590. if (position === 'top') {
  591. borderWidth['border-bottom-width'] = 0;
  592. } else {
  593. borderWidth['border-top-width'] = 0;
  594. }
  595. if (direction === 'left') {
  596. borderWidth['border-right-width'] = 0;
  597. } else {
  598. borderWidth['border-left-width'] = 0;
  599. }
  600. return {
  601. [`border-${position}-${direction}-radius`]: `${radius}px`,
  602. ...borderWidth,
  603. };
  604. },
  605. uploadFile(file) {
  606. fileUpload('Mid', file, { isGlobalprogress: true })
  607. .then(({ file_info_list }) => {
  608. this.$refs.upload.clearFiles();
  609. if (file_info_list.length > 0) {
  610. const fileUrl = file_info_list[0].file_url_open;
  611. const img = new Image();
  612. img.src = fileUrl;
  613. img.onload = () => {
  614. const { width, height } = img;
  615. this.normalComputed(width, height);
  616. this.file_url = fileUrl;
  617. };
  618. }
  619. })
  620. .catch(() => {
  621. this.$message.error('上传失败');
  622. });
  623. },
  624. /**
  625. * 正常填充
  626. * @param {number} width 图片宽度
  627. * @param {number} height 图片高度
  628. */
  629. normalComputed(width, height) {
  630. if (width > this.maxWidth || height > this.maxHeight) {
  631. const wScale = width / this.maxWidth;
  632. const hScale = height / this.maxHeight;
  633. const scale = wScale > hScale ? this.maxWidth / 2 / width : this.maxHeight / 2 / height;
  634. this.imgData = {
  635. width: width * scale,
  636. height: height * scale,
  637. top: 0,
  638. left: 0,
  639. };
  640. } else {
  641. this.imgData = {
  642. width,
  643. height,
  644. top: 0,
  645. left: 0,
  646. };
  647. }
  648. },
  649. confirm() {
  650. this.$emit(
  651. 'setBackground',
  652. this.file_url,
  653. {
  654. width: (this.imgData.width / this.maxWidth) * 100,
  655. height: (this.imgData.height / this.maxHeight) * 100,
  656. top: (this.imgData.top / this.maxHeight) * 100,
  657. left: (this.imgData.left / this.maxWidth) * 100,
  658. imgX: (this.imgData.left / (this.maxWidth - this.imgData.width)) * 100,
  659. imgY: (this.imgData.top / (this.maxHeight - this.imgData.height)) * 100,
  660. },
  661. this.background,
  662. );
  663. this.handleClose();
  664. },
  665. },
  666. };
  667. </script>
  668. <style lang="scss" scoped>
  669. .background-title {
  670. margin-bottom: 12px;
  671. font-size: 16px;
  672. font-weight: 500;
  673. color: $font-light-color;
  674. }
  675. .set-background {
  676. display: flex;
  677. column-gap: 8px;
  678. .background-img {
  679. position: relative;
  680. border: 1px dashed rgba(0, 0, 0, 8%);
  681. &.is-crop-mode {
  682. cursor: crosshair;
  683. }
  684. .crop-tip {
  685. position: absolute;
  686. top: 8px;
  687. left: 8px;
  688. z-index: 3;
  689. padding: 2px 8px;
  690. font-size: 12px;
  691. color: #fff;
  692. pointer-events: none;
  693. background: rgba(0, 0, 0, 45%);
  694. border-radius: 10px;
  695. }
  696. .crop-selection {
  697. position: absolute;
  698. z-index: 2;
  699. pointer-events: none;
  700. border: 1px dashed #fff;
  701. box-shadow: 0 0 0 9999px rgba(0, 0, 0, 35%);
  702. }
  703. .img-set {
  704. display: inline-grid;
  705. grid-template:
  706. ' . . . ' 2px
  707. ' . img . ' auto
  708. ' . . . ' 2px
  709. / 2px auto 2px;
  710. img {
  711. object-fit: cover;
  712. }
  713. .horizontal-line,
  714. .vertical-line {
  715. background-color: $main-color;
  716. }
  717. .horizontal-line {
  718. width: 100%;
  719. height: 2px;
  720. cursor: ns-resize;
  721. }
  722. .vertical-line {
  723. width: 2px;
  724. height: 100%;
  725. cursor: ew-resize;
  726. }
  727. .dot {
  728. z-index: 1;
  729. width: 6px;
  730. height: 6px;
  731. background-color: $main-color;
  732. &.top-left {
  733. top: -2px;
  734. left: -2px;
  735. }
  736. &.top-right {
  737. top: -2px;
  738. right: 2px;
  739. }
  740. &.bottom-left {
  741. bottom: 2px;
  742. left: -2px;
  743. }
  744. &.bottom-right {
  745. right: 2px;
  746. bottom: 2px;
  747. }
  748. &.top-left,
  749. &.bottom-right {
  750. position: relative;
  751. cursor: nwse-resize;
  752. }
  753. &.top-right,
  754. &.bottom-left {
  755. position: relative;
  756. cursor: nesw-resize;
  757. }
  758. }
  759. }
  760. }
  761. .setup {
  762. display: flex;
  763. flex-direction: column;
  764. row-gap: 12px;
  765. width: 200px;
  766. &-item {
  767. display: flex;
  768. flex-direction: column;
  769. column-gap: 8px;
  770. .setup-top {
  771. margin-bottom: 6px;
  772. }
  773. .setup-content {
  774. display: flex;
  775. flex-direction: column;
  776. row-gap: 8px;
  777. .image-set {
  778. display: flex;
  779. align-items: center;
  780. justify-content: space-around;
  781. height: 32px;
  782. color: $font-light-color;
  783. background-color: #f2f3f5;
  784. border: 1px solid #d9d9d9;
  785. border-radius: 4px;
  786. .opacity-icon {
  787. display: inline-block;
  788. width: 20px;
  789. height: 20px;
  790. vertical-align: middle;
  791. background: url('@/assets/icon/opacity.png');
  792. }
  793. }
  794. .mode-list {
  795. display: flex;
  796. padding: 2px 3px;
  797. background-color: #f2f3f5;
  798. border: 1px solid #d9d9d9;
  799. border-radius: 4px;
  800. span {
  801. flex: 1;
  802. padding: 2px 6px;
  803. text-align: center;
  804. cursor: pointer;
  805. &.active {
  806. color: $main-color;
  807. background-color: #fff;
  808. }
  809. }
  810. }
  811. .crop-actions {
  812. z-index: 3001;
  813. display: flex;
  814. }
  815. &.border-radius {
  816. display: grid;
  817. grid-template-rows: 1fr 1fr;
  818. grid-template-columns: 1fr 1fr;
  819. gap: 8px 8px;
  820. .radius-item {
  821. display: flex;
  822. flex: 1;
  823. column-gap: 4px;
  824. align-items: center;
  825. height: 32px;
  826. padding: 2px 6px;
  827. background-color: #f2f3f5;
  828. .span-radius {
  829. display: inline-block;
  830. width: 16px;
  831. height: 16px;
  832. border-color: #000;
  833. border-style: solid;
  834. border-width: 2px;
  835. }
  836. }
  837. }
  838. }
  839. }
  840. }
  841. }
  842. </style>
  843. <style lang="scss">
  844. .el-dialog.background {
  845. .el-dialog__header {
  846. display: none;
  847. }
  848. .el-dialog__body {
  849. padding: 8px 16px;
  850. }
  851. }
  852. </style>