import {
  playAudioByUrl,
  playAnimationOnNode,
  loadImageByUrl,
  asyncLoadRemote,
  getSprNode,
  playLocalAudio,
  asyncGetVideoDuration,
  getVideoUrl
} from "../script/util_DG_FAF";
import { MyCocosSceneComponent } from "../script/MyCocosSceneComponent_DG_FAF";
import { getUUID } from "../script/util_DG_FAF";

const { ccclass, property } = cc._decorator;

// 题型
const TEXT_SELECT = "0";
const IMAGE_SELECT = "1";
const AUDIO_PLAY = "2";
const HOT_ZONE_RADIO = "3";
const HOT_ZONE_CHECKBOX = "4";
const TEXTINPUT = "5";
const CONNECTION = "6";
const PRONUNCIATION_ASSESSMENT = "7";
const RIGHT_OR_WRONG = "8";
const CROSSWORD_PUZZLE = "9";
const SORT_WORDS = "10";
const CONNECTION_CHOICE = "11";
const TEXTINPUT_GROUP = "12";
const VIDEO_PLAY = "13";
const DRAWING = "14";
const DRAWING_CHECK = "15";
const CONNECTION_2LEVEL = "16";
const MULTIPLE_TEXT_SELECT = "17";

// 评分体系
const RS_15_5L_FAF = "0";
const RS_20_5L_FAF = "4";
const RS_25_5L_FAF = "1";
const RS_30_5L_FAF = "2";
const RS_40_5L_FAF = "5";
const RS_100_3L_FAF = "3";
const RS_120_3L_FAF = "6";

const layer_1 = 5 // 布局层1 [热区背景图]
const layer_2 = 10 // 布局层2 [装饰框 色块 结果展示]
const layer_3 = 15 // 布局层3
const layer_4 = 20 // 常规热区布局层 [热区]
const layer_5 = 25 // 布局层 [连线终点]
const layer_6 = 30
const layer_10 = 50
@ccclass
export default class SceneComponent extends MyCocosSceneComponent {
  @property({type: cc.Node, displayName: "键盘"})
  keyboard: cc.Node = null;

  @property({type: cc.Node, displayName: "文字排序"})
  sortWords: cc.Node = null;

  @property({type: cc.Node, displayName: "选择窗"})
  selectModal: cc.Node = null;

  @property({type: cc.Node, displayName: "多选窗"})
  multipleSelectModal: cc.Node = null;
  
  @property({type: cc.Node, displayName: "音频播放动画"})
  aniSpeaker: cc.Node = null;

  @property({type: cc.Node, displayName: "视频提示播放动画"})
  aniVideoTips: cc.Node = null;

  @property({type: cc.Node, displayName: "语音评测"})
  pronunciationAssessmentModal: cc.Node = null;

  @property({type: cc.Node, displayName: "视频进度控制条"})
  videoControlBar: cc.Node = null;

  @property({type: cc.Node, displayName: "画图板模板"})
  drawingBoardTemplate: cc.Node = null;

  addPreloadImage() {
    // TODO 根据自己的配置预加载图片资源
    if(this.data.bgItem.url) {
      this._imageResList.push({ url: this.data.bgItem.url });
    }
    if(this.data.footer_image_url) {
      this._imageResList.push({ url: this.data.footer_image_url });
    }
    if(this.data.header_image_url) {
      this._imageResList.push({ url: this.data.header_image_url });
    }
  }

  addPreloadAudio() {
    // TODO 根据自己的配置预加载音频资源
    this.data.hotZoneConfigArr.forEach(config => {
      if(config.hotZoneType == AUDIO_PLAY && config.audio_url) {
        this._audioResList.push({ url: config.audio_url });
      }
    });
  }

  addPreloadAnima() {}

  isDestroy = false;
  startTimestamp = null;
  async onLoadEnd() {
    // TODO 加载完成后的逻辑写在这里
    console.log(this.data);
    this.disableScroll(); // 禁用页面滚动
    this.initSize(); // 页面尺寸
    this.initMiddleLayer(); // 中间层相关
    this.initEventListener(); // 事件监听
    this.initHeaderAndFooter(); // 顶部和底部图片
    this.initVideoPlayerLayer(); // 视频播放层
    await this.initHotZoneBg(); // 初始化背景
    this.initSlicedHotZoneBg(); // 切片背景图 - 修复某些设备上背景图过长导致的黑屏
    this.initHotZoneItem(); // 初始化热区数据
    this.initScorePanel();
    this.initActionButton();
    this.initScoreShow();
    this.enableScroll(); // 启用页面滚动
    this.startTimestamp = new Date().getTime();
  }

  // 初始化屏幕储存
  _designSize = null;
  _frameSize = null;
  _mapScaleMin = null;
  _mapScaleMax = null;
  _cocosScale = null;
  canvas = null;
  initSize() {
    // 注意cc.winSize只有在适配后(修改fitHeight/fitWidth后)才能获取到正确的值,因此使用cc.getFrameSize()来获取初始的屏幕大小
    let screen_size = cc.view.getFrameSize().width / cc.view.getFrameSize().height;
    let design_size = cc.Canvas.instance.designResolution.width / cc.Canvas.instance.designResolution.height;
    let f = screen_size >= design_size;
    cc.Canvas.instance.fitHeight = f;
    cc.Canvas.instance.fitWidth = !f;

    const frameSize = cc.view.getFrameSize();
    this._frameSize = frameSize;

    this._designSize = cc.view.getDesignResolutionSize();

    let sx = cc.winSize.width / frameSize.width;
    let sy = cc.winSize.height / frameSize.height;
    this._cocosScale = Math.min(sx, sy);

    sx = frameSize.width / this._designSize.width;
    sy = frameSize.height / this._designSize.height;
    this._mapScaleMin = Math.min(sx, sy) * this._cocosScale;
    this._mapScaleMax = Math.max(sx, sy) * this._cocosScale;

    this.canvas = cc.find("Canvas");
  }

  currentInputTarget = null;
  onSelectedCallback = null;
  onMultipleSelectedCallback = null;
  onKeyboardEnterCallback = null;
  onSortWordsEndCallback = null;
  onPronunciationAssessmentCallback = null;
  initEventListener() {
    // 注册键盘输入确定事件
    this.keyboard.on("onEnter", (text)=>{
      this.onKeyboardEnterCallback && this.onKeyboardEnterCallback(this.currentInputTarget, text)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })

    this.pronunciationAssessmentModal.on("onFinish", (e)=>{
      this.onPronunciationAssessmentCallback && this.onPronunciationAssessmentCallback(this.currentInputTarget, e)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })

    this.selectModal.on("onSelected", (e)=>{
      this.onSelectedCallback && this.onSelectedCallback(this.currentInputTarget, e)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })

    this.selectModal.on("onCancel", (e)=>{
      this.onSelectedCallback && this.onSelectedCallback(this.currentInputTarget, e)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })

    this.multipleSelectModal.on("onSelected", (e)=>{
      this.onMultipleSelectedCallback && this.onMultipleSelectedCallback(this.currentInputTarget, e)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })

    this.multipleSelectModal.on("onCancel", (e)=>{
      this.onMultipleSelectedCallback && this.onMultipleSelectedCallback(this.currentInputTarget, e)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })

    

    this.sortWords.on("onEnter", (e)=>{
      this.onSortWordsEndCallback && this.onSortWordsEndCallback(this.currentInputTarget, e)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })

    this.sortWords.on("onCancel", (e)=>{
      this.onSortWordsEndCallback && this.onSortWordsEndCallback(this.currentInputTarget, e)
      if(this.currentInputTarget) {
        this.currentInputTarget = null;
      }
    })
  }

  _middleLayer;
  initMiddleLayer() {
    this._middleLayer = cc.find('middleLayer')?.getComponent('middleLayer');
    if (!this._middleLayer) {
      this._middleLayer = console;
    }
    else {
      this._middleLayer.log("获取到middleLayer");
    }

    let back = cc.find("Canvas/backBtn");
    back.on('click', () => {
      this._middleLayer.log("exit game")
      const middleLayer = cc.find('middleLayer')?.getComponent('middleLayer');
      if (middleLayer) {
        middleLayer.log('exit game');
        // 有些模板需要在返回时上报数据，直接把数据作为参数传入就可以了
        middleLayer.exitGame(this.resultData == null ? this.resultData = null : {});
      } else {
        console.log("==触发返回事件==");
      }
    })
  }

  // 初始化头部和底部图片
  initHeaderAndFooter() {
    const headerNode = cc.find("Canvas/Header");
    loadImageByUrl(this.data.header_image_url, (sf)=>{
      const imgNode = cc.find("Canvas/Header/img");
      const sfSize = sf.getOriginalSize();

      let alignMode = "middle";
      if(this.data.alignMode) {
        alignMode = this.data.alignMode;
      }

      if(alignMode == "middle") {
        const sx = imgNode.parent.width / sfSize.width;
        const sy = imgNode.parent.height / sfSize.height;
        imgNode.scale = Math.max(sx, sy)
        imgNode.getComponent(cc.Sprite).spriteFrame = sf;
      } else if(alignMode == "left") {
        const sx = imgNode.parent.width / sfSize.width;
        const sy = imgNode.parent.height / sfSize.height;
        imgNode.anchorX = 0;
        imgNode.anchorY = 1;
        imgNode.x = -headerNode.width / 2;
        imgNode.y = headerNode.height / 2;
        imgNode.scale = sy
        imgNode.getComponent(cc.Sprite).spriteFrame = sf;
      }
    })
    loadImageByUrl(this.data.footer_image_url, (sf)=>{
      const imgNode = cc.find("Canvas/Footer/img");
      const sfSize = sf.getOriginalSize();
      const sx = imgNode.parent.width / sfSize.width;
      const sy = imgNode.parent.height / sfSize.height;
      imgNode.scale = Math.min(sx, sy)
      imgNode.getComponent(cc.Sprite).spriteFrame = sf;
    })
  }

  // 初始化底部按钮
  showCloseBtn: Function = null;
  initActionButton() {
    const canvas = cc.find("Canvas");
    const actionBtnMaskBg = cc.find("Canvas/ActionBtnMaskBg");
    const actionBtnContainer = cc.find("Canvas/ActionBtn");
    const defaultBtn = cc.find("btn_default", actionBtnContainer)
    const submitBtn = cc.find("btn_submit_new", actionBtnContainer)
    const replayBtn = cc.find("btn_replay_new", actionBtnContainer)
    const closeBtn = cc.find("btn_close", actionBtnContainer)

    const botomY = -canvas.height / 2 - 20;
    const topY = -canvas.height / 2 + actionBtnContainer.height;
    actionBtnContainer.y = botomY;
    
    actionBtnContainer.scale = 0.8;
    submitBtn.active = false;
    replayBtn.active = false;
    closeBtn.active = false;
    submitBtn.opacity = 0;
    replayBtn.opacity = 0;
    closeBtn.opacity = 0;

    const showDefaultBtn = () => {
      defaultBtn.opacity = 0;
      defaultBtn.active = true;
      cc.tween(actionBtnContainer).to(0.1, {y: botomY, scale: 0.8}).start();
      cc.tween(submitBtn).to(0.1, {opacity: 0}).call(()=>{
        submitBtn.active = false;
      }).start();
      cc.tween(replayBtn).to(0.1, {opacity: 0}).call(()=>{
        replayBtn.active = false;
      }).start();
      cc.tween(closeBtn).to(0.1, {opacity: 0}).call(()=>{
        closeBtn.active = false;
      }).start();

      return new Promise((resovle, reject) => {
        cc.tween(defaultBtn).to(0.1, {opacity: 255}).call(()=>{
          resovle()
        }).start();
      })
      
    }

    const showSubmitBtn = () => {
      submitBtn.opacity = 0;
      submitBtn.active = true;
      // 启用透明背景，点击按钮区域外 按钮收回
      actionBtnMaskBg.active = true;
      cc.tween(actionBtnContainer).to(0.1, {y: topY, scale: 1}).start();
      cc.tween(submitBtn).to(0.1, {opacity: 255}).start();
      cc.tween(defaultBtn).to(0.1, {opacity: 0}).call(()=>{
        defaultBtn.active = false;
      }).start();
    }

    const showReplayBtn = () => {
      replayBtn.opacity = 0;
      replayBtn.active = true;
      // 启用透明背景，点击按钮区域外 按钮收回
      actionBtnMaskBg.active = true;
      cc.tween(actionBtnContainer).to(0.1, {y: topY, scale: 1}).start();
      cc.tween(replayBtn).to(0.1, {opacity: 255}).start();
      cc.tween(defaultBtn).to(0.1, {opacity: 0}).call(()=>{
        defaultBtn.active = false;
      }).start();
    }

    this.showCloseBtn = () => {
      closeBtn.opacity = 255;
      closeBtn.active = false;

      cc.tween(actionBtnContainer).to(0.1, {
        scaleX: 0
      }).call(()=>{
        submitBtn.active = false;
        closeBtn.active = true;
      }).to(0.1, {
        scaleX: 1
      }).start()

      // cc.tween(actionBtnContainer).to(0.1, {y: topY, scale: 1}).start();
      // cc.tween(closeBtn).to(0.1, {opacity: 255}).start();
      // cc.tween(defaultBtn).to(0.1, {opacity: 0}).call(()=>{
      //   defaultBtn.active = false;
      // }).start();
    }

    actionBtnMaskBg.on("click", ()=>{
      showDefaultBtn();
      actionBtnMaskBg.active = false;
    })

    defaultBtn.on("click", ()=>{
      if(this.submitted) {
        showReplayBtn()
      } else {
        showSubmitBtn()
      }
    })

    submitBtn.on("click", async ()=>{
      // await showDefaultBtn();
      // 隐藏掉背景 保持返回按钮一直显示
      actionBtnMaskBg.active = false;
      this.submit();
    })

    replayBtn.on("click", async ()=>{
      await showDefaultBtn();
      this.replay();
    })

    closeBtn.on("click", async ()=>{
      await showDefaultBtn();
      this.hideEndScore();
    })
  }
  
  // 初始化视频播放层
  initVideoPlayerLayer() {
    const videoPlayer = cc.find("Canvas/VideoPlayerContainer");
    const closeBtn = cc.find("close", videoPlayer);
    const videoplayer = cc.find("videoPlayer", videoPlayer);

    videoplayer.width = videoPlayer.width;
    videoplayer.height = videoPlayer.height;

    videoPlayer.active = false;
    closeBtn.on("click", ()=>{
      this.hideVideoLayer();
    })

    // 隐藏视频控制组件
    this.videoControlBar.emit("hide_bar", true);

    // 监听组件发来的拖动百分比
    this.videoControlBar.on("on_drag_percent", (percent)=>{
      this.currentVideoPlay.currentTime = percent * this.videoDuration;
    })

    // 监听组件发来视频播放、暂停事件
    this.videoControlBar.on("video_btn_click", (isPlay)=>{
      if(isPlay) {
        this.currentVideoPlay.play();
      } else {
        this.currentVideoPlay.pause();
      }
    })
  }

  scoreShowContainer = null;
  initScoreShow() {
    const canvas = cc.find("Canvas");
    this.scoreShowContainer = cc.find("Canvas/ScoreShow");
    const mask = cc.find("mask", this.scoreShowContainer);
    mask.width = canvas.width;
    mask.height = canvas.height;

    this.scoreShowContainer.active = false;
  }

  // 根据评分体系显示对应的提示语
  showEndAnimationByRatingSystem(score, ratingSystem) {
    const aniName = this.getAniNameByRatingSystem(score, ratingSystem);

    const animationNode = cc.find(`EndAnimation/${aniName}`, this.scoreShowContainer);
    const scoreLabel = cc.find("score/number", this.scoreShowContainer).getComponent(cc.RichText);
    scoreLabel.string = `<outline color=#663333 width=5><color=#FFCC00>${score}</color></outline>`;

    this.scoreShowContainer.opacity = 0;
    this.scoreShowContainer.active = true;
    animationNode.active = true;

    cc.tween(this.scoreShowContainer).to(0.1, {
      opacity: 255
    }).call(async ()=>{
      const dragonDisplay = animationNode.getComponent(dragonBones.ArmatureDisplay);
      dragonDisplay.playAnimation('newAnimation', 1);
      this.showCloseBtn();
      playLocalAudio("finish");
    }).start()
    
  }

  // 根据分数和评分体系 找到对应动画的节点名
  getAniNameByRatingSystem(score, ratingSystem) {
    ratingSystem = ratingSystem + "";
    const scoreTemplate = {}
    scoreTemplate['key_' + RS_15_5L_FAF] = {
      "FAF_5_1": [0, 3], "FAF_5_2": [4, 6], "FAF_5_3": [7, 10], "FAF_5_4": [11, 13], "FAF_5_5": [14, 15],
    }
    scoreTemplate['key_' + RS_20_5L_FAF] = {
      "FAF_5_1": [0, 4], "FAF_5_2": [5, 9], "FAF_5_3": [10, 13], "FAF_5_4": [14, 17], "FAF_5_5": [18, 20],
    }
    scoreTemplate['key_' + RS_25_5L_FAF] = {
      "FAF_5_1": [0, 5], "FAF_5_2": [6, 10], "FAF_5_3": [11, 15], "FAF_5_4": [16, 20], "FAF_5_5": [21, 25],
    }
    scoreTemplate['key_' + RS_30_5L_FAF] = {
      "FAF_5_1": [0, 5], "FAF_5_2": [6, 11], "FAF_5_3": [12, 17], "FAF_5_4": [18, 23], "FAF_5_5": [24, 30],
    }
    scoreTemplate['key_' + RS_40_5L_FAF] = {
      "FAF_5_1": [0, 7], "FAF_5_2": [8, 14], "FAF_5_3": [15, 23], "FAF_5_4": [24, 32], "FAF_5_5": [33, 40],
    }
    scoreTemplate['key_' + RS_100_3L_FAF] = {
      "FAF_3_1": [0, 50], "FAF_3_2": [51, 80], "FAF_3_3": [81, 100],
    }
    scoreTemplate['key_' + RS_120_3L_FAF] = {
      "FAF_3_1": [0, 60], "FAF_3_2": [61, 100], "FAF_3_3": [101, 120],
    }

    const scoreConfig = scoreTemplate['key_' + ratingSystem];
    if(scoreConfig == undefined) {
      return "Default"
    }

    let aniName = "";
    const allScores = [];
    for(let _aniName in scoreConfig) {
      allScores.push(...scoreConfig[_aniName])
      if(aniName == "") {
        if(this.checkScoreInRange(score, scoreConfig[_aniName])) {
          aniName = _aniName;
        }
      }
    }

    if(aniName == "") {
      let maxScore = Math.max(...allScores);
      let minScore = Math.min(...allScores);
      const allAniName = Object.keys(scoreConfig);
      if(score < minScore) {
        aniName = allAniName[0]
      } else if(score > maxScore) {
        aniName = allAniName[allAniName.length - 1]
      } else {
        aniName = "Default"
      }
    }

    return aniName;
  }

  // 检查分数是否在区间里
  checkScoreInRange(score, range) {
    if(score >= range[0] && score <= range[1]) {
      return true;
    } else {
      return false;
    }
  }

  hideEndScore() {
    const endAnimationNode = cc.find("EndAnimation", this.scoreShowContainer)
    this.scoreShowContainer.opacity = 255;
    this.scoreShowContainer.active = true;
    cc.tween(this.scoreShowContainer).to(0.1, {
      opacity: 0
    }).call(()=>{
      this.scoreShowContainer.active = false;
      endAnimationNode.children.forEach(child => {
        child.active = false;
      });
    }).start()
  }

  hotZoneBg = null;
  initHotZoneBg() { 
    const sv = cc.find("Canvas/MainContent/ScorllView/view");
    const svContent = cc.find("Canvas/MainContent/ScorllView/view/content");
    this.hotZoneBg = svContent;
    if (!this.data.bgItem) {
      return;
    }

    return new Promise((resovle, reject) => {
      cc.assetManager.loadRemote(this.data.bgItem.url, (err, img) => {
        // 创建节点 挂载背景图
        const sprNode = new cc.Node();
        sprNode.name = "bgItemSpr";
        var sf = new cc.SpriteFrame(img);
        const spr = svContent.addComponent(cc.Sprite);
        spr.spriteFrame = sf;

        // 背景图横向铺满屏幕
        const sx = this.canvas.width / img.width;
        const sy = this.canvas.height / img.height;
        const s = Math.max(sx, sy);
        svContent.scale = s;
        sv.height = img.height * s;
        resovle(true);
      });
    });
  }

  initSlicedHotZoneBg() {
    if(!this.data.hotZoneBgSliceList || this.data.hotZoneBgSliceList.length == 0 ) {
      return
    }

    const svContentSlicedBg = cc.find("Canvas/MainContent/ScorllView/view/content/slicedBg");
    const pAll = [];
    this.data.hotZoneBgSliceList.forEach((imgObj, index) => {
      pAll.push(new Promise((resovle, reject) =>{
        cc.assetManager.loadRemote(imgObj.image_url, (err, img) => {
          resovle(img);
        });
      }))
    });
    
    Promise.all(pAll).then(res=>{
      res.forEach((img, index)=>{
        // 创建节点 挂载背景图
        const sprNode = new cc.Node();
        sprNode.name = "sliced_" + index;
        var sf = new cc.SpriteFrame(img);
        const spr = sprNode.addComponent(cc.Sprite);
        spr.spriteFrame = sf;
        svContentSlicedBg.addChild(sprNode)
      })
    })
  }

  picNodeArr = [];
  scoreValidater = [];
  // 根据配置的内容类型 生成对应的题型
  initHotZoneItem() {
    this.picNodeArr = [];
    const isDebug = this.data.isDebug?true:false;
    // 根据内容配置 生成需要显示的热区
    this.data.hotZoneConfigArr.forEach((configItem, index) => {
      configItem.index = index;
      let validater = null;
      switch (configItem.hotZoneType) {
        // 文字选择题
        case TEXT_SELECT:
          validater = this.setTextOptionList(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 文字选择题(多选)
        case MULTIPLE_TEXT_SELECT:
          validater = this.setTextOptionListMultiple(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 判断对错
        case RIGHT_OR_WRONG:
          validater = this.setRightOrWrongGroup(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 音频播放按钮
        case AUDIO_PLAY:
          this.setOneAudioBtn(configItem, this.data.hotZoneItemArr[configItem.linkHotZoneIndex]);
          break;
        // 视频播放按钮
        case VIDEO_PLAY:
          this.setOneVideoBtn(configItem, this.data.hotZoneItemArr[configItem.linkHotZoneIndex]);
          break;
        // 热区选项(单选)
        case HOT_ZONE_RADIO:
          validater = this.setHotZoneRadioGroup(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 热区选项(多选)
        case HOT_ZONE_CHECKBOX:
          validater = this.setHotZoneCheckboxGroup(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 连线组
        case CONNECTION:
          validater = this.setConnectionGroup(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 文字输入区
        case TEXTINPUT:
          validater = this.setTextInput(configItem, this.data.hotZoneItemArr[configItem.linkHotZoneIndex], isDebug);
          this.scoreValidater.push(validater);
          break;
        // 纵横字谜游戏
        case CROSSWORD_PUZZLE:
          validater = this.setCrosswordPuzzleInput(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 文字排序
        case SORT_WORDS:
          validater = this.setSortWords(configItem, this.data.hotZoneItemArr[configItem.linkHotZoneIndex], isDebug);
          this.scoreValidater.push(validater);
          break;
        // 语音评测
        case PRONUNCIATION_ASSESSMENT:
          validater = this.setPronunciationAssessment(configItem, this.data.hotZoneItemArr[configItem.linkHotZoneIndex]);
          this.scoreValidater.push(validater);
          break;
        // 连线选择
        case CONNECTION_CHOICE:
          validater = this.setConnectionChoice(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 文字输入组
        case TEXTINPUT_GROUP:
          validater = this.setTextInputGroup(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
        // 画图题
        case DRAWING:
          this.setDrawingArea(configItem, this.data.hotZoneItemArr[configItem.linkHotZoneIndex]);
          break;
        // 画图题（评分）
        case DRAWING_CHECK:
          validater = this.setDrawingAreaWithCheck(configItem, this.data.hotZoneItemArr[configItem.linkHotZoneIndex]);
          this.scoreValidater.push(validater);
          break;
        case CONNECTION_2LEVEL:
          validater = this.setConnectionGroup2L(configItem, isDebug);
          this.scoreValidater.push(validater);
          break;
      }
    });

    if(this.data.submitHotZoneIndex && this.data.submitHotZoneIndex > 0) {
      // 配置提交按钮
      this.setSubmitBtn(this.data.hotZoneItemArr[this.data.submitHotZoneIndex]);
      this.setSubmitDisableBtn(this.data.hotZoneItemArr[this.data.submitHotZoneIndex]);
      // 配置重新开始按钮
      this.setReplayBtn(this.data.hotZoneItemArr[this.data.submitHotZoneIndex]);
    }

    // 检查是否可以提交 - 设置完进行检查 防止没有配置分数 导致的无法提交
    // this.checkCanSubmit();
	};

  // 初始化分数牌
  subScorePanels = [];
  totalScorePanel = null;
  initScorePanel() {
    // 设置总分节点
    let totalScore = 0;
    // 若果索引不是数字 或者 索引小于0 则认为是没有配置索引
    if(this.data.totalScoreHotZoneIndex == null || isNaN(Number(this.data.totalScoreHotZoneIndex)) || Number(this.data.totalScoreHotZoneIndex) < 0) {
      this.totalScorePanel = {
        node: null,
        setScore: (score)=>{},
        initTotalScore: (score)=>{}
      }
    } else {
      this.totalScorePanel = this.setScorePanel(this.data.hotZoneItemArr[this.data.totalScoreHotZoneIndex]);
    }

    // 计算每个分数显示区的数据
    this.subScorePanels = [];
    // 判断如果没有设置过计分组 则跳过
    if(this.data.scoreConfigArr) {
      this.data.scoreConfigArr.forEach(scoreConfig => {
        // 若果索引不是数字 或者 索引小于0 则认为是没有配置索引
        if(scoreConfig.linkHotZoneIndex == null || isNaN(Number(scoreConfig.linkHotZoneIndex)) || Number(scoreConfig.linkHotZoneIndex) < 0) {
          this.subScorePanels.push({
            node: null,
            setScore: (score)=>{},
            initTotalScore: (score)=>{}
          });
          return
        }
        const config = this.setScorePanel(this.data.hotZoneItemArr[Number(scoreConfig.linkHotZoneIndex)]);
        let subTotalScore = 0;
        scoreConfig.linkHotZoneIndexArr.forEach(scoreItemIndex => {
          const contentItem = this.data.hotZoneConfigArr[scoreItemIndex];
          // 类型4 热区多选组 分数存储在选项中 -- 其他类型存储在contentItem中
          if(contentItem.hotZoneType == "4") {
            contentItem.contentList.forEach(item => {
              if(item.score && !isNaN(Number(item.score))) {
                subTotalScore += Number(item.score);
              }
            });
          } else {
            if(contentItem.score && !isNaN(Number(contentItem.score))) {
              subTotalScore += Number(contentItem.score);
            }
          }
        });
        // 设置分数区域总分 - 显示
        config.initTotalScore(subTotalScore);
        // 统计全部总分
        totalScore += subTotalScore;
        // 存全局数组 在提交时设置分时
        this.subScorePanels.push(config);
      });
    }

    // 设置总分 - 显示
    this.totalScorePanel.initTotalScore(totalScore);
  }
  
  // 检查是否可以提交 - 完成全部配置的问题 [这个方法的调用全部注释掉了，暂时保留，后续可删除]
  checkCanSubmit() {
    let result = true;
    this.scoreValidater.some(validater =>{
      if(!validater) {
        // 没有校验器的分数配置
        result = false;
        return true;
      } else {
        // 若果校验结果返回 null 证明校验方法没有被覆写过 可能用户没有进行答题
        if(validater() === null) {
          // 有返回值为空的校验器
          result = false;
          return true
        }
      }
    })

    // 如果可以提交 启用提交按钮
    if(result) {
      if(this.btnSubmit) {
        this.btnSubmit.active = true;
      }
      if(this.btnSubmitDisable) {
        this.btnSubmitDisable.active = false;
      }
    }
    return result;
  }

  btnSubmit = null;
  btnSubmitDisable = null;
  btnReplay = null;
  // 设置提交按钮
  setSubmitBtn(hotZoneData) {
    const {sprNode} = this.newSprNodeByResName(hotZoneData, "btn_submit");
    this.btnSubmit = sprNode;
    this.btnSubmit.active = false;
    this.btnSubmit.on("click", ()=>{
      if(this.submitted) {
        return;
      }
      this.submit();
    })
  }

  setSubmitDisableBtn(hotZoneData) {
    const {sprNode} = this.newSprNodeByResName(hotZoneData, "btn_submit_disable");
    this.btnSubmitDisable = sprNode;
    this.btnSubmitDisable.active = true;
    this.btnSubmitDisable.on("click", ()=>{

    })
  }

  setReplayBtn(hotZoneData) {
    const {sprNode} = this.newSprNodeByResName(hotZoneData, "btn_replay");
    this.btnReplay = sprNode;
    this.btnReplay.active = false;
    this.btnReplay.on("click", ()=>{
      this.replay();
    })
  }

  // 判断正误组
  setRightOrWrongGroup(contentData, debugMode=false) {
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);
    
    let currentCheckList = [];
    // 正确校验方法 - 默认返null
    let validater = () => {
      const results = [];
      // configIndex - 标记当前的内容索引 用于在最后计算分组得分时使用
      // rect - 用于放置正确和错误的符号
      const result = {
        detail: {
          contentType: RIGHT_OR_WRONG,
          configIndex: contentData.index,
          contentIndex: -1,
          currentSelect: [],
          correctSelect: [],
          right: true
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      }
      
      contentData.contentList.forEach((option, index) => {
        result.detail.currentSelect.push(currentCheckList[index].checked);
        result.detail.correctSelect.push(option.isCorrect);
        if(option.isCorrect == currentCheckList[index].checked) {
          // 正确 返回分数
          result.score += option.score && !isNaN(Number(option.score)) ? Number(option.score) : 2;
        } else {
          // 错误
          result.detail.right = false;
          result.allRight = false;
        }
      });

      return [result];
    };

    let showType = "rightWrong_icon";
    switch(contentData.rightOrWrongStyleType) {
      // 勾 叉
      case "symbol": showType = "rightWrong_icon"; break;
      // 笑 哭
      case "iconFace": showType = "rightWrong_face"; break;
      // T F
      case "textTF": showType = "rightWrong_T_F"; break;
      // Yes No
      case "textYesNo": showType = "rightWrong_Yes_No"; break;
      default: showType = "rightWrong_icon";
    }

    contentData.contentList.forEach((option, index) => {
      const hotZoneData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
      const resultRect = this.newRectNode(hotZoneData, layer_2, debugMode);
      this.newDecorativeFrame(hotZoneData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
      const rect = this.newRectNode(hotZoneData, layer_4, debugMode);
      currentCheckList.push({
        checked: null,
        rect: resultRect,
        mask: null
      });
      rect.index = index;

      const handleClick = async () => {
        if(this.submitted) {
          return;
        }

        let selectResult = await this.asyncShowSelectModal([], showType);

        if(currentCheckList[index].checked != null) {
          currentCheckList[index].mask.destroy();
        }

        currentCheckList[index].checked = selectResult;
        if(selectResult) {
          // 哭脸笑脸 - FT - 对错号
          if(contentData.rightOrWrongStyleType == "iconFace") {
            const {sprNode, picNode} = this.newSprNodeByResName(hotZoneData, "icon_laugh", true);
            picNode.zIndex = layer_4;
            currentCheckList[index].mask = picNode;
            sprNode.on("click", handleClick)
          } else if(contentData.rightOrWrongStyleType == "textTF") {
            const textParentNode = this.newRectNode(hotZoneData, layer_4, debugMode);
            const textNode = this.newTextNode("T")
            textNode.x = textParentNode.width / 2;
            textNode.y = textParentNode.height / 2;
            textParentNode.addChild(textNode)
            currentCheckList[index].mask = textParentNode;
            textParentNode.on("click", handleClick)
          } else if(contentData.rightOrWrongStyleType == "textYesNo") {
            const textParentNode = this.newRectNode(hotZoneData, layer_4, debugMode);
            const textNode = this.newTextNode("Yes")
            textNode.x = textParentNode.width / 2;
            textNode.y = textParentNode.height / 2;
            textParentNode.addChild(textNode)
            currentCheckList[index].mask = textParentNode;
            textParentNode.on("click", handleClick)
          } else {
            const {sprNode, picNode} = this.newSprNodeByResName(hotZoneData, "icon_right", true);
            picNode.zIndex = layer_4;
            currentCheckList[index].mask = picNode;
            sprNode.on("click", handleClick)
          }
        } else {
          if(contentData.rightOrWrongStyleType == "iconFace") {
            const {sprNode, picNode} = this.newSprNodeByResName(hotZoneData, "icon_cry", true);
            picNode.zIndex = layer_4;
            currentCheckList[index].mask = picNode;
            sprNode.on("click", handleClick)
          } else if(contentData.rightOrWrongStyleType == "textTF") {
            const textParentNode = this.newRectNode(hotZoneData, layer_4, debugMode);
            const textNode = this.newTextNode("F")
            textNode.x = textParentNode.width / 2;
            textNode.y = textParentNode.height / 2;
            textParentNode.addChild(textNode)
            currentCheckList[index].mask = textParentNode;
            textParentNode.on("click", handleClick)
          } else if(contentData.rightOrWrongStyleType == "textYesNo") {
            const textParentNode = this.newRectNode(hotZoneData, layer_4, debugMode);
            const textNode = this.newTextNode("No")
            textNode.x = textParentNode.width / 2;
            textNode.y = textParentNode.height / 2;
            textParentNode.addChild(textNode)
            currentCheckList[index].mask = textParentNode;
            textParentNode.on("click", handleClick)
          } else {
            const {sprNode, picNode} = this.newSprNodeByResName(hotZoneData, "icon_wrong", true);
            picNode.zIndex = layer_4;
            currentCheckList[index].mask = picNode;
            sprNode.on("click", handleClick)
          }
        }
        // 检查是否可以提交
        // this.checkCanSubmit();
      }

      rect.on("click", handleClick)
    });
    return validater;
  }

  // 热区单选组
  setHotZoneRadioGroup(contentData, debugMode=false) {
    const maskNodes = [];
    const unselLineNodes = [];
    const selLineNodes = [];
    const rightIconNodes = [];

    const selStyle = contentData.selectedStyle ? contentData.selectedStyle : "border";
    const unselStyle = contentData.unselectedStyle ? contentData.unselectedStyle : "mask";

    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);

    // 存储当前选择结果 - 点击事件会更新取值
    let currentSelectIsCorrect = false;
    // 当前选项的展示值 用于检查对错展示（TODO）
    let currentSelectIndex = -1;
    let currentSelectText = "";
    // 当前选项组正确答案的显示文字
    let correctSelectIndex = -1;
    let correctSelectText = "";
    // 正确校验方法 - 默认返null
    let validater = () => {
      const result = {
        detail: {
          contentType: HOT_ZONE_RADIO,
          configIndex: contentData.index,
          contentIndex: -1,
          currentSelectIndex: currentSelectIndex,
          currentSelectText: currentSelectText,
          correctSelectIndex: correctSelectIndex,
          correctSelectText: correctSelectText,
          right: true
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      }
      if(currentSelectIsCorrect) {
        // 正确 返回分数
        result.score = contentData.score && !isNaN(Number(contentData.score)) ? Number(contentData.score) : 0;
        return [result]
      } else {
        // 错误
        result.detail.right = false;
        result.allRight = false;
        return [result]
      }
    };

    contentData.contentList.forEach((option, index) => {
      const hotZoneData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
      const rect = this.newRectNode(hotZoneData, layer_4, debugMode);

      // 创建 结果节点 颜色区域 和 边框
      const maskNode = this.newMaskRectNode(hotZoneData, layer_2, "#00000033");
      const selLineNode = this.newOutlineNode(hotZoneData, layer_2, "#6dbef6");
      const unselLineNode = this.newOutlineNode(hotZoneData, layer_2, "#6dbef6");
      const {sprNode, picNode} = this.newSprNodeByResName(hotZoneData, "icon_right", true);

      maskNode.active = unselStyle == "mask";
      unselLineNode.active = unselStyle == "border";
      selLineNode.active = false;
      sprNode.active = false;

      maskNodes.push(maskNode);
      selLineNodes.push(selLineNode);
      unselLineNodes.push(unselLineNode);
      rightIconNodes.push(sprNode);

      rect.index = index;

      // 如果当前选项是正确选项，则保存当前展示文字 用于报告详情
      if(option.isCorrect) {
        correctSelectText = option.showText ? option.showText : "";
        correctSelectIndex = index;
      }

      rect.on("click", ()=>{
        if(this.submitted) {
          return;
        }

        // 隐藏当前点击的颜色块
        if(unselStyle == "mask") {
          maskNodes.forEach((mask, i) => {
            if(i == index) {
              mask.active = false;
            } else {
              mask.active = true;
            }
          })
        }

        // 隐藏当前点击的边框
        if(unselStyle == "border") {
          unselLineNodes.forEach((unselLineNode, i) => {
            if(i == index) {
              unselLineNode.active = false;
            } else {
              unselLineNode.active = true;
            }
          })
        }
        
        // 显示边框
        if(selStyle == "border") {
          selLineNodes.forEach((line, i) => {
            if(i != index) {
              line.active = false;
            } else {
              line.active = true;
            }
          })
        }

        // 显示对号
        if(selStyle == "right") {
          rightIconNodes.forEach((rightIcon, i) => {
            if(i != index) {
              rightIcon.active = false;
            } else {
              rightIcon.active = true;
            }
          })
        }
        
        currentSelectIndex = index;
        currentSelectText = option.showText ? option.showText : "";
        currentSelectIsCorrect = option.isCorrect;
        
        // 校验必须在上面更新了校验器之后进行 否则本次选择不会生效
        // this.checkCanSubmit();
      })
    });
    return validater;
  }

  // 热区多选组
  setHotZoneCheckboxGroup(contentData, debugMode=false) {
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);
    
    let currentCheckList = [];

    // 存储对应的样式节点
    const maskNodes = [];
    const unselLineNodes = [];
    const selLineNodes = [];
    const rightIconNodes = [];
    // 选中和未选中样式
    const selStyle = contentData.selectedStyle ? contentData.selectedStyle : "border";
    const unselStyle = contentData.unselectedStyle ? contentData.unselectedStyle : "mask";

    // 正确校验方法 - 默认返null
    let validater = () => {
      if(currentCheckList.length != contentData.contentList.length) {
        // 数组长度不匹配 - 还没有初始化 跳过
        return null;
      }

      // configIndex - 标记当前的内容索引 用于在最后计算分组得分时使用
      // rect - 用于放置正确和错误的符号
      // score - 得分
      const reuslt = {
        detail: {
          contentType: HOT_ZONE_CHECKBOX,
          configIndex: contentData.index,
          contentIndex: -1,
          currentSelectText: [],
          currentSelectIndex: [],
          correctSelectText: [],
          correctSelectIndex: [],
          right: true
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      };

      contentData.contentList.forEach((option, index) => {
        if(currentCheckList[index].checked) {
          reuslt.detail.currentSelectText.push(option.showText ? option.showText : "");
          reuslt.detail.currentSelectIndex.push(index);
        }
        if(option.isCheck) {
          reuslt.detail.correctSelectText.push(option.showText ? option.showText : "")
          reuslt.detail.correctSelectIndex.push(index)
        }
        if(option.isCheck == currentCheckList[index].checked) {
          // 正确 累加分数
          reuslt.score += (option.score!=null && !isNaN(Number(option.score))) ? Number(option.score) : 0;
        } else {
          // 有错误 判错
          reuslt.allRight = false;
        }
      });

      // 多选题如果有选择错误的 则为0分
      // if(!reuslt.allRight) {
      //   reuslt.score = 0;
      // }

      return [reuslt];
    };

    contentData.contentList.forEach((option, index) => {
      const hotZoneData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
      const resultRect = this.newRectNode(hotZoneData, layer_2, debugMode);
      // this.newDecorativeFrame(hotZoneData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
      const rect = this.newRectNode(hotZoneData, layer_4, debugMode);

      // 创建 结果节点 颜色区域 和 边框
      const maskNode = this.newMaskRectNode(hotZoneData, layer_2, "#00000033");
      const unselLineNode = this.newOutlineNode(hotZoneData, layer_2, "#6dbef6");
      const selLineNode = this.newOutlineNode(hotZoneData, layer_2, "#6dbef6");
      const {sprNode, picNode} = this.newSprNodeByResName(hotZoneData, "icon_right", true);

      maskNode.active = unselStyle == "mask";
      unselLineNode.active = unselStyle == "border";
      selLineNode.active = false;
      sprNode.active = false;

      maskNodes.push(maskNode);
      unselLineNodes.push(unselLineNode)
      selLineNodes.push(selLineNode);
      rightIconNodes.push(sprNode);

      currentCheckList.push({
        checked: false,
        rect: resultRect,
        mask: null
      });
      rect.index = index;

      rect.on("click", ()=>{
        if(this.submitted) {
          return;
        }
        if(currentCheckList[index].checked) {
          currentCheckList[index].checked = false;
          // 恢复未选中状态 - mask
          if(unselStyle == "mask") {
            maskNodes[index].active = true;
          }

          // 恢复未选中状态 - border
          if(unselStyle == "border") {
            unselLineNodes[index].active = true;
          }
          
          // 隐藏边框
          if(selStyle == "border") {
            selLineNodes[index].active = false;
          }

          // 显示对号
          if(selStyle == "right") {
            rightIconNodes[index].active = false;
          }
        } else {
          currentCheckList[index].checked = true;
          // 恢复未选中状态 - mask
          if(unselStyle == "mask") {
            maskNodes[index].active = false;
          }

          // 恢复未选中状态 - border
          if(unselStyle == "border") {
            unselLineNodes[index].active = false;
          }
          
          // 隐藏边框
          if(selStyle == "border") {
            selLineNodes[index].active = true;
          }

          // 显示对号
          if(selStyle == "right") {
            rightIconNodes[index].active = true;
          }
        }

        // 检查是否可以提交
        // this.checkCanSubmit();
      })
    });
    return validater;
  }

  allConnectionEndPoints = {};
  // 设置连线组
  setConnectionGroup(contentData, debugMode=false) {
    const uid = getUUID();
    this.allConnectionEndPoints[uid] = [];
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);
    // 存储所有连线
    let currentConnectionList = [];
    // 设置校验器
    let validater = () => {
      const result = {
        detail: {
          contentType: CONNECTION,
          configIndex: contentData.index,
          contentIndex: -1,
          results: []
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      };

      contentData.contentList.forEach((option, index) => {
        const detailItem = {
          startIndex: index,
          startText: option.startTextShow ? option.startTextShow : "",
          correctEndIndex: index,
          correctEndText: option.endTextShow ? option.endTextShow : "",
          currentEndIndex: currentConnectionList[index].linkedIndex,
          currentEndText: currentConnectionList[index].textShow,
          right: true
        }
        if(currentConnectionList[index].linkedIndex == index) {
          // 正确 返回分数
          result.score += (option.score != null && !isNaN(Number(option.score))) ? Number(option.score) : 0;
        } else {
          // 错误
          detailItem.right = false;
          result.allRight = false;
        }
        result.detail.results.push(detailItem)
      });

      return [result];
    };
    contentData.contentList.forEach((option, index) => {
      // 开始和结束点
      const startHotZoneData = this.data.hotZoneItemArr[option.selectStartHotZoneIndex];
      const startRect = this.newRectNode(startHotZoneData, layer_4, debugMode);
      const endHotZoneData = this.data.hotZoneItemArr[option.selectEndHotZoneIndex];
      const endRect = this.newRectNode(endHotZoneData, layer_5, debugMode);

      const iconStart = this.getSprNode("icon_connect_start");
      const iconEndActive = this.getSprNode("icon_connect_start");
      const iconEnd = this.getSprNode("icon_connect_end");
      iconStart.zIndex = layer_2;
      iconEnd.zIndex = layer_2;
      iconEndActive.zIndex = layer_2;
      iconStart.x = startRect.width / 2;
      iconStart.y = startRect.height / 2;
      iconEnd.x = endRect.width / 2;
      iconEnd.y = endRect.height / 2;
      iconEndActive.x = endRect.width / 2;
      iconEndActive.y = endRect.height / 2;
      iconEndActive.active = false;

      endRect.addChild(iconEnd);
      endRect.addChild(iconEndActive);
      startRect.addChild(iconStart);

      // 有些题需要在连接后 显示一个字符到指定区域 以下代码为实现
      // 显示字符的节点
      let linkedTextNode = null;
      // 显示的字符
      const endShowText = option.linkedShowText ? option.linkedShowText : "";
      // 判断是否配置了显示热区
      if(option.selectEndHotZoneShowIndex && !isNaN(option.selectEndHotZoneShowIndex) && option.selectEndHotZoneShowIndex >= 0) {
        // 需要显示字符的热区数据
        const linkedTextData = this.data.hotZoneItemArr[option.selectEndHotZoneShowIndex];
        const textNodePartent = this.newRectNode(linkedTextData, layer_4, debugMode);
        const textNode = this.newTextNode("");
        textNode.x = textNodePartent.width / 2;
        textNode.y = textNodePartent.height / 2;
        textNodePartent.addChild(textNode)
        linkedTextNode = textNode;
      }

      // 中心点 左下角 右下角 正确索引
      this.allConnectionEndPoints[uid].push({
        parentNode: endRect,
        endActiveNode: iconEndActive,
        centerPos: endRect.parent.convertToWorldSpaceAR(cc.v2(endRect.x + endRect.width / 2, endRect.y + endRect.height / 2)),
        leftBottomPos: endRect.parent.convertToWorldSpaceAR(cc.v2(endRect.x, endRect.y)),
        rightTopPos: endRect.parent.convertToWorldSpaceAR(cc.v2(endRect.x + endRect.width, endRect.y + endRect.height)),
        connectionIndex: index,
        linkedIndex: -1,
        getEndShowText: () => endShowText
      })

      // 新建节点 放置线条
      const lineNode = new cc.Node();
      lineNode.name = 'connect_' + index;
      startRect.addChild(lineNode)
      const ctx = lineNode.addComponent(cc.Graphics);
      ctx.lineWidth = 4;
      startRect.lineCtx = ctx;

      // 临时存储连接所用 手指在移动时保存上一次命中的索引
      let tempConnectResultIndex = -1;

      // 手点击连线起点 发起连线
      startRect.on(cc.Node.EventType.TOUCH_START, () => {
        if(this.submitted) {
          return
        }

        // 因为页面滚动 所以在发起连线时要更新所有终点坐标
        this.updateAllConnectionEndPoint();
        // 显示所有终点高亮
        this.setActiveEndPointsByUID(true, uid);
        // 禁用滚动 开始连线
        this.disableScroll();
        // console.log("触摸开始") 
      })

      // 松手 离开屏幕
      startRect.on(cc.Node.EventType.TOUCH_CANCEL, () => {
        if(this.submitted) {
          return
        }
        // 启用页面滚动
        this.enableScroll();
        // 检查是否可以提交
        // this.checkCanSubmit();
        // 隐藏所有终点高亮
        this.setActiveEndPointsByUID(false, uid);

        // 如果当前连线已经有连接过 则找到上次的连接索引 清掉
        const oldLinkedIndex = currentConnectionList.findIndex(ccl=>ccl.linkedIndex == index);
        if(oldLinkedIndex >= 0) {
          currentConnectionList[oldLinkedIndex].linkedIndex = -1;
        }

        // 把命中的节点的连接索引更新
        if(tempConnectResultIndex >= 0) {
          // 当前被连接的点如果已经有起点连接过 则清掉之前的连线
          if(currentConnectionList[tempConnectResultIndex].linkedIndex >= 0) {
            // 命中节点已经连过线了
            currentConnectionList[tempConnectResultIndex].linkedRect.lineCtx.clear();
          }
          currentConnectionList[tempConnectResultIndex].linkedIndex = index;
          currentConnectionList[tempConnectResultIndex].linkedRect = startRect;
          tempConnectResultIndex = -1;
        } else {
          ctx.clear();
        }
        // console.log("触摸取消") 
      })

      startRect.on(cc.Node.EventType.TOUCH_END, () => {
        if(this.submitted) {
          return
        }
        // 启动页面滚动
        this.enableScroll();
        // 检查是否可以提交
        // this.checkCanSubmit();
        // 隐藏所有终点高亮
        this.setActiveEndPointsByUID(false, uid);
        
        // 如果当前连线已经有连接过 则找到上次的连接索引 清掉
        const oldLinkedIndex = currentConnectionList.findIndex(ccl=>ccl.linkedIndex == index);
        if(oldLinkedIndex >= 0) {
          currentConnectionList[oldLinkedIndex].linkedIndex = -1;
        }

        // 把命中的节点的连接索引更新
        if(tempConnectResultIndex >= 0) {
          // 当前被连接的点如果已经有起点连接过 则清掉之前的连线
          if(currentConnectionList[tempConnectResultIndex].linkedIndex >= 0) {
            // 命中节点已经连过线了
            currentConnectionList[tempConnectResultIndex].linkedRect.lineCtx.clear();
          }
          currentConnectionList[tempConnectResultIndex].linkedIndex = index;
          currentConnectionList[tempConnectResultIndex].linkedRect = startRect;
          tempConnectResultIndex = -1;
        } else {
          ctx.clear()
        }
        // console.log("触摸结束") 
      })

      startRect.on(cc.Node.EventType.TOUCH_MOVE, (e) => {
        if(this.submitted) {
          return
        }
        // 获取点击的世界坐标
        const worldPos = e.getLocation();
        // 本地坐标
        let localPos = null;
        // 遍历所有终点坐标 找到是否命中某一个终点 
        let hitSomeone = false;
        this.allConnectionEndPoints[uid].forEach(endPoint => {
          if(hitSomeone) {
            // 找到命中点 不再处理其他点
            return
          }
          const checkResult = this.checkHitEndPoint(endPoint, worldPos);
          if(checkResult.hit) {
            hitSomeone = true;
            // 命中终点 把线段终点定为热区中心 会产生吸附效果
            localPos = lineNode.parent.convertToNodeSpaceAR(checkResult.centerPos);
            // 保存信息 当用户抬手时再判断是否命中
            tempConnectResultIndex = checkResult.connectionIndex;
            // 获取需要显示的字符 - 如果配置了
            if(linkedTextNode != null) {
              linkedTextNode.getComponent(cc.Label).string = checkResult.getEndShowText();
            }
          } else {
            // 没有命中任何终点 线段终点为实际手指位置
            localPos = lineNode.parent.convertToNodeSpaceAR(worldPos);
            // 清空保存的连接索引
            tempConnectResultIndex = -1;
            // 清空回显字符 - 如果配置了
            if(linkedTextNode != null) {
              linkedTextNode.getComponent(cc.Label).string = "";
            }
          }
        })
        // 清除上一次的痕迹
        ctx.clear()
        // 移动到当前开始节点的中心开始画线
        ctx.moveTo(startRect.width / 2, startRect.height / 2); 
        // 终点坐标在上面已经计算好了 这里使用
        ctx.lineTo(localPos.x, localPos.y);         
        // 画线
        ctx.stroke();
      })

      currentConnectionList.push({
        rect: startRect,
        linkedIndex: -1,
        textShow: option.textShow ? option.textShow : "",
      })
    });
    
    return validater;
  }

  // 设置2级连线组
  setConnectionGroup2L(contentData, debugMode=false) {
    const uid_1 = getUUID();
    const uid_2 = getUUID();
    this.allConnectionEndPoints[uid_1] = [];
    this.allConnectionEndPoints[uid_2] = [];
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);
    // 存储所有连线
    let currentConnectionList1 = [];
    let currentConnectionList2 = [];
    // 设置校验器
    let validater = () => {
      const result = {
        detail: {
          contentType: CONNECTION,
          configIndex: contentData.index,
          contentIndex: -1,
          results: []
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      };

      contentData.contentList.forEach((option, index) => {
        const detailItem = {
          start1Index: index,
          // start1Text: option.startTextShow ? option.startTextShow : "",
          start2Index: index,
          // start2Text: option.startTextShow ? option.startTextShow : "",
          correct1EndIndex: index,
          // correct1EndText: option.endTextShow ? option.endTextShow : "",
          correct2EndIndex: index,
          // correct2EndText: option.endTextShow ? option.endTextShow : "",
          current1EndIndex: currentConnectionList1[index].linkedIndex,
          // current1EndText: currentConnectionList1[index].textShow,
          current2EndIndex: currentConnectionList2[index].linkedIndex,
          // current2EndText: currentConnectionList2[index].textShow,
          right: true
        }
        if(currentConnectionList1[index].linkedIndex == index && currentConnectionList2[index].linkedIndex == index) {
          // 正确 返回分数
          result.score += (option.score != null && !isNaN(Number(option.score))) ? Number(option.score) : 0;
        } else {
          // 错误
          detailItem.right = false;
          result.allRight = false;
        }
        result.detail.results.push(detailItem)
      });

      return [result];
    };

    contentData.contentList.forEach((option, index) => {
      // 开始和结束点
      const startHotZoneData1 = this.data.hotZoneItemArr[option.selectStartHotZoneIndex_1];
      const startRect1 = this.newRectNode(startHotZoneData1, layer_4, debugMode);
      const startHotZoneData2 = this.data.hotZoneItemArr[option.selectStartHotZoneIndex_2];
      const startRect2 = this.newRectNode(startHotZoneData2, layer_4, debugMode);

      const endHotZoneData1 = this.data.hotZoneItemArr[option.selectEndHotZoneIndex_1];
      const endRect1 = this.newRectNode(endHotZoneData1, layer_5, debugMode);
      const endHotZoneData2 = this.data.hotZoneItemArr[option.selectEndHotZoneIndex_2];
      const endRect2 = this.newRectNode(endHotZoneData2, layer_5, debugMode);

      // 第一级连线
      const iconStart1 = this.getSprNode("icon_connect_start");
      const iconEndActive1 = this.getSprNode("icon_connect_start");
      const iconEnd1 = this.getSprNode("icon_connect_end");
      iconStart1.zIndex = layer_2;
      iconEnd1.zIndex = layer_2;
      iconEndActive1.zIndex = layer_2;
      iconStart1.x = startRect1.width / 2;
      iconStart1.y = startRect1.height / 2;
      iconEnd1.x = endRect1.width / 2;
      iconEnd1.y = endRect1.height / 2;
      iconEndActive1.x = endRect1.width / 2;
      iconEndActive1.y = endRect1.height / 2;
      iconEndActive1.active = false;

      endRect1.addChild(iconEnd1);
      endRect1.addChild(iconEndActive1);
      startRect1.addChild(iconStart1);

      // 第二级连线
      const iconStart2 = this.getSprNode("icon_connect_start");
      const iconEndActive2 = this.getSprNode("icon_connect_start");
      const iconEnd2 = this.getSprNode("icon_connect_end");
      iconStart2.zIndex = layer_2;
      iconEnd2.zIndex = layer_2;
      iconEndActive2.zIndex = layer_2;
      iconStart2.x = startRect2.width / 2;
      iconStart2.y = startRect2.height / 2;
      iconEnd2.x = endRect2.width / 2;
      iconEnd2.y = endRect2.height / 2;
      iconEndActive2.x = endRect2.width / 2;
      iconEndActive2.y = endRect2.height / 2;
      iconEndActive2.active = false;

      endRect2.addChild(iconEnd2);
      endRect2.addChild(iconEndActive2);
      startRect2.addChild(iconStart2);

      // 第一级连线
      // 中心点 左下角 右下角 正确索引
      this.allConnectionEndPoints[uid_1].push({
        parentNode: endRect1,
        endActiveNode: iconEndActive1,
        centerPos: endRect1.parent.convertToWorldSpaceAR(cc.v2(endRect1.x + endRect1.width / 2, endRect1.y + endRect1.height / 2)),
        leftBottomPos: endRect1.parent.convertToWorldSpaceAR(cc.v2(endRect1.x, endRect1.y)),
        rightTopPos: endRect1.parent.convertToWorldSpaceAR(cc.v2(endRect1.x + endRect1.width, endRect1.y + endRect1.height)),
        connectionIndex: index,
        linkedIndex: -1,
        getEndShowText: () => ""
      })

      // 第二级连线
      // 中心点 左下角 右下角 正确索引
      this.allConnectionEndPoints[uid_2].push({
        parentNode: endRect2,
        endActiveNode: iconEndActive2,
        centerPos: endRect2.parent.convertToWorldSpaceAR(cc.v2(endRect2.x + endRect2.width / 2, endRect2.y + endRect2.height / 2)),
        leftBottomPos: endRect2.parent.convertToWorldSpaceAR(cc.v2(endRect2.x, endRect2.y)),
        rightTopPos: endRect2.parent.convertToWorldSpaceAR(cc.v2(endRect2.x + endRect2.width, endRect2.y + endRect2.height)),
        connectionIndex: index,
        linkedIndex: -1,
        getEndShowText: () => ""
      })

      // 第一级连线
      // 新建节点 放置线条
      const lineNode1 = new cc.Node();
      lineNode1.name = 'connect1_' + index;
      startRect1.addChild(lineNode1)
      const ctx1 = lineNode1.addComponent(cc.Graphics);
      ctx1.lineWidth = 4;

      // 第二级连线
      // 新建节点 放置线条
      const lineNode2 = new cc.Node();
      lineNode2.name = 'connect2_' + index;
      startRect2.addChild(lineNode2)
      const ctx2 = lineNode2.addComponent(cc.Graphics);
      ctx2.lineWidth = 4;

      // 临时存储连接所用 手指在移动时保存上一次命中的索引
      let tempConnectResultIndex1 = -1;
      let tempConnectResultIndex2 = -1;

      // 第一级连线
      // 手点击连线起点 发起连线
      startRect1.on(cc.Node.EventType.TOUCH_START, () => {
        if(this.submitted) {
          return
        }

        // 因为页面滚动 所以在发起连线时要更新所有终点坐标
        this.updateAllConnectionEndPoint();
        // 显示所有终点高亮
        this.setActiveEndPointsByUID(true, uid_1);
        // 禁用滚动 开始连线
        this.disableScroll();
        // console.log("触摸开始") 
      })

      // 松手 离开屏幕
      startRect1.on(cc.Node.EventType.TOUCH_CANCEL, () => {
        if(this.submitted) {
          return
        }
        // 启用页面滚动
        this.enableScroll();
        // 检查是否可以提交
        // this.checkCanSubmit();
        // 隐藏所有终点高亮
        this.setActiveEndPointsByUID(false, uid_1);

        // 把命中的节点的连接索引更新
        if(tempConnectResultIndex1 >= 0) {
          currentConnectionList1[tempConnectResultIndex1].linkedIndex = index;
          tempConnectResultIndex1 = -1;
        } else {
          ctx1.clear()
        }

        // console.log("触摸取消") 
      })
      startRect1.on(cc.Node.EventType.TOUCH_END, () => {
        if(this.submitted) {
          return
        }
        // 启动页面滚动
        this.enableScroll();
        // 检查是否可以提交
        // this.checkCanSubmit();
        // 隐藏所有终点高亮
        this.setActiveEndPointsByUID(false, uid_1);
        
        // 把命中的节点的连接索引更新
        if(tempConnectResultIndex1 >= 0) {
          currentConnectionList1[tempConnectResultIndex1].linkedIndex = index;
          tempConnectResultIndex1 = -1;
        } else {
          ctx1.clear()
        }

        // console.log("触摸结束") 
      })

      startRect1.on(cc.Node.EventType.TOUCH_MOVE, (e) => {
        if(this.submitted) {
          return
        }
        // 获取点击的世界坐标
        const worldPos = e.getLocation();
        // 本地坐标
        let localPos = null;
        // 遍历所有终点坐标 找到是否命中某一个终点 
        let hitSomeone = false;
        this.allConnectionEndPoints[uid_1].forEach(endPoint => {
          if(hitSomeone) {
            // 找到命中点 不再处理其他点
            return
          }
          const checkResult = this.checkHitEndPoint(endPoint, worldPos);
          if(checkResult.hit) {
            hitSomeone = true;
            // 命中终点 把线段终点定为热区中心 会产生吸附效果
            localPos = lineNode1.parent.convertToNodeSpaceAR(checkResult.centerPos);
            // 保存信息 当用户抬手时再判断是否命中
            tempConnectResultIndex1 = checkResult.connectionIndex;
          } else {
            // 没有命中任何终点 线段终点为实际手指位置
            localPos = lineNode1.parent.convertToNodeSpaceAR(worldPos);
            // 清空保存的连接索引
            tempConnectResultIndex1 = -1;
          }
        })
        // 清除上一次的痕迹
        ctx1.clear()
        // 移动到当前开始节点的中心开始画线
        ctx1.moveTo(startRect1.width / 2, startRect1.height / 2); 
        // 终点坐标在上面已经计算好了 这里使用
        ctx1.lineTo(localPos.x, localPos.y);         
        // 画线
        ctx1.stroke();
      })

      // 第二级连线
      // 手点击连线起点 发起连线
      startRect2.on(cc.Node.EventType.TOUCH_START, () => {
        if(this.submitted) {
          return
        }

        // 因为页面滚动 所以在发起连线时要更新所有终点坐标
        this.updateAllConnectionEndPoint();
        // 显示所有终点高亮
        this.setActiveEndPointsByUID(true, uid_2);
        // 禁用滚动 开始连线
        this.disableScroll();
        // console.log("触摸开始") 
      })

      // 松手 离开屏幕
      startRect2.on(cc.Node.EventType.TOUCH_CANCEL, () => {
        if(this.submitted) {
          return
        }
        // 启用页面滚动
        this.enableScroll();
        // 检查是否可以提交
        // this.checkCanSubmit();
        // 隐藏所有终点高亮
        this.setActiveEndPointsByUID(false, uid_2);

        // 把命中的节点的连接索引更新
        if(tempConnectResultIndex2 >= 0) {
          currentConnectionList2[tempConnectResultIndex2].linkedIndex = index;
          tempConnectResultIndex2 = -1;
        } else {
          ctx2.clear()
        }

        // console.log("触摸取消") 
      })
      startRect2.on(cc.Node.EventType.TOUCH_END, () => {
        if(this.submitted) {
          return
        }
        // 启动页面滚动
        this.enableScroll();
        // 检查是否可以提交
        // this.checkCanSubmit();
        // 隐藏所有终点高亮
        this.setActiveEndPointsByUID(false, uid_2);
        
        // 把命中的节点的连接索引更新
        if(tempConnectResultIndex2 >= 0) {
          currentConnectionList2[tempConnectResultIndex2].linkedIndex = index;
          tempConnectResultIndex2 = -1;
        } else {
          ctx2.clear()
        }

        // console.log("触摸结束") 
      })

      startRect2.on(cc.Node.EventType.TOUCH_MOVE, (e) => {
        if(this.submitted) {
          return
        }
        // 获取点击的世界坐标
        const worldPos = e.getLocation();
        // 本地坐标
        let localPos = null;
        // 遍历所有终点坐标 找到是否命中某一个终点 
        let hitSomeone = false;
        this.allConnectionEndPoints[uid_2].forEach(endPoint => {
          if(hitSomeone) {
            // 找到命中点 不再处理其他点
            return
          }
          const checkResult = this.checkHitEndPoint(endPoint, worldPos);
          if(checkResult.hit) {
            hitSomeone = true;
            // 命中终点 把线段终点定为热区中心 会产生吸附效果
            localPos = lineNode2.parent.convertToNodeSpaceAR(checkResult.centerPos);
            // 保存信息 当用户抬手时再判断是否命中
            tempConnectResultIndex2 = checkResult.connectionIndex;
          } else {
            // 没有命中任何终点 线段终点为实际手指位置
            localPos = lineNode2.parent.convertToNodeSpaceAR(worldPos);
            // 清空保存的连接索引
            tempConnectResultIndex2 = -1;
          }
        })
        // 清除上一次的痕迹
        ctx2.clear()
        // 移动到当前开始节点的中心开始画线
        ctx2.moveTo(startRect2.width / 2, startRect2.height / 2); 
        // 终点坐标在上面已经计算好了 这里使用
        ctx2.lineTo(localPos.x, localPos.y);         
        // 画线
        ctx2.stroke();
      })

      // 第一级连线
      currentConnectionList1.push({
        rect: startRect1,
        linkedIndex: -1,
        textShow: option.textShow ? option.textShow : "",
      })

      // 第二级连线
      currentConnectionList2.push({
        rect: startRect2,
        linkedIndex: -1,
        textShow: option.textShow ? option.textShow : "",
      })
    });
    
    return validater;
  }

  // 设置连线选择 和联系组的区别是：一个起点可以连接多个终点 、 需要单独为每一个起点配置一个
  setConnectionChoice(contentData, debugMode=false) {
    const uid = getUUID();
    this.allConnectionEndPoints[uid] = [];
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);
    // 存储所有连线
    let currentConnectionList = [];
    // 当前连接点的索引
    let currentConnectIndex = -1;
    // 当前连接点的文字
    let currentConnectTextShow = "";
    // 正确连接点的索引
    let correctConnectIndex = -1;
    // 正确连接点显示文字
    let correctConnectTextShow = "";

    // 设置校验器
    let validater = () => {
      const result = {
        detail: {
          contentType: CONNECTION,
          configIndex: contentData.index,
          contentIndex: -1,
          result: {
            correctEndIndex: correctConnectIndex,
            correctEndText: correctConnectTextShow,
            currentEndIndex: currentConnectIndex,
            currentEndText: currentConnectTextShow,
            right: true
          }
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      };

      if(currentConnectIndex >= 0 && contentData.contentList[currentConnectIndex].isCorrect) {
        // 正确 返回分数
        result.score += (contentData.score != null && !isNaN(Number(contentData.score))) ? Number(contentData.score) : 0;
      } else {
        // 错误
        result.detail.result.right = false;
        result.allRight = false;
      }

      return [result];
    };

    // 开始点
    const startHotZoneData = this.data.hotZoneItemArr[contentData.linkHotZoneIndex];
    const startRect = this.newRectNode(startHotZoneData, layer_4, debugMode);
    const iconStart = this.getSprNode("icon_connect_start");
    iconStart.zIndex = layer_2;
    iconStart.x = startRect.width / 2;
    iconStart.y = startRect.height / 2;
    startRect.addChild(iconStart);

    // 有些题需要在连接后 显示一个字符到指定区域 以下代码为实现
    // 显示字符的节点
    let linkedTextNode = null;
    // 判断是否配置了显示热区
    if(contentData.linkHotZoneShowIndex && !isNaN(contentData.linkHotZoneShowIndex) && contentData.linkHotZoneShowIndex >= 0) {
      // 需要显示字符的热区数据
      const linkedTextData = this.data.hotZoneItemArr[contentData.linkHotZoneShowIndex];
      const textNodePartent = this.newRectNode(linkedTextData, layer_4, debugMode);
      const textNode = this.newTextNode("");
      textNode.x = textNodePartent.width / 2;
      textNode.y = textNodePartent.height / 2;
      textNodePartent.addChild(textNode)
      linkedTextNode = textNode;
    }

    // 新建节点 放置线条
    const lineNode = new cc.Node();
    lineNode.name = 'connect_';
    startRect.addChild(lineNode)
    const ctx = lineNode.addComponent(cc.Graphics);
    ctx.lineWidth = 4;

    // 手点击连线起点 发起连线
    startRect.on(cc.Node.EventType.TOUCH_START, () => {
      if(this.submitted) {
        return
      }

      // 因为页面滚动 所以在发起连线时要更新所有终点坐标
      this.updateAllConnectionEndPoint();
      // 显示所有终点高亮
      this.setActiveEndPointsByUID(true, uid);
      // 禁用滚动 开始连线
      this.disableScroll();
      // console.log("触摸开始") 
    })

    // 松手 离开屏幕
    startRect.on(cc.Node.EventType.TOUCH_CANCEL, () => {
      if(this.submitted) {
        return
      }
      // 启用页面滚动
      this.enableScroll();
      // 检查是否可以提交
      // this.checkCanSubmit();
      // 隐藏所有终点高亮
      this.setActiveEndPointsByUID(false, uid);

      // 如果没有连接任何线 则清空连线
      if(currentConnectIndex < 0) {
        ctx.clear()
      };

      // console.log("触摸取消") 
    })

    startRect.on(cc.Node.EventType.TOUCH_END, () => {
      if(this.submitted) {
        return
      }
      // 启动页面滚动
      this.enableScroll();
      // 检查是否可以提交
      // this.checkCanSubmit();
      // 隐藏所有终点高亮
      this.setActiveEndPointsByUID(false, uid);
      
      // 如果没有连接任何线 则清空连线
      if(currentConnectIndex < 0) {
        ctx.clear()
      };

      // console.log("触摸结束") 
    })

    startRect.on(cc.Node.EventType.TOUCH_MOVE, (e) => {
      if(this.submitted) {
        return
      }
      // 获取点击的世界坐标
      const worldPos = e.getLocation();
      // 本地坐标
      let localPos = null;
      // 遍历所有终点坐标 找到是否命中某一个终点 
      let hitSomeone = false;
      this.allConnectionEndPoints[uid].forEach(endPoint => {
        if(hitSomeone) {
          // 找到命中点 不再处理其他点
          return
        }
        const checkResult = this.checkHitEndPoint(endPoint, worldPos);
        if(checkResult.hit) {
          hitSomeone = true;
          // 命中终点 把线段终点定为热区中心 会产生吸附效果
          localPos = lineNode.parent.convertToNodeSpaceAR(checkResult.centerPos);
          // 保存信息 当用户抬手时再判断是否命中
          currentConnectIndex = checkResult.connectionIndex;
          // 获取需要显示的字符 - 如果配置了
          if(linkedTextNode != null) {
            linkedTextNode.getComponent(cc.Label).string = checkResult.getEndShowText();
          }
        } else {
          // 没有命中任何终点 线段终点为实际手指位置
          localPos = lineNode.parent.convertToNodeSpaceAR(worldPos);
          // 清空保存的连接索引
          currentConnectIndex = -1;
          // 清空回显字符 - 如果配置了
          if(linkedTextNode != null) {
            linkedTextNode.getComponent(cc.Label).string = "";
          }
        }
      })
      // 清除上一次的痕迹
      ctx.clear()
      // 移动到当前开始节点的中心开始画线
      ctx.moveTo(startRect.width / 2, startRect.height / 2); 
      // 终点坐标在上面已经计算好了 这里使用
      ctx.lineTo(localPos.x, localPos.y);         
      // 画线
      ctx.stroke();
    })

    contentData.contentList.forEach((option, index) => {
      // 结束点
      const endHotZoneData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
      const endRect = this.newRectNode(endHotZoneData, layer_5, debugMode);
      const iconEndActive = this.getSprNode("icon_connect_start");
      const iconEnd = this.getSprNode("icon_connect_end");
      
      iconEnd.zIndex = layer_2;
      iconEndActive.zIndex = layer_2;
      iconEnd.x = endRect.width / 2;
      iconEnd.y = endRect.height / 2;
      iconEndActive.x = endRect.width / 2;
      iconEndActive.y = endRect.height / 2;
      iconEndActive.active = false;

      endRect.addChild(iconEnd);
      endRect.addChild(iconEndActive);

      // 显示的字符
      const endShowText = option.linkedShowText ? option.linkedShowText : "";
      // 中心点 左下角 右下角 正确索引
      this.allConnectionEndPoints[uid].push({
        parentNode: endRect,
        endActiveNode: iconEndActive,
        centerPos: endRect.parent.convertToWorldSpaceAR(cc.v2(endRect.x + endRect.width / 2, endRect.y + endRect.height / 2)),
        leftBottomPos: endRect.parent.convertToWorldSpaceAR(cc.v2(endRect.x, endRect.y)),
        rightTopPos: endRect.parent.convertToWorldSpaceAR(cc.v2(endRect.x + endRect.width, endRect.y + endRect.height)),
        connectionIndex: index,
        isCorrectOption: option.isCorrect,
        getEndShowText: () => endShowText
      })
    });
    
    return validater;
  }

  // 更新所有结束点的坐标
  updateAllConnectionEndPoint() {
    for(let key in this.allConnectionEndPoints) {
      const connectGroup = this.allConnectionEndPoints[key];
      connectGroup.forEach(point => {
        point.centerPos = point.parentNode.parent.convertToWorldSpaceAR(cc.v2(point.parentNode.x + point.parentNode.width / 2, point.parentNode.y + point.parentNode.height / 2));
        point.leftBottomPos = point.parentNode.parent.convertToWorldSpaceAR(cc.v2(point.parentNode.x, point.parentNode.y));
        point.rightTopPos = point.parentNode.parent.convertToWorldSpaceAR(cc.v2(point.parentNode.x + point.parentNode.width, point.parentNode.y + point.parentNode.height));
      });
    }
  }

  // 显示所有可用可用终点 - 高亮
  setActiveEndPointsByUID(active, uid) {
    const connectGroup = this.allConnectionEndPoints[uid];
    connectGroup.forEach(point => {
      point.endActiveNode.active = active;      
    });
  }

  // 检查点是否在指定终点范围内 - 用于连线判断
  checkHitEndPoint(endPoint, currentPoint) {
    if(currentPoint.x > endPoint.leftBottomPos.x && currentPoint.x < endPoint.rightTopPos.x && currentPoint.y > endPoint.leftBottomPos.y && currentPoint.y < endPoint.rightTopPos.y) {
      return {
        hit: true,
        centerPos: endPoint.centerPos,
        connectionIndex: endPoint.connectionIndex,
        getEndShowText: endPoint.getEndShowText
      };
    } else {
      return {
        hit: false,
        centerPos: null,
        connectionIndex: -1,
        getEndShowText: () => ""
      };
    }
  }

  // 纵横字谜全局数据
  allCrosswordData = {}
  // 设置纵横字谜题型
  setCrosswordPuzzleInput(contentData, debugMode=false) {
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);

    // 正确校验方法 - 默认返null
    const lineLetters = [];
    let validater = () => {
      const result = {
        detail: {
          contentType: CROSSWORD_PUZZLE,
          configIndex: contentData.index,
          contentIndex: -1,
          correctWord: "",
          inputWord: "",
          right: true
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      }

      result.detail.correctWord = "";
      result.detail.inputWord = "";
      lineLetters.forEach((letter, index) => {
        result.detail.correctWord += letter.correctLetter;
        result.detail.inputWord += letter.currentLetter;
        if(letter.currentLetter != letter.correctLetter) {
          result.allRight = false;
          result.detail.right = false;
        }
      })

      // 如果全部正确，改写比分
      if(result.allRight) {
        result.score = Number(contentData.score);
      }
      return [result]
    };

    // 初始化回显区域 - 可不配置
    let endShowNode = null;
    let endShowText = null
    if(contentData.linkHotZoneShowIndex != null && !isNaN(Number(contentData.linkHotZoneShowIndex)) && Number(contentData.linkHotZoneShowIndex) >= 0) {
      const endShowHotZoneData = this.data.hotZoneItemArr[contentData.linkHotZoneShowIndex];
      endShowNode = this.newRectNode(endShowHotZoneData, layer_4, debugMode);
      endShowText = this.newTextNode("");
      endShowText.x = endShowNode.width / 2;
      endShowText.y = endShowNode.height / 2;
      endShowNode.addChild(endShowText);
    }

    contentData.contentList.forEach((option, index) => {
      if(this.allCrosswordData[option.selectHotZoneIndex + ""] == undefined) {
        this.allCrosswordData[option.selectHotZoneIndex + ""] = {
          inited: false,
          rect: null,
          border: null,
          currentLetter: "",
          correctLetter: "",
        }
      }
      const letterData = this.allCrosswordData[option.selectHotZoneIndex + ""];
      lineLetters.push(letterData);
      // 如果数据没有被初始化过 则初始化各种实例
      if(!letterData.inited) {
        letterData.inited = true;
        // 存储正确的字符 用于提交时比对
        letterData.correctLetter = option.letter;
        const hotZoneData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
        letterData.rect = this.newRectNode(hotZoneData, layer_4, debugMode);
        letterData.resultRect = this.newRectNode(hotZoneData, layer_2, debugMode);
        letterData.border = this.newOutlineNode(hotZoneData, layer_2, "#6dbef6");
        letterData.decorativeFrame = this.newDecorativeFrame(hotZoneData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
        letterData.border.active = false;
        letterData.textNode = this.newTextNode("");
        letterData.textNode.x = letterData.rect.width / 2;
        letterData.textNode.y = letterData.rect.height / 2;
        letterData.rect.addChild(letterData.textNode);
        letterData.decorativeFrame.active = false;

        letterData.rect.on("click", async ()=>{
          if(this.submitted) {
            return
          }

          let currentWord = "";
          // 获得当前方格输入的内容 并 高亮显示
          lineLetters.forEach(letter => {
            currentWord += letter.currentLetter;
            letter.border.active = true;
          });

          const inputText = await this.asyncShowKeyboardModal('');
          if(inputText) {
            // 把用户输入的内容填入格子
            lineLetters.forEach((letter, index) => {
              letter.currentLetter = inputText.charAt(index);
              letter.decorativeFrame.active = true;
              letter.textNode.getComponent(cc.Label).string = letter.currentLetter;
            })
          } else {
            // 把用户输入的内容填入格子
            lineLetters.forEach((letter, index) => {
              letter.currentLetter = "";
              letter.decorativeFrame.active = false;
              letter.textNode.getComponent(cc.Label).string = "";
            })
          }
          
          // 如果设置了回显区域，设置回显字符
          if(endShowText != null) {
            endShowText.getComponent(cc.Label).string = inputText;
          }

          // 清除高亮显示
          lineLetters.forEach(letter => {
            currentWord += letter.currentLetter;
            letter.border.active = false;
          });
          
          // 校验必须在上面更新了校验器之后进行 否则本次选择不会生效
          // this.checkCanSubmit();
        })
      }
    })
    return validater
  }

  // 设置文字输入热区
  setTextInput(contentData, hotZoneItemData, debugMode=false) {
    const resultRect = this.newRectNode(hotZoneItemData, layer_2, debugMode);
    this.newDecorativeFrame(hotZoneItemData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
    const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);
    let currentInputText = "";
    // 正确校验方法 - 默认返null
    let validater = () => {
      const result = {
        detail: {
          contentType: TEXTINPUT,
          configIndex: contentData.index,
          contentIndex: -1,
          currentInputText: "",
          correctText: "",
          right: true
        },
        configIndex: contentData.index,
        rect: resultRect,
        allRight: true,
        score: 0
      }

      result.detail.currentInputText = currentInputText;
      result.detail.correctText = contentData.inputText;

      let right = false;
      // if(!contentData.notAdaptContraction) {
        // 测试新的匹配规则
        // right = this.checkEqualitySentence(currentInputText, contentData.inputText, {
        //   keyWordMatch: contentData.keyWordMatch ? true : false,
        //   keyWordMatchInOrder: contentData.keyWordMatchInOrder ? true : false,
        //   isCaseInsensitive: contentData.isCaseInsensitive ? true : false,
        //   openAnswer: contentData.openAnswer ? true : false,
        //   capitalizedFirstLetter: contentData.capitalizedFirstLetter ? true : false,
        //   notAdaptContraction: contentData.notAdaptContraction ? true : false,
        // })
      // } else 
      let userInputText = currentInputText;
      let configInputText = contentData.inputText;
      if(contentData.keyWordMatch) {
        // 关键词匹配，只有回答文字中包含全部关键词，就算对
        right = this.fuzzyMatchingString(userInputText, configInputText, {
          isCaseInsensitive: contentData.isCaseInsensitive ? true : false,
          keyWordMatchInOrder: contentData.keyWordMatchInOrder ? true : false,
          notAdaptContraction: contentData.notAdaptContraction ? true : false
        });
      } else if(contentData.openAnswer) {
        // 开放型回答 只要有内容就算对
        right = userInputText != "";
      } else {
        // 替换特殊的符号
        const standardApostrophe = "'";
        const isoApostrophe = "’";
        const regApostrophe = new RegExp(isoApostrophe, "g")
        userInputText = userInputText.replace(regApostrophe, standardApostrophe);
        configInputText = configInputText.replace(regApostrophe, standardApostrophe);

        // 除特殊配置, 默认进行缩写通配符替换
        if(!contentData.notAdaptContraction) {
          userInputText = this.adaptContraction(userInputText);
          configInputText = this.adaptContraction(configInputText);
        }

        if(contentData.isCaseInsensitive) {
          // 不区分大小写
          right = userInputText.toLowerCase().trim() == configInputText.toLowerCase().trim();
        } else {
          // 区分大小写 完全相等
          right = userInputText.trim() == configInputText.trim();
        }
      }

      if(right) {
        // 正确 返回分数
        result.score = contentData.score && !isNaN(Number(contentData.score)) ? Number(contentData.score) : 0;
        return [result]
      } else {
        // 错误
        result.allRight = false;
        result.detail.right = false;
        return [result]
      }
    };
		rect.on("click", async () => {
      if(this.submitted) {
        return;
      }
      this.currentInputTarget = rect;
      const optionList = [];
      contentData.contentList.forEach(option => {
        optionList.push({
          label: option.text,
          value: option.text
        })
      });

      // 取得上次输入文字 传到键盘中 作为默认显示 - 清空当前文字节点
      let lastText = "";
      if(rect.cleanLast) {
        lastText = rect.cleanLast()
      }

      let text = await this.asyncShowKeyboardModal(lastText);
      const inputLabel = this.newInputTextNode(text, 0);
      const labelCom = inputLabel.getComponent(cc.Label);
      inputLabel.x = rect.width / 2;
      inputLabel.y = rect.height / 2;
      inputLabel.width = rect.width;
      inputLabel.height = rect.height;
      // 文字根据设置是否居左对齐
      if(contentData.textAlignLeft) {
        labelCom.horizontalAlign = 0;
      } else {
        labelCom.horizontalAlign = 1;
      }
      // 超出宽度换行
      labelCom.overflow = 3;

      currentInputText = text;

      // 更新清除方法
      rect.cleanLast = () => {
        inputLabel.destroy();
        return text;
      }
      
      // 校验必须在上面更新了校验器之后进行 否则本次选择不会生效
      // this.checkCanSubmit();

      rect.addChild(inputLabel);
		})

    return validater
  }

  // 设置文字输入组
  setTextInputGroup(contentData, debugMode=false) {
    const allInputResults = [];
    let validater = () => {
      const allMatchedIndex = [];
      const results = [];
      allInputResults.forEach(resultItem => {
        let right = false;
        let userInputText = resultItem.detail.currentInputText;
        contentData.contentList.forEach((option, index) => {
          if(!right) {
            let configInputText = option.inputText;
            if(option.keyWordMatch) {
              // 关键词匹配，只有回答文字中包含全部关键词，就算对
              right = this.fuzzyMatchingString(userInputText, configInputText, {
                isCaseInsensitive: option.isCaseInsensitive ? true : false,
                keyWordMatchInOrder: option.keyWordMatchInOrder ? true : false,
                notAdaptContraction: option.notAdaptContraction ? true : false
              });
            } else if(option.openAnswer) {
              // 开放型回答 只要有内容就算对
              right = userInputText != "";
            } else {
              // 替换特殊的符号
              const standardApostrophe = "'";
              const isoApostrophe = "’";
              const regApostrophe = new RegExp(isoApostrophe, "g")
              userInputText = userInputText.replace(regApostrophe, standardApostrophe);
              configInputText = configInputText.replace(regApostrophe, standardApostrophe);

              // 除特殊配置, 默认进行缩写通配符替换
              if(!option.notAdaptContraction) {
                userInputText = this.adaptContraction(userInputText);
                configInputText = this.adaptContraction(configInputText);
              }

              if(option.isCaseInsensitive) {
                // 不区分大小写
                right = userInputText.toLowerCase().trim() == configInputText.toLowerCase().trim();
              } else {
                // 区分大小写 完全相等
                right = userInputText.trim() == configInputText.trim();
              }
            }
            
            // 检查当前匹配成功的答案 是否有其他选项匹配过
            if(allMatchedIndex.indexOf(index) != -1) {
              right = false;
            }

            if(right) {
              // 正确 返回分数
              allMatchedIndex.push(index); // 将匹配成功的序号放进数组
              resultItem.detail.correctText = option.inputText;
              resultItem.score = option.score && !isNaN(Number(option.score)) ? Number(option.score) : 0;
              results.push(resultItem)
            } 
          }
        })

        if(!right) {
          // 如果遍历了所有答案一轮也没有找到符合的正确答案，则为错误
          resultItem.allRight = false;
          resultItem.detail.right = false;
          results.push(resultItem)
        }
      })
      
      return results
    }

    contentData.contentList.forEach((option, index) => {
      const hotZoneItemData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
      const resultRect = this.newRectNode(hotZoneItemData, layer_2, debugMode);
      this.newDecorativeFrame(hotZoneItemData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
      const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);

      const result = {
        detail: {
          contentType: TEXTINPUT_GROUP,
          configIndex: contentData.index,
          contentIndex: -1,
          currentInputText: "",
          correctText: "",
          right: true
        },
        configIndex: contentData.index,
        rect: resultRect,
        allRight: true,
        score: 0
      }

      allInputResults.push(result);

      rect.on("click", async () => {
        if(this.submitted) {
          return;
        }
        this.currentInputTarget = rect;
        const optionList = [];
        contentData.contentList.forEach(option => {
          optionList.push({
            label: option.text,
            value: option.text
          })
        });

        // 取得上次输入文字 传到键盘中 作为默认显示 - 清空当前文字节点
        let lastText = "";
        if(rect.cleanLast) {
          lastText = rect.cleanLast()
        }

        let text = await this.asyncShowKeyboardModal(lastText);
        const inputLabel = this.newInputTextNode(text, 0);
        const labelCom = inputLabel.getComponent(cc.Label);
        inputLabel.x = rect.width / 2;
        inputLabel.y = rect.height / 2;
        inputLabel.width = rect.width;
        inputLabel.height = rect.height;
        // 文字根据设置是否居左对齐
        if(option.textAlignLeft) {
          labelCom.horizontalAlign = 0;
        } else {
          labelCom.horizontalAlign = 1;
        }
        // 超出宽度换行
        labelCom.overflow = 3;

        result.detail.currentInputText = text;

        // 更新清除方法
        rect.cleanLast = () => {
          inputLabel.destroy();
          return text;
        }
        
        // 校验必须在上面更新了校验器之后进行 否则本次选择不会生效
        // this.checkCanSubmit();

        rect.addChild(inputLabel);
      })
    });

    return validater
  }

  // 设置单词排序题
  setSortWords(contentData, hotZoneItemData, debugMode=false) {
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);
    
    this.newDecorativeFrame(hotZoneItemData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
    const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);

    // 选项列表
    const optionList = [];
    // 已选的单词列表
    let sortedList = [];

    // 正确校验方法 - 默认返null
    let validater = () => {
      const result = {
        detail: {
          contentType: SORT_WORDS,
          configIndex: contentData.index,
          contentIndex: -1,
          optionList: [],
          sortedList: [],
          right: true
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      }

      result.detail.optionList = optionList;
      result.detail.sortedList = sortedList;
      
      if(optionList.length != sortedList.length) {
        // 如果选项长度和选择的长度不等，则判定为没有完成所有选项
        // 错误
        result.allRight = false;
        result.detail.right = false;
        return [result]
      }

      // 遍历选择的列表 判断是否和设定一致 注意：设定的序号从1开始
      let hasWrong = false;
      sortedList.forEach(word => {
        if((Number(word.currentIndex) + 1) != Number(word.option.sortIndex)) {
          hasWrong = true;
        }
      })

      if(!hasWrong) {
        // 正确 返回分数
        result.score = contentData.score && !isNaN(Number(contentData.score)) ? Number(contentData.score) : 0;
        return [result]
      } else {
        // 错误
        result.allRight = false;
        result.detail.right = false;
        return [result]
      }
    };

   
    contentData.contentList.forEach((option, index) => {
      optionList.push({
        word: option.text,
        index: index,
        sortIndex: option.sortIndex
      })
    });

    rect.on("click", async () => {
      if(this.submitted) {
        return;
      }
      this.currentInputTarget = rect;

      // 取得上次输入文字 传到键盘中 作为默认显示 - 清空当前文字节点
      let lastText = "";
      if(rect.cleanLast) {
        lastText = rect.cleanLast()
      }

      let text = ""
      sortedList = await this.asyncShowSortWordsModal(optionList);
      if(sortedList == undefined) {
        // 点击了取消 还原上次显示
        text = lastText;
      } else {
        const textArr = [];
        sortedList.forEach(word=>{
          textArr.push(word.option.word);
        })
        text = textArr.join(" ");
      }

      const inputLabel = this.newInputTextNode(text, 0);
      inputLabel.x = rect.width / 2;
      inputLabel.y = rect.height / 2;

      // 更新清除方法
      rect.cleanLast = () => {
        inputLabel.destroy();
        return text;
      }
      
      // 校验必须在上面更新了校验器之后进行 否则本次选择不会生效
      // this.checkCanSubmit();

      rect.addChild(inputLabel);
		})

    return validater
  }

  // 设置选项菜单热区
  setTextOptionList(contentData, debugMode=false) {
    // 存放子校验器
    const subValidater = [];
    const resultRects = [];

    // 正确校验方法 - 默认返null
    let validater = () => {
      let results = [];
      let valid = true;
      subValidater.forEach((validaterItem, index) => {
        if(valid) {
          const validResult = validaterItem();
          if(validResult == null) {
            // 用户没有选择 使用默认结果
            results.push(resultRects[index]);
          } else {
            results.push(...validResult);
          }
        }
      })

      if(valid) {
        return results;
      } else {
        return null;
      }
    };
    
    const optionList = [];
    if(contentData.useSelectOptionList) {
      contentData.selectOptionList.forEach((option, index) => {
        let strikeOutHotZoneNode = null;
        if(option.selectStrikeOutHotZoneIndex != null && !isNaN(Number(option.selectStrikeOutHotZoneIndex)) && Number(option.selectStrikeOutHotZoneIndex) >= 0) {
          const strikeOutHotZoneData = this.data.hotZoneItemArr[option.selectStrikeOutHotZoneIndex];
          strikeOutHotZoneNode = this.newRectNode(strikeOutHotZoneData, layer_4, debugMode);
          const lineNode = this.newStrikeOutNode(strikeOutHotZoneNode.width, strikeOutHotZoneNode.height);
          strikeOutHotZoneNode.addChild(lineNode);
          strikeOutHotZoneNode.active = false;
          strikeOutHotZoneNode.refCount = 0;
        }

        optionList.push({
          index: index,
          label: option.optionShowText ? option.optionShowText : option.text,
          value: option.text,
          strikeOutNode: strikeOutHotZoneNode
        })
      });
    }

    const scoreCheckGroup = {};
    contentData.contentList.forEach((option, index) => {
      // 将同一个分组的选项索引记录下来
      if(contentData.useSelectOptionList && option.optionGroupsIndex != undefined) {
        if(scoreCheckGroup[option.optionGroupsIndex + ""] == undefined) {
          scoreCheckGroup[option.optionGroupsIndex + ""]  = [];
        }
        scoreCheckGroup[option.optionGroupsIndex + ""].push(option.selectOptionListIndex);
      }

      const hotZoneItemData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
      const resultRect = this.newRectNode(hotZoneItemData, layer_2, debugMode);
      this.newDecorativeFrame(hotZoneItemData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
      const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);

      resultRects.push({
        detail: {
          contentType: TEXT_SELECT,
          configIndex: contentData.index,
          contentIndex: index,
          currentSelectIndex: null,
          currentSelectText: "",
          correctSelectIndex: index,
          correctSelectText: option.text,
          right: false
        },
        configIndex: contentData.index,
        rect: resultRect,
        allRight: false,
        score: 0
      });

      // 如果不是独立选项数据
      if(!contentData.useSelectOptionList) {
        let strikeOutHotZoneNode = null;
        if(option.selectStrikeOutHotZoneIndex != null && !isNaN(Number(option.selectStrikeOutHotZoneIndex)) && Number(option.selectStrikeOutHotZoneIndex) >= 0) {
          const strikeOutHotZoneData = this.data.hotZoneItemArr[option.selectStrikeOutHotZoneIndex];
          strikeOutHotZoneNode = this.newRectNode(strikeOutHotZoneData, layer_4, debugMode);
          const lineNode = this.newStrikeOutNode(strikeOutHotZoneNode.width, strikeOutHotZoneNode.height);
          strikeOutHotZoneNode.addChild(lineNode);
          strikeOutHotZoneNode.active = false;
          strikeOutHotZoneNode.refCount = 0;
        }

        optionList.push({
          index: index,
          label: option.optionShowText ? option.optionShowText : option.text,
          value: option.text,
          strikeOutNode: strikeOutHotZoneNode
        })
      }

      subValidater.push(()=>null)
      rect.on("click", async () => {
        if(this.submitted) {
          return;
        }
        this.currentInputTarget = rect;

        // 取得上次输入文字 - 清空当前文字节点
        let lastText = "";
        if(rect.cleanLast) {
          lastText = rect.cleanLast()
        }

        let selectData = await this.asyncShowSelectModal(optionList, "textList");
        // 如果点击了取消 则还原上次显示
        const inputLabel = this.newInputTextNode(selectData?selectData.value:lastText, 0);
        inputLabel.x = rect.width / 2;
        inputLabel.y = rect.height / 2;

        // 如果选择了数据 并且 选择的选项是配置了划线的 则显示划线 并把引用加1
        if(selectData && selectData.strikeOutNode) {
          selectData.strikeOutNode.active = true;
          selectData.strikeOutNode.refCount ++;
        }

        // 更新清除方法
        rect.cleanLast = () => {
          inputLabel.destroy();
          if(selectData) {
            if(selectData.strikeOutNode != null) {
              selectData.strikeOutNode.refCount --;
              if(selectData.strikeOutNode.refCount == 0) {
                selectData.strikeOutNode.active = false;
              }
            }
            return selectData.value;
          } else {
            return lastText
          }
        }

        // 如果选择了选项 则覆盖校验方法
        if(selectData) {
          subValidater[index] = () => {
            // configIndex - 标记当前的内容索引 用于在最后计算分组得分时使用
            // rect - 用于放置正确和错误的符号
            const result = {
              detail: {
                contentType: TEXT_SELECT,
                configIndex: contentData.index,
                contentIndex: index,
                currentSelectIndex: selectData.index,
                currentSelectText: selectData.label,
                correctSelectIndex: index,
                correctSelectText: option.text,
                right: true
              },
              configIndex: contentData.index,
              rect: resultRect,
              allRight: true,
              score: 0
            }
            
            let right = false;
            if(contentData.useSelectOptionList) {
              if(option.optionGroupsIndex != undefined) {
                const indexInGroupList = scoreCheckGroup[option.optionGroupsIndex + ""].indexOf(selectData.index)
                if(indexInGroupList >= 0) {
                  // 所组索引中找到当前用户选择的索引
                  right = true;
                  scoreCheckGroup[option.optionGroupsIndex + ""].splice(indexInGroupList, 1)
                }
              } else {
                right = selectData.index == option.selectOptionListIndex;
              }
            } else {
              right = selectData.index == index
            }

            // 选择数据的索引 和 当前索引是否相等 判断是否选择正确
            if(right) {
              // 正确 返回分数
              result.score = option.score && !isNaN(Number(option.score)) ? Number(option.score) : 0;
              return [result]
            } else {
              // 错误
              result.allRight = false;
              result.detail.right = false;
              return [result]
            }
          }
        }
        // 校验必须在上面更新了校验器之后进行 否则本次选择不会生效
        // this.checkCanSubmit();

        rect.addChild(inputLabel);
      });
    });

    // 返回分数校验器
    return validater
  }

  // 设置文字选项 多选
  setTextOptionListMultiple(contentData, debugMode=false) {
    // 存放子校验器
    const subValidater = [];
    const resultRects = [];

    // 正确校验方法 - 默认返null
    let validater = () => {
      let results = [];
      let valid = true;
      subValidater.forEach((validaterItem, index) => {
        if(valid) {
          const validResult = validaterItem();
          if(validResult == null) {
            // 用户没有选择 使用默认结果
            results.push(resultRects[index]);
          } else {
            results.push(...validResult);
          }
        }
      })

      if(valid) {
        return results;
      } else {
        return null;
      }
    };
    
    // 把所有的选项放进数组
    const optionList = [];
    if(contentData.useSelectOptionList) {
      contentData.selectOptionList.forEach((option, index) => {
        optionList.push({
          index: index,
          label: option.optionShowText ? option.optionShowText : option.text,
          value: option.text,
          selected: false,
          selectedLabel: "",
          strikeOutNode: null
        })
      });
    }

    contentData.contentList.forEach((option, index) => {
      const hotZoneItemData = this.data.hotZoneItemArr[option.selectHotZoneIndex];
      const resultRect = this.newRectNode(hotZoneItemData, layer_2, debugMode);
      this.newDecorativeFrame(hotZoneItemData, layer_1, "#FFFFFF", "#6dbef6", debugMode);
      const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);
      let selectData = JSON.parse(JSON.stringify(optionList))
      resultRects.push({
        detail: {
          contentType: TEXT_SELECT,
          configIndex: contentData.index,
          contentIndex: index,
          currentSelectIndex: null,
          currentSelectText: "",
          correctSelectIndex: index,
          correctSelectText: option.text,
          right: false
        },
        configIndex: contentData.index,
        rect: resultRect,
        allRight: false,
        score: 0
      });

      subValidater.push(()=>null)
      rect.on("click", async () => {
        if(this.submitted) {
          return;
        }
        this.currentInputTarget = rect;

        // 取得上次输入文字 - 清空当前文字节点
        let lastText = "";
        if(rect.cleanLast) {
          lastText = rect.cleanLast()
        }
        
        const _selectData = await this.asyncShowMultipleSelectModal(selectData, "textList");
        // 如果点击了取消 则还原上次显示
        if(_selectData) {
          selectData = _selectData
        }
        
        let selectedLabels = [];
        selectData.forEach(opItem => {
          if(opItem.selected) {
            selectedLabels.push(opItem.label)
          }
        });

        let showLabel = ""
        if(selectData.length > 0) {
          showLabel = selectedLabels.join("/");
        } else {
          showLabel = lastText;
        }

        const inputLabel = this.newInputTextNode(showLabel, 0);
        inputLabel.x = rect.width / 2;
        inputLabel.y = rect.height / 2;

        // 更新清除方法
        rect.cleanLast = () => {
          inputLabel.destroy();
          if(selectData) {
            return showLabel;
          } else {
            return lastText
          }
        }

        // 如果选择了选项 则覆盖校验方法
        if(selectData) {
          subValidater[index] = () => {
            // configIndex - 标记当前的内容索引 用于在最后计算分组得分时使用
            // rect - 用于放置正确和错误的符号
            const result = {
              detail: {
                contentType: TEXT_SELECT,
                configIndex: contentData.index,
                contentIndex: index,
                currentSelectIndex: [],
                currentSelectText: showLabel,
                correctSelectIndex: option.selectOptionListIndexArr,
                correctSelectText: option.text,
                right: true
              },
              configIndex: contentData.index,
              rect: resultRect,
              allRight: true,
              score: 0
            }
            const currentSelectIndexArr = [];
            const correctSelectTextArr = [];

            let right = false;
            if(contentData.useSelectOptionList) {
              // TODO:
              // option.selectOptionListIndexArr - 在表单中设定的正确序号数组
              // selectData.indexArr - 当前用户选择的序号数组
              // 需要修改线面的这行代码的逻辑，判断两个数组的元素完全相同 right = true
              // right = selectData.indexArr == option.selectOptionListIndexArr;
              
              let allRight = true;
              option.selectOptionListIndexArr.forEach(opIndex => {
                if(selectData[opIndex]) {
                  if(!selectData[opIndex].selected) {
                    allRight = false;
                  }
                  correctSelectTextArr.push(selectData[opIndex].label)
                }
              });

              selectData.forEach((selItem, index) => {
                if(selItem.selected) {
                  currentSelectIndexArr.push(index)
                }
              });

              right = allRight;
            } else {
              right = selectData.index == index
            }

            result.detail.currentSelectIndex = currentSelectIndexArr;
            result.detail.correctSelectText = correctSelectTextArr.join("/");

            // 选择数据的索引 和 当前索引是否相等 判断是否选择正确
            if(right) {
              // 正确 返回分数
              result.score = option.score && !isNaN(Number(option.score)) ? Number(option.score) : 0;
              return [result]
            } else {
              // 错误
              result.allRight = false;
              result.detail.right = false;
              return [result]
            }
          }
        }
        // 校验必须在上面更新了校验器之后进行 否则本次选择不会生效
        // this.checkCanSubmit();

        rect.addChild(inputLabel);
      });
    });

    // 返回分数校验器
    return validater
  }

  // 设置音频热区
  // 全局只能有一个音频播放 - 保存音乐id 用于停止
  currentAudioPlay = null;
  // 当前正在加载音频
  isLoadingAudio = false;
  setOneAudioBtn(contentData, hotZoneItemData) {
		const {sprNode, picNode} = this.newSprNodeByResName(hotZoneItemData, "icon_play_audio", true);
    const aniAudio = cc.instantiate(this.aniSpeaker)
    aniAudio.x = 0;
    aniAudio.y = 0;
    aniAudio.anchorX = aniAudio.anchorY = 0;
    aniAudio.active = true;
    aniAudio.zIndex = -1;
    aniAudio.width = picNode.width;
    aniAudio.height = picNode.height;
    picNode.addChild(aniAudio);
    let aniPlay = null;
		sprNode.on("click", async () => {
      if(this.submitted || this.isLoadingAudio) {
        return;
      }
      // 如果当前正在播放音频 则停止音频
      if(this.currentAudioPlay != null) {
        // 停止音频播放
        cc.audioEngine.stop(this.currentAudioPlay);
        this.currentAudioPlay = null;
        // 停止动画
        if(aniPlay != null) {
          aniPlay.stop();
        }
        // 显示上层的节点
        sprNode.opacity = 255;
        return 
      }
      // 标记为正在加载
      this.isLoadingAudio = true;
      let audioClip = null;
      if(contentData.audio_url) {
        audioClip = await asyncLoadRemote(contentData.audio_url);
      }
      // 加载完成
      this.isLoadingAudio = false;
      // 需要判断音频是否存在
      if(audioClip) {
        this.currentAudioPlay = cc.audioEngine.play(audioClip, false, 0.8);
        aniPlay = playAnimationOnNode(aniAudio, "audioPlay");
        sprNode.opacity = 1;
        cc.audioEngine.setFinishCallback(this.currentAudioPlay, () => {
          this.currentAudioPlay = null;
          aniPlay.stop();
          aniPlay = null;
          sprNode.opacity = 255;
        });
      }
		})
	};

  // 视频播放题型
  currentVideoPlay = null;
  videoDuration = 0;
  allTipVideos = [];
  setOneVideoBtn(contentData, hotZoneItemData, debugMode=false) {
    const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);
    const aniAudio = cc.instantiate(this.aniVideoTips)
    aniAudio.x = 0;
    aniAudio.y = 0;
    aniAudio.anchorX = aniAudio.anchorY = 0;
    aniAudio.active = true;
    aniAudio.zIndex = -1;
    aniAudio.width = rect.width;
    aniAudio.height = rect.height;
    rect.addChild(aniAudio);
		rect.on("click", async () => {
      this.showVideoLayer();
      const videoplayer = cc.find("Canvas/VideoPlayerContainer/videoPlayer").getComponent(cc.VideoPlayer)
      videoplayer.stayOnBottom = true;
      const video_url = await getVideoUrl(contentData.video_url);
      if( videoplayer.remoteURL != video_url ) {
        videoplayer.remoteURL = video_url;
      }
      this.videoDuration = await asyncGetVideoDuration(videoplayer);
      // 设置进度条的起始和结束标签
      this.videoControlBar.emit("set_duration", this.videoDuration)
      // 显示视频控制组件
      this.videoControlBar.emit("show_bar", true);
      this.currentVideoPlay = videoplayer;
      videoplayer.play();
      this.videoControlBar.emit("update_video_status", 1);
		})
    rect.active = false;
    this.allTipVideos.push(rect);
	};

  // 画图计数，用于同时打开多个画图板控制页面滚动的
  drawingCount = 0;
  // 存储所有涂色热区
  allDrawingBoards = [];
  // 画图题
  setDrawingArea(contentData, hotZoneItemData, debugMode=false) {
    const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);
    const drawingBoard = cc.instantiate(this.drawingBoardTemplate);
    const dbJS = drawingBoard.getComponent("DrawingBoard_DG_FAF")
    dbJS.painterOnCenter = contentData.painterOnCenter ? true : false;
    const uid = getUUID();
    drawingBoard.uid = uid;
    this.allDrawingBoards.push(drawingBoard);
    drawingBoard.width = rect.width;
    drawingBoard.height = rect.height;
    drawingBoard.y = rect.height / 2;
    drawingBoard.x = drawingBoard.width / 2;
    drawingBoard.active = true;
    drawingBoard.on("onPaintingStart", ()=>{
      rect.zIndex = layer_10;
      this.allDrawingBoards.forEach(borad => {
        if(borad.uid != uid) {
          borad.emit("disable");
        }
      })
      this.drawingCount ++;
      if(this.drawingCount == 1) {
        this.disableScroll()
      }
    })
    drawingBoard.on("onPaintingEnd", ()=>{
      rect.zIndex = layer_4;
      this.allDrawingBoards.forEach(borad => {
        if(borad.uid != uid) {
          borad.emit("enable");
        }
      })
      this.drawingCount --;
      if(this.drawingCount == 0) {
        this.enableScroll();
      }
    })
    rect.addChild(drawingBoard);
	};

  setDrawingAreaWithCheck(contentData, hotZoneItemData, debugMode=false) {
    const rect = this.newRectNode(hotZoneItemData, layer_4, debugMode);
    const drawingBoard = cc.instantiate(this.drawingBoardTemplate);
    const dbJS = drawingBoard.getComponent("DrawingBoard_DG_FAF")
    dbJS.painterOnCenter = contentData.painterOnCenter ? true : false;
    const uid = getUUID();
    drawingBoard.uid = uid;
    this.allDrawingBoards.push(drawingBoard);
    drawingBoard.width = rect.width;
    drawingBoard.height = rect.height;
    drawingBoard.y = rect.height / 2;
    drawingBoard.x = drawingBoard.width / 2;
    drawingBoard.active = true;
    drawingBoard.on("onPaintingStart", ()=>{
      rect.zIndex = layer_10;
      this.allDrawingBoards.forEach(borad => {
        if(borad.uid != uid) {
          borad.emit("disable");
        }
      })
      this.drawingCount ++;
      if(this.drawingCount == 1) {
        this.disableScroll()
      }
    })
    drawingBoard.on("onPaintingEnd", ()=>{
      rect.zIndex = layer_4;
      this.allDrawingBoards.forEach(borad => {
        if(borad.uid != uid) {
          borad.emit("enable");
        }
      })
      this.drawingCount --;
      if(this.drawingCount == 0) {
        this.enableScroll();
      }
    })
    rect.addChild(drawingBoard);

    // 判卷结果显示区域
    // const resultRect = this.newRectNode(hotZoneItemData, layer_2, debugMode);
    const resultRectData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultRect = this.newRectNode(resultRectData, layer_2, debugMode);

    // 如果没有配置则为默认空
    if(contentData.drawingCheckCorrectColors == undefined) {
      contentData.drawingCheckCorrectColors = [];
    }

    // // 正确校验方法 - 默认返null
    let validater = () => {
      const result = {
        detail: {
          contentType: DRAWING_CHECK,
          configIndex: contentData.index,
          contentIndex: -1,
          currentInputText: [],
          correctText: contentData.drawingCheckCorrectColors,
          right: true
        },
        configIndex: contentData.index,
        rect: resultRect,
        allRight: true,
        score: 0
      }

      // 验证答案
      const boardScript = drawingBoard.getComponent("DrawingBoard_DG_FAF");
      const colorsExist = boardScript.getColors();

      result.detail.currentInputText = colorsExist;

      let sameColorCount = 0;
      for (let i = 0; i < contentData.drawingCheckCorrectColors.length; i++) {
        if (colorsExist.includes(contentData.drawingCheckCorrectColors[i])) {
          sameColorCount++;
        }
      }
      let isRight = false;
      if (contentData.drawingCheckStrictEqual) {
        isRight = sameColorCount === contentData.drawingCheckCorrectColors.length && sameColorCount === colorsExist.length;
      } else {
        isRight = sameColorCount === contentData.drawingCheckCorrectColors.length;
      }
      
      if(isRight) {
        // 正确 返回分数
        result.score = contentData.score && !isNaN(Number(contentData.score)) ? Number(contentData.score) : 0;
        return [result]
      } else {
        // 错误
        result.allRight = false;
        result.detail.right = false;
        return [result]
      }
    };
    return validater;
	};

  // 语音评测题型
  setPronunciationAssessment(contentData, hotZoneItemData, debugMode=false) {
    let recordUrl = "";
    let recordText = "";
    let evaText = contentData.inputText;
    let evaRes = null;
    
    // 该题型需要配置一个显示正确错误符号的热区
    const resultIconShowData = this.data.hotZoneItemArr[contentData.linkResultShowHotZoneIndex];
    const resultIconRect = this.newRectNode(resultIconShowData, layer_2, debugMode);
    
    // 正确校验方法 - 默认返null
    let validater = () => {
      const result = {
        detail: {
          contentType: PRONUNCIATION_ASSESSMENT,
          configIndex: contentData.index,
          contentIndex: -1,
          recordUrl: recordUrl,
          recordText: recordText,
          evaText: evaText,
          evaRes: evaRes,
          right: true
        },
        configIndex: contentData.index,
        rect: resultIconRect,
        allRight: true,
        score: 0
      }
      
      result.detail.recordUrl = recordUrl;
      result.detail.recordText = recordText;
      result.detail.evaText = evaText;
      result.detail.evaRes = evaRes;

      let right = false;
      // 判断是否启用关键词匹配
      if(contentData.keyWordMatch) {
        right = this.fuzzyMatchingString(recordText, evaText, {
          isCaseInsensitive: contentData.isCaseInsensitive ? true : false,
          keyWordMatchInOrder: contentData.keyWordMatchInOrder ? true : false,
          notAdaptContraction: contentData.notAdaptContraction ? true : false
        })
      } else if(contentData.openAnswer) {
        // 开放型回答 只要有内容就算对
        right = recordText != ""
      }else {
        if(contentData.isCaseInsensitive) {
          // 不区分大小写
          right = recordText.toLowerCase() == evaText.toLowerCase();
        } else {
          // 区分大小写 完全相等
          right = recordText == evaText;
        }
      }

      if(right) {
        // 正确 返回分数
        result.score = contentData.score && !isNaN(Number(contentData.score)) ? Number(contentData.score) : 0;
        return [result]
      } else {
        // 错误
        result.allRight = false;
        result.detail.right = false;
        return [result]
      }
    };

    // {sprNode, picNode}
		const recNode = this.newSprNodeByResName(hotZoneItemData, "icon_PA_record", true);
    const reRecNode = this.newSprNodeByResName(hotZoneItemData, "icon_PA_replay", true);
    reRecNode.sprNode.active = false;

		recNode.sprNode.on("click", async () => {
      if(this.submitted) {
        return;
      }
      const res = await this.asyncShowPAModal("", "", contentData.inputText);
      recordUrl = res.recordUrl;
      recordText = res.recordText;
      evaText = res.evaText;
      evaRes = {};

      if(res.recordUrl) {
        recNode.sprNode.active = false;
        reRecNode.sprNode.active = true;
      }
		})

    reRecNode.sprNode.on("click", async () => {
      if(this.submitted) {
        return;
      }
      const res = await this.asyncShowPAModal(recordText, recordUrl, contentData.inputText);
      recordUrl = res.recordUrl;
      recordText = res.recordText;
      evaText = res.evaText;
      evaRes = {};
		})

    // 返回分数校验器
    return validater
	};


  // 生成一个分数板
  setScorePanel(data, debugMode=false) {
    const rect = this.newRectNode(data, layer_4, debugMode);
    const node = this.newTextNode("");
    const config = {
      node: null,
      setScore: (score)=>{},
      initTotalScore: (score)=>{}
    }
    let totalScore = "";

    config.setScore = (score) => {
      node.getComponent(cc.Label).string = `${score}/${totalScore}`;
    }
    config.initTotalScore = (score) => {
      totalScore = score;
      node.getComponent(cc.Label).string = `/${totalScore}`;
    }

    config.node = node;
    node.x = rect.width / 2;
    node.y = rect.height / 2;
    rect.addChild(node);
    return config
  }

  // 新建矩形区域
  newRectNode(data, layer = layer_4, debugMode) {
    const rate = (this.hotZoneBg.scale * this.hotZoneBg.width) / this.data.bgItem.rect.width;
    const rectNode = new cc.Node();
    rectNode.zIndex = layer;
    rectNode.name = 'rect' + data.index;
    this.hotZoneBg.addChild(rectNode);

    // 红色矩形
    const ctx = rectNode.addComponent(cc.Graphics);
    ctx.lineWidth = 4;
    ctx.strokeColor = cc.Color.BLACK;
    ctx.fillColor = cc.Color.WHITE.setA(100);

    // 设置位置 大小 基准坐标
    const scale = rate / this.hotZoneBg.scaleY;
		rectNode.width = data.rect.width * scale;
		rectNode.height = data.rect.height * scale;
		rectNode.x = -this.hotZoneBg.width / 2 + data.rect.x * scale;
		rectNode.y = 0 - data.rect.height * scale - data.rect.y * scale;
    rectNode.anchorX = rectNode.anchorY = 0;

    if (debugMode) {
      ctx.rect(0, 0, rectNode.width, rectNode.height);
    }
    ctx.stroke();
    ctx.fill();

    rectNode.addComponent(cc.Button);
    
    return rectNode
  }

  // 新建装饰框
  newDecorativeFrame(data, layer = layer_4, colorBg = "", colorBorder = "", debugMode) {
    const rate = (this.hotZoneBg.scale * this.hotZoneBg.width) / this.data.bgItem.rect.width;
    const rectNode = new cc.Node();
    rectNode.zIndex = layer;
    rectNode.name = '_decorative_' + data.index;
    this.hotZoneBg.addChild(rectNode);

    // 红色矩形
    const ctx = rectNode.addComponent(cc.Graphics);
    ctx.lineWidth = 3;
    
    if(colorBg) {
      const bgColor = new cc.Color();
      cc.Color.fromHEX(bgColor, colorBg);
      ctx.fillColor = bgColor;
    }

    if(colorBorder) {
      const borderColor = new cc.Color();
      cc.Color.fromHEX(borderColor, colorBorder);
      ctx.strokeColor = borderColor
    }

    // 设置位置 大小 基准坐标
    const scale = rate / this.hotZoneBg.scaleY;
		rectNode.width = data.rect.width * scale;
		rectNode.height = data.rect.height * scale;
		rectNode.x = -this.hotZoneBg.width / 2 + data.rect.x * scale;
		rectNode.y = 0 - data.rect.height * scale - data.rect.y * scale;
    rectNode.anchorX = rectNode.anchorY = 0;

    ctx.rect(0, 0, rectNode.width, rectNode.height);
    ctx.stroke();
    ctx.fill();

    rectNode.addComponent(cc.Button);
    
    return rectNode
  }

  // 新建素材图片节点 - 需要传入素材名和位置大小数据
  newSprNodeByResName(data, resName, fitHotZoneSize = false) {
    const rate = (this.hotZoneBg.scale * this.hotZoneBg.width) / this.data.bgItem.rect.width;
		const picNode = new cc.Node();
		picNode.name= 'pic' + data.index;
		this.hotZoneBg.addChild(picNode);

		const sprNode = getSprNode(resName);
		sprNode.anchorX = 0;
		sprNode.anchorY = 0;

		picNode.anchorX = 0;
		picNode.anchorY = 0;
		picNode.addChild(sprNode);

		const scale = rate / this.hotZoneBg.scaleY;
		picNode.width = data.rect.width * scale;
		picNode.height = data.rect.height * scale;
    picNode.x = -this.hotZoneBg.width / 2 + data.rect.x * scale;
		picNode.y = 0 - data.rect.height * scale - data.rect.y * scale;

    // 图片填充热区大小
    if(fitHotZoneSize) {
      sprNode.width = picNode.width;
      sprNode.height = picNode.height;
    }

    // 在素材节点上添加按钮
    sprNode.addComponent(cc.Button);

		return {sprNode, picNode};
  }

  // 新建文本节点
  newTextNode(text) {
    const textNode = new cc.Node();
		textNode.name= '_text_';
    const label = textNode.addComponent(cc.Label);
    const color = new cc.Color();
    cc.Color.fromHEX(color, "#333333");
    textNode.color = color;
    label.string = text;
    label.lineHeight = 26;
    label.fontSize = 26;
    label.verticalAlign = 2;
    label.horizontalAlign = 2;
    return textNode;
  }

  // 新建输入文字节点
  newInputTextNode(text, styleType) {
    const inputNode = new cc.Node();
		inputNode.name= '_text_input_';
    const label = inputNode.addComponent(cc.Label);
    const color = new cc.Color();
    cc.Color.fromHEX(color, "#333333");
    inputNode.color = color;
    label.string = text;
    label.lineHeight = 32;
    label.fontSize = 32;
    label.verticalAlign = 2;
    label.horizontalAlign = 2;
    return inputNode;
  }

  // 画线节点 从左下到右上
  newStrikeOutNode(width, height) {
    const node = new cc.Node();
    node.width = width;
    node.height = height;
    node.anchorX = 0;
		node.anchorY = 0;

    const ctx = node.addComponent(cc.Graphics);
    const color = new cc.Color();
    cc.Color.fromHEX(color, "#a8a9ad");
    ctx.strokeColor = color;

    ctx.lineWidth = 3;
    // 移动到当前开始节点的中心开始画线
    ctx.moveTo(0, 0); 
    // 终点坐标在上面已经计算好了 这里使用
    ctx.lineTo(node.width, node.height);         
    // 画线
    ctx.stroke();
    return node
  }

  // 颜色色块
  newMaskRectNode(data, layer = layer_2, maskColor = "#FFFF0066") {
    const rate = (this.hotZoneBg.scale * this.hotZoneBg.width) / this.data.bgItem.rect.width;
    const rectNode = new cc.Node();
    rectNode.name = '_mask_color_';
    // 调整颜色图层 - 置于最下层
    rectNode.zIndex = layer;
    this.hotZoneBg.addChild(rectNode);

    // 矩形
    const ctx = rectNode.addComponent(cc.Graphics);
    ctx.lineWidth = 4;
    const color = new cc.Color();
    cc.Color.fromHEX(color, maskColor);
    ctx.fillColor = color;

    // 设置位置 大小 基准坐标
    const scale = rate / this.hotZoneBg.scaleY;
		rectNode.width = data.rect.width * scale;
		rectNode.height = data.rect.height * scale;
		rectNode.x = -this.hotZoneBg.width / 2 + data.rect.x * scale;
		rectNode.y = 0 - data.rect.height * scale - data.rect.y * scale;
    rectNode.anchorX = rectNode.anchorY = 0;

    ctx.rect(0, 0, rectNode.width, rectNode.height);
    ctx.fill();

    rectNode.addComponent(cc.Button);
    
    return rectNode
  }

  // 颜色框
  newOutlineNode(data, layer = layer_2, lineColor = "#FFFF0066") {
    const rate = (this.hotZoneBg.scale * this.hotZoneBg.width) / this.data.bgItem.rect.width;
    const rectNode = new cc.Node();
    rectNode.name = '_outline_';
    // 调整颜色图层 - 置于最下层
    rectNode.zIndex = layer;
    this.hotZoneBg.addChild(rectNode);

    // 矩形
    const ctx = rectNode.addComponent(cc.Graphics);
    ctx.lineWidth = 3;
    const color = new cc.Color();
    cc.Color.fromHEX(color, lineColor);
    ctx.strokeColor = color;

    // 设置位置 大小 基准坐标
    const scale = rate / this.hotZoneBg.scaleY;
		rectNode.width = data.rect.width * scale;
		rectNode.height = data.rect.height * scale;
		rectNode.x = -this.hotZoneBg.width / 2 + data.rect.x * scale;
		rectNode.y = 0 - data.rect.height * scale - data.rect.y * scale;
    rectNode.anchorX = rectNode.anchorY = 0;

    ctx.rect(0, 0, rectNode.width, rectNode.height);
    ctx.stroke();

    rectNode.addComponent(cc.Button);
    
    return rectNode
  }


  // 提交试卷
  submitted = false;
  // 游戏成绩数据
  resultData = null;
  submit() {
    const resultData = {
      details: [],
      scoreCconfigArr: [],
      basicScore: 0,
      totalScore: 0,
      startTimestamp: this.startTimestamp,
      submitTimestamp: new Date().getTime(),
      answeringTime: null,
    }

    // 计算答题时长
    resultData.answeringTime = resultData.submitTimestamp - resultData.startTimestamp;

    this.data.scoreConfigArr.forEach(scoreGroup => {
      let item = JSON.parse(JSON.stringify(scoreGroup));
      item.score = 0;
      resultData.scoreCconfigArr.push(item);
    });

    this.scoreValidater.forEach(validater => {
      if(validater) {
        // 返回一个校验集合
        let results= validater();
        if(results == null) {
          return
        }
        results.forEach(result => {
          resultData.details.push(result.detail);
          const errIcon = getSprNode("icon_answer_wrong");
          const rightIcon = getSprNode("icon_answer_right");
          // 图标太大 缩小一半
          errIcon.scale = rightIcon.scale = 0.5;
          
          // 找到对应的组 计算该组得分
          resultData.scoreCconfigArr.forEach(item=>{
            if(item.linkHotZoneIndexArr.indexOf(result.configIndex) != -1) {
              item.score += result.score;
            }
          })

          if(result.allRight) {
            // 回答正确
            if(result.rect != null) {
              // 显示在热区的中间
              errIcon.x = rightIcon.x = result.rect.width / 2;
              errIcon.y = rightIcon.y = result.rect.height / 2;
              result.rect.addChild(rightIcon);
            } else if(result.rects) {
              result.rects.forEach(rect => {
                const _errIcon = cc.instantiate(errIcon)
                const _rightIcon = cc.instantiate(rightIcon)
                // 显示在热区的中间
                _errIcon.x = _rightIcon.x = rect.rect.width / 2;
                _errIcon.y = _rightIcon.y = rect.rect.height / 2;
                rect.rect.addChild(rect.right ? _rightIcon : _errIcon);
              });
            }
          } else {
            // 回答错误
            if(result.rect != null) {
              // 显示在热区的中间
              errIcon.x = rightIcon.x = result.rect.width / 2;
              errIcon.y = rightIcon.y = result.rect.height / 2;
              result.rect.addChild(errIcon) / 2;
            } else if(result.rects) {
              result.rects.forEach(rect => {
                const _errIcon = cc.instantiate(errIcon)
                const _rightIcon = cc.instantiate(rightIcon)
                // 显示在热区的中间
                _errIcon.x = _rightIcon.x = rect.rect.width / 2;
                _errIcon.y = _rightIcon.y = rect.rect.height / 2;
                rect.rect.addChild(rect.right ? _rightIcon : _errIcon);
              });
            }
          };
        });
      }
    })

    let totalScore = 0;
    resultData.scoreCconfigArr.forEach((score, index) => {
      // 累加各个分数区域
      totalScore += score.score;
      // 如果配置了分数显示区域 - 显示分数
      if(this.subScorePanels[index]) {
        this.subScorePanels[index].setScore(score.score);
      }
    })

    // 检查是否配置了卷面基础分，为了拉平分值，有的试卷不满15分 30分 100分，或者有画图题
    if(this.data.basicScore != null && !isNaN(Number(this.data.basicScore)) && Number(this.data.basicScore) > 0) {
      totalScore += Number(this.data.basicScore);
      resultData.basicScore = Number(this.data.basicScore);
    }

    // 取整 防止有很长的小数 - 有的分值是5.000000000000004这种
    let _totalScore = Math.floor(totalScore * 100) / 100;
    totalScore = parseFloat(_totalScore + "");

    resultData.totalScore = totalScore;
    this.resultData = resultData;
    console.log("答案校验完毕", resultData)
    this.showEndAnimationByRatingSystem(resultData.totalScore, this.data.ratingSystem)

    // 如果配置了分数显示区域 - 显示分数
    if(this.totalScorePanel) {
      this.totalScorePanel.setScore(totalScore);
    }

    // 标记已经提交 禁止页面事件
    this.submitted = true;
    if(this.btnSubmit) {
      this.btnSubmit.active = false;
    }
    if(this.btnSubmitDisable) {
      this.btnSubmitDisable.active = false;
    }
    if(this.btnReplay) {
      this.btnReplay.active = true;
    }

    // 显示所有视频提示
    this.allTipVideos.forEach(videoRect=>{
      videoRect.active = true;
    })
  }

  // 重新玩
  async replay() {
    this.btnSubmit = null;
    this.btnSubmitDisable = null;
    this.btnReplay = null;
    this.picNodeArr = [];
    this.scoreValidater = [];
    this.currentInputTarget = null;
    this.onSelectedCallback = null;
    this.onMultipleSelectedCallback = null;
    this.onKeyboardEnterCallback = null;
    this.submitted = false;
    this.subScorePanels = [];
    this.allConnectionEndPoints = {};
    this.totalScorePanel = null;
    this.allCrosswordData = {};
    this.currentVideoPlay = null;
    this.videoDuration = null;
    this.drawingCount = 0;
    this.allDrawingBoards = [];
    this.allTipVideos = [];
    this.startTimestamp = new Date().getTime();

    // 停止当前正在播放的音乐
    if(this.currentAudioPlay) {
      cc.audioEngine.stop(this.currentAudioPlay);
    }
    this.currentAudioPlay = null;

    this.hotZoneBg.children.forEach(child=>{
      if(child.name != "slicedBg") {
        child.destroy();
      }
    })

    this.disableScroll(); // 禁用页面滚动
    await this.initHotZoneBg();
    this.initSlicedHotZoneBg(); // 切片背景图 - 修复某些设备上背景图过长导致的黑屏
    this.initHotZoneItem();
    this.initScorePanel();
    this.enableScroll(); // 启用页面滚动
  }

  // 禁用页面滚动
  disableScroll() {
    const scorllView = cc.find("Canvas/MainContent/ScorllView").getComponent(cc.ScrollView);
    scorllView.enabled = false;
  }

  // 启用页面滚动
  enableScroll() {
    const scorllView = cc.find("Canvas/MainContent/ScorllView").getComponent(cc.ScrollView);
    scorllView.enabled = true;
  }

  // 显示视频播放页面
  hideLayerPosSaved = {};
  showVideoLayer() {
    const canvas = cc.find("Canvas");
    const hideNodeNames = ["MainContent", "Header", "Footer", "ActionBtnMaskBg", "ActionBtn", "backBtn"];
    canvas.children.forEach(nodeItem => {
      if(hideNodeNames.indexOf(nodeItem.name) != -1) {
        this.hideLayerPosSaved[nodeItem.name] = {
          x: nodeItem.x, y: nodeItem.y
        }
        nodeItem.x = canvas.width * -1;
        nodeItem.y = canvas.height * -1;
        nodeItem.opacity = 100;
      }

      if(nodeItem.name == "VideoPlayerContainer") {
        nodeItem.active = true;
        nodeItem.opacity = 255;
      }
    });
  }

  // 隐藏视频播放页面
  hideVideoLayer() {
    const canvas = cc.find("Canvas");
    const hideNodeNames = ["MainContent", "Header", "Footer", "ActionBtnMaskBg", "ActionBtn", "backBtn"];

    this.currentVideoPlay.stop();
    this.videoControlBar.emit("update_video_status", 0);
    this.currentVideoPlay = null;
    this.videoDuration = null;
    // 隐藏视频控制组件
    this.videoControlBar.emit("hide_bar", true);

    canvas.children.forEach(nodeItem => {
      if(hideNodeNames.indexOf(nodeItem.name) != -1) {
        nodeItem.x = this.hideLayerPosSaved[nodeItem.name].x;
        nodeItem.y = this.hideLayerPosSaved[nodeItem.name].y;
        nodeItem.opacity = 255;
      }

      if(nodeItem.name == "VideoPlayerContainer") {
        nodeItem.active = false;
        nodeItem.opacity = 0;
      }
    });
  }

  // 模糊匹配字符串 1,2,3
  fuzzyMatchingString(testString, matchString, options){
    const {
      isCaseInsensitive,
      keyWordMatchInOrder,
      notAdaptContraction
    } = options;

    matchString.replace(/，/g,",");

    const _keyWordGroup = matchString.split("|"); // 大的分组 任何一个组匹配了 都算对
    const keyWordGroup = []
    _keyWordGroup.forEach(item => {
      const keyWords = item.split(",")
      keyWordGroup.push(keyWords.map(item => item.trim()))
    });

    let rightInGroup = false;
    if(keyWordMatchInOrder) {
      // 关键词按序匹配
      keyWordGroup.forEach(gpKeys => {
        // 如果没有正确分组 进行查找
        if(!rightInGroup) {
          // 分组查找
          let keyIndex = -1;
          let matched = true;
          const result = gpKeys.forEach(key=>{
            // 判断当前组是否还符合匹配顺序
            if(matched) {
              let _keyIndex = -1;
              if(isCaseInsensitive) {
                // 不区分大小写
                _keyIndex = testString.toLowerCase().indexOf(key.toLowerCase())
              } else {
                // 区分大小写
                _keyIndex = testString.indexOf(key)
              }

              // 如果找到的索引 比 上一次找到的索引小 则认为不符合
              if(_keyIndex < keyIndex) {
                matched = false;
              } else {
                keyIndex = _keyIndex;
              }
            }
          })

          // 如果符合 并且索引大于0
          if(matched && keyIndex >= 0) {
            rightInGroup = true;
          }
        }
      });
    } else {
      keyWordGroup.forEach(gpKeys => {
        // 如果没有正确分组 进行查找
        if(!rightInGroup) {
          // 分组查找
          const result = gpKeys.find(key=>{
            if(isCaseInsensitive) {
              // 不区分大小写
              return testString.toLowerCase().indexOf(key.toLowerCase()) == -1
            } else {
              // 区分大小写
              return testString.indexOf(key) == -1
            }
          })
          if(result == undefined) {
            rightInGroup = true;
          }
        }
      });
    }

    return rightInGroup;
  }

  // 句子相同匹配
  checkEqualitySentence(userStr, configStr, options) {
    const {
      keyWordMatch,
      keyWordMatchInOrder,
      isCaseInsensitive,
      openAnswer,
      capitalizedFirstLetter,
      notAdaptContraction
    } = options;

    // 去掉字符串 首尾空格
    userStr = userStr.trim();
    configStr = configStr.trim();

    // 如果是开放型回答 只有有内容即可
    if(openAnswer) {
      // 检查结果默认为正确
      let right = true;
      // 如果是开放型回答 并且有首字母大写要求
      if(capitalizedFirstLetter) {
        let capitalized = /^[A-Z]/
        if(!capitalized.test(userStr)) {
          right = false
        } else {
          right = userStr != ""
        }
      } else {
        right = userStr != ""
      }
      // 直接返回结果 其他的不需要检查
      return right;
    }

    // 首字母需要大写检查 - 如果不满足，直接返回False
    // if(capitalizedFirstLetter) {
    //   let capitalized = /^[A-Z]/
    //   if(!capitalized.test(userStr)) {
    //     return false
    //   }
    // }

    // 把一些常见缩写替换统一字符串
    if(!notAdaptContraction) {
      userStr = this.adaptContraction(userStr);
      configStr = this.adaptContraction(configStr);
    }

    // 如果是大小写不敏感则统一转换成小写
    if(isCaseInsensitive) {
      userStr = userStr.toLowerCase();
      configStr = configStr.toLowerCase();
    }

    return userStr == configStr;
  }

  // 把对应缩写都转换为统一串 AC = Adapt-Contraction
  adaptContraction(str) {
    const contractionTemplate = {
      // I am
      "I am": "[@AC(Iam)]",
      "I'm": "[@AC(Iam)]",
      "I was": "[@AC(Iwas)]",

      "i am": "[@AC(iam)]",
      "i'm": "[@AC(iam)]",
      "i was": "[@AC(iwas)]",

      // You are
      "You are not": "[@AC(Youarenot)]",
      "You're not": "[@AC(Youarenot)]",
      "You aren't": "[@AC(Youarenot)]",
      "You are": "[@AC(Youare)]",
      "You're": "[@AC(Youare)]",
      "You were not": "[@AC(Youwerenot)]",
      "You weren't": "[@AC(Youwerenot)]",
      "You were": "[@AC(Youwere)]",

      "you are not": "[@AC(youarenot)]",
      "you're not": "[@AC(youarenot)]",
      "you aren't": "[@AC(youarenot)]",
      "you are": "[@AC(youare)]",
      "you're": "[@AC(youare)]",
      "you were not": "[@AC(youwerenot)]",
      "you weren't": "[@AC(youwerenot)]",
      "you were": "[@AC(youwere)]",

      // He is
      "He is not": "[@AC(Heisnot)]",
      "He's not": "[@AC(Heisnot)]",
      "He isn't": "[@AC(Heisnot)]",
      "He is": "[@AC(Heis)]",
      "He's": "[@AC(Heis)]",
      "He was not": "[@AC(Hewasnot)]",
      "He wasn't": "[@AC(Hewasnot)]",
      "He was": "[@AC(Hewas)]",

      "he is not": "[@AC(heisnot)]",
      "he's not": "[@AC(heisnot)]",
      "he isn't": "[@AC(heisnot)]",
      "he is": "[@AC(heis)]",
      "he's": "[@AC(heis)]",
      "he was not": "[@AC(hewasnot)]",
      "he wasn't": "[@AC(hewasnot)]",
      "he was": "[@AC(hewas)]",

      // She is
      "She is not": "[@AC(Sheisnot)]",
      "She's not": "[@AC(Sheisnot)]",
      "She isn't": "[@AC(Sheisnot)]",
      "She is": "[@AC(Sheis)]",
      "She's": "[@AC(Sheis)]",
      "She was not": "[@AC(Shewasnot)]",
      "She wasn't": "[@AC(Shewasnot)]",
      "She was": "[@AC(Shewas)]",

      "she is not": "[@AC(sheisnot)]",
      "she's not": "[@AC(sheisnot)]",
      "she isn't": "[@AC(sheisnot)]",
      "she is": "[@AC(sheis)]",
      "she's": "[@AC(sheis)]",
      "she was not": "[@AC(shewasnot)]",
      "she wasn't": "[@AC(shewasnot)]",
      "she was": "[@AC(shewas)]",

      // It is
      "It is not": "[@AC(Itisnot)]",
      "It's not": "[@AC(Itisnot)]",
      "It isn't": "[@AC(Itisnot)]",
      "It is": "[@AC(Itis)]",
      "It's": "[@AC(Itis)]",
      "It was not": "[@AC(Itwasnot)]",
      "It wasn't": "[@AC(Itwasnot)]",
      "It was": "[@AC(Itwas)]",
      
      "it is not": "[@AC(itisnot)]",
      "it's not": "[@AC(itisnot)]",
      "it isn't": "[@AC(itisnot)]",
      "it is": "[@AC(itis)]",
      "it's": "[@AC(itis)]",
      "it was not": "[@AC(itwasnot)]",
      "it wasn't": "[@AC(itwasnot)]",
      "it was": "[@AC(itwas)]",
      
      // They are
      "They are not": "[@AC(Theyarenot)]",
      "They're not": "[@AC(Theyarenot)]",
      "They aren't": "[@AC(Theyarenot)]",
      "They are": "[@AC(Theyare)]",
      "They're": "[@AC(Theyare)]",
      "They were not": "[@AC(Theywerenot)]",
      "They weren't": "[@AC(Theywerenot)]",
      "They were": "[@AC(Theywere)]",

      "they are not": "[@AC(theyarenot)]",
      "they're not": "[@AC(theyarenot)]",
      "they aren't": "[@AC(theyarenot)]",
      "they are": "[@AC(theyare)]",
      "they're": "[@AC(theyare)]",
      "they were not": "[@AC(theywerenot)]",
      "they weren't": "[@AC(theywerenot)]",
      "they were": "[@AC(theywere)]",
      
      // is not
      "Is not": "[@AC(Isnot)]",
      "Isn't": "[@AC(Isnot)]",
      "is not": "[@AC(isnot)]",
      "isn't": "[@AC(isnot)]",
      "Are not": "[@AC(Arenot)]",
      "Aren't": "[@AC(Arenot)]",
      "are not": "[@AC(arenot)]",
      "aren't": "[@AC(arenot)]",
      "Was not": "[@AC(Wasnot)]",
      "Wasn't": "[@AC(Wasnot)]",
      "was not": "[@AC(wasnot)]",
      "wasn't": "[@AC(wasnot)]",
      "Were not": "[@AC(Werenot)]",
      "Weren't": "[@AC(Werenot)]",
      "were not": "[@AC(werenot)]",
      "weren't": "[@AC(werenot)]",

      // Do
      "do not": "[@AC(donot)]",
      "don't": "[@AC(donot)]",
      "does not": "[@AC(doesnot)]",
      "doesn't": "[@AC(doesnot)]",
      "did not": "[@AC(didnot)]",
      "didn't": "[@AC(didnot)]",

      // Where What Who How Why When
      "Where is": "[@AC(Whereis)]",
      "Where's": "[@AC(Whereis)]",
      "where is": "[@AC(whereis)]",
      "where's": "[@AC(whereis)]",
      "What is": "[@AC(Whatis)]",
      "What's": "[@AC(Whatis)]",
      "what is": "[@AC(whatis)]",
      "what's": "[@AC(whatis)]",
      "Who is": "[@AC(Whois)]",
      "Who's": "[@AC(Whois)]",
      "who is": "[@AC(whois)]",
      "who's": "[@AC(whois)]",
      "How is": "[@AC(Howis)]",
      "How's": "[@AC(Howis)]",
      "how is": "[@AC(howis)]",
      "how's": "[@AC(howis)]",
      "Why is": "[@AC(Whyis)]",
      "Why's": "[@AC(Whyis)]",
      "why is": "[@AC(whyis)]",
      "why's": "[@AC(whyis)]",
      "When is": "[@AC(Whenis)]",
      "When's": "[@AC(Whenis)]",
      "when is": "[@AC(whenis)]",
      "when's": "[@AC(whenis)]",

      "Where are": "[@AC(Whereare)]",
      "Where're": "[@AC(Whereare)]",
      "where are": "[@AC(whereare)]",
      "where're": "[@AC(whereare)]",
      "What are": "[@AC(Whatare)]",
      "What're": "[@AC(Whatare)]",
      "what are": "[@AC(whatare)]",
      "what're": "[@AC(whatare)]",
      "Who are": "[@AC(Whoare)]",
      "Who're": "[@AC(Whoare)]",
      "who are": "[@AC(whoare)]",
      "who're": "[@AC(whoare)]",
      "How are": "[@AC(Howare)]",
      "How're": "[@AC(Howare)]",
      "how are": "[@AC(howare)]",
      "how're": "[@AC(howare)]",
      "Why are": "[@AC(Whyare)]",
      "Why're": "[@AC(Whyare)]",
      "why are": "[@AC(whyare)]",
      "why're": "[@AC(whyare)]",
      "When are": "[@AC(Whenare)]",
      "When're": "[@AC(Whenare)]",
      "when are": "[@AC(whenare)]",
      "when're": "[@AC(whenare)]",

      // thoes these this that there
      "Those are": "[@AC(Thoseare)]",
      "Those're": "[@AC(Thoseare)]",
      "those are": "[@AC(thoseare)]",
      "those're": "[@AC(thoseare)]",
      "These are": "[@AC(Theseare)]",
      "These're": "[@AC(Theseare)]",
      "these are": "[@AC(theseare)]",
      "these're": "[@AC(theseare)]",
      "This is": "[@AC(Thisis)]",
      "This's": "[@AC(Thisis)]",
      "this is": "[@AC(thisis)]",
      "this's": "[@AC(thisis)]",
      "That is": "[@AC(Thatis)]",
      "That's": "[@AC(Thatis)]",
      "that is": "[@AC(thatis)]",
      "that's": "[@AC(thatis)]",
      "There is": "[@AC(Thereis)]",
      "There's": "[@AC(Thereis)]",
      "there is": "[@AC(thereis)]",
      "there's": "[@AC(thereis)]",
      "There are": "[@AC(Thereare)]",
      "There're": "[@AC(Thereare)]",
      "there are": "[@AC(thereare)]",
      "there're": "[@AC(thereare)]",

      // can
      "Cannot": "[@AC(Cannot)]",
      "Can't": "[@AC(Cannot)]",
      "Can not": "[@AC(Cannot)]",
      "cannot": "[@AC(cannot)]",
      "can't": "[@AC(cannot)]",
      "can not": "[@AC(cannot)]",

      // have not, has not
      "Has not": "[@AC(Hasnot)]",
      "Hasn't": "[@AC(Hasnot)]",
      "has not": "[@AC(hasnot)]",
      "hasn't": "[@AC(hasnot)]",
      "Have not": "[@AC(Havenot)]",
      "Haven't": "[@AC(Havenot)]",
      "have not": "[@AC(havenot)]",
      "haven't": "[@AC(havenot)]",
      "Should not": "[@AC(Shouldnot)]",
      "Shouldn't": "[@AC(Shouldnot)]",
      "should not": "[@AC(shouldnot)]",
      "shouldn't": "[@AC(shouldnot)]",

      // will not
      "Will not": "[@AC(Willnot)]",
      "Won't": "[@AC(Willnot)]",
      "will not": "[@AC(willnot)]",
      "won't": "[@AC(willnot)]",
      "I will": "[@AC(Iwill)]",
      "I'll": "[@AC(Iwill)]",
      "i will": "[@AC(iwill)]",
      "i'll": "[@AC(iwill)]",
      "You will": "[@AC(Youwill)]",
      "You'll": "[@AC(Youwill)]",
      "you will": "[@AC(youwill)]",
      "you'll": "[@AC(youwill)]",
      "It will": "[@AC(Itwill)]",
      "It'll": "[@AC(Itwill)]",
      "it will": "[@AC(itwill)]",
      "it'll": "[@AC(itwill)]",
      "She will": "[@AC(Shewill)]",
      "She'll": "[@AC(Shewill)]",
      "she will": "[@AC(shewill)]",
      "she'll": "[@AC(shewill)]",
      "He will": "[@AC(Hewill)]",
      "He'll": "[@AC(Hewill)]",
      "he will": "[@AC(hewill)]",
      "he'll": "[@AC(hewill)]",
      "They will": "[@AC(Theywill)]",
      "They'll": "[@AC(Theywill)]",
      "they will": "[@AC(theywill)]",
      "they'll": "[@AC(theywill)]",
      "Who will": "[@AC(Whowill)]",
      "Who'll": "[@AC(Whowill)]",
      "who will": "[@AC(whowill)]",
      "who'll": "[@AC(whowill)]",

      // would
      "I would": "[@AC(Iwould)]",
      "I'd": "[@AC(Iwould)]",
      "i would": "[@AC(iwould)]",
      "i'd": "[@AC(iwould)]",
      "You would": "[@AC(Youwould)]",
      "You'd": "[@AC(Youwould)]",
      "you would": "[@AC(youwould)]",
      "you'd": "[@AC(youwould)]",
      "It would": "[@AC(Itwould)]",
      "It'd": "[@AC(Itwould)]",
      "it would": "[@AC(itwould)]",
      "it'd": "[@AC(itwould)]",
      "She would": "[@AC(Shewould)]",
      "She'd": "[@AC(Shewould)]",
      "she would": "[@AC(shewould)]",
      "she'd": "[@AC(shewould)]",
      "He would": "[@AC(Hewould)]",
      "He'd": "[@AC(Hewould)]",
      "he would": "[@AC(hewould)]",
      "he'd": "[@AC(hewould)]",
      "They would": "[@AC(Theywould)]",
      "They'd": "[@AC(Theywould)]",
      "they would": "[@AC(theywould)]",
      "they'd": "[@AC(theywould)]",

      // let everyone name must
      "Let us": "[@AC(Letus)]",
      "Let's": "[@AC(Letus)]",
      "let us": "[@AC(letus)]",
      "let's": "[@AC(letus)]",
      "Everyone is": "[@AC(Everyoneis)]",
      "Everyone's": "[@AC(Everyoneis)]",
      "everyone is": "[@AC(everyoneis)]",
      "everyone's": "[@AC(everyoneis)]",
      "Name is": "[@AC(Nameis)]",
      "Name's": "[@AC(Nameis)]",
      "name is": "[@AC(nameis)]",
      "name's": "[@AC(nameis)]",
      "Must not": "[@AC(Mustnot)]",
      "Mustn't": "[@AC(Mustnot)]",
      "must not": "[@AC(mustnot)]",
      "mustn't": "[@AC(mustnot)]",

    }

    for(let contraction in contractionTemplate) {
      str = str.replace(new RegExp(contraction, "g"), contractionTemplate[contraction])
    }

    return str;
  }

  // 同步方式显示选择模态框
  asyncShowSelectModal(option, type) {
    return new Promise((resovle, reject) => {
      this.selectModal.emit("show", option, type);
      this.onSelectedCallback = (target, e) => {
        if(e == undefined) {
          // 点击了取消
          resovle(null)
        } else {
          resovle(e);
        }
      }
    })
  }

  // 同步方式显示选择模态框(多选)
  asyncShowMultipleSelectModal(option, type) {
    return new Promise((resovle, reject) => {
      this.multipleSelectModal.emit("show", option, type);
      this.onMultipleSelectedCallback = (target, e) => {
        if(e == undefined) {
          // 点击了取消
          resovle(null)
        } else {
          resovle(e);
        }
      }
    })
  }

  // 同步方式显示输入模态框
  asyncShowKeyboardModal(defaultText) {
    return new Promise((resovle, reject) => {
      this.keyboard.emit("show", defaultText);
      this.onKeyboardEnterCallback = (target, e) => {
        resovle(e);
      }
    })
  }

  // 同步方式显示单词排序框
  asyncShowSortWordsModal(options) {
    return new Promise((resovle, reject) => {
      this.sortWords.emit("show", options);
      this.onSortWordsEndCallback = (target, e) => {
        resovle(e);
      }
    })
  }

  // 同步方式显示语音评测模态框
  asyncShowPAModal(defaultText, audio_url, evaText) {
    return new Promise((resovle, reject) => {
      this.pronunciationAssessmentModal.emit("show", {
        recordUrl: audio_url,
        recordText: defaultText,
        evaText: evaText,
      });
      this.onPronunciationAssessmentCallback = (target, e) => {
        resovle(e);
      }
    })
  }

  onDestroy() {
    this.isDestroy = true;
    cc.audioEngine.stop(this.currentAudioPlay);
  }

  update() {
    if (this.isDestroy) {
      return;
    }
    
    if(this.currentVideoPlay != null) {
      // 更新当前进度条
      if(this.videoDuration== null) {
        this.videoControlBar.emit("update_video_progress", 0)
      } else {
        this.videoControlBar.emit("update_video_progress", this.currentVideoPlay.currentTime)
      }
    }
  }
}
