mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-30 18:00:56 +01:00
updated frontend
This commit is contained in:
178
frontend/src/components/input/InputWithButton.vue
Normal file
178
frontend/src/components/input/InputWithButton.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="input-with-button">
|
||||
<!-- Input -->
|
||||
<el-input
|
||||
v-model="internalValue"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
class="input"
|
||||
:disabled="disabled"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@keyup.enter="onBlur"
|
||||
/>
|
||||
<!-- ./Input -->
|
||||
|
||||
<!-- Button -->
|
||||
<Button v-if="buttonLabel" :disabled="disabled" :size="size" :type="buttonType" class="button" no-margin>
|
||||
<Icon v-if="buttonIcon" :icon="buttonIcon"/>
|
||||
{{ buttonLabel }}
|
||||
</Button>
|
||||
<template v-else-if="buttonIcon">
|
||||
<FaIconButton
|
||||
v-if="isFaIcon"
|
||||
:disabled="disabled"
|
||||
:icon="buttonIcon"
|
||||
:size="size"
|
||||
:type="buttonType"
|
||||
class="button"
|
||||
/>
|
||||
<IconButton
|
||||
v-else
|
||||
:disabled="disabled"
|
||||
:icon="buttonIcon"
|
||||
:size="size"
|
||||
:type="buttonType"
|
||||
class="button"
|
||||
/>
|
||||
</template>
|
||||
<!-- ./Button -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onMounted, PropType, ref, watch} from 'vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
import Icon from '@/components/icon/Icon.vue';
|
||||
import FaIconButton from '@/components/button/FaIconButton.vue';
|
||||
import useIcon from '@/components/icon/icon';
|
||||
import IconButton from '@/components/button/IconButton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InputWithButton',
|
||||
components: {
|
||||
IconButton,
|
||||
FaIconButton,
|
||||
Icon,
|
||||
Button,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'mini',
|
||||
},
|
||||
buttonType: {
|
||||
type: String as PropType<BasicType>,
|
||||
default: 'primary',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: 'Click',
|
||||
},
|
||||
buttonIcon: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:model-value',
|
||||
'input',
|
||||
'click',
|
||||
'blur',
|
||||
'focus',
|
||||
'keyup.enter',
|
||||
],
|
||||
setup(props: InputWithButtonProps, {emit}) {
|
||||
const internalValue = ref<string>();
|
||||
|
||||
const {
|
||||
isFaIcon: _isFaIcon,
|
||||
} = useIcon();
|
||||
|
||||
const isFaIcon = () => {
|
||||
const {buttonIcon} = props;
|
||||
if (!buttonIcon) return false;
|
||||
return _isFaIcon(buttonIcon);
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
internalValue.value = props.modelValue;
|
||||
});
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:model-value', value);
|
||||
emit('input', value);
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
emit('focus');
|
||||
};
|
||||
|
||||
const onKeyUpEnter = () => {
|
||||
emit('keyup.enter');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const {modelValue} = props;
|
||||
internalValue.value = modelValue;
|
||||
});
|
||||
|
||||
return {
|
||||
internalValue,
|
||||
isFaIcon,
|
||||
onClick,
|
||||
onInput,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onKeyUpEnter,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-with-button {
|
||||
display: inline-table;
|
||||
vertical-align: middle;
|
||||
//align-items: start;
|
||||
|
||||
.input {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.input-with-button >>> .input.el-input .el-input__inner {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-with-button >>> .button .el-button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
height: 28px;
|
||||
}
|
||||
</style>
|
||||
228
frontend/src/components/input/TagInput.vue
Normal file
228
frontend/src/components/input/TagInput.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="tag-input">
|
||||
<template v-for="(item, $index) in selectedValue" :key="$index">
|
||||
<TagInputItem
|
||||
v-if="item.isEdit"
|
||||
ref="inputItemRef"
|
||||
v-model="selectedValue[$index]"
|
||||
:disabled="disabled"
|
||||
placeholder="Tag Name"
|
||||
size="mini"
|
||||
@blur="onBlur($index, $event)"
|
||||
@check="onCheck($index, $event)"
|
||||
@close="onClose($index, $event)"
|
||||
@delete="onDelete($index, $event)"
|
||||
@focus="onFocus($index, $event)"
|
||||
/>
|
||||
<Tag
|
||||
v-else
|
||||
:closable="!disabled"
|
||||
:color="item.color"
|
||||
:disabled="disabled"
|
||||
:label="item.name"
|
||||
clickable
|
||||
size="small"
|
||||
type="plain"
|
||||
@click="onEdit($index, $event)"
|
||||
@close="onDelete($index, $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<el-tooltip :content="addButtonTooltip" :disabled="!addButtonTooltip">
|
||||
<Tab
|
||||
:icon="['fa', 'plus']"
|
||||
:show-close="false"
|
||||
:show-title="false"
|
||||
class="add-btn"
|
||||
:class="disabled ? 'disabled' : ''"
|
||||
@click="onAdd"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType, ref, watch} from 'vue';
|
||||
import TagComp from '@/components/tag/Tag.vue';
|
||||
import Tab from '@/components/tab/Tab.vue';
|
||||
import TagInputItem from '@/components/input/TagInputItem.vue';
|
||||
import {cloneArray} from '@/utils/object';
|
||||
import {getNewTag} from '@/components/tag/tag';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TagInput',
|
||||
components: {
|
||||
TagInputItem,
|
||||
Tag: TagComp,
|
||||
Tab,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Tag[]>,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'change',
|
||||
'update:model-value',
|
||||
],
|
||||
setup(props: TagInputProps, {emit}) {
|
||||
const activeIndex = ref<number>(-1);
|
||||
const inputItemRef = ref<typeof TagInputItem>();
|
||||
|
||||
const selectedValue = ref<TagInputOption[]>([]);
|
||||
|
||||
const emitValue = () => {
|
||||
emit('change', selectedValue.value);
|
||||
emit('update:model-value', selectedValue.value.map(d => {
|
||||
return {
|
||||
_id: d._id,
|
||||
name: d.name,
|
||||
color: d.color,
|
||||
} as Tag;
|
||||
}));
|
||||
};
|
||||
|
||||
const disabled = computed<boolean>(() => props.disabled);
|
||||
|
||||
const addButtonTooltip = computed<string>(() => disabled.value ? '' : 'Add Tag');
|
||||
|
||||
const onEdit = (index: number, ev?: Event) => {
|
||||
// check disabled
|
||||
if (disabled.value) return;
|
||||
|
||||
ev?.stopPropagation();
|
||||
const item = selectedValue.value[index];
|
||||
item.isEdit = true;
|
||||
|
||||
// auto focus
|
||||
setTimeout(() => inputItemRef.value?.focus(), 0);
|
||||
};
|
||||
|
||||
const onDelete = (index: number, ev?: Event) => {
|
||||
// check disabled
|
||||
if (disabled.value) return;
|
||||
|
||||
ev?.stopPropagation();
|
||||
selectedValue.value.splice(index, 1);
|
||||
|
||||
// commit change
|
||||
emitValue();
|
||||
};
|
||||
|
||||
const onFocus = (index: number, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
activeIndex.value = index;
|
||||
};
|
||||
|
||||
const onBlur = (index: number, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
activeIndex.value = -1;
|
||||
};
|
||||
|
||||
const onCheck = (index: number, value?: Tag, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
const item = selectedValue.value[index];
|
||||
if (!item) return;
|
||||
item.isEdit = false;
|
||||
if (!value) return;
|
||||
const {name, hex} = value;
|
||||
item.name = name;
|
||||
item.hex = hex;
|
||||
|
||||
// commit change
|
||||
emitValue();
|
||||
};
|
||||
|
||||
const onClose = (index: number, ev?: Event) => {
|
||||
ev?.stopPropagation();
|
||||
const item = selectedValue.value[index];
|
||||
if (!item) return;
|
||||
item.isEdit = false;
|
||||
if (!item.name) {
|
||||
selectedValue.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
// check disabled
|
||||
if (disabled.value) return;
|
||||
|
||||
// add value to array
|
||||
selectedValue.value.push({
|
||||
...getNewTag(),
|
||||
isEdit: true,
|
||||
});
|
||||
|
||||
// auto focus
|
||||
setTimeout(() => inputItemRef.value?.focus(), 0);
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
const modelValue = props.modelValue || [];
|
||||
selectedValue.value = cloneArray(modelValue);
|
||||
});
|
||||
|
||||
return {
|
||||
inputItemRef,
|
||||
selectedValue,
|
||||
addButtonTooltip,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCheck,
|
||||
onClose,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.tag-input {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
|
||||
.tag-input-item {
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:not(.disabled) {
|
||||
background-color: $white;
|
||||
color: $infoMediumColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.tag-input >>> .tag {
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
353
frontend/src/components/input/TagInputItem.vue
Normal file
353
frontend/src/components/input/TagInputItem.vue
Normal file
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
isFocus ? 'is-focus' : '',
|
||||
isNew ? 'is-new' : '',
|
||||
]"
|
||||
class="tag-input-item"
|
||||
>
|
||||
<!-- Input -->
|
||||
<div class="input-wrapper">
|
||||
<el-autocomplete
|
||||
ref="inputRef"
|
||||
v-model="internalValue.name"
|
||||
:disabled="disabled"
|
||||
:fetch-suggestions="fetchSuggestions"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
popper-class="tag-input-item-popper"
|
||||
class="input"
|
||||
value-key="name"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@select="onSelect"
|
||||
@keyup.enter="onCheck"
|
||||
/>
|
||||
<div class="actions">
|
||||
<font-awesome-icon
|
||||
:class="[isDisabled('check') ? 'disabled' : '']"
|
||||
:icon="['fa', 'check']"
|
||||
class="action-btn check"
|
||||
@click="onCheck"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
:class="[isDisabled('close') ? 'disabled' : '']"
|
||||
:icon="['fa', 'times']"
|
||||
class="action-btn close"
|
||||
@click="onClose"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
:class="[isDisabled('delete') ? 'disabled' : '']"
|
||||
:icon="['fa', 'trash']"
|
||||
class="action-btn delete"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ./Input -->
|
||||
|
||||
<!-- Color Picker -->
|
||||
<ColorPicker
|
||||
v-model="internalValue.color"
|
||||
:disabled="!isNew"
|
||||
:predefine="predefinedColors"
|
||||
class="color-picker"
|
||||
show-alpha
|
||||
/>
|
||||
<!-- <el-color-picker-->
|
||||
<!-- v-model="internalValue.color"-->
|
||||
<!-- :disabled="!isNew"-->
|
||||
<!-- :predefine="predefinedColors"-->
|
||||
<!-- class="color-picker"-->
|
||||
<!-- show-alpha-->
|
||||
<!-- />-->
|
||||
<!-- ./Color Picker -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, inject, onMounted, PropType, readonly, ref, watch} from 'vue';
|
||||
import {ElInput} from 'element-plus';
|
||||
import {plainClone} from '@/utils/object';
|
||||
import useTagService from '@/services/tag/tagService';
|
||||
import {useStore} from 'vuex';
|
||||
import {FILTER_OP_CONTAINS, FILTER_OP_EQUAL} from '@/constants/filter';
|
||||
import {getNewTag} from '@/components/tag/tag';
|
||||
import {getPredefinedColors} from '@/utils/color';
|
||||
import ColorPicker from '@/components/color/ColorPicker.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TagInputItem',
|
||||
components: {
|
||||
ColorPicker,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as PropType<Tag>,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<BasicSize>,
|
||||
default: 'mini',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:model-value',
|
||||
'input',
|
||||
'click',
|
||||
'blur',
|
||||
'focus',
|
||||
'keyup.enter',
|
||||
'close',
|
||||
'check',
|
||||
'delete',
|
||||
],
|
||||
setup(props: TagInputItemProps, {emit}) {
|
||||
const store = useStore();
|
||||
|
||||
const internalValue = ref<Tag>(getNewTag());
|
||||
|
||||
const isFocus = ref<boolean>(false);
|
||||
|
||||
const inputRef = ref<typeof ElInput>();
|
||||
|
||||
const isNew = computed<boolean>(() => !internalValue.value._id);
|
||||
|
||||
// predefined colors
|
||||
const predefinedColors = readonly<string[]>(getPredefinedColors());
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
if (!props.modelValue) {
|
||||
internalValue.value = getNewTag();
|
||||
} else {
|
||||
internalValue.value = plainClone(props.modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
const isDisabled = (key: string) => {
|
||||
switch (key) {
|
||||
case 'check':
|
||||
return !internalValue.value.name;
|
||||
case 'close':
|
||||
return false;
|
||||
case 'delete':
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onInput = (name: string) => {
|
||||
const value = {...props.modelValue, name};
|
||||
emit('input', value);
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
isFocus.value = false;
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
isFocus.value = true;
|
||||
emit('focus');
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
inputRef.value?.focus();
|
||||
};
|
||||
|
||||
const onSelect = (value: Tag) => {
|
||||
internalValue.value = value;
|
||||
};
|
||||
|
||||
const onCheck = () => {
|
||||
if (isDisabled('check')) return;
|
||||
emit('update:model-value', internalValue.value);
|
||||
emit('check', internalValue.value);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
if (isDisabled('close')) return;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
if (isDisabled('delete')) return;
|
||||
emit('delete');
|
||||
};
|
||||
|
||||
const ctx = inject<ListStoreContext<BaseModel>>('store-context');
|
||||
|
||||
const fetchSuggestions = async (queryString: string, callback: (data: Tag[]) => void) => {
|
||||
const {
|
||||
getList,
|
||||
} = useTagService(store);
|
||||
const params = {
|
||||
page: 1,
|
||||
size: 50,
|
||||
conditions: [
|
||||
{key: 'col', op: FILTER_OP_EQUAL, value: `${ctx?.namespace}s`}
|
||||
]
|
||||
} as ListRequestParams;
|
||||
if (queryString) {
|
||||
const conditions = params.conditions as FilterConditionData[];
|
||||
conditions.push({key: 'name', op: FILTER_OP_CONTAINS, value: queryString});
|
||||
}
|
||||
try {
|
||||
const res = await getList(params);
|
||||
return callback(res.data || []);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
callback([]);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.modelValue) {
|
||||
internalValue.value = getNewTag();
|
||||
} else {
|
||||
internalValue.value = plainClone(props.modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
predefinedColors,
|
||||
internalValue,
|
||||
isFocus,
|
||||
inputRef,
|
||||
isNew,
|
||||
onClick,
|
||||
onInput,
|
||||
onBlur,
|
||||
onFocus,
|
||||
focus,
|
||||
onCheck,
|
||||
onClose,
|
||||
onDelete,
|
||||
onSelect,
|
||||
isDisabled,
|
||||
fetchSuggestions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/variables.scss";
|
||||
|
||||
.tag-input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
//height: 28px;
|
||||
|
||||
.input-wrapper {
|
||||
display: inherit;
|
||||
border: none;
|
||||
position: relative;
|
||||
height: 28px;
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
|
||||
.action-btn {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
padding: 3px;
|
||||
color: $infoMediumColor;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
&.check {
|
||||
color: $successColor;
|
||||
}
|
||||
|
||||
&.close {
|
||||
color: $infoColor;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
color: $dangerColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $infoMediumLightColor;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.tag-input-item >>> .input,
|
||||
.tag-input-item >>> .actions,
|
||||
.tag-input-item >>> .color-picker,
|
||||
.tag-input-item >>> .color-picker .el-color-picker {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .input {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .input .el-input__inner {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .color-picker .el-color-picker__trigger {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: none;
|
||||
border-top: 1px solid #DCDFE6;
|
||||
border-right: 1px solid #DCDFE6;
|
||||
border-bottom: 1px solid #DCDFE6;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tag-input-item.is-focus >>> .color-picker .el-color-picker__trigger {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .color-picker .el-color-picker__color {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .color-picker .el-color-picker__mask {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
left: 0;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.tag-input-item >>> .el-autocomplete-suggestion__list > li {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.tag-input-item-popper >>> .el-autocomplete-suggestion__list > li {
|
||||
height: 28px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user