Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front
This commit is contained in:
3
src/assets/icons/clipAdd.svg
Normal file
3
src/assets/icons/clipAdd.svg
Normal 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 |
3
src/assets/icons/clipMinus.svg
Normal file
3
src/assets/icons/clipMinus.svg
Normal 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 |
3
src/assets/icons/dc/create.svg
Normal file
3
src/assets/icons/dc/create.svg
Normal 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 |
3
src/assets/icons/dc/reset.svg
Normal file
3
src/assets/icons/dc/reset.svg
Normal 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 |
@@ -86,7 +86,7 @@
|
||||
width: 46.69rem;
|
||||
height: 56.6rem;
|
||||
flex-shrink: 0;
|
||||
// background-color: #fff;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 19.44px 27.22px 0px #0000000d;
|
||||
border: 1px solid;
|
||||
border-image-source: linear-gradient(
|
||||
|
||||
@@ -10,8 +10,14 @@
|
||||
<span class="icon"><svg-icon :name="item.name" size="16" /></span>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
</div>
|
||||
<button @click="onCreate">创建</button>
|
||||
<button @click="onReset">重置</button>
|
||||
<button @click="onCreate">
|
||||
<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>
|
||||
</transition>
|
||||
<brush-control-panel v-if="show" :currentTool="currentTool2" style="top: 14rem" />
|
||||
@@ -114,5 +120,22 @@
|
||||
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>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<svg-icon :name="icon" :size="iconSize" />
|
||||
</span>
|
||||
<span v-show="before" class="before">{{ before }}</span>
|
||||
|
||||
<input
|
||||
v-if="!isColor"
|
||||
v-bind="attrs"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
@@ -13,15 +15,17 @@
|
||||
@copy.stop
|
||||
@keydown.stop
|
||||
/>
|
||||
<input
|
||||
v-if="isColor"
|
||||
readonly
|
||||
type="text"
|
||||
:value="colorObj.color"
|
||||
@copy.stop
|
||||
@keydown.stop
|
||||
/>
|
||||
<template v-if="isColor">
|
||||
<template v-else>
|
||||
<input
|
||||
v-bind="attrs"
|
||||
type="color"
|
||||
:value="colorObj.color"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
@copy.stop
|
||||
@keydown.stop
|
||||
/>
|
||||
<input readonly :value="colorObj.color" @copy.stop @keydown.stop />
|
||||
<span class="decorate marginl"></span>
|
||||
<input
|
||||
class="alpha"
|
||||
@@ -35,6 +39,7 @@
|
||||
@keydown.stop
|
||||
/>
|
||||
</template>
|
||||
|
||||
<span v-show="after" class="after">{{ after }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -221,12 +221,13 @@ export class AISelectboxToolManager {
|
||||
if (!this.demoObject) return
|
||||
const fobject = this.demoObject
|
||||
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 scaleX = fobject.scaleX
|
||||
const top = fobject.top
|
||||
const left = fobject.left
|
||||
const arr = traceImageContour(canvas);
|
||||
let minX = fobject.width;
|
||||
let minY = fobject.height;
|
||||
const str = arr.map((v) => {
|
||||
|
||||
@@ -136,10 +136,11 @@ export class LayerManager {
|
||||
copyLayerById(id) {
|
||||
const object = this.canvasManager.getObjectById(id)
|
||||
if (!object) return console.warn('复制图层失败,对象不存在ID:', id)
|
||||
this.canvasManager.discardActiveObject()
|
||||
cloneObjects([object]).then(objects => {
|
||||
const newObject = objects[0]
|
||||
const info = JSON.parse(JSON.stringify(newObject.info))
|
||||
info.id = createId("image")
|
||||
info.id = createId("copylayer")
|
||||
// info.name = info.name
|
||||
newObject.set({
|
||||
top: newObject.top + 15,
|
||||
|
||||
@@ -753,6 +753,23 @@ export class CanvasEventManager {
|
||||
this.canvas.on("object:removed", (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() {
|
||||
// 双击处理
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="my-select">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
<input class="color" type="color" ref="colorInput" @change="changeColor" />
|
||||
<div class="interval"></div>
|
||||
<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 class="interval"></div>
|
||||
<div class="size">
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<threeModel :currentData="currentData" />
|
||||
</template>
|
||||
</baseModal>
|
||||
<Assistant />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -69,6 +70,7 @@
|
||||
import { computed, ref, watch, onMounted, nextTick, provide, onBeforeUnmount } from 'vue'
|
||||
import { useLayout } from '@/utils/treeDiagram'
|
||||
import { NODE_TYPE, NODE_COMPONENT } from './tools/index.d'
|
||||
import Assistant from '@/components/Assistant/assistant.vue'
|
||||
// 组件
|
||||
import headerTools from './components/header-tools.vue'
|
||||
import zoom from '../components/zoom.vue'
|
||||
|
||||
@@ -78,6 +78,24 @@ export const base64Tofile = (base64: string,name: string) => {
|
||||
const file = new File([blob], name, { type: mime })
|
||||
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
|
||||
export const getCurrentTime = () => {
|
||||
const now = new Date()
|
||||
|
||||
738
src/components/clipDialog.vue
Normal file
738
src/components/clipDialog.vue
Normal 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>
|
||||
@@ -204,45 +204,52 @@ export default {
|
||||
download: 'Download'
|
||||
},
|
||||
DepthCanvas: {
|
||||
layer: 'Layer',
|
||||
editDetails: 'Edit Details',
|
||||
export: 'Export',
|
||||
save: 'Save',
|
||||
workbench: 'Workbench',
|
||||
position: 'Position',
|
||||
size: 'Size',
|
||||
appearance: 'Appearance',
|
||||
opacity: 'Opacity',
|
||||
cornerRadius: 'Cor Radius',
|
||||
strokeWidth: 'Stroke Width',
|
||||
color: 'Color',
|
||||
image: 'Image',
|
||||
settings: 'Settings',
|
||||
rotation: 'Rotation',
|
||||
scale: 'Scale',
|
||||
gapX: 'Gap X',
|
||||
gapY: 'Gap Y',
|
||||
offset: 'Offset',
|
||||
emptyLayer: 'Empty Layer',
|
||||
aiGroupLayer: 'AI Group Layer',
|
||||
textLayer: 'Text Layer',
|
||||
rectLayer: 'Rect Layer',
|
||||
lineLayer: 'Line Layer',
|
||||
ellipseLayer: 'Ellipse Layer',
|
||||
triangleLayer: 'Triangle Layer',
|
||||
starLayer: 'Star Layer',
|
||||
arrowLayer: 'Arrow Layer',
|
||||
imageLayer: 'Image Layer',
|
||||
mergeLayer: 'Merge Layer',
|
||||
rectangle: 'Rectangle',
|
||||
line: 'Line',
|
||||
arrow: 'Arrow',
|
||||
ellipse: 'Ellipse',
|
||||
triangle: 'Triangle',
|
||||
star: 'Star',
|
||||
add: 'Add',
|
||||
remove: 'Remove',
|
||||
brush: 'Brush',
|
||||
erase: 'Erase'
|
||||
layer: "Layer",
|
||||
editDetails: "Edit Details",
|
||||
export: "Export",
|
||||
save: "Save",
|
||||
workbench: "Workbench",
|
||||
position: "Position",
|
||||
size: "Size",
|
||||
appearance: "Appearance",
|
||||
opacity: "Opacity",
|
||||
cornerRadius: "Cor Radius",
|
||||
strokeWidth: "Stroke Width",
|
||||
color: "Color",
|
||||
image: "Image",
|
||||
settings: "Settings",
|
||||
rotation: "Rotation",
|
||||
scale: "Scale",
|
||||
gapX: "Gap X",
|
||||
gapY: "Gap Y",
|
||||
offset: "Offset",
|
||||
emptyLayer: "Empty Layer",
|
||||
aiGroupLayer: "AI Group Layer",
|
||||
textLayer: "Text Layer",
|
||||
rectLayer: "Rect Layer",
|
||||
lineLayer: "Line Layer",
|
||||
ellipseLayer: "Ellipse Layer",
|
||||
triangleLayer: "Triangle Layer",
|
||||
starLayer: "Star Layer",
|
||||
arrowLayer: "Arrow Layer",
|
||||
imageLayer: "Image Layer",
|
||||
mergeLayer: "Merge Layer",
|
||||
rectangle: "Rectangle",
|
||||
line: "Line",
|
||||
arrow: "Arrow",
|
||||
ellipse: "Ellipse",
|
||||
triangle: "Triangle",
|
||||
star: "Star",
|
||||
add: "Add",
|
||||
remove: "Remove",
|
||||
brush: "Brush",
|
||||
erase: "Erase",
|
||||
create: "Create",
|
||||
reset: "Reset"
|
||||
},
|
||||
clipDialog: {
|
||||
title: 'Upload your profile photo',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Save'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,45 +200,52 @@ export default {
|
||||
download: '下载'
|
||||
},
|
||||
DepthCanvas: {
|
||||
layer: '图层',
|
||||
editDetails: '编辑详情',
|
||||
export: '导出',
|
||||
save: '保存',
|
||||
workbench: '工作台',
|
||||
position: '位置',
|
||||
size: '大小',
|
||||
appearance: '外观',
|
||||
opacity: '透明度',
|
||||
cornerRadius: '圆角半径',
|
||||
strokeWidth: '边框宽度',
|
||||
color: '颜色',
|
||||
image: '图片',
|
||||
settings: '设置',
|
||||
rotation: '旋转角度',
|
||||
scale: '缩放',
|
||||
gapX: '水平间距',
|
||||
gapY: '垂直间距',
|
||||
offset: '偏移量',
|
||||
emptyLayer: '空图层',
|
||||
aiGroupLayer: '智能选区组',
|
||||
textLayer: '文本图层',
|
||||
rectLayer: '矩形图层',
|
||||
lineLayer: '直线图层',
|
||||
ellipseLayer: '椭圆图层',
|
||||
triangleLayer: '三角形图层',
|
||||
starLayer: '五角星图层',
|
||||
arrowLayer: '箭头图层',
|
||||
imageLayer: '图片图层',
|
||||
mergeLayer: '合并图层',
|
||||
rectangle: '矩形',
|
||||
line: '直线',
|
||||
arrow: '箭头',
|
||||
ellipse: '椭圆',
|
||||
triangle: '三角形',
|
||||
star: '五角星',
|
||||
add: '添加',
|
||||
remove: '删除',
|
||||
brush: '画笔',
|
||||
erase: '擦除'
|
||||
layer: "图层",
|
||||
editDetails: "编辑详情",
|
||||
export: "导出",
|
||||
save: "保存",
|
||||
workbench: "工作台",
|
||||
position: "位置",
|
||||
size: "大小",
|
||||
appearance: "外观",
|
||||
opacity: "透明度",
|
||||
cornerRadius: "圆角半径",
|
||||
strokeWidth: "边框宽度",
|
||||
color: "颜色",
|
||||
image: "图片",
|
||||
settings: "设置",
|
||||
rotation: "旋转角度",
|
||||
scale: "缩放",
|
||||
gapX: "水平间距",
|
||||
gapY: "垂直间距",
|
||||
offset: "偏移量",
|
||||
emptyLayer: "空图层",
|
||||
aiGroupLayer: "智能选区组",
|
||||
textLayer: "文本图层",
|
||||
rectLayer: "矩形图层",
|
||||
lineLayer: "直线图层",
|
||||
ellipseLayer: "椭圆图层",
|
||||
triangleLayer: "三角形图层",
|
||||
starLayer: "五角星图层",
|
||||
arrowLayer: "箭头图层",
|
||||
imageLayer: "图片图层",
|
||||
mergeLayer: "合并图层",
|
||||
rectangle: "矩形",
|
||||
line: "直线",
|
||||
arrow: "箭头",
|
||||
ellipse: "椭圆",
|
||||
triangle: "三角形",
|
||||
star: "五角星",
|
||||
add: "添加",
|
||||
remove: "删除",
|
||||
brush: "画笔",
|
||||
erase: "擦除",
|
||||
create: "创建",
|
||||
reset: "重置"
|
||||
},
|
||||
clipDialog: {
|
||||
title: '上传您的个人资料照片',
|
||||
cancel: '取消',
|
||||
confirm: '保存'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<div class="label">{{ $t('Home.logoutDevice') }}</div>
|
||||
<button class="logout-btn" @click="logout">{{ $t('Home.logout') }}</button>
|
||||
</div>
|
||||
<clip-dialog ref="clipDialogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -41,6 +42,8 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { uploadImage } from '@/api/upload'
|
||||
import { UpdateUserAvatar, getAvatarLimit } from '@/api/user'
|
||||
import clipDialog from '@/components/clipDialog.vue'
|
||||
import { fileToBase64, base64Tofile } from '../../../components/Canvas/tools/tools'
|
||||
const router = useRouter()
|
||||
const { locale } = useI18n()
|
||||
const userInfoStore = useUserInfoStore()
|
||||
@@ -50,6 +53,7 @@
|
||||
{ label: '中文', value: 'CHINESE_SIMPLIFIED' }
|
||||
])
|
||||
const remainingNum = ref(0)
|
||||
const clipDialogRef = ref<typeof clipDialog>()
|
||||
const changeLang = (value: string) => {
|
||||
locale.value = value
|
||||
localStorage.setItem('language', value)
|
||||
@@ -72,13 +76,17 @@
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/png, image/jpeg, image/jpg'
|
||||
input.addEventListener('change', (e) => {
|
||||
input.addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0]
|
||||
const formData = new FormData()
|
||||
formData.append('avatar', file)
|
||||
UpdateUserAvatar(formData).then((res) => {
|
||||
userInfoStore.updateUserInfo({avatar: res})
|
||||
getAvatarLimitNum()
|
||||
let base64Img = await fileToBase64(file)
|
||||
clipDialogRef.value?.open(base64Img).then((base64: string)=>{
|
||||
const fileData = base64Tofile(base64,file.name)
|
||||
const formData = new FormData()
|
||||
formData.append('avatar', fileData)
|
||||
UpdateUserAvatar(formData).then((res) => {
|
||||
userInfoStore.updateUserInfo({avatar: res})
|
||||
getAvatarLimitNum()
|
||||
})
|
||||
})
|
||||
})
|
||||
input.click()
|
||||
|
||||
Reference in New Issue
Block a user