提交代码
This commit is contained in:
parent
2467ebca1d
commit
578b0f7b8d
|
|
@ -43,6 +43,7 @@
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"echarts": "5.4.0",
|
"echarts": "5.4.0",
|
||||||
"element-ui": "2.15.14",
|
"element-ui": "2.15.14",
|
||||||
|
"fabric": "^5.3.0",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"fuse.js": "6.4.3",
|
"fuse.js": "6.4.3",
|
||||||
"highlight.js": "9.18.5",
|
"highlight.js": "9.18.5",
|
||||||
|
|
@ -51,6 +52,7 @@
|
||||||
"jsencrypt": "3.0.0-rc.1",
|
"jsencrypt": "3.0.0-rc.1",
|
||||||
"label-studio": "^1.0.1",
|
"label-studio": "^1.0.1",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"quill": "1.3.7",
|
"quill": "1.3.7",
|
||||||
"screenfull": "5.0.2",
|
"screenfull": "5.0.2",
|
||||||
"sm-crypto": "^0.3.13",
|
"sm-crypto": "^0.3.13",
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,13 @@ export function getMyAnnotationFiles(annotationStatus,taskId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function manualAnnotate(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/annotationTask/manualAnnotate',
|
||||||
|
method: 'post',
|
||||||
|
data:data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,144 +1,176 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="label-studio-annotator">
|
||||||
<div ref="labelStudio" id="label-studio-container"></div>
|
<div id="label-studio" class="annotation-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import LabelStudio from 'label-studio'
|
import LabelStudio from 'label-studio';
|
||||||
import 'label-studio/build/static/css/main.css'
|
import 'label-studio/build/static/css/main.css';
|
||||||
|
import { manualAnnotate } from '../../../../api/dataCenter/annotationTask';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CustomLabelStudio',
|
name: 'LabelStudioAnnotator',
|
||||||
props: {
|
props: {
|
||||||
task: {
|
fileUrl: { type: String, required: true },
|
||||||
type: String,
|
taskId: { type: Number, required: true },
|
||||||
required: true
|
config: { type: String, required: true },
|
||||||
|
id: { type: Number, required: true },
|
||||||
|
itemIndex: { type: Number, required: true },
|
||||||
|
annotations: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [] // 默认值为空数组
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
index: {
|
||||||
|
get() {
|
||||||
|
return this.itemIndex;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$parent.updateItemIndex(value); // 更新父组件的索引
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
labelStudio: null,
|
labelStudio: null,
|
||||||
customButtons: [
|
annotationsList: [] // 用于存储当前标注的结果
|
||||||
{
|
};
|
||||||
id: "custom-button",
|
|
||||||
name: "自定义按钮",
|
|
||||||
icon: "🔍",
|
|
||||||
action: () => {
|
|
||||||
console.log("自定义按钮被点击了!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
chineseLocalization: {
|
|
||||||
"DONE": "完成",
|
|
||||||
"SKIP": "跳过",
|
|
||||||
"SUBMIT": "提交",
|
|
||||||
"ANNOTATION": "标注",
|
|
||||||
"ANNOTATIONS": "标注",
|
|
||||||
"LABEL": "标签",
|
|
||||||
"LABELS": "标签",
|
|
||||||
"RELATIONS": "关系",
|
|
||||||
"REGIONS": "区域",
|
|
||||||
"RESULTS": "结果",
|
|
||||||
// 添加更多需要翻译的文本...
|
|
||||||
},
|
|
||||||
labelConfig: `
|
|
||||||
<View>
|
|
||||||
<Image name="image" value="$image"/>
|
|
||||||
<RectangleLabels name="label" toName="image">
|
|
||||||
<Label value="人" background="#ff0000"/>
|
|
||||||
<Label value="车" background="#00ff00"/>
|
|
||||||
</RectangleLabels>
|
|
||||||
</View>
|
|
||||||
`,
|
|
||||||
previousAnnotations: [] // 用于保存上次的标注框数据,用于比较变化
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch:{
|
watch: {
|
||||||
|
fileUrl: 'resetLabelStudio' // 当文件地址变化时,重置LabelStudio
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initLabelStudio();
|
this.initLabelStudio(); // 组件挂载完成后初始化LabelStudio
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
if (this.labelStudio) {
|
|
||||||
this.labelStudio.destroy();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 处理返回的标注数据
|
||||||
|
getAnnotations() {
|
||||||
|
return [{
|
||||||
|
result: this.annotations.map(annotation => ({
|
||||||
|
type: "rectanglelabels",
|
||||||
|
from_name: "label",
|
||||||
|
to_name: "image",
|
||||||
|
value: {
|
||||||
|
rectanglelabels: [annotation.label],
|
||||||
|
x: annotation.x,
|
||||||
|
y: annotation.y,
|
||||||
|
width: annotation.width,
|
||||||
|
height: annotation.height
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
// 初始化 LabelStudio
|
||||||
initLabelStudio() {
|
initLabelStudio() {
|
||||||
this.labelStudio = new LabelStudio('label-studio-container', {
|
console.log(this.annotations);
|
||||||
config: this.labelConfig,
|
this.cleanupLabelStudio(); // 清理之前的实例
|
||||||
interfaces: [
|
|
||||||
"panel",
|
|
||||||
"update",
|
|
||||||
"controls",
|
|
||||||
],
|
|
||||||
user: {
|
|
||||||
pk: 1,
|
|
||||||
firstName: "James",
|
|
||||||
lastName: "Dean"
|
|
||||||
},
|
|
||||||
task:JSON.parse( this.task),
|
|
||||||
locale: 'zh_CN',
|
|
||||||
messages: this.chineseLocalization,
|
|
||||||
onLabelStudioLoad: (LS) => {
|
|
||||||
console.log("Label Studio 已加载", LS);
|
|
||||||
|
|
||||||
// 定期检查标注框数据
|
const task = {
|
||||||
setInterval(() => {
|
id: this.id,
|
||||||
this.checkAnnotations(LS);
|
data: { image: this.fileUrl },
|
||||||
}, 1000); // 每秒检查一次
|
};
|
||||||
|
|
||||||
// 手动添加一个标注框并选中
|
if (this.annotations.length) {
|
||||||
const annotation = LS.annotationStore.addAnnotation({
|
task.annotations = this.getAnnotations();
|
||||||
userGenerate: true
|
}
|
||||||
});
|
|
||||||
// 获取并输出标注的几何信息
|
this.$nextTick(() => {
|
||||||
const box = annotation.geometry;
|
const labelStudioElement = document.getElementById('label-studio');
|
||||||
|
if (!labelStudioElement) {
|
||||||
|
console.error('Label Studio element not found');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.labelStudio = new LabelStudio('label-studio', {
|
||||||
|
config: this.config,
|
||||||
|
interfaces: ["update", "submit", "controls"],
|
||||||
|
user: { pk: 1, firstName: '标注者', lastName: '用户' },
|
||||||
|
task,
|
||||||
|
onLabelStudioLoad: (LS) => {
|
||||||
|
if (!LS.annotationStore.selectedAnnotation) {
|
||||||
|
const annotation = LS.annotationStore.addAnnotation({ userGenerate: true });
|
||||||
|
LS.annotationStore.selectAnnotation(annotation.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmitAnnotation: (LS, annotation) => this.handleAnnotationSubmit(LS, annotation, task),
|
||||||
|
onUpdateAnnotation:(LS, annotation) => this.handleAnnotationSubmit(LS, annotation, task),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 提交标注结果
|
||||||
|
async handleAnnotationSubmit(LS, annotation, task) {
|
||||||
|
const results = annotation.serializeAnnotation();
|
||||||
|
if (!results) return;
|
||||||
|
|
||||||
// 检查标注框数据
|
const formattedAnnotations = results.map(result => {
|
||||||
checkAnnotations(LS) {
|
if (result.type === 'rectanglelabels') {
|
||||||
const currentAnnotations = LS.annotationStore.data; // 获取当前的标注数据
|
return {
|
||||||
// 如果标注数据发生了变化
|
label: result.value.rectanglelabels[0],
|
||||||
if (this.hasAnnotationsChanged(currentAnnotations)) {
|
x: result.value.x,
|
||||||
console.log('标注数据发生变化:', currentAnnotations);
|
y: result.value.y,
|
||||||
this.previousAnnotations = currentAnnotations; // 更新保存的标注数据
|
width: result.value.width,
|
||||||
|
height: result.value.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
annotationResult: JSON.stringify(formattedAnnotations),
|
||||||
|
taskId: this.taskId,
|
||||||
|
fileId: this.id
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await manualAnnotate(data);
|
||||||
|
this.annotationsList.push({
|
||||||
|
taskId: task.id,
|
||||||
|
annotation: JSON.stringify(formattedAnnotations),
|
||||||
|
url: this.fileUrl
|
||||||
|
});
|
||||||
|
this.index++; // 更新索引
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to submit annotation:', error);
|
||||||
|
} finally {
|
||||||
|
this.cleanupLabelStudio(); // 清理 LabelStudio 实例
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 重置 LabelStudio
|
||||||
// 比较标注数据是否发生变化
|
resetLabelStudio() {
|
||||||
hasAnnotationsChanged(currentAnnotations) {
|
this.cleanupLabelStudio();
|
||||||
if (currentAnnotations.length !== this.previousAnnotations.length) {
|
this.initLabelStudio(); // 重新初始化
|
||||||
return true; // 数量不一样说明发生了变化
|
},
|
||||||
|
// 清理 LabelStudio 实例
|
||||||
|
cleanupLabelStudio() {
|
||||||
|
if (this.labelStudio && this.labelStudio.destroy) {
|
||||||
|
this.labelStudio.destroy();
|
||||||
}
|
}
|
||||||
|
const labelStudioElement = document.getElementById('label-studio');
|
||||||
// 比较每个标注框的几何信息
|
if (labelStudioElement) {
|
||||||
for (let i = 0; i < currentAnnotations.length; i++) {
|
labelStudioElement.innerHTML = ''; // 清空容器
|
||||||
const current = currentAnnotations[i];
|
|
||||||
const previous = this.previousAnnotations[i];
|
|
||||||
|
|
||||||
// 如果标注框的几何信息有变化,返回 true
|
|
||||||
if (JSON.stringify(current.geometry) !== JSON.stringify(previous.geometry)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.labelStudio = null; // 置空实例
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.label-studio-container {
|
.label-studio-annotator {
|
||||||
height: 600px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #e0e0e0;
|
height: 100vh;
|
||||||
border-radius: 8px;
|
display: flex;
|
||||||
overflow: hidden;
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 500px; /* 设置最小高度 */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,7 @@
|
||||||
<!-- 顶部区域 -->
|
<!-- 顶部区域 -->
|
||||||
<div class="top-part">
|
<div class="top-part">
|
||||||
<div class="top-content">
|
<div class="top-content">
|
||||||
<!-- 左侧按钮组 -->
|
|
||||||
<div class="top-content-left">
|
|
||||||
<el-button v-for="(icon, index) in leftIcons"
|
|
||||||
:key="index"
|
|
||||||
:icon="icon"
|
|
||||||
circle
|
|
||||||
>
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
<!-- 右侧选择框和设置按钮 -->
|
<!-- 右侧选择框和设置按钮 -->
|
||||||
<div class="top-content-right">
|
<div class="top-content-right">
|
||||||
<el-select
|
<el-select
|
||||||
|
|
@ -29,9 +21,9 @@
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<span class="settings-icon">
|
<!-- <span class="settings-icon">
|
||||||
<i class="el-icon-setting"></i>
|
<i class="el-icon-setting"></i>
|
||||||
</span>
|
</span>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,10 +70,11 @@
|
||||||
<div class="bottom-content-left-bottom">
|
<div class="bottom-content-left-bottom">
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="item in images" class="list-item">
|
<li v-for="(item, index) in images" :key="item.fileId" class="list-item">
|
||||||
<div @click="setItem(item)">
|
<div @click="setItem(item, index)">
|
||||||
<input type="checkbox" :checked="false" disabled>
|
<input type="checkbox" :checked="item.fileAnnotationStatus==='1'" disabled>
|
||||||
<span style="font-size: 14px;margin-left: 5px">{{ item.fileName }}</span>
|
<span :class="{'highlighted': itemIndex === index}" style="font-size: 14px; margin-left: 5px;"
|
||||||
|
>{{ item.fileName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -90,43 +83,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-content-center">
|
<div class="bottom-content-center">
|
||||||
<div>
|
<div>
|
||||||
<custom-label-studio :task="JSON.stringify(task)"/>
|
<custom-label-studio :annotations="annotationResult" :taskId="taskId" :item-index="itemIndex"
|
||||||
</div>
|
:config="labelConfig" :id="task.id" :file-url="task.data.image"
|
||||||
</div>
|
@update-itemIndex="updateItemIndex"
|
||||||
<div class="bottom-content-right">
|
></custom-label-studio>
|
||||||
<div class="bottom-content-right-center">
|
|
||||||
<div class="bottom-content-right-top">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom-content-right-middle">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom-content-right-bottom">
|
|
||||||
<div>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
size="mini"
|
|
||||||
>上一张
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
size="mini"
|
|
||||||
>下一张
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
size="mini"
|
|
||||||
>保存
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,20 +96,32 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import customLabelStudio from './customLabelStudio.vue'
|
import customLabelStudio from './customLabelStudio.vue'
|
||||||
import { getMyNoAnnotatedTask,getMyAnnotationFiles } from '../../../../api/dataCenter/annotationTask'
|
import { getMyNoAnnotatedTask, getMyAnnotationFiles } from '../../../../api/dataCenter/annotationTask'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dicts: ['ai_annotate_type'],
|
dicts: ['ai_annotate_type'],
|
||||||
components: { customLabelStudio },
|
components: { customLabelStudio },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
taskList:[],
|
itemIndex: 0,
|
||||||
|
annotationResult: [],
|
||||||
|
item: {},
|
||||||
|
taskList: [],
|
||||||
task: {
|
task: {
|
||||||
id: 1,
|
id: 0,
|
||||||
data: {
|
data: {
|
||||||
image: 'http://192.168.0.14:9090/bonus/1/测试/xigu_wangwang_3787075_37.jpg'
|
image: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
labelConfig: `
|
||||||
|
<View>
|
||||||
|
<Image name="image" value="$image"/>
|
||||||
|
<RectangleLabels name="label" toName="image">
|
||||||
|
<Label value="人" background="#ff0000"/>
|
||||||
|
<Label value="车" background="#00ff00"/>
|
||||||
|
</RectangleLabels>
|
||||||
|
</View>
|
||||||
|
`,
|
||||||
taskId: undefined, // 当前选中的任务 ID
|
taskId: undefined, // 当前选中的任务 ID
|
||||||
leftIcons: [
|
leftIcons: [
|
||||||
'el-icon-circle-plus-outline',
|
'el-icon-circle-plus-outline',
|
||||||
|
|
@ -165,38 +137,67 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.listSelection()
|
this.loadTaskList(); // 初始化时加载任务列表
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
listSelection() {
|
// 更新项的索引并切换任务数据
|
||||||
getMyNoAnnotatedTask({}).then(res => {
|
updateItemIndex(val) {
|
||||||
this.taskList = res.data;
|
const item = this.images[val];
|
||||||
})
|
this.images[val - 1].fileAnnotationStatus = '1';
|
||||||
|
this.itemIndex = val;
|
||||||
|
this.updateTaskData(item);
|
||||||
},
|
},
|
||||||
selectTask(id){
|
|
||||||
this.images = [];
|
// 加载未标注的任务列表
|
||||||
this.annotationType = 0;
|
loadTaskList() {
|
||||||
|
getMyNoAnnotatedTask().then(res => {
|
||||||
|
this.taskList = res.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 根据任务ID选择任务并加载对应的文件
|
||||||
|
selectTask(id) {
|
||||||
|
this.resetImages();
|
||||||
this.taskId = id;
|
this.taskId = id;
|
||||||
getMyAnnotationFiles(this.annotationType,id).then(response => {
|
this.fetchImages();
|
||||||
this.images =response.data;
|
|
||||||
})
|
|
||||||
},
|
|
||||||
load() {
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 切换标注类型并加载任务文件
|
||||||
toggleSelected(type) {
|
toggleSelected(type) {
|
||||||
this.images = [];
|
if (this.annotationType === type) return;
|
||||||
if (this.annotationType !== type) {
|
this.annotationType = type;
|
||||||
this.annotationType = type
|
this.resetImages();
|
||||||
}
|
this.fetchImages();
|
||||||
if (!this.taskId){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
getMyAnnotationFiles(this.annotationType,this.taskId).then(response => {
|
|
||||||
this.images =response.data;
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
setItem(item) {
|
|
||||||
console.log(item)
|
// 设置标注项,更新任务数据和标注结果
|
||||||
|
setItem(item, index) {
|
||||||
|
this.annotationResult = JSON.parse(item.annotationResult);
|
||||||
|
this.itemIndex = index;
|
||||||
|
this.updateTaskData(item);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置图像数据
|
||||||
|
resetImages() {
|
||||||
|
this.images = [];
|
||||||
|
//this.annotationType = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前任务ID和标注类型的图像数据
|
||||||
|
fetchImages() {
|
||||||
|
if (!this.taskId) return;
|
||||||
|
getMyAnnotationFiles(this.annotationType, this.taskId).then(response => {
|
||||||
|
this.itemIndex = 0;
|
||||||
|
this.images = response.data;
|
||||||
|
this.updateTaskData(this.images[this.itemIndex]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新任务的数据
|
||||||
|
updateTaskData(item) {
|
||||||
|
this.task.data.image = `http://192.168.0.14:9090/bonus/${item.fileUrl}`;
|
||||||
|
this.task.id = item.fileId;
|
||||||
|
this.annotationResult = JSON.parse(item.annotationResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -383,7 +384,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-content-center {
|
.bottom-content-center {
|
||||||
width: 60%;
|
width: 80%;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
|
@ -453,4 +454,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
color: rgb(64, 158, 255); /* 选中项文本颜色 */
|
||||||
|
font-size: 16px; /* 选中项字体大小 */
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue