Procházet zdrojové kódy

Merge branch 'master' of http://60.205.254.193:3000/GCLS/GCLS_Page_Exercise

dusenyao před 1 rokem
rodič
revize
edc649c72e

+ 32 - 2
src/icons/svg/hanzi-writer-bg.svg

@@ -1,3 +1,33 @@
-<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path id="&#232;&#190;&#185;&#230;&#161;&#134;" d="M0 31H62M31 62V0" stroke="currentColor" stroke-dasharray="3 6"/>
+<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>

+ 258 - 26
src/views/exercise_questions/preview/ChinesePreview.vue

@@ -8,22 +8,79 @@
     <div v-if="isEnable(data.property.is_enable_description)" class="description">{{ data.description }}</div>
 
     <!-- 笔画学习 -->
-    <div class="words-box">
-      <div v-for="(item, index) in data.option_list" :key="index" class="words-item">
+    <div :class="['words-box', 'words-box-' + data.property.learn_type]">
+      <div v-for="(item, index) in data.option_list" :key="index" :class="['words-item']">
         <template v-if="item.content.trim()">
+          <div
+            v-if="data.property.learn_type !== 'learn'"
+            class="words-top"
+            :style="{
+              width:
+                data.property.learn_type === 'paint'
+                  ? 64 * (writer_number + 6) + 'px'
+                  : 64 * (writer_number + 1) + 'px',
+            }"
+          >
+            <div class="words-left">
+              <AudioPlay :file-id="item.audio_file_id" theme-color="gray" />
+              <span class="pinyin">{{ item.pinyin }}</span>
+            </div>
+            <p class="words-right">{{ item.definition }}</p>
+          </div>
           <template v-if="data.property.learn_type === 'paint'">
             <!-- 描红 -->
+            <Strockplayredline
+              :play-storkes="true"
+              :book-text="item.content"
+              :target-div="'pre' + item.content + index"
+              :book-strokes="item.strokes"
+              :class="['strock-chinese', 'border-right-none']"
+            />
             <Strockred
+              v-for="itemI in 5"
+              :key="itemI + data.property.learn_type"
               :book-text="item.content"
               :hanzi-color="hanzi_color"
               :reset="true"
-              :target-div="'write-praT' + item.content"
+              :target-div="'write-praT' + item.content + itemI + Math.random().toString(36).substring(2, 10)"
               :book-strokes="item.strokes"
-              class="strock-chinese"
+              :class="['strock-chinese', itemI !== 5 ? 'border-right-none' : '']"
             />
+            <div
+              v-for="(items, indexs) in item.imgArr"
+              :key="indexs"
+              :class="['strockplay-newWord border-left-none']"
+              @click="freeWrite(items, index, indexs)"
+            >
+              <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
+              <img
+                v-if="!play_status && items && items.strokes_image_url"
+                class="hanzi-writer-img"
+                :src="items.strokes_image_url"
+                alt=""
+              />
+            </div>
           </template>
           <template v-else-if="data.property.learn_type === 'write'">
             <!-- 书写 -->
+            <Strockplayredline
+              :play-storkes="true"
+              :book-text="item.content"
+              :target-div="'pre' + item.content + index"
+              :book-strokes="item.strokes"
+              :class="['strock-chinese']"
+            />
+            <div v-for="(items, indexs) in item.imgArr" :key="indexs" class="con-box">
+              <div :class="['strockplay-newWord border-left-none']" @click="freeWrite(items, index, indexs)">
+                <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
+                <img
+                  v-if="!play_status && items && items.strokes_image_url"
+                  class="hanzi-writer-img"
+                  :src="items.strokes_image_url"
+                  alt=""
+                />
+              </div>
+            </div>
           </template>
           <template v-else>
             <div class="words-info">
@@ -33,7 +90,7 @@
             <Strockplayredline
               :play-storkes="true"
               :book-text="item.content"
-              :target-div="'pra' + item.content + index"
+              :target-div="'pre' + item.content + index"
               :book-strokes="item.strokes"
               class="strock-chinese"
             />
@@ -41,6 +98,20 @@
         </template>
       </div>
     </div>
+    <div v-if="if_free_show" class="practiceBox practice-box-strock">
+      <FreewriteLettle
+        ref="freePaint"
+        :current-tree-i-d="'1234'"
+        :current-hz="current_hz"
+        :curren-hz-data="current_hz_data"
+        :row-index="active_index"
+        :col-index="active_col_index"
+        @closeIfFreeShow="closeIfFreeShow"
+        @changePraShow="changePraShow"
+        @changeCurQue="changeCurQue"
+        @deleteWriteRecord="deleteWriteRecord"
+      />
+    </div>
   </div>
 </template>
 
@@ -48,6 +119,7 @@
 import PreviewMixin from './components/PreviewMixin';
 import Strockplayredline from './components/common/Strockplayredline.vue';
 import Strockred from './components/common/Strockred.vue';
+import FreewriteLettle from './components/common/FreewriteLettle.vue';
 import { GetStaticResources } from '@/api/exercise';
 
 export default {
@@ -55,35 +127,110 @@ export default {
   components: {
     Strockplayredline,
     Strockred,
+    FreewriteLettle,
   },
   mixins: [PreviewMixin],
   data() {
     return {
       hanzi_color: '#404040', // 描红汉字底色
-      writer_number: 19, // 书写个数
+      writer_number_yuan: 19,
+      writer_number: null, // 书写个数
+      answer_list: {
+        write_model: {},
+      }, // 用户答题数据
+      if_free_show: false,
+      free_img: [],
+      active_index: null,
+      active_col_index: null,
+      current_hz: '', // 当前汉字
+      current_hz_data: null, // 当前汉字数据
+      play_status: false, // 播放状态
     };
   },
+  watch: {
+    'data.property.learn_type': {
+      handler(val, oldVal) {
+        if (val !== oldVal) {
+          this.handleData();
+        }
+      },
+      deep: true,
+    },
+  },
   created() {
-    console.log(this.data);
     this.handleData();
   },
+  mounted() {},
   methods: {
     // 初始化数据
     handleData() {
-      // console.log(document.getElementsByClassName('preview-content')[0].clientWidth);
-
+      if (
+        document.getElementsByClassName('preview-content') &&
+        document.getElementsByClassName('preview-content')[0] &&
+        this.data.property.learn_type !== 'learn' &&
+        !this.writer_number
+      ) {
+        this.writer_number_yuan =
+          Math.floor((document.getElementsByClassName('preview-content')[0].clientWidth - 128) / 64) - 1;
+      }
+      if (this.data.property.learn_type === 'paint') {
+        this.writer_number = this.writer_number_yuan - 5;
+      } else {
+        this.writer_number = this.writer_number_yuan;
+      }
       this.data.option_list.forEach((item) => {
         if (item.content.trim()) {
-          let MethodName = 'hz_resource_manager-GetHZStrokesContent';
-          let data = {
-            hz: item.content.trim(),
-          };
-          GetStaticResources(MethodName, data).then((res) => {
-            this.$set(item, 'strokes', res);
-          });
+          let arr = [];
+          // let MethodName = 'hz_resource_manager-GetHZStrokesContent';
+          // let data = {
+          //   hz: item.content.trim(),
+          // };
+          // GetStaticResources(MethodName, data).then((res) => {
+          //   this.$set(item, 'strokes', res);
+          // });
+          for (let i = 0; i < this.writer_number; i++) {
+            arr.push(null);
+          }
+          item.imgArr = arr;
+          this.answer_list.write_model[item.content.trim()] = arr;
         }
       });
     },
+    changePraShow() {
+      this.if_free_show = false;
+    },
+    closeIfFreeShow(data, rowIndex, colIndex) {
+      this.data.option_list[rowIndex].imgArr[colIndex] = data;
+      this.if_free_show = false;
+      this.freeWrite(data, rowIndex, colIndex);
+      this.$forceUpdate();
+    },
+    freeWrite(imgUrl, index, indexs) {
+      this.if_free_show = true;
+      this.active_index = index;
+      this.active_col_index = indexs;
+      this.current_hz = this.data.option_list[index].content;
+      this.current_hz_data = imgUrl;
+    },
+    // 删除记录
+    deleteWriteRecord(rowIndex, colIndex, current_hz) {
+      this.$set(this.data.option_list[rowIndex].imgArr, colIndex, {});
+      let answer = {
+        hz: current_hz,
+        strokes_content: '',
+        strokes_image_url: '',
+      };
+      this.changeCurQue(answer, colIndex);
+      this.current_hz_data = null;
+      this.$forceUpdate();
+    },
+    changeCurQue(answer, colIndex) {
+      if (answer) {
+        let write_model = this.answer_list.write_model;
+        let hz = answer.hz;
+        write_model[hz][colIndex] = answer;
+      }
+    },
   },
 };
 </script>
@@ -95,11 +242,36 @@ export default {
   @include preview;
 
   .words-box {
-    display: flex;
-    column-gap: 24px;
-
     .words-item {
+      display: flex;
+      flex-wrap: wrap;
       min-width: 64px;
+      margin-bottom: 24px;
+    }
+
+    .words-top {
+      display: flex;
+      width: 100%;
+      border: 1px solid #e81b1b;
+      border-bottom: none;
+
+      .words-left {
+        box-sizing: border-box;
+        display: flex;
+        column-gap: 4px;
+        align-items: center;
+        justify-content: center;
+        width: 64px;
+        border-right: 1px solid #e81b1b;
+      }
+
+      .words-right {
+        padding: 8px 16px;
+        margin: 0;
+        font-size: 14px;
+        line-height: 14px;
+        color: #000;
+      }
     }
 
     .audio-wrapper {
@@ -127,19 +299,79 @@ export default {
       column-gap: 4px;
       align-items: center;
       justify-content: center;
+      margin-bottom: 9px;
+    }
 
-      .pinyin {
-        font-family: 'League';
-        font-size: 12px;
-        font-weight: 500;
-        color: #000;
-      }
+    .pinyin {
+      font-family: 'League';
+      font-size: 12px;
+      font-weight: 500;
+      color: #000;
     }
 
     .strock-chinese {
-      margin-top: 9px;
       border: 1px solid #e81b1b;
     }
+
+    .strockplay-newWord {
+      position: relative;
+      box-sizing: border-box;
+      flex-shrink: 0;
+      width: 64px;
+      height: 64px;
+      border: 1px solid #e81b1b;
+
+      .character-target-bg,
+      .hanzi-writer-img {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        color: #dedede;
+      }
+
+      .hanzi-writer-img {
+        z-index: 1;
+      }
+    }
+
+    .border-left-none {
+      border-left: none;
+    }
+
+    .border-right-none {
+      border-right: none;
+    }
+
+    &-learn {
+      display: flex;
+      column-gap: 24px;
+
+      .words-item {
+        display: block;
+      }
+    }
+  }
+
+  .practiceBox {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 101;
+    box-sizing: border-box;
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+    overflow-y: auto;
+    background: rgba(0, 0, 0, 19%);
+
+    &.practice-box-strock {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding-top: 0;
+    }
   }
 }
 </style>

+ 1 - 2
src/views/exercise_questions/preview/ChooseTonePreview.vue

@@ -106,8 +106,7 @@ export default {
   },
   methods: {
     chooseTone(item, value, i) {
-      if (this.select_item_index !== i && this.data.property.answer_mode === 'label') return;
-      if (!this.active_letter && this.data.property.answer_mode === 'label') return;
+      if ((!this.active_letter || this.select_item_index !== i) && this.data.property.answer_mode === 'label') return;
       item.user_answer[item.item_active_index].select_tone = value;
       if (this.data.property.answer_mode === 'label') {
         item.user_answer[item.item_active_index].select_letter = this.active_letter;

+ 261 - 0
src/views/exercise_questions/preview/components/common/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>

+ 475 - 0
src/views/exercise_questions/preview/components/common/FreewriteLettle.vue

@@ -0,0 +1,475 @@
+<!--  -->
+<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_url">
+          <img class="img" :src="data.strokes_image_url" alt="" />
+        </template>
+        <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 class="clean-btn" @click="resetHuahua">
+          <SvgIcon icon-class="reset" class="reset-btn" />
+        </a>
+      </div>
+      <ul class="nav-list">
+        <li :class="currenHzData && currenHzData.history ? '' : 'disabled'" @click="play()">播放</li>
+        <li @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_url: '',
+        history: [],
+      }),
+    },
+    rowIndex: {
+      type: Number,
+      default: 0,
+    },
+    colIndex: {
+      type: Number,
+      default: 0,
+    },
+  },
+  data() {
+    return {
+      width: 256,
+      height: 256,
+      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,
+    };
+  },
+  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_url) {
+      _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.history) {
+        if (this.hasPlay) {
+          this.$message.warning('请等待播放完成');
+          return;
+        }
+        this.$refs.esign.reset();
+        this.hasPlay = 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 = _this.currenHzData.history;
+        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_url = '';
+      }
+      _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.$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_url: write_img,
+        };
+        this.$emit('changeCurQue', answer, this.colIndex);
+        let obj = {
+          history: this.$refs.esign.history,
+          strokes_image_url: 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_url: 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_url: 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: 320px;
+  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: 288px;
+    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: 124px;
+        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: 256px;
+      height: 250px;
+      color: #dedede;
+    }
+
+    .right-strockred {
+      position: relative;
+      width: 256px;
+      height: 256px;
+      overflow: hidden;
+      border-radius: 8px;
+
+      .img {
+        position: absolute;
+      }
+    }
+
+    .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-family: 'FZJCGFKTK';
+    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: 150%;
+    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>

+ 1 - 0
src/views/exercise_questions/preview/components/common/Strockplayredline.vue

@@ -83,6 +83,7 @@ export default {
 <style lang="scss" scoped>
 .strockplay-redInner {
   position: relative;
+  box-sizing: border-box;
   width: 64px; // 444px
   height: 64px; // 480px
 }

+ 1 - 1
src/views/exercise_questions/preview/components/common/Strockred.vue

@@ -46,7 +46,7 @@ export default {
   },
   computed: {},
   watch: {
-    hanziColor(newVal, oldVal) {
+    hanziColor(newVal) {
       this.updateColor(newVal);
     },
     Book_text: {