Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
2026-03-27 09:57:46 +08:00
18 changed files with 951 additions and 103 deletions

View File

@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 0C12.0125 0 12.8333 0.820811 12.8333 1.83333V9.16667H20.1667C21.1792 9.16667 22 9.98748 22 11C22 12.0125 21.1792 12.8333 20.1667 12.8333H12.8333V20.1667C12.8333 21.1792 12.0125 22 11 22C9.98748 22 9.16667 21.1792 9.16667 20.1667V12.8333H1.83333C0.820811 12.8333 0 12.0125 0 11C0 9.98748 0.820811 9.16667 1.83333 9.16667H9.16667V1.83333C9.16667 0.820811 9.98748 0 11 0Z" fill="#0D0D0D"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

View File

@@ -0,0 +1,3 @@
<svg width="22" height="4" viewBox="0 0 22 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 2C0 0.89543 0.820811 0 1.83333 0H20.1667C21.1792 0 22 0.89543 22 2C22 3.10457 21.1792 4 20.1667 4H1.83333C0.820811 4 0 3.10457 0 2Z" fill="#0D0D0D"/>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 0C5.41421 0 5.75 0.335786 5.75 0.75V4.25H9.25C9.66421 4.25 10 4.58579 10 5C10 5.41421 9.66421 5.75 9.25 5.75H5.75V9.25C5.75 9.66421 5.41421 10 5 10C4.58579 10 4.25 9.66421 4.25 9.25V5.75H0.75C0.335786 5.75 0 5.41421 0 5C0 4.58579 0.335786 4.25 0.75 4.25H4.25V0.75C4.25 0.335786 4.58579 0 5 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

View File

@@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.02518 0C1.31675 0 1.55311 0.23564 1.55311 0.526316V1.35367C2.44992 0.514522 3.65683 0 4.98466 0C7.75456 0 10 2.23858 10 5C10 7.76142 7.75456 10 4.98466 10C2.41401 10 0.295731 8.07235 0.00363895 5.58758C-0.0302986 5.29888 0.176946 5.03741 0.466532 5.00358C0.756118 4.96974 1.01839 5.17635 1.05232 5.46505C1.28281 7.42573 2.95587 8.94737 4.98466 8.94737C7.17142 8.94737 8.94414 7.18007 8.94414 5C8.94414 2.81993 7.17142 1.05263 4.98466 1.05263C3.81245 1.05263 2.7587 1.56036 2.03329 2.36842H3.41571C3.70728 2.36842 3.94364 2.60406 3.94364 2.89474C3.94364 3.18541 3.70728 3.42105 3.41571 3.42105H1.02518C0.740438 3.42105 0.508345 3.19631 0.497639 2.91505C0.497164 2.90406 0.497034 2.89304 0.497254 2.882V0.526316C0.497254 0.23564 0.733616 0 1.02518 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 880 B

View File

@@ -86,7 +86,7 @@
width: 46.69rem; width: 46.69rem;
height: 56.6rem; height: 56.6rem;
flex-shrink: 0; flex-shrink: 0;
// background-color: #fff; background-color: #fff;
box-shadow: 0px 19.44px 27.22px 0px #0000000d; box-shadow: 0px 19.44px 27.22px 0px #0000000d;
border: 1px solid; border: 1px solid;
border-image-source: linear-gradient( border-image-source: linear-gradient(

View File

@@ -10,8 +10,14 @@
<span class="icon"><svg-icon :name="item.name" size="16" /></span> <span class="icon"><svg-icon :name="item.name" size="16" /></span>
<span class="label">{{ item.label }}</span> <span class="label">{{ item.label }}</span>
</div> </div>
<button @click="onCreate">创建</button> <button @click="onCreate">
<button @click="onReset">重置</button> <span class="icon"><svg-icon name="dc-create" size="12" /></span>
<span class="text">{{ $t('DepthCanvas.create') }}</span>
</button>
<button @click="onReset">
<span class="icon"><svg-icon name="dc-reset" size="12" /></span>
<span class="text">{{ $t('DepthCanvas.reset') }}</span>
</button>
</div> </div>
</transition> </transition>
<brush-control-panel v-if="show" :currentTool="currentTool2" style="top: 14rem" /> <brush-control-panel v-if="show" :currentTool="currentTool2" style="top: 14rem" />
@@ -114,5 +120,22 @@
background: rgba(235, 235, 235, 0.9); background: rgba(235, 235, 235, 0.9);
} }
} }
> button {
cursor: pointer;
border: none;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
background-color: rgba(13, 13, 13, 1);
color: #fff;
font-size: 1.2rem;
border-radius: 0.4rem;
padding: 0 0.8rem;
gap: 0.8rem;
&:active {
opacity: 0.8;
}
}
} }
</style> </style>

View File

@@ -5,7 +5,9 @@
<svg-icon :name="icon" :size="iconSize" /> <svg-icon :name="icon" :size="iconSize" />
</span> </span>
<span v-show="before" class="before">{{ before }}</span> <span v-show="before" class="before">{{ before }}</span>
<input <input
v-if="!isColor"
v-bind="attrs" v-bind="attrs"
:value="modelValue" :value="modelValue"
@input="onInput" @input="onInput"
@@ -13,15 +15,17 @@
@copy.stop @copy.stop
@keydown.stop @keydown.stop
/> />
<input <template v-else>
v-if="isColor" <input
readonly v-bind="attrs"
type="text" type="color"
:value="colorObj.color" :value="colorObj.color"
@copy.stop @input="onInput"
@keydown.stop @change="onChange"
/> @copy.stop
<template v-if="isColor"> @keydown.stop
/>
<input readonly :value="colorObj.color" @copy.stop @keydown.stop />
<span class="decorate marginl"></span> <span class="decorate marginl"></span>
<input <input
class="alpha" class="alpha"
@@ -35,6 +39,7 @@
@keydown.stop @keydown.stop
/> />
</template> </template>
<span v-show="after" class="after">{{ after }}</span> <span v-show="after" class="after">{{ after }}</span>
</div> </div>
</template> </template>

View File

@@ -221,12 +221,13 @@ export class AISelectboxToolManager {
if (!this.demoObject) return if (!this.demoObject) return
const fobject = this.demoObject const fobject = this.demoObject
this.clearDemoObject() this.clearDemoObject()
const canvas = getObjectAlphaToCanvas(fobject, null, 0, { r: 255, g: 0, b: 0, a: 255 }); const tcanvas = await this.createStaticCanvas(fobject)
const canvas = getObjectAlphaToCanvas(tcanvas, null, 0, { r: 255, g: 0, b: 0, a: 255 });
const arr = traceImageContour(canvas);
const scaleY = fobject.scaleY const scaleY = fobject.scaleY
const scaleX = fobject.scaleX const scaleX = fobject.scaleX
const top = fobject.top const top = fobject.top
const left = fobject.left const left = fobject.left
const arr = traceImageContour(canvas);
let minX = fobject.width; let minX = fobject.width;
let minY = fobject.height; let minY = fobject.height;
const str = arr.map((v) => { const str = arr.map((v) => {

View File

@@ -136,10 +136,11 @@ export class LayerManager {
copyLayerById(id) { copyLayerById(id) {
const object = this.canvasManager.getObjectById(id) const object = this.canvasManager.getObjectById(id)
if (!object) return console.warn('复制图层失败对象不存在ID:', id) if (!object) return console.warn('复制图层失败对象不存在ID:', id)
this.canvasManager.discardActiveObject()
cloneObjects([object]).then(objects => { cloneObjects([object]).then(objects => {
const newObject = objects[0] const newObject = objects[0]
const info = JSON.parse(JSON.stringify(newObject.info)) const info = JSON.parse(JSON.stringify(newObject.info))
info.id = createId("image") info.id = createId("copylayer")
// info.name = info.name // info.name = info.name
newObject.set({ newObject.set({
top: newObject.top + 15, top: newObject.top + 15,

View File

@@ -753,6 +753,23 @@ export class CanvasEventManager {
this.canvas.on("object:removed", (e) => { this.canvas.on("object:removed", (e) => {
// updateLayers(e); // updateLayers(e);
}); });
this.canvas.on("erasing:start", (e) => {
// console.log("erasing:start", e);
});
this.canvas.on("erasing:end", async (e) => {
// console.log("erasing:end", e);
const targets = e.targets;
var isRecord = false;
for (let i = 0; i < targets.length; i++) {
const target = targets[i];
const id = target?.info?.id;
if (id) {
isRecord = true;
await this.layerManager.updateLayerThumbnailsById(id)
}
}
if (isRecord) this.stateManager.recordState();
});
} }
setupDoubleClickEvents() { setupDoubleClickEvents() {
// 双击处理 // 双击处理

View File

@@ -1,7 +1,12 @@
<template> <template>
<div class="my-select"> <div class="my-select">
<el-select :model-value="modelValue" @change="onChange" v-bind="attrs"> <el-select :model-value="modelValue" @change="onChange" v-bind="attrs">
<el-option v-for="v in list" :key="v.value" :label="v.label" :value="v.value" /> <el-option v-for="v in list" :key="v.value" :label="v.label" :value="v.value" >
<slot name="option" :item="v">
<!-- 默认内容 -->
<span>{{ v.label }}</span>
</slot>
</el-option>
</el-select> </el-select>
</div> </div>
</template> </template>

View File

@@ -3,7 +3,11 @@
<input class="color" type="color" ref="colorInput" @change="changeColor" /> <input class="color" type="color" ref="colorInput" @change="changeColor" />
<div class="interval"></div> <div class="interval"></div>
<div class="fontFamily"> <div class="fontFamily">
<my-select v-model="textStyle['--font-family']" @change="changeFontFamily" :list="fontFamilyList[locale]" /> <my-select v-model="textStyle['--font-family']" @change="changeFontFamily" :list="fontFamilyList[locale]" >
<template #option="{ item }">
<span :style="{'font-family': item.value,}">{{ item.label }}</span>
</template>
</my-select>
</div> </div>
<div class="interval"></div> <div class="interval"></div>
<div class="size"> <div class="size">

View File

@@ -62,6 +62,7 @@
<threeModel :currentData="currentData" /> <threeModel :currentData="currentData" />
</template> </template>
</baseModal> </baseModal>
<Assistant />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -69,6 +70,7 @@
import { computed, ref, watch, onMounted, nextTick, provide, onBeforeUnmount } from 'vue' import { computed, ref, watch, onMounted, nextTick, provide, onBeforeUnmount } from 'vue'
import { useLayout } from '@/utils/treeDiagram' import { useLayout } from '@/utils/treeDiagram'
import { NODE_TYPE, NODE_COMPONENT } from './tools/index.d' import { NODE_TYPE, NODE_COMPONENT } from './tools/index.d'
import Assistant from '@/components/Assistant/assistant.vue'
// 组件 // 组件
import headerTools from './components/header-tools.vue' import headerTools from './components/header-tools.vue'
import zoom from '../components/zoom.vue' import zoom from '../components/zoom.vue'

View File

@@ -78,6 +78,24 @@ export const base64Tofile = (base64: string,name: string) => {
const file = new File([blob], name, { type: mime }) const file = new File([blob], name, { type: mime })
return file return file
} }
/** 二进制转base64 */
/**
* File 对象转 Base64
* @param {File} file - 文件对象
* @returns {Promise<string>} Base64 字符串
*/
export const fileToBase64 = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
resolve(reader.result) // Base64 字符串
}
reader.onerror = (error) => {
reject(error)
}
reader.readAsDataURL(file)
})
}
//获取当前时间2026-03-20 11:38:29 //获取当前时间2026-03-20 11:38:29
export const getCurrentTime = () => { export const getCurrentTime = () => {
const now = new Date() const now = new Date()

View File

@@ -0,0 +1,738 @@
<template>
<el-dialog
class="clipDialog"
v-model="showPanel"
align-center
:show-close="false"
width="75rem"
height="69rem"
style="border-radius: 2rem; padding: 4rem; --el-dialog-padding-primary: 1rem"
>
<template #header="{ close }">
<div class="clip-header">
<div class="title">{{ $t("clipDialog.title") }}</div>
<span class="close" @click="close">
<svg-icon name="close" size="12" color="#000" />
</span>
</div>
</template>
<div class="crop-image-modal">
<div class="modal-content">
<div class="clip">
<!-- 图片剪切 -->
<div class="image-clip" ref="el" v-if="data.url">
<div
class="box"
ref="box"
:style="{
width: clipData.img_width + 'px',
height: clipData.img_height + 'px',
}"
>
<img :src="data.url" />
<div class="shade"></div>
<div
ref="clipRef"
class="clip"
:style="{
top: clipData.top + 'px',
left: clipData.left + 'px',
width: clipData.width + 'px',
height: clipData.height + 'px',
}"
@mousedown.stop="clipMousedown"
@touchstart.stop="clipMousedown"
>
<div class="img">
<img
:src="data.url"
:style="{
width: clipData.img_width + 'px',
height: clipData.img_height + 'px',
top: -clipData.top + 'px',
left: -clipData.left + 'px',
}"
/>
</div>
<div
class="top"
@mousedown.stop="topMousedown"
@touchstart.stop="topMousedown"
></div>
<div
class="right"
@mousedown.stop="rightMousedown"
@touchstart.stop="rightMousedown"
></div>
<div
class="bottom"
@mousedown.stop="bottomMousedown"
@touchstart.stop="bottomMousedown"
></div>
<div
class="left"
@mousedown.stop="leftMousedown"
@touchstart.stop="leftMousedown"
></div>
<span
class="top"
@mousedown.stop="topMousedown"
@touchstart.stop="topMousedown"
></span>
<span
class="right"
@mousedown.stop="rightMousedown"
@touchstart.stop="rightMousedown"
></span>
<span
class="bottom"
@mousedown.stop="bottomMousedown"
@touchstart.stop="bottomMousedown"
></span>
<span
class="left"
@mousedown.stop="leftMousedown"
@touchstart.stop="leftMousedown"
></span>
</div>
</div>
</div>
<!-- 空状态 -->
<!-- <div class="empty-state" v-else>
<div class="empty-icon">📷</div>
<p></p>
</div> -->
</div>
</div>
<!-- <div class="operation">
<div class="operation-btn">
<svg-icon name="clipMinus" size="22" color="#000" />
</div>
<div class="slider">
<el-slider v-model="clipSize" />
</div>
<div class="operation-btn">
<svg-icon name="clipAdd" size="22" color="#000" />
</div>
</div> -->
<div class="modal-footer">
<div class="image-count" @click="close">
{{ $t("clipDialog.cancel") }}
</div>
<div class="image-submit" @click="confirm">
{{ $t("clipDialog.confirm") }}
</div>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import {
ref,
computed,
onDeactivated,
reactive,
nextTick,
} from "vue";
import { useI18n } from "vue-i18n";
// Props
const props = defineProps(
{
isRatio: {
type: Boolean,
default: true,
}
}
);
const { t } = useI18n();
const clipSize = ref(0);
var resolveFn: (value: string | PromiseLike<string>) => void;
const showPanel = ref(false);
const open = (url: string) => {
showPanel.value = true;
setImgage(url);
return new Promise((resolve) => (resolveFn = resolve));
};
const close = () => {
showPanel.value = false;
};
//提交选中的T图片
const confirm = () => {
const base64 = getImageBase64();
resolveFn && resolveFn(base64);
close();
};
const data = reactive({
url: "",
});
const el = ref();
const box = ref();
const clipRef = ref();
const clipData = reactive({
top: 0,
left: 0,
width: 0,
height: 0,
img_width: 0,
img_height: 0,
});
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const reload = () => {
if (!el.value) return;
const size = Math.min(
1,
el.value.offsetWidth / canvas.width,
el.value.offsetHeight / canvas.height
);
const width = size * canvas.width;
const height = size * canvas.height;
clipSize.value = Math.min(width, height);
clipData.left = 0;
clipData.top = 0;
clipData.width = props.isRatio ? clipSize.value : width;
clipData.height = props.isRatio ? clipSize.value : height;
clipData.img_width = width;
clipData.img_height = height;
};
window.addEventListener("resize", reload);
onDeactivated(() => {
window.removeEventListener("resize", reload);
});
const setImgage = (url: string) => {
if (!url) return;
const img = new Image();
img.onload = () => {
const width = img.width;
const height = img.height;
canvas.width = width;
canvas.height = height;
ctx?.clearRect(0, 0, width, height);
ctx?.drawImage(img, 0, 0, width, height);
nextTick(() => reload());
};
img.src = url;
data.url = url;
};
const canvasImageDataToBase64 = (imageData: any) => {
// 创建一个临时的canvas元素
const canvas = document.createElement("canvas");
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext("2d");
ctx?.putImageData(imageData, 0, 0);
const dataURL = canvas.toDataURL("image/png");
return dataURL;
};
const getImageBase64 = () => {
const scale = canvas.width / clipData.img_width;
const imageData = ctx?.getImageData(
clipData.left * scale,
clipData.top * scale,
clipData.width * scale,
clipData.height * scale
);
const base64 = canvasImageDataToBase64(imageData);
return base64;
};
// 移动裁剪框
const clipMousedown = (e: MouseEvent | TouchEvent) => {
e.preventDefault();
var pageX = 0;
var pageY = 0;
if (e.type == "touchstart") {
const touch = e["touches"][0];
pageX = touch.pageX;
pageY = touch.pageY;
} else {
pageX = e["pageX"];
pageY = e["pageY"];
}
const elInfo = box.value.getBoundingClientRect();
const clipInfo = clipRef.value.getBoundingClientRect();
// 鼠标相对元素的位置
const CX = pageX - clipInfo.x;
const CY = pageY - clipInfo.y;
const mousemove = (e: MouseEvent | TouchEvent) => {
e.preventDefault();
var pageX = 0;
var pageY = 0;
if (e.type == "touchmove") {
const touch = e["touches"][0];
pageX = touch.pageX;
pageY = touch.pageY;
} else {
pageX = e["pageX"];
pageY = e["pageY"];
}
let x = pageX - elInfo.x - CX;
let y = pageY - elInfo.y - CY;
if (x < 0) {
x = 0;
} else if (x > elInfo.width - clipInfo.width) {
x = elInfo.width - clipInfo.width;
}
if (y < 0) {
y = 0;
} else if (y > elInfo.height - clipInfo.height) {
y = elInfo.height - clipInfo.height;
}
clipData.top = y;
clipData.left = x;
clipData.img_width = elInfo.width;
clipData.img_height = elInfo.height;
};
const mouseup = () => {
window.removeEventListener("mousemove", mousemove);
window.removeEventListener("mouseup", mouseup);
window.removeEventListener("touchmove", mousemove);
window.removeEventListener("touchend", mouseup);
};
if (e.type == "touchstart") {
window.addEventListener("touchmove", mousemove);
window.addEventListener("touchend", mouseup);
} else {
window.addEventListener("mousemove", mousemove);
window.addEventListener("mouseup", mouseup);
}
};
// 移动裁剪框的四个边框
const mousedown = (
e: MouseEvent | TouchEvent,
type: "top" | "bottom" | "right" | "left"
) => {
const minWidth = 20;
const minHeight = 20;
const elInfo = box.value.getBoundingClientRect();
const R = elInfo.width - clipData.left - clipData.width;
const B = elInfo.height - clipData.top - clipData.height;
const noRatioMousemove = (e: MouseEvent | TouchEven)=>{
var x = 0;
var y = 0;
if (e.type == "touchmove") {
const touch = e["touches"][0];
x = touch.pageX - elInfo.x;
y = touch.pageY - elInfo.y;
} else {
x = e["pageX"] - elInfo.x;
y = e["pageY"] - elInfo.y;
}
if (type == "right") {
let width = x - clipData.left;
if (width + clipData.left > elInfo.width) {
width = elInfo.width - clipData.left;
}
if (width < minWidth) width = minWidth;
clipData.width = width;
} else if (type == "bottom") {
let height = y - clipData.top;
if (height + clipData.top > elInfo.height) {
height = elInfo.height - clipData.top;
}
if (height < minHeight) height = minHeight;
clipData.height = height;
} else if (type == "left") {
let left = x;
let width = clipData.width;
if (left < 0) {
left = 0;
} else if (left > elInfo.width - R - minWidth) {
left = elInfo.width - R - minWidth;
}
width = elInfo.width - R - left;
clipData.left = left;
clipData.width = width;
clipData.img_width = elInfo.width;
clipData.img_height = elInfo.height;
} else if (type == "top") {
let top = y;
let height = clipData.height;
if (top < 0) {
top = 0;
} else if (top > elInfo.height - B - minHeight) {
top = elInfo.height - B - minHeight;
}
height = elInfo.height - B - top;
clipData.top = top;
clipData.height = height;
clipData.img_width = elInfo.width;
}
}
const ratioMousemove = (e: MouseEvent | TouchEvent) => {
var x = 0;
var y = 0;
if (e.type == "touchmove") {
const touch = e["touches"][0];
x = touch.pageX - elInfo.x;
y = touch.pageY - elInfo.y;
} else {
x = e["pageX"] - elInfo.x;
y = e["pageY"] - elInfo.y;
}
if (type == "right" || type == "bottom") {
let width = x - clipData.left;
let height = y - clipData.top;
if (width + clipData.left > elInfo.width) {
width = elInfo.width - clipData.left;
}
if (height + clipData.top > elInfo.height) {
height = elInfo.height - clipData.top;
}
if (type == "right") {
if (width < minWidth) {
width = minWidth;
}
height = width;
if (height + clipData.top > elInfo.height) {
height = elInfo.height - clipData.top;
width = height;
}
} else if (type == "bottom") {
if (height < minHeight) {
height = minHeight;
}
width = height;
if (width + clipData.left > elInfo.width) {
width = elInfo.width - clipData.left;
height = width;
}
}
clipData.width = width;
clipData.height = height;
} else if (type == "left" || type == "top") {
let left = x;
let top = y;
let width = clipData.width;
let height = clipData.height;
if (type == "left") {
if (left < 0) {
left = 0;
} else if (left > elInfo.width - R - minWidth) {
left = elInfo.width - R - minWidth;
}
width = elInfo.width - R - left;
top = elInfo.height - B - width;
if (top < 0) {
top = 0;
width = height;
left = elInfo.width - R - width;
}
height = elInfo.height - B - top;
} else if (type == "top") {
if (top < 0) {
top = 0;
} else if (top > elInfo.height - B - minHeight) {
top = elInfo.height - B - minHeight;
}
height = elInfo.height - B - top;
left = elInfo.width - R - height;
if (left < 0) {
left = 0;
height = width;
top = elInfo.height - B - height;
}
width = elInfo.width - R - left;
}
clipData.top = top;
clipData.left = left;
clipData.width = width;
clipData.height = height;
clipData.img_width = elInfo.width;
clipData.img_height = elInfo.height;
}
}
const mousemove = (e: MouseEvent | TouchEvent) => {
if(props.isRatio){
ratioMousemove(e)
}else{
noRatioMousemove(e)
}
};
const mouseup = () => {
window.removeEventListener("mousemove", mousemove);
window.removeEventListener("mouseup", mouseup);
window.removeEventListener("touchmove", mousemove);
window.removeEventListener("touchend", mouseup);
};
if (e.type == "touchstart") {
window.addEventListener("touchmove", mousemove);
window.addEventListener("touchend", mouseup);
} else {
window.addEventListener("mousemove", mousemove);
window.addEventListener("mouseup", mouseup);
}
};
const topMousedown = (e: MouseEvent | TouchEvent) => {
mousedown(e, "top");
};
const rightMousedown = (e: MouseEvent | TouchEvent) => {
mousedown(e, "right");
};
const bottomMousedown = (e: MouseEvent | TouchEvent) => {
mousedown(e, "bottom");
};
const leftMousedown = (e: MouseEvent | TouchEvent) => {
mousedown(e, "left");
};
defineExpose({
open,
close,
});
</script>
<style scoped lang="less">
.clip-header {
margin-top: 0.2rem;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 3.3rem;
border-bottom: 0.1rem solid rgba(0, 0, 0, 0.1);
> .title {
font-family: Semibold;
font-size: 1.6rem;
color: #000;
}
.close{
cursor: pointer;
}
}
/* 弹窗主体 */
.crop-image-modal {
background-color: #fff;
display: flex;
flex-direction: column;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
line-height: 1;
opacity: 0.7;
transition: all 0.2s;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
opacity: 1;
color: #333;
transform: scale(1.1);
}
}
/* 弹窗内容 */
.modal-content {
// padding: 20px;
overflow: hidden;
background-color: #fff;
-webkit-overflow-scrolling: touch;
display: flex;
width: 47.2rem;
height: 35.20rem;
margin: 4rem auto;
> .clip{
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAGUExURf///9ra2sgNdccAAAARSURBVAjXY2A/wICMfjAgIwB8gwi84a8abQAAAABJRU5ErkJggg==);
flex: 1;
display: flex;
}
}
.operation{
display: flex;
align-items: center;
width: 52rem;
margin: 0 auto;
gap: 2rem;
> .operation-btn{
cursor: pointer;
}
> .slider{
flex: 1;
:deep(.el-slider){
--el-slider-main-bg-color: #000;
}
}
}
.image-clip {
user-select: none;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
> .box {
position: relative;
> img {
display: block;
width: 100%;
height: 100%;
}
> .shade {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
> .clip {
position: absolute;
cursor: move;
outline: 1px solid #39f;
> * {
position: absolute;
}
> .img {
pointer-events: none;
width: 100%;
height: 100%;
overflow: hidden;
> img {
position: absolute;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAGUExURf///9ra2sgNdccAAAARSURBVAjXY2A/wICMfjAgIwB8gwi84a8abQAAAABJRU5ErkJggg==);
}
}
> .top {
cursor: n-resize;
}
> .bottom {
cursor: s-resize;
}
> .right {
cursor: e-resize;
}
> .left {
cursor: w-resize;
}
--border: 4px;
--trbl: calc(0px - var(--border) / 2);
--ball-size: 8px;
--ball-trbl: calc(0px - var(--ball-size) / 2);
> div.top {
top: var(--trbl);
left: 0;
right: 0;
height: var(--border);
}
> div.bottom {
bottom: var(--trbl);
left: 0;
right: 0;
height: var(--border);
}
> div.left {
top: 0;
left: var(--trbl);
bottom: 0;
width: var(--border);
}
> div.right {
top: 0;
right: var(--trbl);
bottom: 0;
width: var(--border);
}
> span {
position: absolute;
width: var(--ball-size);
height: var(--ball-size);
border-radius: 50%;
background-color: #39f;
}
> span.top {
top: var(--ball-trbl);
left: 50%;
transform: translateX(-50%);
}
> span.right {
right: var(--ball-trbl);
top: 50%;
transform: translateY(-50%);
}
> span.bottom {
bottom: var(--ball-trbl);
left: 50%;
transform: translateX(-50%);
}
> span.left {
left: var(--ball-trbl);
top: 50%;
transform: translateY(-50%);
}
}
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
min-height: 300px;
flex: 1;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
p {
font-size: 16px;
margin: 0;
}
}
/* 弹窗底部 */
.modal-footer {
display: flex;
justify-content: flex-end;
align-items: center;
flex-shrink: 0;
gap: 1.6rem;
// margin-top: 6.6rem;
> div {
cursor: pointer;
width: 7.7rem;
text-align: center;
font-size: 1.2rem;
line-height: 2.8rem;
font-weight: 500;
border-radius: 2rem;
border: 1px solid #000;
}
.image-count{
background-color: #fff;
color: #000;
}
.image-submit{
background-color: #000;
color: #fff;
}
}
.image-count {
font-size: 14px;
color: #666;
font-weight: 500;
}
</style>

View File

@@ -204,45 +204,52 @@ export default {
download: 'Download' download: 'Download'
}, },
DepthCanvas: { DepthCanvas: {
layer: 'Layer', layer: "Layer",
editDetails: 'Edit Details', editDetails: "Edit Details",
export: 'Export', export: "Export",
save: 'Save', save: "Save",
workbench: 'Workbench', workbench: "Workbench",
position: 'Position', position: "Position",
size: 'Size', size: "Size",
appearance: 'Appearance', appearance: "Appearance",
opacity: 'Opacity', opacity: "Opacity",
cornerRadius: 'Cor Radius', cornerRadius: "Cor Radius",
strokeWidth: 'Stroke Width', strokeWidth: "Stroke Width",
color: 'Color', color: "Color",
image: 'Image', image: "Image",
settings: 'Settings', settings: "Settings",
rotation: 'Rotation', rotation: "Rotation",
scale: 'Scale', scale: "Scale",
gapX: 'Gap X', gapX: "Gap X",
gapY: 'Gap Y', gapY: "Gap Y",
offset: 'Offset', offset: "Offset",
emptyLayer: 'Empty Layer', emptyLayer: "Empty Layer",
aiGroupLayer: 'AI Group Layer', aiGroupLayer: "AI Group Layer",
textLayer: 'Text Layer', textLayer: "Text Layer",
rectLayer: 'Rect Layer', rectLayer: "Rect Layer",
lineLayer: 'Line Layer', lineLayer: "Line Layer",
ellipseLayer: 'Ellipse Layer', ellipseLayer: "Ellipse Layer",
triangleLayer: 'Triangle Layer', triangleLayer: "Triangle Layer",
starLayer: 'Star Layer', starLayer: "Star Layer",
arrowLayer: 'Arrow Layer', arrowLayer: "Arrow Layer",
imageLayer: 'Image Layer', imageLayer: "Image Layer",
mergeLayer: 'Merge Layer', mergeLayer: "Merge Layer",
rectangle: 'Rectangle', rectangle: "Rectangle",
line: 'Line', line: "Line",
arrow: 'Arrow', arrow: "Arrow",
ellipse: 'Ellipse', ellipse: "Ellipse",
triangle: 'Triangle', triangle: "Triangle",
star: 'Star', star: "Star",
add: 'Add', add: "Add",
remove: 'Remove', remove: "Remove",
brush: 'Brush', brush: "Brush",
erase: 'Erase' erase: "Erase",
create: "Create",
reset: "Reset"
},
clipDialog: {
title: 'Upload your profile photo',
cancel: 'Cancel',
confirm: 'Save'
} }
} }

View File

@@ -200,45 +200,52 @@ export default {
download: '下载' download: '下载'
}, },
DepthCanvas: { DepthCanvas: {
layer: '图层', layer: "图层",
editDetails: '编辑详情', editDetails: "编辑详情",
export: '导出', export: "导出",
save: '保存', save: "保存",
workbench: '工作台', workbench: "工作台",
position: '位置', position: "位置",
size: '大小', size: "大小",
appearance: '外观', appearance: "外观",
opacity: '透明度', opacity: "透明度",
cornerRadius: '圆角半径', cornerRadius: "圆角半径",
strokeWidth: '边框宽度', strokeWidth: "边框宽度",
color: '颜色', color: "颜色",
image: '图片', image: "图片",
settings: '设置', settings: "设置",
rotation: '旋转角度', rotation: "旋转角度",
scale: '缩放', scale: "缩放",
gapX: '水平间距', gapX: "水平间距",
gapY: '垂直间距', gapY: "垂直间距",
offset: '偏移量', offset: "偏移量",
emptyLayer: '空图层', emptyLayer: "空图层",
aiGroupLayer: '智能选区组', aiGroupLayer: "智能选区组",
textLayer: '文本图层', textLayer: "文本图层",
rectLayer: '矩形图层', rectLayer: "矩形图层",
lineLayer: '直线图层', lineLayer: "直线图层",
ellipseLayer: '椭圆图层', ellipseLayer: "椭圆图层",
triangleLayer: '三角形图层', triangleLayer: "三角形图层",
starLayer: '五角星图层', starLayer: "五角星图层",
arrowLayer: '箭头图层', arrowLayer: "箭头图层",
imageLayer: '图片图层', imageLayer: "图片图层",
mergeLayer: '合并图层', mergeLayer: "合并图层",
rectangle: '矩形', rectangle: "矩形",
line: '直线', line: "直线",
arrow: '箭头', arrow: "箭头",
ellipse: '椭圆', ellipse: "椭圆",
triangle: '三角形', triangle: "三角形",
star: '五角星', star: "五角星",
add: '添加', add: "添加",
remove: '删除', remove: "删除",
brush: '画笔', brush: "画笔",
erase: '擦除' erase: "擦除",
create: "创建",
reset: "重置"
},
clipDialog: {
title: '上传您的个人资料照片',
cancel: '取消',
confirm: '保存'
} }
} }

View File

@@ -31,6 +31,7 @@
<div class="label">{{ $t('Home.logoutDevice') }}</div> <div class="label">{{ $t('Home.logoutDevice') }}</div>
<button class="logout-btn" @click="logout">{{ $t('Home.logout') }}</button> <button class="logout-btn" @click="logout">{{ $t('Home.logout') }}</button>
</div> </div>
<clip-dialog ref="clipDialogRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -41,6 +42,8 @@
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { uploadImage } from '@/api/upload' import { uploadImage } from '@/api/upload'
import { UpdateUserAvatar, getAvatarLimit } from '@/api/user' import { UpdateUserAvatar, getAvatarLimit } from '@/api/user'
import clipDialog from '@/components/clipDialog.vue'
import { fileToBase64, base64Tofile } from '../../../components/Canvas/tools/tools'
const router = useRouter() const router = useRouter()
const { locale } = useI18n() const { locale } = useI18n()
const userInfoStore = useUserInfoStore() const userInfoStore = useUserInfoStore()
@@ -50,6 +53,7 @@
{ label: '中文', value: 'CHINESE_SIMPLIFIED' } { label: '中文', value: 'CHINESE_SIMPLIFIED' }
]) ])
const remainingNum = ref(0) const remainingNum = ref(0)
const clipDialogRef = ref<typeof clipDialog>()
const changeLang = (value: string) => { const changeLang = (value: string) => {
locale.value = value locale.value = value
localStorage.setItem('language', value) localStorage.setItem('language', value)
@@ -72,13 +76,17 @@
const input = document.createElement('input') const input = document.createElement('input')
input.type = 'file' input.type = 'file'
input.accept = 'image/png, image/jpeg, image/jpg' input.accept = 'image/png, image/jpeg, image/jpg'
input.addEventListener('change', (e) => { input.addEventListener('change', async (e) => {
const file = e.target.files[0] const file = e.target.files[0]
const formData = new FormData() let base64Img = await fileToBase64(file)
formData.append('avatar', file) clipDialogRef.value?.open(base64Img).then((base64: string)=>{
UpdateUserAvatar(formData).then((res) => { const fileData = base64Tofile(base64,file.name)
userInfoStore.updateUserInfo({avatar: res}) const formData = new FormData()
getAvatarLimitNum() formData.append('avatar', fileData)
UpdateUserAvatar(formData).then((res) => {
userInfoStore.updateUserInfo({avatar: res})
getAvatarLimitNum()
})
}) })
}) })
input.click() input.click()