Browse Source

文字特殊标记

zhangjie 1 year ago
parent
commit
23fd7b808c

+ 157 - 38
src/features/check/CommonMarkBody.vue

@@ -35,7 +35,6 @@
               :sliceImageWidth="item.originalImageWidth"
               :dx="item.dx"
               :dy="item.dy"
-              @delete-specialtag="(tag) => deleteSpecialtag(item, tag)"
               @click-specialtag="(event) => clickSpecialtag(event, item)"
             />
             <div
@@ -46,6 +45,7 @@
                 moveElement: specialMouseMove,
                 moveStop: specialMouseStop,
               }"
+              @click="(event) => canvasClick(event, item)"
             >
               <template v-if="curSliceImagesWithTrackItem?.url === item.url">
                 <div
@@ -53,9 +53,25 @@
                   :style="specialLenStyle"
                 ></div>
                 <div
-                  v-if="store.currentSpecialTagType === 'CIRCLE'"
+                  v-else-if="store.currentSpecialTagType === 'CIRCLE'"
                   :style="specialCircleStyle"
                 ></div>
+                <div
+                  v-else-if="store.currentSpecialTagType === 'TEXT'"
+                  v-show="cacheTextTrack.id"
+                  :id="`text-edit-box-${cacheTextTrack.id}`"
+                  :key="cacheTextTrack.id"
+                  class="text-edit-box"
+                  contenteditable
+                  :style="specialTextStyle"
+                  @input="textTrackInput"
+                  @blur="textTrackBlur"
+                  @keypress.stop
+                  @keydown.stop
+                  @mousedown.stop
+                  @mousemove.stop
+                  @mouseup.stop
+                ></div>
               </template>
             </div>
           </div>
@@ -71,12 +87,19 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, onUnmounted, reactive, watch, watchEffect } from "vue";
+import {
+  onMounted,
+  onUnmounted,
+  reactive,
+  watch,
+  watchEffect,
+  nextTick,
+} from "vue";
 import { store } from "@/store/store";
 import MarkDrawTrack from "../mark/MarkDrawTrack.vue";
 import type { SliceImage, SpecialTag, Track } from "@/types";
 import { useTimers } from "@/setups/useTimers";
-import { loadImage } from "@/utils/utils";
+import { loadImage, randomCode } from "@/utils/utils";
 import { dragImage } from "../mark/use/draggable";
 import MultiMediaMarkBody from "../mark/MultiMediaMarkBody.vue";
 import "viewerjs/dist/viewer.css";
@@ -102,27 +125,6 @@ const {
 
 const emit = defineEmits(["error"]);
 
-const deleteSpecialtag = (item, tag) => {
-  const findInd = (tagList, curTag) => {
-    return tagList.findIndex(
-      (itemTag) =>
-        itemTag.tagName === curTag.tagName &&
-        itemTag.offsetX === curTag.offsetX &&
-        itemTag.offsetY === curTag.offsetY
-    );
-  };
-
-  const tagIndex = findInd(item.tagList, tag);
-  if (tagIndex === -1) return;
-  item.tagList.splice(tagIndex, 1);
-
-  const stagIndex = findInd(
-    store.currentTaskEnsured.markResult.specialTagList,
-    tag
-  );
-  if (stagIndex === -1) return;
-  store.currentTaskEnsured.markResult.specialTagList.splice(tagIndex, 1);
-};
 const clickSpecialtag = (event: MouseEvent, item: SliceImage) => {
   // console.log(event);
   const e = {
@@ -134,14 +136,6 @@ const clickSpecialtag = (event: MouseEvent, item: SliceImage) => {
   makeTrack(e as MouseEvent, item, maxImageWidth, theFinalHeight);
 };
 
-const clearEmptySpecialTag = (item) => {
-  item.tagList
-    .filter((item) => !item.tagName.trim().replace("\n", ""))
-    .forEach((tag) => {
-      deleteSpecialtag(item, tag);
-    });
-};
-
 //#region : 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
 const { dragContainer } = dragImage();
 //#endregion : 图片拖动
@@ -431,19 +425,26 @@ const innerMakeTrack = (event: MouseEvent, item: SliceImage) => {
     void message.warn("轨迹位置距离边界太近");
     return;
   }
-  clearEmptySpecialTag(item);
   makeTrack(event, item, maxImageWidth, theFinalHeight);
 };
 //#endregion : 评分
 
-//#region : 特殊标记:画线、框
+//#region : 特殊标记:画线、框、文字
 const isCustomSpecialTag = $computed(() => {
-  return ["CIRCLE", "LINE"].includes(store.currentSpecialTagType);
+  return ["CIRCLE", "LINE", "TEXT"].includes(store.currentSpecialTagType);
 });
 
 let specialPoint = $ref({ x: 0, y: 0, ex: 0, ey: 0 });
 let curImageTarget: HTMLElement = null;
 let curSliceImagesWithTrackItem: SliceImage = $ref(null);
+let cacheTextTrack = $ref({
+  id: "",
+  x: 0,
+  y: 0,
+  maxW: 0,
+  maxY: 0,
+  content: "",
+});
 
 const specialLenStyle = $computed(() => {
   if (specialPoint.ex <= specialPoint.x) return { display: "none" };
@@ -478,18 +479,34 @@ const specialCircleStyle = $computed(() => {
     zIndex: 9,
   };
 });
+const specialTextStyle = $computed(() => {
+  return {
+    top: cacheTextTrack.y + "px",
+    left: cacheTextTrack.x + "px",
+    minWidth: "30px",
+    minHeight: "30px",
+    maxWidth: curImageTarget.width - cacheTextTrack.x + "px",
+    maxHeight: curImageTarget.height - cacheTextTrack.y + "px",
+  };
+});
 
 function specialMouseStart(e: MouseEvent, item: SliceImage) {
+  if (store.currentSpecialTagType === "TEXT") return;
+
   curImageTarget = e.target.parentElement.childNodes[0];
   curSliceImagesWithTrackItem = item;
   specialPoint.x = e.offsetX;
   specialPoint.y = e.offsetY;
 }
 function specialMouseMove({ left, top }) {
+  if (store.currentSpecialTagType === "TEXT") return;
+
   specialPoint.ex = left + specialPoint.x;
   specialPoint.ey = top + specialPoint.y;
 }
 function specialMouseStop(e: MouseEvent) {
+  if (store.currentSpecialTagType === "TEXT") return;
+
   if (
     store.currentSpecialTagType === "LINE" &&
     specialPoint.ex <= specialPoint.x
@@ -515,11 +532,13 @@ function specialMouseStop(e: MouseEvent) {
       curSliceImagesWithTrackItem.dy,
     positionX: -1,
     positionY: -1,
+    groupNumber: store.currentQuestion.groupNumber,
+    color: "green",
   };
   track.positionX =
-    (specialPoint.x - curSliceImagesWithTrackItem.dx) / maxImageWidth;
+    (track.offsetX - curSliceImagesWithTrackItem.dx) / maxImageWidth;
   track.positionY =
-    (specialPoint.y -
+    (track.offsetY -
       curSliceImagesWithTrackItem.dy +
       curSliceImagesWithTrackItem.accumTopHeight) /
     theFinalHeight;
@@ -546,6 +565,90 @@ function specialMouseStop(e: MouseEvent) {
   curSliceImagesWithTrackItem.tagList.push(track);
   specialPoint = { x: 0, y: 0, ex: 0, ey: 0 };
 }
+
+function canvasClick(e: Event, item: SliceImage) {
+  if (cacheTextTrack.id) {
+    textTrackBlur();
+  }
+
+  curImageTarget = e.target.parentElement.childNodes[0];
+  curSliceImagesWithTrackItem = item;
+
+  cacheTextTrack.x = e.offsetX;
+  cacheTextTrack.y = e.offsetY;
+  cacheTextTrack.id = randomCode();
+  cacheTextTrack.content = "";
+
+  nextTick(() => {
+    document.getElementById(`text-edit-box-${cacheTextTrack.id}`).focus();
+  });
+}
+
+function textTrackInput(e: Event) {
+  cacheTextTrack.content = e.target.outerText;
+}
+function initCacheTextTrack() {
+  cacheTextTrack = {
+    x: 0,
+    y: 0,
+    maxW: 0,
+    maxY: 0,
+    content: "",
+    id: "",
+  };
+}
+function textTrackBlur() {
+  if (!cacheTextTrack.content) {
+    initCacheTextTrack();
+    return;
+  }
+  const textBoxDom = document.getElementById(
+    `text-edit-box-${cacheTextTrack.id}`
+  );
+
+  // 减去内边距所占宽高
+  const tagName = JSON.stringify({
+    width: textBoxDom.offsetWidth - 10,
+    height: textBoxDom.offsetHeight - 10,
+    content: cacheTextTrack.content,
+  });
+
+  const track: SpecialTag = {
+    tagName,
+    tagType: store.currentSpecialTagType,
+    offsetIndex: curSliceImagesWithTrackItem.indexInSliceUrls,
+    offsetX:
+      cacheTextTrack.x * (curImageTarget.naturalWidth / curImageTarget.width) +
+      curSliceImagesWithTrackItem.dx,
+    offsetY:
+      cacheTextTrack.y *
+        (curImageTarget.naturalHeight / curImageTarget.height) +
+      curSliceImagesWithTrackItem.dy,
+    positionX: -1,
+    positionY: -1,
+    groupNumber: store.currentQuestion.groupNumber,
+    color: "green",
+  };
+  track.positionX =
+    (track.offsetX - curSliceImagesWithTrackItem.dx) / maxImageWidth;
+  track.positionY =
+    (track.offsetY -
+      curSliceImagesWithTrackItem.dy +
+      curSliceImagesWithTrackItem.accumTopHeight) /
+    theFinalHeight;
+
+  store.currentTaskEnsured.markResult.specialTagList.push(track);
+  curSliceImagesWithTrackItem.tagList.push(track);
+  initCacheTextTrack();
+}
+watch(
+  () => store.currentSpecialTagType,
+  () => {
+    if (cacheTextTrack.id) {
+      initCacheTextTrack();
+    }
+  }
+);
 //#endregion
 
 //#region : 显示大图,供查看和翻转
@@ -685,4 +788,20 @@ function scrollToFirstScore() {
   bottom: 0;
   z-index: 9;
 }
+.text-edit-box {
+  position: absolute;
+  border: 1px solid #ff0000;
+  line-height: 24px;
+  padding: 5px;
+  font-size: 20px;
+  border-radius: 4px;
+  margin: -15px 0 0 -5px;
+  outline: none;
+  z-index: 9;
+  font-family: 黑体, arial, sans-serif;
+  color: #ff0000;
+}
+.text-edit-box:focus {
+  border-color: #ff5050;
+}
 </style>

+ 19 - 2
src/features/check/MarkBody.vue

@@ -6,7 +6,10 @@
   />
   <div class="cursor">
     <div class="cursor-border">
-      <span v-if="store.currentSpecialTagType === 'TEXT'" class="text">文</span>
+      <span
+        v-if="store.currentSpecialTagType === 'TEXT'"
+        class="text text-edit"
+      ></span>
       <span
         v-else-if="
           store.currentSpecialTagType === 'LINE' ||
@@ -297,7 +300,21 @@ onUnmounted(() => {
   opacity: 0;
   transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
 }
-
+.cursor .point {
+  display: inline-block;
+  vertical-align: middle;
+  width: 4px;
+  height: 4px;
+  border-radius: 50%;
+  background: red;
+}
+.cursor .text-edit {
+  display: inline-block;
+  vertical-align: middle;
+  width: 0;
+  height: 20px;
+  border-left: 2px solid #ff5050;
+}
 .cursor.cursor--off-screen {
   opacity: 0;
 }

+ 155 - 44
src/features/mark/CommonMarkBody.vue

@@ -35,7 +35,6 @@
               :sliceImageHeight="item.sliceImageHeight"
               :dx="item.dx"
               :dy="item.dy"
-              @delete-specialtag="(tag) => deleteSpecialtag(item, tag)"
               @click-specialtag="(event) => clickSpecialtag(event, item)"
             />
             <div
@@ -46,6 +45,7 @@
                 moveElement: specialMouseMove,
                 moveStop: specialMouseStop,
               }"
+              @click="(event) => canvasClick(event, item)"
             >
               <template v-if="curSliceImagesWithTrackItem?.url === item.url">
                 <div
@@ -53,9 +53,25 @@
                   :style="specialLenStyle"
                 ></div>
                 <div
-                  v-if="store.currentSpecialTagType === 'CIRCLE'"
+                  v-else-if="store.currentSpecialTagType === 'CIRCLE'"
                   :style="specialCircleStyle"
                 ></div>
+                <div
+                  v-else-if="store.currentSpecialTagType === 'TEXT'"
+                  v-show="cacheTextTrack.id"
+                  :id="`text-edit-box-${cacheTextTrack.id}`"
+                  :key="cacheTextTrack.id"
+                  class="text-edit-box"
+                  contenteditable
+                  :style="specialTextStyle"
+                  @input="textTrackInput"
+                  @blur="textTrackBlur"
+                  @keypress.stop
+                  @keydown.stop
+                  @mousedown.stop
+                  @mousemove.stop
+                  @mouseup.stop
+                ></div>
               </template>
             </div>
           </div>
@@ -71,7 +87,14 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, onUnmounted, reactive, watch, watchEffect } from "vue";
+import {
+  nextTick,
+  onMounted,
+  onUnmounted,
+  reactive,
+  watch,
+  watchEffect,
+} from "vue";
 import { store } from "@/store/store";
 import MarkDrawTrack from "./MarkDrawTrack.vue";
 import type { SliceImage } from "@/types";
@@ -80,6 +103,7 @@ import {
   getDataUrlForSliceConfig,
   getDataUrlForSplitConfig,
   loadImage,
+  randomCode,
 } from "@/utils/utils";
 import { dragImage } from "./use/draggable";
 import MultiMediaMarkBody from "./MultiMediaMarkBody.vue";
@@ -454,27 +478,6 @@ async function processSplitConfig() {
   }, 300);
 }
 
-const deleteSpecialtag = (item, tag) => {
-  const findInd = (tagList, curTag) => {
-    return tagList.findIndex(
-      (itemTag) =>
-        itemTag.tagName === curTag.tagName &&
-        itemTag.offsetX === curTag.offsetX &&
-        itemTag.offsetY === curTag.offsetY
-    );
-  };
-
-  const tagIndex = findInd(item.tagList, tag);
-  if (tagIndex === -1) return;
-  item.tagList.splice(tagIndex, 1);
-
-  const stagIndex = findInd(
-    store.currentTaskEnsured.markResult.specialTagList,
-    tag
-  );
-  if (stagIndex === -1) return;
-  store.currentTaskEnsured.markResult.specialTagList.splice(tagIndex, 1);
-};
 const clickSpecialtag = (event: MouseEvent, item: SliceImage) => {
   // console.log(event);
   const e = {
@@ -486,14 +489,6 @@ const clickSpecialtag = (event: MouseEvent, item: SliceImage) => {
   makeTrack(e as MouseEvent, item, maxSliceWidth, theFinalHeight);
 };
 
-const clearEmptySpecialTag = (item) => {
-  item.tagList
-    .filter((item) => !item.tagName.trim().replace("\n", ""))
-    .forEach((tag) => {
-      deleteSpecialtag(item, tag);
-    });
-};
-
 // should not render twice at the same time
 let renderLock = false;
 const renderPaperAndMark = async () => {
@@ -657,19 +652,26 @@ const innerMakeTrack = (event: MouseEvent, item: SliceImage) => {
     return;
   }
 
-  clearEmptySpecialTag(item);
   makeTrack(event, item, maxSliceWidth, theFinalHeight);
 };
 //#endregion : 评分
 
-//#region : 特殊标记:画线、框
+//#region : 特殊标记:画线、框、文字
 const isCustomSpecialTag = $computed(() => {
-  return ["CIRCLE", "LINE"].includes(store.currentSpecialTagType);
+  return ["CIRCLE", "LINE", "TEXT"].includes(store.currentSpecialTagType);
 });
 
 let specialPoint = $ref({ x: 0, y: 0, ex: 0, ey: 0 });
 let curImageTarget: HTMLElement = null;
 let curSliceImagesWithTrackItem: SliceImage = $ref(null);
+let cacheTextTrack = $ref({
+  id: "",
+  x: 0,
+  y: 0,
+  maxW: 0,
+  maxY: 0,
+  content: "",
+});
 
 const specialLenStyle = $computed(() => {
   if (specialPoint.ex <= specialPoint.x) return { display: "none" };
@@ -704,28 +706,46 @@ const specialCircleStyle = $computed(() => {
     zIndex: 9,
   };
 });
+const specialTextStyle = $computed(() => {
+  return {
+    top: cacheTextTrack.y + "px",
+    left: cacheTextTrack.x + "px",
+    minWidth: "30px",
+    minHeight: "30px",
+    maxWidth: curImageTarget.width - cacheTextTrack.x + "px",
+    maxHeight: curImageTarget.height - cacheTextTrack.y + "px",
+  };
+});
 
 function specialMouseStart(e: MouseEvent, item: SliceImage) {
+  if (store.currentSpecialTagType === "TEXT") return;
+
   curImageTarget = e.target.parentElement.childNodes[0];
   curSliceImagesWithTrackItem = item;
   specialPoint.x = e.offsetX;
   specialPoint.y = e.offsetY;
 }
 function specialMouseMove({ left, top }) {
+  if (store.currentSpecialTagType === "TEXT") return;
+
   specialPoint.ex = left + specialPoint.x;
   specialPoint.ey = top + specialPoint.y;
 }
 function specialMouseStop(e: MouseEvent) {
+  if (store.currentSpecialTagType === "TEXT") return;
+
   if (
     store.currentSpecialTagType === "LINE" &&
     specialPoint.ex <= specialPoint.x
   ) {
+    specialPoint = { x: 0, y: 0, ex: 0, ey: 0 };
     return;
   }
   if (
     store.currentSpecialTagType === "CIRCLE" &&
     (specialPoint.ex <= specialPoint.x || specialPoint.ey <= specialPoint.y)
   ) {
+    specialPoint = { x: 0, y: 0, ex: 0, ey: 0 };
     return;
   }
 
@@ -741,11 +761,12 @@ function specialMouseStop(e: MouseEvent) {
       curSliceImagesWithTrackItem.dy,
     positionX: -1,
     positionY: -1,
+    groupNumber: store.currentQuestion.groupNumber,
   };
   track.positionX =
-    (specialPoint.x - curSliceImagesWithTrackItem.dx) / maxSliceWidth;
+    (track.offsetX - curSliceImagesWithTrackItem.dx) / maxSliceWidth;
   track.positionY =
-    (specialPoint.y -
+    (track.offsetY -
       curSliceImagesWithTrackItem.dy +
       curSliceImagesWithTrackItem.accumTopHeight) /
     theFinalHeight;
@@ -772,6 +793,89 @@ function specialMouseStop(e: MouseEvent) {
   curSliceImagesWithTrackItem.tagList.push(track);
   specialPoint = { x: 0, y: 0, ex: 0, ey: 0 };
 }
+
+function canvasClick(e: Event, item: SliceImage) {
+  if (cacheTextTrack.id) {
+    textTrackBlur();
+  }
+
+  curImageTarget = e.target.parentElement.childNodes[0];
+  curSliceImagesWithTrackItem = item;
+
+  cacheTextTrack.x = e.offsetX;
+  cacheTextTrack.y = e.offsetY;
+  cacheTextTrack.id = randomCode();
+  cacheTextTrack.content = "";
+
+  nextTick(() => {
+    document.getElementById(`text-edit-box-${cacheTextTrack.id}`).focus();
+  });
+}
+
+function textTrackInput(e: Event) {
+  cacheTextTrack.content = e.target.outerText;
+}
+function initCacheTextTrack() {
+  cacheTextTrack = {
+    x: 0,
+    y: 0,
+    maxW: 0,
+    maxY: 0,
+    content: "",
+    id: "",
+  };
+}
+function textTrackBlur() {
+  if (!cacheTextTrack.content) {
+    initCacheTextTrack();
+    return;
+  }
+  const textBoxDom = document.getElementById(
+    `text-edit-box-${cacheTextTrack.id}`
+  );
+
+  // 减去内边距所占宽高
+  const tagName = JSON.stringify({
+    width: textBoxDom.offsetWidth - 10,
+    height: textBoxDom.offsetHeight - 10,
+    content: cacheTextTrack.content,
+  });
+
+  const track: SpecialTag = {
+    tagName,
+    tagType: store.currentSpecialTagType,
+    offsetIndex: curSliceImagesWithTrackItem.indexInSliceUrls,
+    offsetX:
+      cacheTextTrack.x * (curImageTarget.naturalWidth / curImageTarget.width) +
+      curSliceImagesWithTrackItem.dx,
+    offsetY:
+      cacheTextTrack.y *
+        (curImageTarget.naturalHeight / curImageTarget.height) +
+      curSliceImagesWithTrackItem.dy,
+    positionX: -1,
+    positionY: -1,
+    groupNumber: store.currentQuestion.groupNumber,
+  };
+  track.positionX =
+    (track.offsetX - curSliceImagesWithTrackItem.dx) / maxSliceWidth;
+  track.positionY =
+    (track.offsetY -
+      curSliceImagesWithTrackItem.dy +
+      curSliceImagesWithTrackItem.accumTopHeight) /
+    theFinalHeight;
+
+  store.currentTaskEnsured.markResult.specialTagList.push(track);
+  curSliceImagesWithTrackItem.tagList.push(track);
+  initCacheTextTrack();
+}
+watch(
+  () => store.currentSpecialTagType,
+  () => {
+    if (cacheTextTrack.id) {
+      initCacheTextTrack();
+    }
+  }
+);
 //#endregion
 
 //#region : 显示大图,供查看和翻转
@@ -911,14 +1015,21 @@ function scrollToFirstScore() {
   bottom: 0;
   z-index: 9;
 }
-.double-triangle {
-  background-color: #ef7c78;
-  width: 30px;
-  height: 6px;
-  clip-path: polygon(0 0, 0 6px, 50% 0, 100% 0, 100% 6px, 50% 0);
-
+.text-edit-box {
   position: absolute;
-  bottom: -5px;
+  border: 1px solid #ff0000;
+  line-height: 24px;
+  padding: 5px;
+  font-size: 20px;
+  border-radius: 4px;
+  margin: -15px 0 0 -5px;
+  outline: none;
+  z-index: 9;
+  font-family: 黑体, arial, sans-serif;
+  color: #ff0000;
+}
+.text-edit-box:focus {
+  border-color: #ff5050;
 }
 
 @keyframes rotate {

+ 12 - 1
src/features/mark/MarkBody.vue

@@ -6,7 +6,10 @@
   />
   <div class="cursor">
     <div class="cursor-border">
-      <span v-if="store.currentSpecialTagType === 'TEXT'" class="text">文</span>
+      <span
+        v-if="store.currentSpecialTagType === 'TEXT'"
+        class="text text-edit"
+      ></span>
       <span
         v-else-if="
           store.currentSpecialTagType === 'LINE' ||
@@ -301,6 +304,14 @@ onUnmounted(() => {
   background: red;
 }
 
+.cursor .text-edit {
+  display: inline-block;
+  vertical-align: middle;
+  width: 0;
+  height: 20px;
+  border-left: 2px solid #ff5050;
+}
+
 .cursor.cursor--off-screen {
   opacity: 0;
 }

+ 82 - 48
src/features/mark/MarkDrawTrack.vue

@@ -21,28 +21,12 @@
     v-for="tag in specialTagList"
     :key="`${tag.offsetX}_${tag.offsetY}`"
   >
-    <div
+    <img
       v-if="tag.tagType === 'TEXT'"
-      class="score-container special-container"
-      :style="computeTopAndLeft(tag)"
-      @keypress.stop
-      @mousemove.stop
-      @mousedown.stop
-      @mouseup.stop
-    >
-      <a-textarea
-        :key="`${tag.offsetX}_${tag.offsetY}`"
-        v-model:value="tag.tagName"
-        :class="[
-          'tag-textarea',
-          'tw-m-auto',
-          { 'is-empty': checkTagContentIsEmpty(tag.tagName) },
-        ]"
-        :autosize="{ minRows: 1, maxRows: 6 }"
-        :maxlength="32"
-        @blur="specialTagBlur(tag)"
-      />
-    </div>
+      class="special-text"
+      :style="computeSpecialTextStyle(tag)"
+      :src="getSpecialTextImg(tag)"
+    />
     <div
       v-else-if="tag.tagType === 'LINE'"
       class="special-line"
@@ -95,7 +79,7 @@ const props = defineProps<{
   dx: number;
   dy: number;
 }>();
-const emit = defineEmits(["delete-specialtag", "click-specialtag"]);
+const emit = defineEmits(["click-specialtag"]);
 
 const { trackList } = toRefs(props);
 
@@ -112,7 +96,7 @@ const computeSpecialLineStyle = (track: SpecialTag) => {
     left: leftInsideSliceRatio * 100 + "%",
     width: (100 * tagProp.len) / props.sliceImageWidth + "%",
     position: "absolute",
-    borderTop: "1px solid red",
+    borderTop: `1px solid ${track.color || "red"}`,
     zIndex: 9,
   };
 };
@@ -130,11 +114,85 @@ const computeSpecialCircleStyle = (track: SpecialTag) => {
     width: (100 * tagProp.width) / props.sliceImageWidth + "%",
     height: (100 * tagProp.height) / props.sliceImageHeight + "%",
     position: "absolute",
-    border: "1px solid red",
+    border: `1px solid ${track.color || "red"}`,
     borderRadius: "50%",
     zIndex: 9,
   };
 };
+
+const computeSpecialTextStyle = (track: SpecialTag) => {
+  // {"tagName":"{\"len\":241.9842519685039}","tagType":"LINE","offsetIndex":2,"offsetX":324.8193228048039,"offsetY":759.8560783391572,"positionX":0.06189180773871382,"positionY":-0.01054037309709878}
+  const tagProp = JSON.parse(track.tagName);
+
+  const topInsideSlice = track.offsetY - props.dy;
+  const leftInsideSlice = track.offsetX - props.dx;
+  const topInsideSliceRatio = topInsideSlice / props.sliceImageHeight;
+  const leftInsideSliceRatio = leftInsideSlice / props.sliceImageWidth;
+
+  return {
+    top: topInsideSliceRatio * 100 + "%",
+    left: leftInsideSliceRatio * 100 + "%",
+    width: (100 * tagProp.width) / props.sliceImageWidth + "%",
+    height: (100 * tagProp.height) / props.sliceImageHeight + "%",
+    position: "absolute",
+    marginTop: "-10px",
+    zIndex: 9,
+  };
+};
+const getSpecialTextImg = (track: SpecialTag) => {
+  const tagProp = JSON.parse(track.tagName);
+  const canvas = document.createElement("canvas");
+  canvas.width = tagProp.width * 2;
+  canvas.height = tagProp.height * 2;
+  const ctx = canvas.getContext("2d");
+
+  ctx.fillStyle = track.color || "#ff0000";
+  ctx.font = "normal 40px 黑体";
+
+  const contents = tagProp.content.split("\n");
+  const lineHeight = 48;
+  let y = 42;
+  const x = 0;
+
+  // 每次回车换行时会多一个换行符,需要剔除
+  let conts = [];
+  let lastContIsEmpty = false;
+  contents.forEach((cont) => {
+    if (cont.trim()) {
+      conts.push(cont);
+      lastContIsEmpty = false;
+    } else {
+      if (lastContIsEmpty) return;
+      conts.push(cont);
+      lastContIsEmpty = true;
+    }
+  });
+
+  conts.forEach((cont, index) => {
+    if (!cont) {
+      y += lineHeight;
+      return;
+    }
+    let arrText = cont.split("");
+    let line = "";
+    for (let n = 0; n < arrText.length; n++) {
+      let textLine = line + arrText[n];
+      const metrics = ctx.measureText(textLine);
+      if (metrics.width > canvas.width && n > 0) {
+        ctx.fillText(line, x, y);
+        line = arrText[n];
+        y += lineHeight;
+      } else {
+        line = textLine;
+      }
+    }
+    ctx.fillText(line, x, y);
+    y += lineHeight;
+  });
+
+  return canvas.toDataURL();
+};
+
 const computeTopAndLeft = (track: Track | SpecialTag) => {
   const topInsideSlice = track.offsetY - props.dy;
   const leftInsideSlice = track.offsetX - props.dx;
@@ -176,15 +234,6 @@ const hasMember = (track: any) => {
 const focusedTrack = (track: Track) => {
   return store.focusTracks.includes(track) || hasMember(track);
 };
-const checkTagContentIsEmpty = (cont) => {
-  return !cont.trim().replace("\n", "");
-};
-const specialTagBlur = (tag) => {
-  const contIsEmpty = checkTagContentIsEmpty(tag.tagName);
-  if (!contIsEmpty) return;
-  emit("delete-specialtag", tag);
-};
-
 const circleTagClickHandle = (event: MouseEvent) => {
   emit("click-specialtag", event);
 };
@@ -261,21 +310,6 @@ watch(
 .score-animation {
   animation: 0.5s ease-in-out 0s infinite alternate change_size;
 }
-.score-container .tag-textarea {
-  background-color: transparent;
-  color: #38985f;
-  font-size: inherit;
-  resize: none;
-  line-height: 1.1;
-  border-color: transparent;
-  outline: none;
-}
-.score-container .tag-textarea.is-empty,
-.score-container .tag-textarea:hover,
-.score-container .tag-textarea:focus {
-  background-color: rgba(255, 255, 255, 0.8);
-  border-color: #38985f;
-}
 
 @keyframes change_size {
   from {