mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-05-19 20:48:10 +08:00
feat(editor): 画布自适应大小
This commit is contained in:
parent
3c7d756d19
commit
ab3e113904
@ -55,7 +55,7 @@ import eventsService from '@editor/services/events';
|
|||||||
import historyService from '@editor/services/history';
|
import historyService from '@editor/services/history';
|
||||||
import propsService from '@editor/services/props';
|
import propsService from '@editor/services/props';
|
||||||
import uiService from '@editor/services/ui';
|
import uiService from '@editor/services/ui';
|
||||||
import type { ComponentGroup, MenuBarData, Services, SideBarData } from '@editor/type';
|
import type { ComponentGroup, MenuBarData, Services, SideBarData, StageRect } from '@editor/type';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'm-editor',
|
name: 'm-editor',
|
||||||
@ -133,8 +133,8 @@ export default defineComponent({
|
|||||||
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
|
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
stageStyle: {
|
stageRect: {
|
||||||
type: [String, Object] as PropType<Record<string, string | number>>,
|
type: [String, Object] as PropType<StageRect>,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -208,8 +208,8 @@ export default defineComponent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.stageStyle,
|
() => props.stageRect,
|
||||||
(stageStyle) => uiService.set('stageStyle', stageStyle),
|
(stageRect) => stageRect && uiService.set('stageRect', stageRect),
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="m-editor-stage">
|
<div class="m-editor-stage" ref="stageWrap">
|
||||||
<div
|
<div
|
||||||
class="m-editor-stage-container"
|
class="m-editor-stage-container"
|
||||||
ref="stageContainer"
|
ref="stageContainer"
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
inject,
|
inject,
|
||||||
|
nextTick,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
PropType,
|
PropType,
|
||||||
@ -31,7 +32,7 @@ import type { MApp, MNode, MPage } from '@tmagic/schema';
|
|||||||
import type { MoveableOptions, Runtime, SortEventData, UpdateEventData } from '@tmagic/stage';
|
import type { MoveableOptions, Runtime, SortEventData, UpdateEventData } from '@tmagic/stage';
|
||||||
import StageCore from '@tmagic/stage';
|
import StageCore from '@tmagic/stage';
|
||||||
|
|
||||||
import type { Services } from '@editor/type';
|
import type { Services, StageRect } from '@editor/type';
|
||||||
|
|
||||||
import ViewerMenu from './ViewerMenu.vue';
|
import ViewerMenu from './ViewerMenu.vue';
|
||||||
|
|
||||||
@ -88,26 +89,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
runtimeUrl: String,
|
runtimeUrl: String,
|
||||||
|
|
||||||
root: {
|
|
||||||
type: Object as PropType<MApp>,
|
|
||||||
},
|
|
||||||
|
|
||||||
page: {
|
|
||||||
type: Object as PropType<MPage>,
|
|
||||||
},
|
|
||||||
|
|
||||||
node: {
|
|
||||||
type: Object as PropType<MNode>,
|
|
||||||
},
|
|
||||||
|
|
||||||
uiSelectMode: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
|
|
||||||
zoom: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
|
|
||||||
canSelect: {
|
canSelect: {
|
||||||
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
|
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
|
||||||
default: (el: HTMLElement) => Boolean(el.id),
|
default: (el: HTMLElement) => Boolean(el.id),
|
||||||
@ -125,11 +106,20 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const services = inject<Services>('services');
|
const services = inject<Services>('services');
|
||||||
|
|
||||||
|
const stageWrap = ref<HTMLDivElement>();
|
||||||
const stageContainer = ref<HTMLDivElement>();
|
const stageContainer = ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
|
||||||
|
const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
|
||||||
|
const root = computed(() => services?.editorService.get<MApp>('root'));
|
||||||
|
const page = computed(() => services?.editorService.get<MPage>('page'));
|
||||||
|
const zoom = computed(() => services?.uiService.get<number>('zoom'));
|
||||||
|
const node = computed(() => services?.editorService.get<MNode>('node'));
|
||||||
const stageStyle = computed(() => ({
|
const stageStyle = computed(() => ({
|
||||||
...services?.uiService.get<Record<string, string | number>>('stageStyle'),
|
width: `${stageRect.value?.width}px`,
|
||||||
transform: `scale(${props.zoom}) translate3d(0, -50%, 0)`,
|
height: `${stageRect.value?.height}px`,
|
||||||
|
transform: `scale(${zoom.value})`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let stage: StageCore | null = null;
|
let stage: StageCore | null = null;
|
||||||
@ -139,16 +129,16 @@ export default defineComponent({
|
|||||||
if (stage) return;
|
if (stage) return;
|
||||||
|
|
||||||
if (!stageContainer.value) return;
|
if (!stageContainer.value) return;
|
||||||
if (!(props.runtimeUrl || props.render) || !props.root) return;
|
if (!(props.runtimeUrl || props.render) || !root.value) return;
|
||||||
|
|
||||||
stage = new StageCore({
|
stage = new StageCore({
|
||||||
render: props.render,
|
render: props.render,
|
||||||
runtimeUrl: props.runtimeUrl,
|
runtimeUrl: props.runtimeUrl,
|
||||||
zoom: props.zoom,
|
zoom: zoom.value,
|
||||||
canSelect: (el, stop) => {
|
canSelect: (el, stop) => {
|
||||||
const elCanSelect = props.canSelect(el);
|
const elCanSelect = props.canSelect(el);
|
||||||
// 在组件联动过程中不能再往下选择,返回并触发 ui-select
|
// 在组件联动过程中不能再往下选择,返回并触发 ui-select
|
||||||
if (props.uiSelectMode && elCanSelect) {
|
if (uiSelectMode.value && elCanSelect) {
|
||||||
document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
|
document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
|
||||||
return stop();
|
return stop();
|
||||||
}
|
}
|
||||||
@ -176,45 +166,63 @@ export default defineComponent({
|
|||||||
services?.uiService.set('showGuides', true);
|
services?.uiService.set('showGuides', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!props.node?.id) return;
|
if (!node.value?.id) return;
|
||||||
stage?.on('runtime-ready', (rt) => {
|
stage?.on('runtime-ready', (rt) => {
|
||||||
runtime = rt;
|
runtime = rt;
|
||||||
// toRaw返回的值是一个引用而非快照,需要cloneDeep
|
// toRaw返回的值是一个引用而非快照,需要cloneDeep
|
||||||
props.root && runtime?.updateRootConfig(cloneDeep(toRaw(props.root)));
|
root.value && runtime?.updateRootConfig(cloneDeep(toRaw(root.value)));
|
||||||
props.page?.id && runtime?.updatePageId?.(props.page.id);
|
page.value?.id && runtime?.updatePageId?.(page.value.id);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.node && stage?.select(toRaw(props.node.id));
|
node.value && stage?.select(toRaw(node.value.id));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(zoom, (zoom) => {
|
||||||
|
if (!stage || !zoom) return;
|
||||||
|
stage.setZoom(zoom);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(root, (root) => {
|
||||||
|
if (runtime && root) {
|
||||||
|
runtime.updateRootConfig(cloneDeep(toRaw(root)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.zoom,
|
() => node.value?.id,
|
||||||
(zoom) => {
|
(id) => {
|
||||||
if (!stage || !zoom) return;
|
nextTick(() => {
|
||||||
stage?.setZoom(zoom);
|
// 等待相关dom变更完成后,再select,适用大多数场景
|
||||||
|
id && stage?.select(id);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
() => props.root,
|
for (const { contentRect } of entries) {
|
||||||
(root) => {
|
services?.uiService.set('stageContainerRect', {
|
||||||
if (runtime && root) {
|
width: contentRect.width,
|
||||||
runtime.updateRootConfig(cloneDeep(toRaw(root)));
|
height: contentRect.height,
|
||||||
}
|
});
|
||||||
},
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
stageWrap.value && resizeObserver.observe(stageWrap.value);
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stage?.destroy();
|
stage?.destroy();
|
||||||
|
resizeObserver.disconnect();
|
||||||
services?.editorService.set('stage', null);
|
services?.editorService.set('stage', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
stageWrap,
|
||||||
|
stageContainer,
|
||||||
stageStyle,
|
stageStyle,
|
||||||
...useMenu(),
|
...useMenu(),
|
||||||
|
|
||||||
stageContainer,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -4,11 +4,6 @@
|
|||||||
:key="page?.id"
|
:key="page?.id"
|
||||||
:runtime-url="runtimeUrl"
|
:runtime-url="runtimeUrl"
|
||||||
:render="render"
|
:render="render"
|
||||||
:ui-select-mode="uiSelectMode"
|
|
||||||
:root="root"
|
|
||||||
:page="page"
|
|
||||||
:node="node"
|
|
||||||
:zoom="zoom"
|
|
||||||
:moveable-options="moveableOptions"
|
:moveable-options="moveableOptions"
|
||||||
:can-select="canSelect"
|
:can-select="canSelect"
|
||||||
@select="selectHandler"
|
@select="selectHandler"
|
||||||
@ -26,9 +21,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, inject, nextTick, PropType, watch } from 'vue';
|
import { computed, defineComponent, inject, PropType } from 'vue';
|
||||||
|
|
||||||
import type { MApp, MComponent, MContainer, MNode, MPage } from '@tmagic/schema';
|
import type { MComponent, MContainer, MPage } from '@tmagic/schema';
|
||||||
import type { MoveableOptions, SortEventData } from '@tmagic/stage';
|
import type { MoveableOptions, SortEventData } from '@tmagic/stage';
|
||||||
import StageCore from '@tmagic/stage';
|
import StageCore from '@tmagic/stage';
|
||||||
|
|
||||||
@ -63,23 +58,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const services = inject<Services>('services');
|
const services = inject<Services>('services');
|
||||||
const node = computed(() => services?.editorService.get<MNode>('node'));
|
|
||||||
const stage = computed(() => services?.editorService.get<StageCore>('stage'));
|
|
||||||
|
|
||||||
watch([() => node.value?.id, stage], ([id, stage]) => {
|
|
||||||
nextTick(() => {
|
|
||||||
// 等待相关dom变更完成后,再select,适用大多数场景
|
|
||||||
id && stage?.select(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uiSelectMode: computed(() => services?.uiService.get<boolean>('uiSelectMode')),
|
|
||||||
root: computed(() => services?.editorService.get<MApp>('root')),
|
|
||||||
page: computed(() => services?.editorService.get<MPage>('page')),
|
page: computed(() => services?.editorService.get<MPage>('page')),
|
||||||
zoom: computed(() => services?.uiService.get<number>('zoom')),
|
|
||||||
|
|
||||||
node,
|
|
||||||
|
|
||||||
selectHandler(el: HTMLElement) {
|
selectHandler(el: HTMLElement) {
|
||||||
services?.editorService.select(el.id);
|
services?.editorService.select(el.id);
|
||||||
|
@ -21,7 +21,7 @@ import { reactive, toRaw } from 'vue';
|
|||||||
import type StageCore from '@tmagic/stage';
|
import type StageCore from '@tmagic/stage';
|
||||||
|
|
||||||
import editorService from '@editor/services/editor';
|
import editorService from '@editor/services/editor';
|
||||||
import { GetColumnWidth, SetColumnWidth, UiState } from '@editor/type';
|
import type { GetColumnWidth, SetColumnWidth, StageRect, UiState } from '@editor/type';
|
||||||
|
|
||||||
import BaseService from './BaseService';
|
import BaseService from './BaseService';
|
||||||
|
|
||||||
@ -29,7 +29,14 @@ const state = reactive<UiState>({
|
|||||||
uiSelectMode: false,
|
uiSelectMode: false,
|
||||||
showSrc: false,
|
showSrc: false,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
stageStyle: {},
|
stageContainerRect: {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
stageRect: {
|
||||||
|
width: 375,
|
||||||
|
height: 817,
|
||||||
|
},
|
||||||
columnWidth: {
|
columnWidth: {
|
||||||
left: 310,
|
left: 310,
|
||||||
center: globalThis.document.body.clientWidth - 310 - 400,
|
center: globalThis.document.body.clientWidth - 310 - 400,
|
||||||
@ -57,6 +64,11 @@ class Ui extends BaseService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'stageRect') {
|
||||||
|
this.setStageRect(value as unknown as StageRect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'showGuides') {
|
if (name === 'showGuides') {
|
||||||
mask?.showGuides(value as unknown as boolean);
|
mask?.showGuides(value as unknown as boolean);
|
||||||
}
|
}
|
||||||
@ -66,6 +78,10 @@ class Ui extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(state as any)[name] = value;
|
(state as any)[name] = value;
|
||||||
|
|
||||||
|
if (name === 'stageContainerRect') {
|
||||||
|
state.zoom = this.calcZoom();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get<T>(name: keyof typeof state): T {
|
public get<T>(name: keyof typeof state): T {
|
||||||
@ -94,6 +110,24 @@ class Ui extends BaseService {
|
|||||||
|
|
||||||
state.columnWidth = columnWidth;
|
state.columnWidth = columnWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setStageRect(value: StageRect) {
|
||||||
|
state.stageRect = {
|
||||||
|
...state.stageRect,
|
||||||
|
...value,
|
||||||
|
};
|
||||||
|
state.zoom = this.calcZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private calcZoom() {
|
||||||
|
const { stageRect, stageContainerRect } = state;
|
||||||
|
const { height, width } = stageContainerRect;
|
||||||
|
if (!width || !height) return 1;
|
||||||
|
if (width > stageRect.width && height > stageRect.height) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.min((width - 100) / stageRect.width || 1, (height - 100) / stageRect.height || 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UiService = Ui;
|
export type UiService = Ui;
|
||||||
|
@ -2,18 +2,16 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - $--page-bar-height);
|
height: calc(100% - $--page-bar-height);
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-editor-stage-container {
|
.m-editor-stage-container {
|
||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
transform-origin: center -50%;
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
top: 50%;
|
position: absolute;
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
width: 375px;
|
|
||||||
height: 80%;
|
|
||||||
border: 1px solid $--border-color;
|
border: 1px solid $--border-color;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
import { FormConfig } from '@tmagic/form';
|
import type { FormConfig } from '@tmagic/form';
|
||||||
import { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
import type { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
|
||||||
import StageCore from '@tmagic/stage';
|
import type StageCore from '@tmagic/stage';
|
||||||
|
|
||||||
import type { ComponentListService } from '@editor/services/componentList';
|
import type { ComponentListService } from '@editor/services/componentList';
|
||||||
import type { EditorService } from '@editor/services/editor';
|
import type { EditorService } from '@editor/services/editor';
|
||||||
@ -75,6 +75,11 @@ export interface GetColumnWidth {
|
|||||||
right: number;
|
right: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StageRect {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UiState {
|
export interface UiState {
|
||||||
/** 当前点击画布是否触发选中,true: 不触发,false: 触发,默认为false */
|
/** 当前点击画布是否触发选中,true: 不触发,false: 触发,默认为false */
|
||||||
uiSelectMode: boolean;
|
uiSelectMode: boolean;
|
||||||
@ -82,8 +87,10 @@ export interface UiState {
|
|||||||
showSrc: boolean;
|
showSrc: boolean;
|
||||||
/** 画布显示放大倍数,默认为 1 */
|
/** 画布显示放大倍数,默认为 1 */
|
||||||
zoom: number;
|
zoom: number;
|
||||||
/** 画布顶层div的样式,可用于改变画布的大小 */
|
/** 画布容器的宽高 */
|
||||||
stageStyle: Record<string, string | number>;
|
stageContainerRect: StageRect;
|
||||||
|
/** 画布顶层div的宽高,可用于改变画布的大小 */
|
||||||
|
stageRect: StageRect;
|
||||||
/** 编辑器列布局每一列的宽度,分为左中右三列 */
|
/** 编辑器列布局每一列的宽度,分为左中右三列 */
|
||||||
columnWidth: GetColumnWidth;
|
columnWidth: GetColumnWidth;
|
||||||
/** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
|
/** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mount } from '@vue/test-utils';
|
|
||||||
|
|
||||||
import Editor from '@editor/layouts/CodeEditor.vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @jest-environment jsdom
|
|
||||||
*/
|
|
||||||
describe('编辑器', () => {
|
|
||||||
it('初始化', () => {
|
|
||||||
const wrapper = mount(Editor, {
|
|
||||||
props: {
|
|
||||||
initValues: [
|
|
||||||
{
|
|
||||||
type: 'app',
|
|
||||||
id: 1,
|
|
||||||
name: 'app',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'page',
|
|
||||||
id: 2,
|
|
||||||
name: 'page',
|
|
||||||
items: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
@ -20,6 +20,14 @@ import { mount } from '@vue/test-utils';
|
|||||||
|
|
||||||
import Stage from '@editor/layouts/workspace/Stage.vue';
|
import Stage from '@editor/layouts/workspace/Stage.vue';
|
||||||
|
|
||||||
|
globalThis.ResizeObserver =
|
||||||
|
globalThis.ResizeObserver ||
|
||||||
|
jest.fn().mockImplementation(() => ({
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
observe: jest.fn(),
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Stage.vue', () => {
|
describe('Stage.vue', () => {
|
||||||
(global as any).fetch = jest.fn(() =>
|
(global as any).fetch = jest.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user