|
@@ -0,0 +1,263 @@
|
|
|
+<template>
|
|
|
+ <div id="app">
|
|
|
+ <el-container style="padding: 20px;">
|
|
|
+ <el-header>
|
|
|
+ <h1 style="text-align: center;">文本标注工具</h1>
|
|
|
+ </el-header>
|
|
|
+
|
|
|
+ <el-main>
|
|
|
+ <!-- 标注文本和操作按钮 -->
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="18">
|
|
|
+ <h3 style="display: inline-block; margin-right: 20px;">标注文本</h3>
|
|
|
+ <el-button type="primary" @click="highlightText">高亮标注</el-button>
|
|
|
+ <el-color-picker v-model="selectedColor" style="margin-left: 10px;"></el-color-picker>
|
|
|
+ <input type="file" @change="handleFileUpload" accept=".txt" style="display: none;" ref="fileInput">
|
|
|
+ <el-button type="primary" @click="triggerFileUpload" style="margin-left: 10px;">上传文件</el-button>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 标注文本区域 -->
|
|
|
+ <el-row style="margin-top: 20px;">
|
|
|
+ <el-col :span="24">
|
|
|
+ <div
|
|
|
+ id="textInput"
|
|
|
+ contenteditable="true"
|
|
|
+ @mouseup="handleTextSelection"
|
|
|
+ v-html="highlightedText"
|
|
|
+ style="border: 1px solid #ddd; padding: 10px; min-height: 100px;"
|
|
|
+ ></div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 选定内容、开始位置、结束位置 -->
|
|
|
+ <el-row style="margin-top: 20px;">
|
|
|
+ <el-col :span="8">
|
|
|
+ <h3>选定内容</h3>
|
|
|
+ <input type="text" v-model="selectedText" readonly>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <h3>开始位置</h3>
|
|
|
+ <input type="text" v-model="startOffset" readonly>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <h3>结束位置</h3>
|
|
|
+ <input type="text" v-model="endOffset" readonly>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 关系标注 -->
|
|
|
+ <h3>关系标注</h3>
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-select v-model="headEntityId" placeholder="选择头实体" style="width: 100%;">
|
|
|
+ <el-option v-for="entity in entities" :key="entity.id" :label="entity.text" :value="entity.id"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-select v-model="tailEntityId" placeholder="选择尾实体" style="width: 100%;">
|
|
|
+ <el-option v-for="entity in entities" :key="entity.id" :label="entity.text" :value="entity.id"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-select v-model="currentRelationType" placeholder="选择关系类型" style="width: 100%;">
|
|
|
+ <el-option v-for="type in relationTypes" :key="type" :label="type" :value="type"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-button type="primary" @click="saveRelation">保存关系</el-button>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 关系列表 -->
|
|
|
+ <h3>关系列表</h3>
|
|
|
+ <ul>
|
|
|
+ <li v-for="relation in relations" :key="relation.id">
|
|
|
+ {{ getEntityText(relation.headEntityId) }} -> {{ getEntityText(relation.tailEntityId) }}({{ relation.type }})
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </el-main>
|
|
|
+ </el-container>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'AutoRemark',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ text: "这是一些需要标注的文本。用户可以选择文本并标记。",
|
|
|
+ highlightedText: "这是一些需要标注的文本。用户可以选择文本并标记。",
|
|
|
+ selectedLabel: "重要",
|
|
|
+ selectedText: '', // 选定的内容
|
|
|
+ startOffset: -1, // 开始位置
|
|
|
+ endOffset: -1, // 结束位置
|
|
|
+ entities: [], // 存储所有标注的实体
|
|
|
+ relations: [], // 存储所有标注的关系
|
|
|
+ relationTypes: ['位于', '属于', '创建'], // 关系类型列表
|
|
|
+ headEntityId: '', // 关系中的头实体ID
|
|
|
+ tailEntityId: '', // 关系中的尾实体ID
|
|
|
+ currentRelationType: '', // 当前选择的关系类型
|
|
|
+ selectedColor: '#ffeb3b' // 默认高亮颜色
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 触发文件上传
|
|
|
+ triggerFileUpload() {
|
|
|
+ this.$refs.fileInput.click();
|
|
|
+ },
|
|
|
+ // 处理文件上传
|
|
|
+ handleFileUpload(event) {
|
|
|
+ const file = event.target.files[0];
|
|
|
+ if (file && file.type === 'text/plain') {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => {
|
|
|
+ const content = e.target.result;
|
|
|
+ this.text = content;
|
|
|
+ this.highlightedText = content;
|
|
|
+ this.$message({
|
|
|
+ message: '文件上传成功!',
|
|
|
+ type: 'success'
|
|
|
+ });
|
|
|
+ };
|
|
|
+ reader.readAsText(file);
|
|
|
+ } else {
|
|
|
+ this.$message({
|
|
|
+ message: '请上传有效的文本文件(.txt)!',
|
|
|
+ type: 'warning'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ highlightText() {
|
|
|
+ if (this.selectedText) {
|
|
|
+ // 创建实体对象
|
|
|
+ const entity = {
|
|
|
+ id: `entity-${this.entities.length + 1}`, // 生成唯一ID
|
|
|
+ text: this.selectedText,
|
|
|
+ startOffset: this.startOffset,
|
|
|
+ endOffset: this.endOffset,
|
|
|
+ label: this.selectedLabel,
|
|
|
+ color: this.selectedColor // 保存标注颜色
|
|
|
+ };
|
|
|
+ this.entities.push(entity); // 保存实体
|
|
|
+
|
|
|
+ // 高亮显示
|
|
|
+ this.updateHighlightedText();
|
|
|
+ } else {
|
|
|
+ this.$message({
|
|
|
+ message: '请先选择文本!',
|
|
|
+ type: 'warning'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleTextSelection() {
|
|
|
+ const textInput = document.getElementById('textInput');
|
|
|
+ const selection = window.getSelection();
|
|
|
+ if (selection.rangeCount > 0) {
|
|
|
+ const range = selection.getRangeAt(0);
|
|
|
+ this.selectedText = range.toString();
|
|
|
+ this.startOffset = this.getOffset(range.startContainer, range.startOffset);
|
|
|
+ this.endOffset = this.getOffset(range.endContainer, range.endOffset);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getOffset(node, offset) {
|
|
|
+ let totalOffset = 0;
|
|
|
+ const textInput = document.getElementById('textInput');
|
|
|
+ const walker = document.createTreeWalker(textInput, NodeFilter.SHOW_TEXT, null, false);
|
|
|
+ let currentNode;
|
|
|
+
|
|
|
+ while ((currentNode = walker.nextNode())) {
|
|
|
+ if (currentNode === node) {
|
|
|
+ totalOffset += offset;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ totalOffset += currentNode.textContent.length;
|
|
|
+ }
|
|
|
+ return totalOffset;
|
|
|
+ },
|
|
|
+ updateHighlightedText() {
|
|
|
+ // 初始化高亮文本
|
|
|
+ let highlightedText = this.text;
|
|
|
+
|
|
|
+ // 遍历所有实体,添加高亮效果
|
|
|
+ this.entities.forEach(entity => {
|
|
|
+ const highlightedEntity = `<span style="background-color: ${entity.color}; color: black;" title="${entity.label}">${entity.text}</span>`;
|
|
|
+ highlightedText = highlightedText.replace(entity.text, highlightedEntity);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新高亮文本
|
|
|
+ this.highlightedText = highlightedText;
|
|
|
+ },
|
|
|
+ saveRelation() {
|
|
|
+ if (this.headEntityId && this.tailEntityId && this.currentRelationType) {
|
|
|
+ // 创建关系对象
|
|
|
+ const relation = {
|
|
|
+ id: `relation-${this.relations.length + 1}`, // 生成唯一ID
|
|
|
+ headEntityId: this.headEntityId,
|
|
|
+ tailEntityId: this.tailEntityId,
|
|
|
+ type: this.currentRelationType
|
|
|
+ };
|
|
|
+ this.relations.push(relation); // 保存关系
|
|
|
+ this.$message({
|
|
|
+ message: '关系保存成功!',
|
|
|
+ type: 'success'
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ this.$message({
|
|
|
+ message: '请选择头实体、尾实体和关系类型!',
|
|
|
+ type: 'warning'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getEntityText(entityId) {
|
|
|
+ const entity = this.entities.find(e => e.id === entityId);
|
|
|
+ return entity ? entity.text : '未知实体';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+#app {
|
|
|
+ font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
|
+ -webkit-font-smoothing: antialiased;
|
|
|
+ -moz-osx-font-smoothing: grayscale;
|
|
|
+ color: #2c3e50;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+#textInput {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 100px;
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 14px;
|
|
|
+ white-space: pre-wrap; /* 保留换行和空格 */
|
|
|
+}
|
|
|
+
|
|
|
+input[readonly] {
|
|
|
+ width: 100%;
|
|
|
+ padding: 5px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-row {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-color-picker {
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+
|
|
|
+.el-button {
|
|
|
+ margin-left: 10px;
|
|
|
+}
|
|
|
+</style>
|