【文本标注组件】ys-text-annotation

20 阅读13分钟

ys-text-annotation

npm github gitee


0. 示例效果

  • 创建标注、创建关系

创建标注、创建关系

  • 编辑标注、编辑关系

创建标注、创建关系

  • 指定位置滚动、创建远距离标注

创建标注、创建关系


1. 组件名称及用法

组件名称

ys-text-annotation

基本用法

<!-- 引入组件 -->
<script type="module" src="./dist/ys-text-annotation.js"></script>

<!-- 使用组件 -->
<ys-text-annotation id="annotator"></ys-text-annotation>

<script>
  const annotator = document.getElementById('annotator');
  
  // 初始化组件
  annotator.init({
    editable: true,
    content: '这是一段需要标注的文本。\n可以包含多行内容。',
    annotationType: [
      { type: '人名', color: '#2d0bdf' },
      { type: '地名', color: '#c3427f' }
    ],
    relationshipType: [
      { type: '位于', color: '#ff6b6b' },
      { type: '属于', color: '#4ecdc4' }
    ]
  });
</script>

简单示例

// 1. 获取组件实例
const annotator = document.querySelector('ys-text-annotation');

// 2. 初始化配置
annotator.init({
  editable: true,
  content: '张三在北京工作。',
  annotationType: [
    { type: '人名', color: '#2d0bdf' },
    { type: '地名', color: '#c3427f' }
  ],
  relationshipType: [
    { type: '工作于', color: '#ff6b6b' }
  ]
});

// 3. 设置初始数据(可选)
annotator.setData({
  annotations: [
    {
      id: '1',
      lineId: 0,
      start: 0,
      end: 2,
      content: '张三',
      type: '人名',
      description: '人物实体',
      color: '#2d0bdf'
    }
  ],
  relationships: []
});

// 4. 获取标注结果
const result = annotator.getData();
console.log(result); // { node: [...], line: [...] }

2. 核心方法:init、setData、getData

2.1 init(config) - 初始化方法

功能说明:统一初始化组件配置,包括编辑模式、文本内容、标注类型、关系类型以及各种生命周期回调函数。

参数类型

init(config: {
  editable?: boolean;                              // 是否启用编辑模式
  content?: string;                                // 文本内容(支持\n换行)
  annotations?: AnnotationItem[];                  // 初始标注数据
  relationships?: RelationshipItem[];              // 初始关系数据
  annotationType?: AnnotationType[];               // 标注类型配置
  relationshipType?: RelationshipType[];           // 关系类型配置
  relationshipTypeResolver?: relationshipTypeResolver;     // 关系选择器(生命周期)
  relationshipTypeFilter?: RelationshipTypeFilter;         // 关系类型过滤器(生命周期)
  annotationValidator?: AnnotationValidator;               // 标注验证器(生命周期)
  annotationConfirmValidator?: AnnotationConfirmValidator; // 标注确认验证器(生命周期)
  relationshipValidator?: RelationshipValidator;           // 关系验证器(生命周期)
}): void

使用示例

annotator.init({
  // 基础配置
  editable: true,
  content: '张三在北京工作。\n李四在上海生活。',
  
  // 标注类型
  annotationType: [
    { type: '人名', color: '#2d0bdf' },
    { type: '地名', color: '#c3427f' }
  ],
  
  // 关系类型
  relationshipType: [
    { type: '工作于', color: '#ff6b6b' },
    { type: '居住于', color: '#4ecdc4' }
  ],
  
  // 初始数据
  annotations: [],
  relationships: [],
  
  // 生命周期回调(详见第3节)
  relationshipTypeResolver: (start, end) => {
    // 根据标注类型自动选择关系类型
    if (start.type === '人名' && end.type === '地名') {
      return { type: '工作于', color: '#ff6b6b' };
    }
    return null;
  }
});

2.2 setData(config) - 设置数据方法

功能说明:动态更新标注和关系数据,用于外部数据同步或批量更新。

参数类型

setData(config: {
  annotations?: AnnotationItem[];      // 标注数据
  relationships?: RelationshipItem[];  // 关系数据
}): void

使用示例

// 示例1:设置标注数据
annotator.setData({
  annotations: [
    {
      id: '1',
      lineId: 0,
      start: 0,
      end: 2,
      content: '张三',
      type: '人名',
      description: '主要人物',
      color: '#2d0bdf'
    },
    {
      id: '2',
      lineId: 0,
      start: 3,
      end: 5,
      content: '北京',
      type: '地名',
      description: '工作地点',
      color: '#c3427f'
    }
  ]
});

// 示例2:设置关系数据
annotator.setData({
  relationships: [
    {
      id: 'rel-1',
      startId: '1',
      endId: '2',
      type: '工作于',
      description: '工作关系',
      color: '#ff6b6b'
    }
  ]
});

// 示例3:同时设置标注和关系
annotator.setData({
  annotations: [...],
  relationships: [...]
});

2.3 getData() - 获取数据方法

功能说明:获取当前所有标注和关系数据,用于保存或导出。

返回类型

getData(): {
  node: AnnotationItem[];      // 所有标注数据
  line: RelationshipItem[];    // 所有关系数据
}

使用示例

// 获取当前标注结果
const result = annotator.getData();

console.log('标注数据:', result.node);
console.log('关系数据:', result.line);

// 保存到服务器
fetch('/api/save', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(result)
});

// 导出为JSON文件
const dataStr = JSON.stringify(result, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'annotations.json';
a.click();

3. 生命周期方法(init中的回调函数)

生命周期方法在 init() 中初始化,用于在特定时机执行自定义逻辑。

3.1 relationshipTypeResolver - 关系选择器

触发时机:创建关系时,用于自动决定使用哪个关系类型。

函数签名

relationshipTypeResolver: (
  startAnnotation: AnnotationItem,
  endAnnotation: AnnotationItem
) => RelationshipType | string | null

参数说明

  • startAnnotation: 关系起点标注
  • endAnnotation: 关系终点标注

返回值

  • RelationshipType 对象:使用该关系类型
  • string:关系类型名称,组件会从 relationshipType 中查找对应类型
  • null:禁止创建关系

使用示例

annotator.init({
  relationshipType: [
    { type: '工作于', color: '#ff6b6b' },
    { type: '居住于', color: '#4ecdc4' },
    { type: '学习于', color: '#95e1d3' }
  ],
  
  // 示例1:返回RelationshipType对象
  relationshipTypeResolver: (start, end) => {
    if (start.type === '人名' && end.type === '地名') {
      return { type: '工作于', color: '#ff6b6b' };
    }
    return null; // 禁止创建
  },
  
  // 示例2:返回类型名称字符串
  relationshipTypeResolver: (start, end) => {
    if (start.type === '人名' && end.type === '学校') {
      return '学习于';
    }
    if (start.type === '人名' && end.type === '公司') {
      return '工作于';
    }
    return null;
  },
  
  // 示例3:复杂业务逻辑
  relationshipTypeResolver: (start, end) => {
    // 根据标注内容判断
    if (start.content.includes('学生') && end.type === '地名') {
      return '学习于';
    }
    // 根据描述判断
    if (start.description?.includes('员工')) {
      return '工作于';
    }
    // 默认返回第一个类型
    return { type: '工作于', color: '#ff6b6b' };
  }
});

3.2 relationshipTypeFilter - 关系类型过滤器

触发时机:编辑关系时,用于过滤可选的关系类型列表。

函数签名

relationshipTypeFilter: (
  relationship: RelationshipItem,
  startAnnotation: AnnotationItem,
  endAnnotation: AnnotationItem
) => RelationshipType[]

参数说明

  • relationship: 当前正在编辑的关系
  • startAnnotation: 关系起点标注
  • endAnnotation: 关系终点标注

返回值

  • RelationshipType[]:可选的关系类型列表

使用示例

annotator.init({
  relationshipType: [
    { type: '工作于', color: '#ff6b6b' },
    { type: '居住于', color: '#4ecdc4' },
    { type: '学习于', color: '#95e1d3' },
    { type: '属于', color: '#f38181' }
  ],
  
  // 示例1:根据标注类型过滤
  relationshipTypeFilter: (rel, start, end) => {
    if (start.type === '人名' && end.type === '地名') {
      return [
        { type: '工作于', color: '#ff6b6b' },
        { type: '居住于', color: '#4ecdc4' }
      ];
    }
    if (start.type === '人名' && end.type === '学校') {
      return [
        { type: '学习于', color: '#95e1d3' }
      ];
    }
    // 默认返回所有类型
    return [
      { type: '工作于', color: '#ff6b6b' },
      { type: '居住于', color: '#4ecdc4' },
      { type: '学习于', color: '#95e1d3' }
    ];
  },
  
  // 示例2:根据当前关系类型过滤
  relationshipTypeFilter: (rel, start, end) => {
    // 如果当前是"工作于",只允许切换到"居住于"
    if (rel.type === '工作于') {
      return [
        { type: '工作于', color: '#ff6b6b' },
        { type: '居住于', color: '#4ecdc4' }
      ];
    }
    return [
      { type: '工作于', color: '#ff6b6b' },
      { type: '居住于', color: '#4ecdc4' },
      { type: '学习于', color: '#95e1d3' }
    ];
  }
});

3.3 annotationValidator - 标注验证器(选中文本阶段)

触发时机:选中文本后、显示编辑层前,用于验证是否允许创建标注。

函数签名

annotationValidator: (
  selectedText: SelectedTextInfo,
  existingAnnotations: AnnotationItem[]
) => { valid: boolean; message?: string }

参数说明

  • selectedText: 选中的文本信息
    • lineId: 行号
    • start: 起始位置
    • end: 结束位置
    • content: 选中的文本内容
  • existingAnnotations: 已有的标注列表

返回值

  • { valid: true }: 验证通过,允许显示编辑层
  • { valid: false, message?: string }: 验证失败,阻止显示编辑层,可选的 message 用于错误提示

使用示例

annotator.init({
  // 示例1:限制标注长度
  annotationValidator: (selectedText, existingAnnotations) => {
    if (selectedText.content.length > 20) {
      return {
        valid: false,
        message: '标注内容不能超过20个字符'
      };
    }
    return { valid: true };
  },
  
  // 示例2:禁止重复标注
  annotationValidator: (selectedText, existingAnnotations) => {
    const isDuplicate = existingAnnotations.some(
      ann => ann.content === selectedText.content
    );
    if (isDuplicate) {
      return {
        valid: false,
        message: '该内容已被标注'
      };
    }
    return { valid: true };
  },
  
  // 示例3:限制每行标注数量
  annotationValidator: (selectedText, existingAnnotations) => {
    const lineAnnotations = existingAnnotations.filter(
      ann => ann.lineId === selectedText.lineId
    );
    if (lineAnnotations.length >= 5) {
      return {
        valid: false,
        message: '每行最多只能标注5个实体'
      };
    }
    return { valid: true };
  },
  
  // 示例4:正则验证
  annotationValidator: (selectedText, existingAnnotations) => {
    // 只允许标注中文
    if (!/^[\u4e00-\u9fa5]+$/.test(selectedText.content)) {
      return {
        valid: false,
        message: '只能标注中文内容'
      };
    }
    return { valid: true };
  }
});

// 监听错误事件,显示错误提示
annotator.addEventListener('error', (e) => {
  if (e.detail.code === 'ANNOTATION_VALIDATION_FAILED') {
    alert(e.detail.message);
  }
});

3.4 annotationConfirmValidator - 标注确认验证器(确认创建阶段)

触发时机:用户选择类型、输入描述后、点击确认按钮前,用于验证是否允许创建标注。

函数签名

annotationConfirmValidator: (
  annotation: Omit<AnnotationItem, 'id'>,
  existingAnnotations: AnnotationItem[]
) => { valid: boolean; message?: string }

参数说明

  • annotation: 待创建的标注(不含id)
    • lineId: 行号
    • start: 起始位置
    • end: 结束位置
    • content: 标注内容
    • type: 标注类型
    • description: 描述
    • color: 颜色
  • existingAnnotations: 已有的标注列表

返回值

  • { valid: true }: 验证通过,允许创建标注
  • { valid: false, message?: string }: 验证失败,阻止创建标注

使用示例

annotator.init({
  // 示例1:必须填写描述
  annotationConfirmValidator: (annotation, existingAnnotations) => {
    if (!annotation.description || annotation.description.trim() === '') {
      return {
        valid: false,
        message: '请填写标注描述'
      };
    }
    return { valid: true };
  },
  
  // 示例2:限制特定类型的标注数量
  annotationConfirmValidator: (annotation, existingAnnotations) => {
    if (annotation.type === '人名') {
      const personCount = existingAnnotations.filter(
        ann => ann.type === '人名'
      ).length;
      if (personCount >= 10) {
        return {
          valid: false,
          message: '人名标注数量已达上限(10个)'
        };
      }
    }
    return { valid: true };
  },
  
  // 示例3:验证描述格式
  annotationConfirmValidator: (annotation, existingAnnotations) => {
    if (annotation.type === '日期') {
      // 描述必须是日期格式
      if (!/^\d{4}-\d{2}-\d{2}$/.test(annotation.description)) {
        return {
          valid: false,
          message: '日期描述格式必须为 YYYY-MM-DD'
        };
      }
    }
    return { valid: true };
  },
  
  // 示例4:组合验证
  annotationConfirmValidator: (annotation, existingAnnotations) => {
    // 验证1:描述长度
    if (annotation.description.length > 100) {
      return {
        valid: false,
        message: '描述不能超过100个字符'
      };
    }
    
    // 验证2:同一行不能有相同类型的重复标注
    const hasDuplicate = existingAnnotations.some(
      ann => ann.lineId === annotation.lineId &&
             ann.type === annotation.type &&
             ann.content === annotation.content
    );
    if (hasDuplicate) {
      return {
        valid: false,
        message: '该行已存在相同类型的标注'
      };
    }
    
    return { valid: true };
  }
});

// 监听错误事件
annotator.addEventListener('error', (e) => {
  if (e.detail.code === 'ANNOTATION_CONFIRM_VALIDATION_FAILED') {
    alert(e.detail.message);
  }
});

3.5 relationshipValidator - 关系验证器

触发时机:创建关系前,用于验证是否允许创建该关系。

函数签名

relationshipValidator: (
  startAnnotation: AnnotationItem,
  endAnnotation: AnnotationItem,
  existingRelationships: RelationshipItem[]
) => { valid: boolean; message?: string }

参数说明

  • startAnnotation: 关系起点标注
  • endAnnotation: 关系终点标注
  • existingRelationships: 已有的关系列表

返回值

  • { valid: true }: 验证通过,允许创建关系
  • { valid: false, message?: string }: 验证失败,阻止创建关系

使用示例

annotator.init({
  // 示例1:禁止重复关系
  relationshipValidator: (start, end, existingRelationships) => {
    const isDuplicate = existingRelationships.some(
      rel => rel.startId === start.id && rel.endId === end.id
    );
    if (isDuplicate) {
      return {
        valid: false,
        message: '该关系已存在'
      };
    }
    return { valid: true };
  },
  
  // 示例2:限制关系类型组合
  relationshipValidator: (start, end, existingRelationships) => {
    // 人名只能与地名建立关系
    if (start.type === '人名' && end.type !== '地名') {
      return {
        valid: false,
        message: '人名只能与地名建立关系'
      };
    }
    return { valid: true };
  },
  
  // 示例3:限制每个标注的关系数量
  relationshipValidator: (start, end, existingRelationships) => {
    const startRelCount = existingRelationships.filter(
      rel => rel.startId === start.id || rel.endId === start.id
    ).length;
    
    if (startRelCount >= 5) {
      return {
        valid: false,
        message: `标注"${start.content}"的关系数量已达上限(5个)`
      };
    }
    return { valid: true };
  },
  
  // 示例4:禁止自环关系
  relationshipValidator: (start, end, existingRelationships) => {
    if (start.id === end.id) {
      return {
        valid: false,
        message: '不能创建指向自己的关系'
      };
    }
    return { valid: true };
  },
  
  // 示例5:复杂业务规则
  relationshipValidator: (start, end, existingRelationships) => {
    // 规则1:同一对标注之间最多只能有2个关系
    const existingCount = existingRelationships.filter(
      rel => (rel.startId === start.id && rel.endId === end.id) ||
             (rel.startId === end.id && rel.endId === start.id)
    ).length;
    
    if (existingCount >= 2) {
      return {
        valid: false,
        message: '同一对标注之间最多只能有2个关系'
      };
    }
    
    // 规则2:检查是否会形成循环
    // (这里简化处理,实际可能需要更复杂的图算法)
    const wouldCreateCycle = existingRelationships.some(
      rel => rel.startId === end.id && rel.endId === start.id
    );
    
    if (wouldCreateCycle) {
      return {
        valid: false,
        message: '不能创建循环关系'
      };
    }
    
    return { valid: true };
  }
});

// 监听错误事件
annotator.addEventListener('error', (e) => {
  if (e.detail.code === 'RELATIONSHIP_VALIDATION_FAILED') {
    alert(e.detail.message);
  }
});

4. 事件监听

组件提供多种事件监听,用于响应用户操作和数据变化。

4.1 data-change - 数据变化事件

触发时机:标注或关系数据发生变化时(增删改)。

事件详情类型

interface DataChangeEventDetail {
  annotations: AnnotationItem[];      // 当前所有标注
  relationships: RelationshipItem[];  // 当前所有关系
}

使用示例

annotator.addEventListener('data-change', (event) => {
  console.log('数据已变化');
  console.log('当前标注:', event.detail.annotations);
  console.log('当前关系:', event.detail.relationships);
  
  // 自动保存到本地存储
  localStorage.setItem('annotations', JSON.stringify(event.detail));
  
  // 或保存到服务器
  fetch('/api/save', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(event.detail)
  });
});

4.2 error - 错误事件

触发时机:组件内部发生错误时(如验证失败、验证器执行错误等)。

事件详情类型

interface ErrorEventDetail {
  message: string;  // 错误消息
  code?: string;    // 错误代码
  data?: any;       // 附加数据
}

错误代码列表

  • ANNOTATION_VALIDATION_FAILED: 标注验证失败(选中文本阶段)
  • ANNOTATION_CONFIRM_VALIDATION_FAILED: 标注确认验证失败(确认创建阶段)
  • RELATIONSHIP_VALIDATION_FAILED: 关系验证失败
  • VALIDATOR_ERROR: 验证器执行错误
  • SELECTOR_ERROR: 关系选择器执行错误
  • FILTER_ERROR: 关系类型过滤器执行错误

使用示例

annotator.addEventListener('error', (event) => {
  const { message, code, data } = event.detail;
  
  console.error('错误:', message);
  console.error('错误代码:', code);
  console.error('附加数据:', data);
  
  // 根据错误代码显示不同的提示
  switch (code) {
    case 'ANNOTATION_VALIDATION_FAILED':
      alert(`标注验证失败: ${message}`);
      break;
    case 'ANNOTATION_CONFIRM_VALIDATION_FAILED':
      alert(`标注确认失败: ${message}`);
      break;
    case 'RELATIONSHIP_VALIDATION_FAILED':
      alert(`关系验证失败: ${message}`);
      break;
    case 'VALIDATOR_ERROR':
      console.error('验证器执行错误:', data);
      alert('验证器执行出错,请检查配置');
      break;
    case 'SELECTOR_ERROR':
      console.error('关系选择器执行错误:', data);
      alert('关系选择器执行出错,请检查配置');
      break;
    case 'FILTER_ERROR':
      console.error('关系类型过滤器执行错误:', data);
      alert('关系类型过滤器执行出错,请检查配置');
      break;
    default:
      alert(`发生错误: ${message}`);
  }
});

// 示例:使用Toast库显示错误
annotator.addEventListener('error', (event) => {
  // 假设使用了某个Toast库
  Toast.error(event.detail.message);
});

5. 数据类型定义

5.1 AnnotationItem - 标注项

interface AnnotationItem {
  id: string;          // 唯一标识
  lineId: number;      // 所在行号(从0开始)
  start: number;       // 起始位置(字符索引)
  end: number;         // 结束位置(字符索引)
  content: string;     // 标注内容
  type: string;        // 标注类型
  description: string; // 描述信息
  color?: string;      // 颜色(可选,默认使用类型颜色)
}

5.2 RelationshipItem - 关系项

interface RelationshipItem {
  id: string;          // 唯一标识
  startId: string;     // 起点标注ID
  endId: string;       // 终点标注ID
  type: string;        // 关系类型
  description: string; // 描述信息
  color?: string;      // 颜色(可选,默认使用类型颜色)
}

5.3 AnnotationType - 标注类型

interface AnnotationType {
  type: string;   // 类型名称(唯一标识)
  color: string;  // 颜色值(支持hex、rgb等)
}

5.4 RelationshipType - 关系类型

interface RelationshipType {
  type: string;   // 类型名称(唯一标识)
  color: string;  // 颜色值(支持hex、rgb等)
}

6. 完整示例

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>文本标注示例</title>
    <style>
      body {
        margin: 0;
        padding: 20px;
        font-family: Arial, sans-serif;
      }
      ys-text-annotation {
        width: 100%;
        height: 600px;
        display: block;
        border: 1px solid #ddd;
      }
      .controls {
        margin-bottom: 20px;
      }
      button {
        margin-right: 10px;
        padding: 8px 16px;
      }
    </style>
    <script type="module" src="/src/ys-text-annotation.ts"></script>
  </head>
  <body>
    <div class="controls">
      <button onclick="saveData()">保存数据</button>
      <button onclick="loadData()">加载数据</button>
      <button onclick="clearData()">清空数据</button>
    </div>

    <ys-text-annotation id="annotator"></ys-text-annotation>

    <script type="module">
      const annotator = document.getElementById('annotator')

      // 初始化组件
      annotator.init({
        editable: true,
        content: `旧历的年底毕竟最像年底,村镇上不必说,就在天空中也显出将到新年的气象来。灰白色的沉重的晚云中间时时发出闪光,接着一声钝响,是送灶的爆竹;近处燃放的可就更强烈了,震耳的大音还没有息,空气里已经散满了幽微的火药香。我是正在这一夜回到我的故乡鲁镇的。虽说故乡,然而已没有家,所以只得暂寓在鲁四老爷的宅子里。他是我的本家,比我长一辈,应该称之曰“四叔”,是一个讲理学的老监生。他比先前并没有什么大改变,单是老了些,但也还未留胡子,一见面是寒喧,寒喧之后说我“胖了”,说我“胖了”之后即大骂其新党。但我知道,这并非借题在骂我:因为他所骂的还是康有为。但是,谈话是总不投机的了,于是不多久,我便一个人剩在书房里。

第二天我起得很迟,午饭之后,出去看了几个本家和朋友;第三夭也照样。他们也都没有什么大改变,单是老了些;家中却一律忙,都在准备着“祝福”。这是鲁镇年终的大典,致敬尽礼,迎接福神,拜求来年一年中的好运气的。杀鸡,宰鹅,买猪肉,用心细细的洗,女人的臂膊都在水里浸得通红,有的还带着绞丝银镯子。煮熟之后,横七竖八的插些筷子在这类东西上,可就称为“福礼”了,五更天陈列起来,并且点上香烛,恭请福神们来享用;拜的却只限于男人,拜完自然仍然是放爆竹。年年如此,家家如此,——只要买得起福礼和爆竹之类的,——今年自然也如此。天色愈阴暗了,下午竟下起雪来,雪花大的有梅花那么大,满天飞舞,夹着烟霭和忙碌的气色,将鲁镇乱成一团糟。我回到四叔的书房里时,瓦楞上已经雪白,房里也映得较光明,极分明的显出壁上挂着的朱拓的大“寿”字,陈抟老祖写的;一边的对联已经脱落,松松的卷了放在长桌上,一边的还在,道是“事理通达心气和平”。我又无聊赖的到窗下的案头去一翻,只见一堆似乎未必完全的《康熙字典》,一部《近思录集注》和一部《四书衬》。无论如何,我明天决计要走了。`,

        // 标注类型配置
        annotationType: [
          { type: '人名', color: '#2d0bdf' },
          { type: '地名', color: '#c3427f' },
          { type: '组织', color: '#ff6b6b' }
        ],

        // 关系类型配置
        relationshipType: [
          { type: '工作于', color: '#ff6b6b' },
          { type: '发生于', color: '#4ecdc4' },
          { type: '学习于', color: '#107c10' }
        ],

        // 关系选择器:根据标注类型自动选择关系类型
        relationshipTypeResolver: (start, end) => {
          if (start.type === '人名' && end.type === '地名') {
            // 根据上下文智能选择
            if (start.content.includes('学生') || start.description?.includes('学习')) {
              return '学习于'
            }
            return '工作于'
          }
          return null
        },

        // 标注验证器:限制标注长度
        annotationValidator: (selectedText, existingAnnotations) => {
          if (selectedText.content.length > 20) {
            return {
              valid: false,
              message: '标注内容不能超过20个字符'
            }
          }
          return { valid: true }
        },

        // 关系验证器:禁止重复关系
        relationshipValidator: (start, end, existingRelationships) => {
          const isDuplicate = existingRelationships.some(rel => rel.startId === start.id && rel.endId === end.id)
          if (isDuplicate) {
            return {
              valid: false,
              message: '该关系已存在'
            }
          }
          return { valid: true }
        }
      })

      // 监听数据变化事件
      annotator.addEventListener('data-change', event => {
        console.log('数据已变化:', event.detail)
        // 自动保存到本地存储
        localStorage.setItem('annotationData', JSON.stringify(event.detail))
      })

      // 监听错误事件
      annotator.addEventListener('error', event => {
        console.error('错误:', event.detail)
        alert(event.detail.message)
      })

      // 保存数据
      window.saveData = function () {
        const data = annotator.getData()
        const dataStr = JSON.stringify(data, null, 2)
        const blob = new Blob([dataStr], { type: 'application/json' })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = 'annotations.json'
        a.click()
        URL.revokeObjectURL(url)
        alert('数据已导出')
      }

      // 加载数据
      window.loadData = function () {
        const savedData = localStorage.getItem('annotationData')
        if (savedData) {
          const data = JSON.parse(savedData)
          annotator.setData(data)
          alert('数据已加载')
        } else {
          alert('没有保存的数据')
        }
      }

      // 清空数据
      window.clearData = function () {
        if (confirm('确定要清空所有标注数据吗?')) {
          annotator.setData({
            annotations: [],
            relationships: []
          })
          localStorage.removeItem('annotationData')
          alert('数据已清空')
        }
      }
    </script>
  </body>
</html>

7. 属性配置

7.1 editingEnabled - 编辑模式

类型boolean
默认值false
说明:是否启用编辑模式。设置为 true 时,用户可以创建、编辑、删除标注和关系。

// 通过 init 方法设置
annotator.init({ editable: true });

// 或直接设置属性
annotator.editingEnabled = true;

7.2 content - 文本内容

类型string
默认值''
说明:要标注的文本内容,支持 \n 换行符。

// 通过 init 方法设置
annotator.init({
  content: '第一行文本\n第二行文本\n第三行文本'
});

// 或直接设置属性
annotator.content = '新的文本内容';

7.3 showLineNumber - 显示行号

类型boolean
默认值true
说明:是否显示行号。

// 直接设置属性
annotator.showLineNumber = false; // 隐藏行号

8. CSS 自定义变量

组件支持通过 CSS 变量自定义样式:

ys-text-annotation {
  /* 默认标注颜色 */
  --default-node-color: #2d0bdf;
  /* 默认关系颜色 */
  --default-line-color: #c3427f;
}

9. 常见问题

9.1 如何禁止创建某些关系?

使用 relationshipTypeResolver 返回 null

annotator.init({
  relationshipTypeResolver: (start, end) => {
    // 只允许人名与地名建立关系
    if (start.type === '人名' && end.type === '地名') {
      return { type: '工作于', color: '#ff6b6b' };
    }
    // 其他情况禁止创建
    return null;
  }
});

9.2 如何限制标注数量?

使用 annotationValidatorannotationConfirmValidator

annotator.init({
  annotationConfirmValidator: (annotation, existingAnnotations) => {
    if (existingAnnotations.length >= 100) {
      return {
        valid: false,
        message: '标注数量已达上限(100个)'
      };
    }
    return { valid: true };
  }
});

9.3 如何实现自动保存?

监听 data-change 事件:

annotator.addEventListener('data-change', (event) => {
  // 保存到本地存储
  localStorage.setItem('annotations', JSON.stringify(event.detail));
  
  // 或保存到服务器
  fetch('/api/save', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(event.detail)
  });
});

9.4 如何处理大文本?

组件内置虚拟滚动功能,可以高效处理大量文本:

// 支持数万行文本
const largeText = Array(10000).fill('这是一行文本').join('\n');
annotator.init({
  content: largeText
});

10. 贡献

欢迎提交 Issue 和 Pull Request!