1 <template> 2 <el-dialog :model-value="addDialog" width="648" @close="closeDialog" :lock-scroll="true" 3 :close-on-click-modal="false"> 4 <template #header="{ close, titleId, titleClass }"> 5 <div class="my-header"> 6 <div :id="titleId" :class="titleClass" class="my-header-title"><span>區域性重繪</span></div> 7 <p>繪製蒙版區域,AI將在蒙版區域內進行畫面重繪</p> 8 </div> 9 </template> 10 <div class="my-content"> 11 <div class="my-content-menu"> 12 13 <div class="my-content-menu-item" v-for="(item, index) in btnList" :key="index" 14 :class="{ active: currentKey == item.id }" @click="changeMenu(item)"> 15 <el-tooltip :visible="item.visible" placement="top" effect="light" :offset="20"> 16 <template #content> 17 <div style="display: flex;flex-direction: column;gap:8px;padding:0px 8px 8px 8px;"> 18 <span>畫筆大小</span> 19 <div> 20 <Pencil @updateNum="updateNum"></Pencil> 21 </div> 22 </div> 23 </template> 24 <div style="display: flex;align-items: center;gap:8px;"> 25 <SvgIcon :name="item.icon" class="my-content-menu-item-icon"></SvgIcon> 26 <span>{{ item.name }}</span> 27 <el-tooltip placement="bottom" effect="light" :offset="20"> 28 <template #content> 29 <div style="display: flex;flex-direction: column;gap:8px;padding:6px 8px 8px 8px;"> 30 <span style="font-size:14px">上傳遮罩圖片,精準控制重繪區域</span> 31 <p style="color:#797979">白色部分為重繪區域,遮罩圖需要與原圖尺寸 32 <br>保持一致。例如下方圖片為手指區域重繪。 33 </p> 34 <div style="display: flex;justify-content: space-between;"> 35 <img src="../../../../../assets/function/aipaint/shadow-source.png" /> 36 <img src="../../../../../assets/function/aipaint/shadow-pic.png" /> 37 </div> 38 </div> 39 </template> 40 <SvgIcon name="aipaint-why" class="my-content-menu-item-icon" v-if="item.id == 2"> 41 </SvgIcon> 42 </el-tooltip> 43 </div> 44 </el-tooltip> 45 46 </div> 47 <input type="file" @change.self="uploadShadowPic" style="display: none" ref="shadowInput" /> 48 </div> 49 <div class="my-content-image"> 50 <div class="canvascontainer"> 51 <!-- <img :src="proxy.$loginUrl + props.reviewUrl" alt=""> --> 52 <canvas ref="backgroundCanvas" width="600px" height="600px"></canvas> 53 <canvas ref="drawingCanvas" @mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing" 54 width="600px" height="600px"></canvas> 55 <!-- <canvas ref="resultCanvas" width="500" height="500" v-if="showResult"></canvas> --> 56 </div> 57 58 </div> 59 </div> 60 <div class="my-button"> 61 <div class="my-button-left" @click="closeDialog">取消關閉</div> 62 <div class="my-button-right" @click="finishReview">完成繪製</div> 63 </div> 64 65 </el-dialog> 66 </template> 67 <script lang="ts" setup> 68 import { ref, getCurrentInstance, onMounted, nextTick } from 'vue'; 69 import SvgIcon from "@/components/index.vue"; 70 import Pencil from './pencil.vue'; 71 const currentKey = ref(100); 72 import { ElMessageBox, ElMessage } from "element-plus"; 73 const drawingCanvas: any = ref<HTMLCanvasElement | null>(null); 74 const backgroundCanvas: any = ref(null); 75 const ctx: any = ref<CanvasRenderingContext2D | null>(null); 76 const bgCtx: any = ref<CanvasRenderingContext2D | null>(null); 77 const popoverRef = ref() 78 const drawing = ref(false); 79 const color = ref('#85E822') 80 const size = ref(2); 81 const imgs: any = ref([]); 82 const showResult: any = ref(false); 83 const backFlag: any = ref(false); 84 const imgWidth: any = ref(0); 85 const imgHeight: any = ref(0); 86 const startX: any = ref(0); 87 const startY: any = ref(0); 88 const localUrl:any = ref('http://localhost:8888/api') 89 const shadowInput: any = ref(null); 90 import { upLoadFile } from "@/api/api"; 91 const isDraw = ref(false) 92 let image = new Image(); 93 const { proxy } = getCurrentInstance() as any; 94 const btnList = ref([{ 95 name: '撤銷', 96 icon: 'aipaint-review-cancel', 97 id: 0, 98 visible: false, 99 }, 100 { 101 name: '畫筆', 102 icon: 'aipaint-review-paint', 103 id: 1, 104 visible: false, 105 }, 106 { 107 name: '上傳遮罩', 108 icon: 'aipaint-review-shadow', 109 id: 2, 110 visible: false 111 }, 112 { 113 name: '清除繪製', 114 icon: 'aipaint-review-clear', 115 id: 3, 116 visible: false, 117 }]) 118 const video: any = ref(''); 119 const props = defineProps({ 120 addDialog: { 121 type: Boolean, 122 default: false, 123 }, 124 title: { 125 default: '', 126 type: String 127 }, 128 reviewUrl: { 129 default: '', 130 type: String 131 } 132 }); 133 onMounted(() => { 134 135 136 // let canvas = document.getElementById("canvas"); 137 // canvas!.width = video.offsetWidth; 138 // canvas!.height = video.offsetHeight; 139 140 }) 141 const emit = defineEmits(["update:addDialog", 'getImgUrl']); 142 const closeDialog = () => { 143 btnList.value.map((item) => { 144 item.visible = false; 145 }) 146 emit("update:addDialog", false); 147 }; 148 const updateNum = (val: any) => { 149 size.value = val / 10 150 } 151 const beforeUpload = (file: any) => { 152 // 檢查檔案型別是否為JPEG或PNG 153 const isJPG = ["image/jpeg", "image/png", "image/jpg"].includes(file.type); 154 if (!isJPG) { 155 // 如果檔案型別不是JPEG或PNG,顯示錯誤提示訊息 156 ElMessage({ 157 message: "上傳圖片只能是JPG/png格式!", 158 type: "error", 159 }); 160 } 161 // 返回檔案型別檢查結果,決定是否允許上傳 162 return isJPG; 163 }; 164 const uploadShadowPic = async (event: any) => { 165 const file = event.target.files[0]; 166 if (beforeUpload(file)) { 167 const data: any = await upLoadFile({ icon: file }); 168 169 console.log(data); 170 if (data.status == 1) { 171 172 const url = data.file_path; 173 emit('getImgUrl', url); 174 ElMessage({ 175 message: "上傳成功", 176 type: "success", 177 }) 178 closeDialog() 179 // picMadeUrl.value = data.file_path; 180 } 181 } 182 } 183 // const finishReview = () => { 184 // const tempCanvas = document.createElement('canvas'); 185 // tempCanvas.width = imgWidth.value; 186 // tempCanvas.height = imgHeight.value; 187 // const tempCtx = tempCanvas.getContext('2d'); 188 // console.log(tempCtx,'ccc') 189 // const backgroundImage = new Image(); 190 // console.log(backgroundImage) 191 // backgroundImage.src = '../../../../../assets/function/aipaint/template.png' ; 192 // // backgroundImage.onload = () => { 193 // // console.log(tempCtx,'ggg') 194 // // tempCtx!.drawImage(backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height, 0, 0, imgWidth.value, imgHeight.value); 195 // // } 196 // tempCtx!.drawImage(backgroundImage, startX.value, startY.value, imgWidth.value, imgHeight.value); 197 // // tempCtx!.drawImage(canvas.value, 0, 0, canvas.value?.width || 0, canvas.value?.height || 0); 198 199 // tempCtx!.drawImage(canvas.value, startX.value, startY.value, imgWidth.value, imgHeight.value, 0, 0, imgWidth.value, imgHeight.value); 200 // const dataURL = tempCanvas.toDataURL('image/png'); 201 // const url = getBase64Url(dataURL) 202 // 203 // // closeDialog() 204 // } 205 const finishReview = () => { 206 const bgCanvas = backgroundCanvas.value; 207 208 const finalCanvas = document.createElement('canvas'); 209 finalCanvas.width = bgCanvas.width; 210 finalCanvas.height = bgCanvas.height; 211 const finalCtx = finalCanvas.getContext('2d'); 212 213 // 繪製背景 214 // finalCtx!.drawImage(bgCanvas, 0, 0); 215 // 繪製使用者繪製的內容 216 finalCtx!.fillStyle = 'black'; 217 finalCtx!.fillRect(0, 0, finalCanvas.width, finalCanvas.height); 218 finalCtx!.drawImage(drawingCanvas.value, startX.value, startY.value, imgWidth.value, imgHeight.value); 219 220 // 儲存最終結果 221 const dataURL = finalCanvas.toDataURL('image/png'); 222 const file = base64ToFile(dataURL, 'drawing.png'); 223 console.log(dataURL) 224 uploadFiles(file); 225 clearCanvas() 226 // const url = getBase64Url(dataURL); 227 // console.log(url) 228 229 // emit('getImgUrl', url) 230 231 } 232 const uploadFiles = async (file: any) => { 233 // beforeFileUpload 234 235 const data: any = await upLoadFile({ icon: file }); 236 if (data.status == 1) { 237 const url = data.file_path; 238 emit('getImgUrl', url); 239 closeDialog() 240 } 241 // if (beforeUpload(file)) { 242 // const data: any = await upLoadFile({ icon: file }); 243 244 // console.log(data); 245 // if (data.status == 1) { 246 // reviewUrl.value = data.file_path; 247 // reWriteRef.value.initData(reviewUrl.value); 248 // showRewrite.value = true; 249 // } 250 // } 251 }; 252 function base64ToFile(base64: any, fileName: any) { 253 // 將base64按照 , 進行分割 將字首 與後續內容分隔開 254 let data = base64.split(','); 255 // 利用正規表示式 從字首中獲取圖片的型別資訊(image/png、image/jpeg、image/webp等) 256 let type = data[0].match(/:(.*?);/)[1]; 257 // 從圖片的型別資訊中 獲取具體的檔案格式字尾(png、jpeg、webp) 258 let suffix = type.split('/')[1]; 259 // 使用atob()對base64資料進行解碼 結果是一個檔案資料流 以字串的格式輸出 260 const bstr = window.atob(data[1]); 261 // 獲取解碼結果字串的長度 262 let n = bstr.length 263 // 根據解碼結果字串的長度建立一個等長的整形數字陣列 264 // 但在建立時 所有元素初始值都為 0 265 const u8arr = new Uint8Array(n) 266 // 將整形陣列的每個元素填充為解碼結果字串對應位置字元的UTF-16 編碼單元 267 while (n--) { 268 // charCodeAt():獲取給定索引處字元對應的 UTF-16 程式碼單元 269 u8arr[n] = bstr.charCodeAt(n) 270 } 271 // 利用建構函式建立File檔案物件 272 // new File(bits, name, options) 273 const file = new File([u8arr], `${fileName}.${suffix}`, { 274 type: type 275 }) 276 // 將File檔案物件返回給方法的呼叫者 277 return file; 278 } 279 280 const getBase64Url = (pic: any) => { 281 const blob = base64ImgtoFile(pic) 282 const blobUrl = window.URL.createObjectURL(blob); 283 console.log(blobUrl) 284 return blobUrl 285 286 } 287 function base64ImgtoFile(dataurl: any, filename = 'file') { 288 //將base64格式分割:['data:image/png;base64','XXXX'] 289 const arr = dataurl.split(',') 290 // .*? 表示匹配任意字元到下一個符合條件的字元 剛好匹配到: 291 // image/png 292 const mime = arr[0].match(/:(.*?);/)[1] //image/png 293 //[image,png] 獲取圖片型別字尾 294 const suffix = mime.split('/')[1] //png 295 const bstr = atob(arr[1]) //atob() 方法用於解碼使用 base-64 編碼的字串 296 let n = bstr.length 297 const u8arr = new Uint8Array(n) 298 while (n--) { 299 u8arr[n] = bstr.charCodeAt(n) 300 } 301 return new File([u8arr], `${filename}.${suffix}`, { 302 type: mime 303 }) 304 } 305 const changeMenu = (item: any) => { 306 if (item.id == 1 || item.id == 2) { 307 currentKey.value = item.id 308 } 309 if (item.id == 3) { 310 clearCanvas() 311 initData(props.reviewUrl) 312 } 313 if (item.id == 1) { 314 // drawing.value = true; 315 isDraw.value = true; 316 317 item.visible = true; 318 } else { 319 btnList.value.map((item) => { 320 item.visible = false; 321 }) 322 } 323 if (item.id == 0) { 324 isDraw.value = false; 325 currentKey.value = 100; 326 327 undo() 328 } else if (item.id == 2) { 329 console.log('xxxx') 330 shadowInput.value.click(); 331 } 332 } 333 const initData = (val: any) => { 334 335 336 nextTick(() => { 337 console.log(proxy.$loginUrl + val) 338 if (backgroundCanvas.value) { 339 bgCtx.value = backgroundCanvas.value.getContext('2d'); 340 341 image.src = proxy.$loginUrl + val; 342 console.log(image) 343 image.onload = () => { 344 345 if (image.width < image.height) { 346 imgWidth.value = image.width * (backgroundCanvas.value!.height / image.height); 347 imgHeight.value = image.height * (backgroundCanvas.value!.height / image.height); 348 } else { 349 imgWidth.value = image.width * (backgroundCanvas.value!.width / image.width); 350 imgHeight.value = image.height * (backgroundCanvas.value!.width / image.width); 351 } 352 353 354 startX.value = (backgroundCanvas.value!.width - imgWidth.value) / 2; 355 startY.value = (backgroundCanvas.value!.height - imgHeight.value) / 2; 356 // canvas.value!.width = imgWidth; 357 // canvas.value!.height = imgHeight; 358 bgCtx.value!.drawImage(image, startX.value, startY.value, imgWidth.value, imgHeight.value); 359 }; 360 } 361 362 }) 363 } 364 const startDrawing = (event: MouseEvent) => { 365 ctx.value = drawingCanvas.value.getContext('2d'); 366 ctx.value.fillStyle = 'white'; 367 368 if (isDraw.value) { 369 drawing.value = true; 370 backFlag.value = true; 371 ctx.value!.beginPath(); 372 ctx.value!.moveTo(event.offsetX, event.offsetY); 373 let obj = ctx.value!.getImageData(0, 0, drawingCanvas.value!.width, drawingCanvas.value!.height); 374 imgs.value.push(obj) 375 } 376 377 378 379 380 }; 381 382 const undo = () => { 383 if (backFlag.value) { 384 let img = imgs.value.pop() 385 ctx.value!.putImageData(img, 0, 0); 386 // 撤銷按鈕的樣式 387 if (imgs.value.length > 0) { 388 389 backFlag.value = true 390 } else { 391 392 backFlag.value = false 393 } 394 } 395 396 397 }; 398 const draw = (event: MouseEvent) => { 399 if (!drawing.value) return; 400 401 ctx.value!.lineWidth = size.value; 402 // ctx.value!.strokeStyle = color.value; 403 ctx.value!.strokeStyle = '#FFFFFF'; 404 ctx.value!.lineTo(event.offsetX, event.offsetY); 405 ctx.value!.stroke(); 406 }; 407 408 const stopDrawing = () => { 409 drawing.value = false; 410 ctx.value!.closePath(); 411 412 }; 413 414 const clearCanvas = () => { 415 ctx.value!.clearRect(0, 0, drawingCanvas.value!.width, drawingCanvas.value!.height) 416 bgCtx.value.clearRect(0, 0, backgroundCanvas.value!.width, backgroundCanvas.value!.height) 417 } 418 defineExpose({ 419 initData, 420 421 }) 422 423 </script> 424 <style lang="scss"> 425 .el-text { 426 width: 100%; 427 max-width: 928px; 428 // min-width: 800px; 429 // min-height: 24px; 430 // margin-left: 64px; 431 border: none !important; 432 --el-input-border-color: none !important; 433 --el-input-focus-border: none !important; 434 --el-input-border: none !important; 435 --el-input-focus-border-color: none !important; 436 --el-input-hover-border-color: none !important; 437 --el-input-clear-hover-color: none !important; 438 border-radius: 0px 0px 8px 8px; 439 440 .el-textarea__inner { 441 // height: auto !important; 442 // min-height: 24px !important; 443 font-size: 14px; 444 padding: 8px 8px 8px 8px; 445 446 min-height: 240px !important; 447 // max-height: 150px !important; 448 line-height: 24px; 449 background-color: rgba(0, 0, 0, 0.03); 450 // line-height: 56px !important; 451 border: none; 452 border: 1px solid rgba(0, 0, 0, 0.01); 453 border-radius: 8px; 454 // border-radius: 8px; 455 } 456 } 457 </style> 458 <style lang="scss" scoped> 459 .canvascontainer { 460 max-width: 600px; 461 max-height: 600px; 462 position: relative; 463 464 } 465 466 canvas { 467 position: absolute; 468 top: 50%; 469 left: 50%; 470 471 transform: translate(-50%, -50%); 472 display: block; 473 } 474 475 .my-header { 476 display: flex; 477 flex-direction: row; 478 align-items: center; 479 // justify-content: space-between; 480 gap: 24px; 481 padding: 0px 0px 12px 0px; 482 margin-left: 12px; 483 border-bottom: 1px solid #eee; 484 485 &-title { 486 font-size: 14px; 487 color: #1f1f1f; 488 position: relative; 489 display: flex; 490 491 font-size: 20px; 492 493 494 span { 495 position: relative; 496 z-index: 100; 497 498 } 499 500 } 501 502 &-title:after { 503 content: ""; 504 position: absolute; 505 bottom: 0px; 506 width: 100%; 507 left: 0px; 508 height: 8px; 509 background: linear-gradient(to right, 510 rgba(133, 232, 34, 1), 511 rgba(133, 232, 34, 0)); 512 } 513 514 p { 515 color: #797979; 516 font-size: 14px; 517 } 518 } 519 520 .my-content { 521 // display: flex; 522 // gap: 16px; 523 padding: 0px 10px; 524 525 &-menu { 526 display: flex; 527 gap: 16px; 528 justify-content: space-between; 529 align-items: center; 530 width: 100%; 531 532 &-item { 533 width: 25%; 534 height: 40px; 535 display: flex; 536 justify-content: center; 537 align-items: center; 538 gap: 4px; 539 background-color: rgba(0, 0, 0, 0.03); 540 font-size: 14px; 541 border-radius: 4px; 542 color: #4c4c4c; 543 cursor: pointer; 544 545 &-icon { 546 width: 16px; 547 height: 16px; 548 // color:#4c4c4c; 549 } 550 } 551 552 &-item:hover { 553 background-color: rgba(133, 232, 34, 0.1); 554 color: #65C115; 555 556 } 557 558 .active { 559 background-color: rgba(133, 232, 34, 0.1); 560 color: #65C115; 561 } 562 } 563 564 &-image { 565 width: 100%; 566 height: 600px; 567 margin-top: 16px; 568 border: 1px solid #eee; 569 border-radius: 8px; 570 display: flex; 571 justify-content: center; 572 align-items: center; 573 574 &-canvas { 575 max-width: 600px; 576 max-height: 600px; 577 578 } 579 580 581 582 583 } 584 } 585 586 .my-button { 587 display: flex; 588 flex-direction: row; 589 justify-content: center; 590 padding: 16px 0px 0px 0px; 591 gap: 16px; 592 font-size: 14px; 593 color: #1f1f1f; 594 595 &-left { 596 // width: 597 background-color: #F2F3F6; 598 border-radius: 4px; 599 width: 288px; 600 height: 40px; 601 text-align: center; 602 line-height: 40px; 603 cursor: pointer; 604 } 605 606 &-right { 607 background-color: #85E822; 608 border-radius: 4px; 609 width: 288px; 610 height: 40px; 611 line-height: 40px; 612 text-align: center; 613 cursor: pointer; 614 } 615 } 616 </style>