Kaynağa Gözat

汉字基础展示预览

natasha 1 yıl önce
ebeveyn
işleme
85ceab2a50

Dosya farkı çok büyük olduğundan ihmal edildi
+ 401 - 831
package-lock.json


+ 1 - 0
package.json

@@ -24,6 +24,7 @@
     "core-js": "^3.37.1",
     "dompurify": "^3.1.5",
     "element-ui": "^2.15.14",
+    "hanzi-writer": "^3.7.0",
     "js-audio-recorder": "^1.0.7",
     "js-cookie": "^3.0.5",
     "md5": "^2.3.0",

BIN
src/assets/voice-play-gray.png


BIN
src/assets/voice-play-white.png


+ 3 - 0
src/icons/svg/audio-stop.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1 0H13C13.5523 0 14 0.44772 14 1V13C14 13.5523 13.5523 14 13 14H1C0.44772 14 0 13.5523 0 13V1C0 0.44772 0.44772 0 1 0Z" fill="white"/>
+</svg>

+ 10 - 0
src/icons/svg/audio.svg

@@ -0,0 +1,10 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_955_16891)">
+<path d="M5.50251 8.33366L8.3335 6.01738V13.9832L5.50251 11.667H2.50016V8.33366H5.50251ZM1.66683 13.3337H4.90757L9.31966 16.9435C9.39408 17.0044 9.48733 17.0377 9.5835 17.0377C9.81358 17.0377 10.0002 16.8512 10.0002 16.621V3.37957C10.0002 3.28339 9.96691 3.19016 9.906 3.11572C9.76025 2.93762 9.49775 2.91137 9.31966 3.05709L4.90757 6.66697H1.66683C1.2066 6.66697 0.833496 7.04006 0.833496 7.5003V12.5003C0.833496 12.9606 1.2066 13.3337 1.66683 13.3337ZM19.1668 10.0002C19.1668 12.7436 17.9617 15.2055 16.052 16.8854L14.8706 15.7039C16.48 14.3283 17.5002 12.2834 17.5002 10.0002C17.5002 7.71704 16.48 5.67215 14.8706 4.29655L16.052 3.11507C17.9617 4.79498 19.1668 7.25687 19.1668 10.0002ZM15.0002 10.0002C15.0002 8.40716 14.2552 6.98814 13.0946 6.07252L11.9037 7.26344C12.7679 7.8657 13.3335 8.86691 13.3335 10.0002C13.3335 11.1336 12.7679 12.1347 11.9037 12.737L13.0946 13.9279C14.2552 13.0123 15.0002 11.5932 15.0002 10.0002Z" fill="currentColor"/>
+</g>
+<defs>
+<clipPath id="clip0_955_16891">
+<rect width="20" height="20" fill="currentColor"/>
+</clipPath>
+</defs>
+</svg>

+ 6 - 0
src/icons/svg/hanzi-strock-play.svg

@@ -0,0 +1,6 @@
+<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Group 4124">
+<path id="Rectangle 7" d="M0 0H11V11C5.50879 9.77973 1.22027 5.49121 0 0Z" fill="currentColor"/>
+<path id="play.fill" d="M6.26786 6C6.34821 6 6.41901 5.96579 6.50702 5.90422L8.76658 4.34664C8.93112 4.23261 9 4.14367 9 4C9 3.85633 8.93112 3.76967 8.76658 3.65336L6.50702 2.09578C6.41901 2.03421 6.34821 2 6.26786 2C6.11097 2 6 2.14367 6 2.36944V5.63056C6 5.85861 6.11097 6 6.26786 6Z" fill="white"/>
+</g>
+</svg>

+ 33 - 0
src/icons/svg/hanzi-writer-bg.svg

@@ -0,0 +1,33 @@
+<svg width="69" height="69" viewBox="0 0 69 69" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2206_19367)">
+<rect y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="6" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="12" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="18" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="24" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="30" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="36" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="42" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="48" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="54" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="60" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="66" y="34" width="3" height="1" rx="0.5" fill="currentColor"/>
+<rect x="35" width="3" height="1" rx="0.5" transform="rotate(90 35 0)" fill="currentColor"/>
+<rect x="35" y="6" width="3" height="1" rx="0.5" transform="rotate(90 35 6)" fill="currentColor"/>
+<rect x="35" y="12" width="3" height="1" rx="0.5" transform="rotate(90 35 12)" fill="currentColor"/>
+<rect x="35" y="18" width="3" height="1" rx="0.5" transform="rotate(90 35 18)" fill="currentColor"/>
+<rect x="35" y="24" width="3" height="1" rx="0.5" transform="rotate(90 35 24)" fill="currentColor"/>
+<rect x="35" y="30" width="3" height="1" rx="0.5" transform="rotate(90 35 30)" fill="currentColor"/>
+<rect x="35" y="36" width="3" height="1" rx="0.5" transform="rotate(90 35 36)" fill="currentColor"/>
+<rect x="35" y="42" width="3" height="1" rx="0.5" transform="rotate(90 35 42)" fill="currentColor"/>
+<rect x="35" y="48" width="3" height="1" rx="0.5" transform="rotate(90 35 48)" fill="currentColor"/>
+<rect x="35" y="54" width="3" height="1" rx="0.5" transform="rotate(90 35 54)" fill="currentColor"/>
+<rect x="35" y="60" width="3" height="1" rx="0.5" transform="rotate(90 35 60)" fill="currentColor"/>
+<rect x="35" y="66" width="3" height="1" rx="0.5" transform="rotate(90 35 66)" fill="currentColor"/>
+</g>
+<defs>
+<clipPath id="clip0_2206_19367">
+<rect width="69" height="69" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 4 - 4
src/views/book/courseware/create/components/base/character_base/CharacterBaseSetting.vue

@@ -57,15 +57,15 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label="笔迹回放">
-          <el-radio-group v-model="property.is_enable_play_back">
+        <el-form-item label="错误提示" v-if="isEnable(property.is_enable_miao)">
+          <el-radio-group v-model="property.is_enable_error">
             <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value" :value="value">
               {{ label }}
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label="错误提示">
-          <el-radio-group v-model="property.is_enable_error">
+        <el-form-item label="笔迹回放" v-else>
+          <el-radio-group v-model="property.is_enable_play_back">
             <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value" :value="value">
               {{ label }}
             </el-radio>

+ 1 - 0
src/views/book/courseware/data/characterBase.js

@@ -103,6 +103,7 @@ export function getCharacterBaseData() {
     audio_file_id: '',
     hz_strokes_list: [],
     mark: getRandomNumber(),
+    record_list: [],
     answer: {
       answer_list: [],
     },

+ 92 - 91
src/views/book/courseware/preview/components/character_base/CharacterBasePreview.vue

@@ -1,28 +1,54 @@
 <!-- eslint-disable vue/no-v-html -->
 <template>
-  <div class="select-preview" :style="getAreaStyle()">
+  <div class="character-preview" :style="getAreaStyle()">
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
 
-    <div class="main" :style="getMainStyle()">预览开发中</div>
+    <div class="main">
+      <div class="main-top" :style="{ width: data.hz_strokes_list.length * 64 + 'px' }">
+        <AudioPlay v-if="isEnable(data.property.is_enable_voice)" :file-id="data.audio_file_id" theme-color="gray" />
+        <span class="pinyin" v-if="isEnable(data.property.is_enable_pinyin)">{{ data.pinyin }}</span>
+      </div>
+      <div class="strock-chinese-box">
+        <Strockplayredline
+          v-for="(items, indexs) in data.hz_strokes_list"
+          :key="indexs"
+          :play-storkes="true"
+          :book-text="data.content"
+          :target-div="'pre' + data.content + indexs"
+          :book-strokes="items.strokes"
+          :class="['strock-chinese', indexs !== data.hz_strokes_list.length - 1 ? 'border-right-none' : '']"
+          :bg-type="data.property.frame_type"
+          :frame-color="data.property.frame_color"
+        />
+      </div>
+      <p class="words-right" v-if="data.definition">{{ data.definition }}</p>
+      <template v-if="isEnable(data.property.is_enable_voice_answer)">
+        <SoundRecord
+          ref="record"
+          type="normal"
+          class="record-box"
+          :answer-record-list="data.record_list"
+          :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
+          @handleWav="handleWav"
+        />
+      </template>
+    </div>
   </div>
 </template>
 
 <script>
-import {
-  getCharacterBaseData,
-  fillFontList,
-  arrangeTypeList,
-  audioPositionList,
-} from '@/views/book/courseware/data/characterBase';
+import { getCharacterBaseData } from '@/views/book/courseware/data/characterBase';
 
 import PreviewMixin from '../common/PreviewMixin';
-import AudioFill from '../fill/components/AudioFillPlay.vue';
+import AudioPlay from './components/AudioPlay.vue';
+import Strockplayredline from './components/Strockplayredline.vue';
 import SoundRecord from '../../common/SoundRecord.vue';
 
 export default {
   name: 'CharacterBasePreview',
   components: {
-    AudioFill,
+    AudioPlay,
+    Strockplayredline,
     SoundRecord,
   },
   mixins: [PreviewMixin],
@@ -31,49 +57,18 @@ export default {
       data: getCharacterBaseData(),
     };
   },
-  computed: {
-    fontFamily() {
-      return fillFontList.find(({ value }) => this.data.property.fill_font === value).font;
+  watch: {
+    'data.record_list'(val) {
+      this.data.record_list = val;
     },
   },
+  computed: {},
   created() {
-    this.answer.answer_list = this.data.model_essay
-      .map((item) => {
-        return item
-          .map(({ type, content, mark }) => {
-            if (type === 'input') {
-              return {
-                value: content,
-                mark,
-              };
-            }
-          })
-          .filter((item) => item);
-      })
-      .flat();
+    console.log(this.data);
   },
   methods: {
-    getMainStyle() {
-      const isRow = this.data.property.arrange_type === arrangeTypeList[0].value;
-      const isFront = this.data.property.audio_position === audioPositionList[0].value;
-      const isEnableVoice = this.data.property.is_enable_voice_answer === 'true';
-      let _list = [
-        { name: 'audio', value: '24px' },
-        { name: 'fill', value: '1fr' },
-      ];
-      if (!isFront) {
-        _list = _list.reverse();
-      }
-      let grid = isRow
-        ? `"${_list[0].name} ${_list[1].name}${isEnableVoice ? ' record' : ''}" auto / ${_list[0].value} ${_list[1].value}${isEnableVoice ? ' 160px' : ''}`
-        : `"${_list[0].name}" ${_list[0].value} "${_list[1].name}" ${_list[1].value}${isEnableVoice ? `" record" 32px ` : ''} / 1fr`;
-      let style = {
-        'grid-auto-flow': isRow ? 'column' : 'row',
-        'column-gap': isRow ? '16px' : undefined,
-        'row-gap': isRow ? undefined : '8px',
-        grid,
-      };
-      return style;
+    handleWav(data) {
+      this.data.record_list = data;
     },
   },
 };
@@ -82,60 +77,66 @@ export default {
 <style lang="scss" scoped>
 @use '@/styles/mixin.scss' as *;
 
-.select-preview {
+.character-preview {
   @include preview-base;
 
-  .main {
-    display: grid;
-    align-items: center;
-  }
+  .audio-wrapper-nobg {
+    height: 16px;
 
-  .fill-wrapper {
-    grid-area: fill;
-    font-size: 16pt;
+    :deep .audio-play {
+      width: 16px;
+      height: 16px;
+      color: #000;
+      background-color: initial;
+    }
 
-    p {
-      margin: 0;
+    :deep .audio-play.not-url {
+      color: #a1a1a1;
     }
 
-    .el-input {
-      display: inline-flex;
-      align-items: center;
-      width: 120px;
-      margin: 0 2px;
-
-      &.pinyin :deep input.el-input__inner {
-        font-family: 'PINYIN-B', sans-serif;
-      }
-
-      &.chinese :deep input.el-input__inner {
-        font-family: 'arial', sans-serif;
-      }
-
-      &.english :deep input.el-input__inner {
-        font-family: 'arial', sans-serif;
-      }
-
-      :deep input.el-input__inner {
-        padding: 0;
-        font-size: 16pt;
-        color: $font-color;
-        text-align: center;
-        background-color: #fff;
-        border-width: 0;
-        border-bottom: 1px solid $font-color;
-        border-radius: 0;
-      }
+    :deep .voice-play {
+      width: 16px;
+      height: 16px;
     }
   }
 
+  .main-top {
+    display: flex;
+    column-gap: 4px;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .pinyin {
+    font-family: 'League';
+    font-size: 12px;
+    font-weight: 500;
+    color: #000;
+  }
+
+  .words-right {
+    padding: 8px 4px;
+    margin: 0;
+    font-size: 14px;
+    line-height: 14px;
+    color: #000;
+  }
+
+  .strock-chinese-box {
+    display: flex;
+    flex-wrap: wrap;
+    margin: 8px 0;
+  }
+
   .record-box {
-    padding: 6px 12px;
-    background-color: $fill-color;
+    padding: 7px 12px;
+    margin-top: 8px;
+    background: #f2f3f5;
+    border-radius: 2px;
+  }
 
-    :deep .record-time {
-      width: 100px;
-    }
+  .border-right-none {
+    border-right: none;
   }
 }
 </style>

+ 246 - 0
src/views/book/courseware/preview/components/character_base/components/AudioPlay.vue

@@ -0,0 +1,246 @@
+<template>
+  <div class="audio-wrapper-nobg">
+    <span
+      :class="[url ? 'audio-play' : 'audio-play not-url']"
+      :style="{
+        padding: showSlider ? '4px 16px' : '',
+        width: showSlider ? '230px' : '',
+        justifyContent: showSlider && !showProgress ? 'space-between' : 'center',
+        backgroundColor: backgroundColor,
+      }"
+      @click="playAudio"
+    >
+      <SvgIcon v-if="audio.paused" :size="audio.paused ? 20 : 14" :icon-class="iconClass" />
+      <img
+        v-else
+        :src="
+          themeColor === 'gray' ? require('@/assets/voice-play-gray.png') : require('@/assets/voice-play-white.png')
+        "
+        class="voice-play"
+      />
+
+      <template v-if="showSlider">
+        <template v-if="showProgress">
+          <span class="audio-time">{{ secondFormatConversion(audio.current_time) }}</span>
+          <el-slider
+            v-model="play_value"
+            class="audio-slider"
+            :format-tooltip="formatProcessToolTip"
+            @change="changeCurrentTime"
+          />
+        </template>
+        <span class="audio-time">{{ audio_allTime }}</span>
+      </template>
+    </span>
+
+    <audio
+      :id="fileId"
+      :ref="fileId"
+      :src="url"
+      preload="metadata"
+      @loadedmetadata="onLoadedmetadata"
+      @timeupdate="onTimeupdate"
+      @canplaythrough="oncanplaythrough"
+    ></audio>
+  </div>
+</template>
+
+<script>
+import { GetFileURLMap } from '@/api/app';
+import { secondFormatConversion } from '@/utils/transform';
+
+export default {
+  name: 'AudioPlay',
+  props: {
+    fileId: {
+      type: String,
+      required: true,
+    },
+    themeColor: {
+      type: String,
+      default: '',
+    },
+    showSlider: {
+      type: Boolean,
+      default: false,
+    },
+    // 是否显示音频进度条
+    showProgress: {
+      type: Boolean,
+      default: true,
+    },
+    // 播放背景色
+    backgroundColor: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      secondFormatConversion,
+      url: '',
+      audio: {
+        paused: true,
+        playing: false,
+        // 音频当前播放时长
+        current_time: 0,
+        // 音频最大播放时长
+        max_time: 0,
+        isPlaying: false,
+        loading: false,
+      },
+      play_value: 0,
+      audio_allTime: null, // 展示总时间
+    };
+  },
+  computed: {
+    iconClass() {
+      return this.audio.paused ? 'audio' : 'audio-stop';
+    },
+  },
+  watch: {
+    fileId: {
+      handler(val) {
+        if (!val) return;
+        GetFileURLMap({ file_id_list: [val] }).then(({ url_map }) => {
+          this.url = url_map[val];
+        });
+      },
+      immediate: true,
+    },
+  },
+  mounted() {
+    if (!this.fileId) return;
+    this.$refs[this.fileId].addEventListener('ended', () => {
+      this.audio.paused = true;
+    });
+    this.$refs[this.fileId].addEventListener('pause', () => {
+      this.audio.paused = true;
+    });
+    this.$refs[this.fileId].addEventListener('play', () => {
+      this.audio.paused = false;
+    });
+  },
+  methods: {
+    playAudio() {
+      if (!this.url) return;
+      const audio = this.$refs[this.fileId];
+      let audioArr = document.getElementsByTagName('audio');
+      if (audioArr && audioArr.length > 0) {
+        for (let i = 0; i < audioArr.length; i++) {
+          if (audioArr[i].src === this.url) {
+            if (audioArr[i].id !== this.fileId) {
+              audioArr[i].pause();
+            }
+          } else {
+            audioArr[i].pause();
+          }
+        }
+      }
+      audio.paused ? audio.play() : audio.pause();
+    },
+    // 进度条格式化toolTip
+    formatProcessToolTip(index) {
+      let indexs = parseInt((this.audio.max_time / 100) * index);
+      return secondFormatConversion(indexs);
+    },
+    // 点击 拖拽播放音频
+    changeCurrentTime(value) {
+      let audioId = this.fileId;
+      this.$refs[audioId].play();
+      this.audio.playing = true;
+      this.$refs[audioId].currentTime = parseInt((value / 100) * this.audio.max_time);
+    },
+    // 音频加载完之后
+    onLoadedmetadata(res) {
+      this.audio.max_time = parseInt(res.target.duration);
+      this.audio_allTime = secondFormatConversion(this.audio.max_time);
+    },
+    // 当音频当前时间改变后,进度条也要改变
+    onTimeupdate(res) {
+      let audioId = this.fileId;
+      this.audio.current_time = res.target.currentTime;
+      this.play_value = (this.audio.current_time / this.audio.max_time) * 100;
+      if (this.audio.current_time * 1000 > this.ed) {
+        this.$refs[audioId].pause();
+      }
+    },
+    onTimeupdateTime(res, playFlag) {
+      if (!res && res !== 0) return;
+      let audioId = this.audioId;
+      this.$refs[audioId].currentTime = res;
+      this.play_value = (res / this.audio.max_time) * 100;
+      if (playFlag) {
+        let audio = document.getElementsByTagName('audio');
+        audio.forEach((item) => {
+          if (item.id !== audioId) {
+            item.pause();
+          }
+        });
+        this.$refs[audioId].play();
+      }
+    },
+    oncanplaythrough() {
+      this.audio.loading = false;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.audio-wrapper-nobg {
+  .audio-play {
+    display: flex;
+    column-gap: 8px;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    color: #fff;
+    cursor: pointer;
+    background-color: $main-color;
+    border-radius: 20px;
+
+    &.not-url {
+      color: #a1a1a1;
+      cursor: not-allowed;
+    }
+
+    .voice-play {
+      width: 20px;
+      height: 20px;
+    }
+
+    .audio-time {
+      min-width: 35px;
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 20px;
+    }
+
+    .audio-slider {
+      width: 76px;
+
+      :deep .el-slider__runway {
+        height: 4px;
+        background-color: #1853c6;
+      }
+
+      :deep .el-slider__button {
+        width: 4px;
+        height: 4px;
+        border: none;
+      }
+
+      :deep .el-slider__bar {
+        height: 4px;
+        background-color: #fff;
+      }
+
+      :deep .el-slider__button-wrapper {
+        top: -16px;
+      }
+    }
+  }
+}
+</style>

+ 261 - 0
src/views/book/courseware/preview/components/character_base/components/FreeWriteQP.vue

@@ -0,0 +1,261 @@
+<template>
+  <canvas id="panel1" ref="canvas"></canvas>
+</template>
+
+<script>
+export default {
+  props: {
+    width: {
+      type: Number,
+      default: 800,
+    },
+    height: {
+      type: Number,
+      default: 300,
+    },
+    lineWidth: {
+      type: Number,
+      default: 4,
+    },
+    lineColor: {
+      type: String,
+      default: '#000000',
+    },
+    bgColor: {
+      type: String,
+      default: '',
+    },
+    isCrop: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      hasDrew: false,
+      resultImg: '',
+      canvasTxt: null,
+      canvas: null,
+      redrawCanvas: null,
+      isMouseDown: false,
+      lastLoc: { x: 0, y: 0 },
+      history: [],
+      sratio: 1,
+    };
+  },
+  computed: {
+    ratio() {
+      return this.height / this.width;
+    },
+    stageInfo() {
+      return this.$refs.canvas.getBoundingClientRect();
+    },
+    myBg() {
+      return this.bgColor ? this.bgColor : 'rgba(255, 255, 255, 0)';
+    },
+    context() {
+      return this.$refs.canvas.getContext('2d');
+    },
+    redrawCxt() {
+      return this.$refs.canvas.getContext('2d');
+    },
+  },
+  watch: {
+    myBg(newVal) {
+      this.$refs.canvas.style.background = newVal;
+    },
+  },
+  beforeMount() {
+    window.addEventListener('resize', this.$_resizeHandler);
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.$_resizeHandler);
+  },
+  mounted() {
+    const canvas = this.$refs.canvas;
+    canvas.height = this.height;
+    canvas.width = this.width;
+    canvas.style.background = this.myBg;
+    this.$_resizeHandler();
+    // 在画板以外松开鼠标后冻结画笔
+    document.onmouseup = () => {
+      this.isMouseDown = false;
+    };
+    this.canvas = canvas;
+    this.redrawCanvas = canvas;
+    this.init();
+  },
+  methods: {
+    reload() {
+      this.reset();
+      this.redraw(this.redrawCxt);
+    },
+    init() {
+      this.canvas.onmousedown = (e) => {
+        this.isMouseDown = true;
+        this.hasDrew = true;
+        this.lastLoc = this.window2Canvas(e.clientX, e.clientY);
+      };
+      this.canvas.ontouchstart = (e) => {
+        this.isMouseDown = true;
+        this.hasDrew = true;
+        this.lastLoc = this.window2Canvas(e.touches[0].clientX, e.touches[0].clientY);
+      };
+      this.canvas.onmouseout = () => {
+        this.isMouseDown = false;
+      };
+      this.canvas.onmousemove = (e) => {
+        if (this.isMouseDown) {
+          let curLoc = this.window2Canvas(e.clientX, e.clientY); // 获得当前坐标
+          this.draw(this.context, this.lastLoc, curLoc);
+          this.history.push([this.lastLoc, curLoc]);
+          this.lastLoc = Object.assign({}, curLoc); // U know what I mean.
+        }
+      };
+      this.canvas.ontouchmove = (e) => {
+        if (this.isMouseDown) {
+          let curLoc = this.window2Canvas(e.touches[0].clientX, e.touches[0].clientY); // 获得当前坐标
+          this.draw(this.context, this.lastLoc, curLoc);
+          this.history.push([this.lastLoc, curLoc]);
+          this.lastLoc = Object.assign({}, curLoc); // U know what I mean.
+        }
+      };
+      this.canvas.onmouseup = () => {
+        this.isMouseDown = false;
+        if (history.length) {
+          localStorage.setItem('history', JSON.stringify(this.history));
+        }
+      };
+      this.canvas.onTouchend = () => {
+        this.isMouseDown = false;
+        if (history.length) {
+          localStorage.setItem('history', JSON.stringify(this.history));
+        }
+      };
+    },
+    window2Canvas(x, y) {
+      let bbox = this.canvas.getBoundingClientRect();
+      return { x: Math.round(x - bbox.left), y: Math.round(y - bbox.top) };
+    },
+    draw(context, lastLoc, curLoc) {
+      if (context) {
+        context.lineWidth = 5;
+        context.beginPath();
+        context.moveTo(lastLoc.x, lastLoc.y);
+        context.lineTo(curLoc.x, curLoc.y);
+        context.strokeStyle = '#000';
+        context.lineCap = 'round';
+        context.lineJoin = 'round';
+        context.stroke();
+      } else {
+        this.redrawCxt.lineWidth = 5;
+        this.redrawCxt.beginPath();
+        this.redrawCxt.moveTo(lastLoc.x, lastLoc.y);
+        this.redrawCxt.lineTo(curLoc.x, curLoc.y);
+        this.redrawCxt.strokeStyle = '#000';
+        this.redrawCxt.lineCap = 'round';
+        this.redrawCxt.lineJoin = 'round';
+        this.redrawCxt.stroke();
+      }
+    },
+    redraw(context) {
+      if (localStorage.getItem('history')) {
+        let history = JSON.parse(localStorage.getItem('history'));
+        const len = history.length;
+        let i = 0;
+        const runner = () => {
+          i += 1;
+          if (i < len) {
+            this.draw(context, history[i][0], history[i][1]);
+            requestAnimationFrame(runner);
+          }
+        };
+        requestAnimationFrame(runner);
+      }
+    },
+
+    $_resizeHandler() {
+      const canvas = this.$refs.canvas;
+      canvas.style.width = `${this.width}px`;
+      const realw = parseFloat(window.getComputedStyle(canvas).width);
+      canvas.style.height = `${this.ratio * realw}px`;
+      this.canvasTxt = canvas.getContext('2d');
+      this.canvasTxt.scale(Number(this.sratio), Number(this.sratio));
+      this.sratio = realw / this.width;
+      this.canvasTxt.scale(1 / this.sratio, 1 / this.sratio);
+    },
+    // 操作
+    generate() {
+      const pm = new Promise((resolve, reject) => {
+        if (!this.hasDrew) {
+          reject(`Warning: Not Signned!`);
+          return;
+        }
+        let resImgData = this.canvasTxt.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
+        this.canvasTxt.globalCompositeOperation = 'destination-over';
+        this.canvasTxt.fillStyle = this.myBg;
+        this.canvasTxt.fillRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
+        this.resultImg = this.$refs.canvas.toDataURL();
+        let resultImg = this.resultImg;
+        this.canvasTxt.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
+        this.canvasTxt.putImageData(resImgData, 0, 0);
+        this.canvasTxt.globalCompositeOperation = 'source-over';
+        if (this.isCrop) {
+          const crop_area = this.getCropArea(resImgData.data);
+          let crop_canvas = document.createElement('canvas');
+          const crop_ctx = crop_canvas.getContext('2d');
+          crop_canvas.width = crop_area[2] - crop_area[0];
+          crop_canvas.height = crop_area[3] - crop_area[1];
+          const crop_imgData = this.canvasTxt.getImageData(...crop_area);
+          crop_ctx.globalCompositeOperation = 'destination-over';
+          crop_ctx.putImageData(crop_imgData, 0, 0);
+          crop_ctx.fillStyle = this.myBg;
+          crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height);
+          resultImg = crop_canvas.toDataURL();
+          crop_canvas = null;
+        }
+        resolve(resultImg);
+      });
+      return pm;
+    },
+    reset() {
+      this.canvasTxt.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
+      this.$emit('update:bgColor', '');
+      this.$refs.canvas.style.background = 'rgba(255, 255, 255, 0)';
+      this.history = [];
+      this.hasDrew = false;
+      this.resultImg = '';
+    },
+    getCropArea(imgData) {
+      let topX = this.$refs.canvas.width;
+      let btmX = 0;
+      let topY = this.$refs.canvas.height;
+      let btnY = 0;
+      for (let i = 0; i < this.$refs.canvas.width; i++) {
+        for (let j = 0; j < this.$refs.canvas.height; j++) {
+          let pos = (i + this.$refs.canvas.width * j) * 4;
+          if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
+            btnY = Math.max(j, btnY);
+            btmX = Math.max(i, btmX);
+            topY = Math.min(j, topY);
+            topX = Math.min(i, topX);
+          }
+        }
+      }
+      topX += 1;
+      btmX += 1;
+      topY += 1;
+      btnY += 1;
+      const data = [topX, topY, btmX, btnY];
+      return data;
+    },
+  },
+};
+</script>
+
+<style scoped>
+canvas {
+  display: block;
+  max-width: 100%;
+}
+</style>

+ 485 - 0
src/views/book/courseware/preview/components/character_base/components/FreewriteLettle.vue

@@ -0,0 +1,485 @@
+<template>
+  <div class="practice practiceSingleNPC">
+    <i class="el-icon-close close-icon" @click="changePraShow()"></i>
+    <div class="right-content">
+      <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
+      <div class="right-strockred">
+        <template v-if="!hasPlay && data && data.strokes_image">
+          <img class="img" :src="data.strokes_image" alt="" />
+        </template>
+        <div class="zhezhao" v-if="disabled"></div>
+        <FreeWriteQP
+          id="esign"
+          ref="esign"
+          :bg-color.sync="bgColor"
+          :height="height"
+          :is-crop="isCrop"
+          :line-color="hanzicolor"
+          :line-width="hanziweight"
+          :width="width"
+          class="vueEsign"
+        />
+        <a v-if="!disabled" class="clean-btn" @click="resetHuahua">
+          <SvgIcon icon-class="reset" class="reset-btn" />
+        </a>
+      </div>
+      <ul class="nav-list">
+        <li :class="currenHzData && currenHzData.strokes_content ? '' : 'disabled'" @click="play()">播放</li>
+        <li :class="disabled ? 'disabled' : ''" @click="handleWriteImg">保存</li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+import FreeWriteQP from './FreeWriteQP.vue';
+export default {
+  components: {
+    FreeWriteQP,
+  },
+  props: {
+    currentTreeID: {
+      type: String,
+      default: '',
+    },
+    currentHz: {
+      type: String,
+      default: '',
+    },
+    currenHzData: {
+      type: Object,
+      default: () => ({
+        strokes_image: '',
+        strokes_content: '[]',
+      }),
+    },
+    rowIndex: {
+      type: Number,
+      default: 0,
+    },
+    colIndex: {
+      type: Number,
+      default: 0,
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      width: 300,
+      height: 300,
+      bgColor: '',
+      isCrop: false,
+      //   learn_mode: "",
+      playStorkes: false,
+      navIndex: 0,
+      colorsList: ['#404040', '#f65d4d', '#19b068', '#52a1ea', '#ff8c49'],
+      weightList: [6, 10],
+      colorIndex: 0,
+      penIndex: 0,
+      hanzicolor: '',
+      hanziweight: '',
+      imgOrCans: false,
+      hasPlay: false,
+      data: null,
+      imgShow: false,
+    };
+  },
+  computed: {},
+  watch: {},
+  // 生命周期 - 创建完成(可以访问当前this实例)
+  created() {
+    let _this = this;
+    let color = _this.colorsList[_this.colorIndex];
+    _this.hanzicolor = color;
+    _this.hanziweight = 6;
+    if (_this.currenHzData && _this.currenHzData.strokes_image) {
+      _this.imgOrCans = true;
+    }
+    _this.data = JSON.parse(JSON.stringify(_this.currenHzData));
+  },
+  // 生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {},
+  beforeCreate() {}, // 生命周期 - 创建之前
+  beforeMount() {}, // 生命周期 - 挂载之前
+  beforeUpdate() {}, // 生命周期 - 更新之前
+  updated() {}, // 生命周期 - 更新之后
+  beforeDestroy() {}, // 生命周期 - 销毁之前
+  destroyed() {}, // 生命周期 - 销毁完成
+  activated() {},
+  // 方法集合
+  methods: {
+    play() {
+      let _this = this;
+      if (this.currenHzData && this.currenHzData.strokes_content) {
+        if (this.hasPlay) {
+          this.$message.warning('请等待播放完成');
+          return;
+        }
+        if (this.$refs.esign) this.$refs.esign.reset();
+        this.hasPlay = true;
+        this.imgShow = true;
+        let c = document.getElementById('esign');
+        let cxt = document.getElementById('esign').getContext('2d');
+        cxt.clearRect(0, 0, c.width, c.height);
+        let history = null;
+        history = JSON.parse(_this.currenHzData.strokes_content);
+        const len = history.length;
+        let i = 0;
+        const runner = () => {
+          i += 1;
+          if (i < len) {
+            _this.$refs.esign.draw(null, history[i][0], history[i][1]);
+            requestAnimationFrame(runner);
+          } else {
+            _this.hasPlay = false;
+          }
+        };
+        requestAnimationFrame(runner);
+      }
+    },
+
+    changeNav(index) {
+      this.navIndex = index;
+    },
+    changeColor(index) {
+      let _this = this;
+      _this.colorIndex = index;
+      let color = _this.colorsList[index];
+      _this.hanzicolor = color;
+    },
+    changeLearnMode(mode) {
+      this.learn_mode = mode;
+    },
+    resetHuahua() {
+      let _this = this;
+      if (_this.hasPlay) {
+        _this.$message.warning('请等待播放完成');
+        return;
+      }
+      _this.imgOrCans = false;
+      _this.$refs.esign.reset();
+      if (_this.data) {
+        _this.data.strokes_image = '';
+      }
+      _this.$emit('deleteWriteRecord', _this.rowIndex, _this.colIndex, _this.currentHz);
+      // this.removeImage();
+    },
+    removeImage() {
+      let _this = this;
+      if (_this.data) {
+        // let MethodName = 'teaching-practice_manager-DeleteMyHZHandwrittenRecord';
+        // let data = {
+        //   hz_handwritten_record_id: this.data.hz_handwritten_record_id,
+        // };
+        // LearnWebSI(MethodName, data).then((res) => {
+        _this.$message.success('删除成功');
+        _this.data = {};
+        _this.$emit('deleteWriteRecord', _this.rowIndex, _this.colIndex);
+        // });
+      }
+    },
+    // 不保存到记录列表
+    handleWriteImg() {
+      if (this.disabled) return;
+      if (this.$refs.esign.history.length === 0) return;
+      this.$refs.esign.generate().then((res) => {
+        let Book_img = res.replace('data:image/png;base64,', '');
+        let write_img = `data:image/png;base64,${Book_img}`;
+        let answer = {};
+        answer = {
+          hz: this.currentHz,
+          strokes_content: JSON.stringify(this.$refs.esign.history),
+          strokes_image: write_img,
+        };
+        this.$emit('changeCurQue', answer, this.colIndex);
+        let obj = {
+          hz: this.currentHz,
+          strokes_content: JSON.stringify(this.$refs.esign.history),
+          strokes_image: write_img,
+        };
+        this.$emit('closeIfFreeShow', obj, this.rowIndex, this.colIndex);
+        // this.$message.warning("请先书写在保存");
+      });
+    },
+    // 保存到记录列表
+    handleWriteImg_save() {
+      this.$refs.esign
+        .generate()
+        .then((res) => {
+          let Book_img = res.replace('data:image/png;base64,', '');
+          let write_img = `data:image/png;base64,${Book_img}`;
+          let answer = {};
+          answer = {
+            hz: this.currentHz,
+            strokes_content: JSON.stringify(this.$refs.esign.history),
+            strokes_image: write_img,
+          };
+          this.$emit('changeCurQue', answer, this.colIndex);
+
+          this.$message.success('保存成功!');
+          let obj = {
+            hz_handwritten_record_id: res.hz_handwritten_record_id,
+            history: this.$refs.esign.history,
+            strokes_image: write_img,
+          };
+          this.$emit('closeIfFreeShow', obj, this.rowIndex, this.colIndex);
+        })
+        .catch(() => {
+          this.$message.warning('请先书写在保存');
+        });
+    },
+    changePraShow() {
+      this.$emit('changePraShow');
+    },
+  }, // 如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="scss" scoped>
+.practice {
+  position: relative;
+  width: 332px;
+  max-height: 400px;
+  margin: 0 auto;
+  background: #f3f3f3;
+  border-radius: 8px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 15%);
+
+  .clean-btn {
+    position: absolute;
+    right: 8px;
+    bottom: 8px;
+    width: 16px;
+    height: 16px;
+    margin: 0 4px;
+    color: $text-color;
+    cursor: pointer;
+
+    &:hover {
+      color: #000;
+    }
+  }
+
+  .close-icon {
+    position: absolute;
+    top: 0;
+    right: 8px;
+    z-index: 2;
+    width: 32px;
+    height: 32px;
+    padding: 8px;
+    cursor: pointer;
+  }
+
+  .Book_content {
+    position: relative;
+    box-sizing: border-box;
+    display: flex;
+    align-items: flex-start;
+    width: 100%;
+    height: 100%;
+  }
+
+  .left-content {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    width: 144px;
+
+    .left-content-pra {
+      height: 162px;
+    }
+  }
+
+  .right-content {
+    position: relative;
+    box-sizing: border-box;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: flex-start;
+    width: 332px;
+
+    // height: 360px;
+    padding: 30px 16px;
+    margin: 0 auto;
+    background: #f3f3f3;
+    border-radius: 16px;
+
+    .nav-list {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 100%;
+      height: 34px;
+      padding: 0;
+      margin-top: 16px;
+      list-style: none;
+
+      > li {
+        width: 140px;
+        height: 34px;
+        font-size: 14px;
+        font-style: normal;
+        font-weight: bold;
+        line-height: 34px;
+        color: #fff;
+        text-align: center;
+        cursor: pointer;
+        background: #de4444;
+        border-radius: 8px;
+
+        &:hover {
+          background: #f76565;
+        }
+
+        &:active {
+          background: #c43c3c;
+        }
+
+        &.disabled {
+          color: #fff;
+          cursor: not-allowed;
+          background-color: #c8c9cc;
+          background-image: none;
+          border-color: #c8c9cc;
+        }
+      }
+    }
+
+    .character-target-bg {
+      position: absolute;
+      top: 30px;
+      left: 16px;
+      width: 300px;
+      height: 300px;
+      color: #dedede;
+    }
+
+    .right-strockred {
+      position: relative;
+      width: 300px;
+      height: 300px;
+      overflow: hidden;
+      border-radius: 8px;
+
+      .img,
+      .zhezhao {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .footer {
+      position: absolute;
+      bottom: 80px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      width: 100%;
+      padding-right: 40px;
+    }
+  }
+}
+
+.strockplay {
+  box-sizing: border-box;
+  width: 144px;
+  height: 144px;
+  overflow: hidden;
+  border: 2px solid #de4444;
+  border-radius: 8px;
+
+  .strockplayRedInner {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.left-content .footer {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  cursor: pointer;
+
+  .bg-box {
+    box-sizing: border-box;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 76px;
+    height: 32px;
+    padding: 4px 8px;
+    font-size: 16px;
+    line-height: 150%;
+    color: #000;
+    text-align: center;
+    background: #fff;
+    border: 1px solid rgba(0, 0, 0, 10%);
+    border-radius: 4px;
+
+    img {
+      width: 24px;
+      height: 24px;
+      margin: 0;
+    }
+  }
+
+  .practice-icon {
+    height: 36px;
+    margin-top: 12px;
+  }
+
+  > span {
+    margin-bottom: 9px;
+    font-size: 24px;
+    font-weight: 600;
+    line-height: 34px;
+    color: #ba7d21;
+    text-align: center;
+  }
+}
+
+.el-tabs {
+  width: 100%;
+}
+</style>
+<style lang="scss">
+.practiceSingleNPC {
+  .el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active {
+    color: #000;
+  }
+
+  .el-tabs--border-card > .el-tabs__header .el-tabs__item:not(.is-disabled):hover {
+    color: #000;
+  }
+
+  .el-tabs__item,
+  .el-tabs--border-card > .el-tabs__header .el-tabs__item {
+    width: 80px;
+    height: 48px;
+    font-size: 16px;
+    font-weight: normal;
+    line-height: 48px;
+    color: #000;
+    text-align: center;
+    border: none;
+  }
+
+  .el-tabs--border-card > .el-tabs__header {
+    background: #f3f3f3;
+    border: none;
+  }
+
+  .el-tabs--border-card > .el-tabs__content {
+    padding: 0;
+  }
+
+  .el-tab-pane {
+    display: flex;
+  }
+}
+</style>

+ 145 - 0
src/views/book/courseware/preview/components/character_base/components/Strockplayredline.vue

@@ -0,0 +1,145 @@
+<template>
+  <div v-if="bookText" class="strockplay-redInner" :style="{ borderColor: frameColor }">
+    <div v-if="playStorkes" :class="['strock-play-box']" @click="playHanzi" :style="{ color: frameColor }">
+      <SvgIcon icon-class="hanzi-strock-play" class="strock-play-btn" />
+    </div>
+    <div class="character-target-box">
+      <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" v-if="bgType === 'tian'" />
+      <div :id="targetDiv" :ref="targetDiv" class="character-target-div"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+const HanziWriter = require('hanzi-writer');
+export default {
+  components: {},
+  props: {
+    bookText: {
+      type: String,
+      default: '',
+    },
+    targetDiv: {
+      type: String,
+      default: '',
+    },
+    playStorkes: {
+      type: Boolean,
+      default: true,
+    },
+    strokeColor: {
+      type: String,
+      default: '',
+    },
+    bookStrokes: {
+      type: Object,
+      default: () => {},
+    },
+    bgType: {
+      type: String,
+      default: 'tian',
+    },
+    frameColor: {
+      type: String,
+      default: '#F13232',
+    },
+  },
+  data() {
+    return {
+      writer: null,
+    };
+  },
+  computed: {},
+  watch: {
+    targetDiv: {
+      handler(val, oldVal) {
+        if (val !== oldVal) {
+          this.$nextTick(() => {
+            this.initHanziwrite();
+          });
+        }
+      },
+      deep: true,
+    },
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initHanziwrite();
+    });
+  },
+  // 方法集合
+  methods: {
+    initHanziwrite() {
+      let _this = this;
+      if (this.$refs[this.targetDiv]) {
+        let svg_arr = this.$refs[this.targetDiv].querySelectorAll('svg');
+        svg_arr.forEach((item) => {
+          item.remove();
+        });
+      }
+      this.writer = HanziWriter.default.create(this.targetDiv, this.bookText, {
+        charDataLoader(char, onComplete) {
+          onComplete(_this.bookStrokes);
+        },
+        padding: 5,
+        showOutline: true,
+        strokeColor: this.strokeColor ? this.strokeColor : '#000',
+      });
+    },
+    playHanzi(event) {
+      event.stopPropagation();
+      this.writer.animateCharacter();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.strockplay-redInner {
+  position: relative;
+  box-sizing: border-box;
+  width: 64px; // 444px
+  height: 64px; // 480px
+  border: 1px solid #e81b1b;
+}
+
+.character-target-div {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 99;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.character-target-box {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+.character-target-bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  color: #dedede;
+}
+
+.strock-play-box {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 11px;
+  height: 11px;
+  cursor: pointer;
+}
+</style>

+ 163 - 0
src/views/book/courseware/preview/components/character_base/components/Strockred.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="strockredBox">
+    <div class="strockred">
+      <div class="character-target-box">
+        <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
+        <div :id="targetDiv" class="character-target-div"></div>
+      </div>
+      <!-- <div v-if="reset" :class="['reset-box']" @click="resetHanzi">
+        <SvgIcon icon-class="reset" class="reset-btn" />
+      </div> -->
+    </div>
+  </div>
+</template>
+
+<script>
+const HanziWriter = require('hanzi-writer');
+export default {
+  name: 'Strockred',
+  components: {},
+  props: {
+    bookText: {
+      type: String,
+      default: '',
+    },
+    targetDiv: {
+      type: String,
+      default: '',
+    },
+    reset: {
+      type: Boolean,
+      default: true,
+    },
+    hanziColor: {
+      type: String,
+      default: '',
+    },
+    bookStrokes: {
+      type: Object,
+      default: () => {},
+    },
+    isAnswer: {
+      type: String,
+      default: 'false',
+    },
+  },
+  data() {
+    return {
+      writer: null,
+    };
+  },
+  computed: {},
+  watch: {
+    hanziColor(newVal) {
+      this.updateColor(newVal);
+    },
+    Book_text: {
+      handler(val, oldVal) {
+        if (val !== oldVal) {
+          this.$nextTick(() => {
+            this.initHanziwrite();
+          });
+        }
+      },
+      deep: true,
+    },
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initHanziwrite();
+    });
+  },
+  // 方法集合
+  methods: {
+    initHanziwrite() {
+      let _this = this;
+      let options = {
+        charDataLoader(char, onComplete) {
+          onComplete(_this.bookStrokes);
+        },
+        padding: 5,
+        showCharacter: false,
+        strokeColor: this.hanziColor,
+        drawingColor: this.hanziColor,
+        drawingWidth: 6,
+      };
+      this.writer = HanziWriter.default.create(this.targetDiv, this.Book_text, options);
+      this.writer.quiz({
+        onComplete: function (summaryData) {
+          // 禁用操作 清除后传参给父级保存答案
+          _this.$emit('update:isAnswer', 'true');
+        },
+      });
+    },
+    resetHanzi() {
+      this.writer.quiz();
+      this.$emit('update:isAnswer', 'false');
+    },
+    updateColor(color) {
+      this.writer.updateColor('strokeColor', color);
+      this.writer.updateColor('drawingColor', color);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.strockredBox {
+  width: 64px; // 444px
+  height: 64px; // 480px
+}
+
+.strockred {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  margin: 0 auto;
+
+  .character-target-div {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 99;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+  }
+
+  .character-target-box {
+    position: relative;
+    width: 100%;
+    height: 100%;
+  }
+
+  .character-target-bg {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    color: #dedede;
+  }
+
+  .reset-box {
+    position: absolute;
+    right: 2px;
+    bottom: 2px;
+    z-index: 100;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 11px;
+    height: 11px;
+    color: $text-color;
+    cursor: pointer;
+
+    &:hover {
+      color: #000;
+    }
+  }
+}
+</style>

+ 4 - 4
src/views/book/courseware/preview/components/pinyin_base/PinyinBasePreview.vue

@@ -9,7 +9,7 @@
         :class="[data.property.arrange_type === 'horizontal' ? 'content-box-flex' : 'content-box-vertical']"
       >
         <div class="first-con">
-          <AudioFill
+          <AudioPlay
             v-if="data.audio_file_id && data.property.audio_position === 'front'"
             :file-id="data.audio_file_id"
           />
@@ -62,7 +62,7 @@
               </span>
             </template>
           </div>
-          <AudioFill
+          <AudioPlay
             v-if="data.audio_file_id && data.property.audio_position === 'back'"
             :file-id="data.audio_file_id"
           />
@@ -122,13 +122,13 @@
 import { getPinyinBaseData, arrangeTypeList, audioPositionList } from '@/views/book/courseware/data/pinyinBase';
 
 import PreviewMixin from '../common/PreviewMixin';
-import AudioFill from '../fill/components/AudioFillPlay.vue';
+import AudioPlay from '../character_base/components/AudioPlay.vue';
 import SoundRecord from '../../common/SoundRecord.vue';
 
 export default {
   name: 'PinyinBasePreview',
   components: {
-    AudioFill,
+    AudioPlay,
     SoundRecord,
   },
   mixins: [PreviewMixin],

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor