Browse Source

书写组件

natasha 1 year ago
parent
commit
83e018afe0

+ 25 - 0
src/views/book/courseware/create/components/question/write/Write.vue

@@ -38,6 +38,7 @@ import UploadFile from '@/views/book/courseware/create/components/base/common/Up
 import { getWriteData } from '@/views/book/courseware/data/write';
 import { GetStaticResources } from '@/api/app';
 import cnchar from 'cnchar';
+import { getRandomNumber } from '@/utils';
 
 export default {
   name: 'WritePage',
@@ -65,10 +66,34 @@ export default {
         let contentList = [];
         contentArr.forEach((item, index) => {
           if (item.trim()) {
+            let content_arr = item.trim().split('');
+            let content_arrs = [];
+            let content_arr_strokes = [];
+            content_arr.forEach((itemc) => {
+              if (itemc.trim()) {
+                content_arrs.push(itemc.trim());
+              }
+            });
+            content_arrs.forEach((itemc, indexc) => {
+              content_arr_strokes.push(null);
+              let MethodName = 'hz_resource_manager-GetHZStrokesContent';
+              let data = {
+                hz: itemc,
+              };
+              GetStaticResources(MethodName, data).then((res) => {
+                let obj = {
+                  hz: itemc.trim(),
+                  strokes: res,
+                };
+                content_arr_strokes[indexc] = obj;
+              });
+            });
             contentList.push({
               con: item.trim(),
               pinyin: cnchar.spell(item.trim(), 'array', 'low', 'tone').join(' '),
               audio_file_id: '',
+              hz_strokes_list: content_arr_strokes,
+              mark: getRandomNumber(),
             });
             this.handleMatic(item.trim(), contentList.length - 1);
           }

+ 4 - 4
src/views/book/courseware/create/components/question/write/WriteSetting.vue

@@ -9,13 +9,13 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="汉字内容">
+      <!-- <el-form-item label="汉字内容">
         <el-radio-group v-model="property.content_type">
           <el-radio v-for="{ value, label } in conList" :key="value" :label="value" :value="value">
             {{ label }}
           </el-radio>
         </el-radio-group>
-      </el-form-item>
+      </el-form-item> -->
       <el-form-item label="框颜色">
         <el-color-picker v-model="property.frame_color"></el-color-picker>
       </el-form-item>
@@ -28,13 +28,13 @@
         <el-input-number v-model="property.write_number" :min="0" :step="1" />
       </el-form-item>
       <el-divider />
-      <el-form-item label="错误提示" v-if="property.content_type === 'con'">
+      <!-- <el-form-item label="错误提示" v-if="property.content_type === 'con'">
         <el-radio-group v-model="property.is_enable_error">
           <el-radio v-for="{ value, label } in showList" :key="value" :label="value">
             {{ label }}
           </el-radio>
         </el-radio-group>
-      </el-form-item>
+      </el-form-item> -->
       <el-form-item label="笔迹回放">
         <el-radio-group v-model="property.is_enable_play_back">
           <el-radio v-for="{ value, label } in showList" :key="value" :label="value">

+ 8 - 8
src/views/book/courseware/data/bookType.js

@@ -236,14 +236,14 @@ export const bookTypeOption = [
         set: RecordInputSetting,
         preview: RecordInputPreview,
       },
-      {
-        value: 'new_word',
-        label: '生词组件',
-        icon: '',
-        component: NewWord,
-        set: NewWordSetting,
-        preview: NewWordPreview,
-      },
+      // {
+      //   value: 'new_word',
+      //   label: '生词组件',
+      //   icon: '',
+      //   component: NewWord,
+      //   set: NewWordSetting,
+      //   preview: NewWordPreview,
+      // },
       {
         value: 'notes',
         label: '注释组件',

+ 1 - 1
src/views/book/courseware/preview/components/character/CharacterPreview.vue

@@ -1,6 +1,6 @@
 <!-- eslint-disable vue/no-v-html -->
 <template>
-  <div class="character-preview">
+  <div class="character-preview" :style="getAreaStyle()">
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
 
     <div class="main">

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

@@ -69,7 +69,7 @@ export default {
     },
     showPlay: {
       type: Boolean,
-      default: false,
+      default: true,
     },
   },
   data() {

+ 522 - 80
src/views/book/courseware/preview/components/write/WritePreview.vue

@@ -1,74 +1,327 @@
 <!-- eslint-disable vue/no-v-html -->
 <template>
-  <div class="select-preview" :style="getAreaStyle()">
+  <div class="chinese-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="['words-box']">
+        <div v-for="(item, index) in option_list" :key="index" :class="['words-item']">
+          <template
+            v-if="
+              item.con &&
+              item.con.trim() &&
+              item.hz_strokes_list &&
+              item.hz_strokes_list[0] &&
+              item.hz_strokes_list[0].strokes
+            "
+          >
+            <div
+              class="words-top"
+              :style="{
+                width: 64 * item.hz_strokes_list.length + 'px',
+              }"
+            >
+              <div class="words-left" :style="{ width: '64' * item.hz_strokes_list.length + 'px' }">
+                <AudioPlay :file-id="item.audio_file_id" theme-color="gray" />
+                <span class="pinyin">{{ item.pinyin }}</span>
+              </div>
+            </div>
+            <div class="card-box">
+              <!-- 描红 -->
+              <template v-for="(items, indexs) in item.hz_strokes_list">
+                <Strockplayredline
+                  :key="indexs"
+                  :play-storkes="true"
+                  :book-text="item.con"
+                  :target-div="'pre' + item.con + index + indexs + Math.random().toString(36).substring(2, 10)"
+                  :book-strokes="items.strokes"
+                  :class="['strock-chinese', 'border-right-none']"
+                />
+              </template>
+
+              <div
+                v-for="(itemI, indexI) in miao_arr[index]"
+                :key="indexI + index"
+                style="display: flex"
+                @click="miaoStorkes(index, indexI, item.mark, item.con, itemI.strokes)"
+              >
+                <Strockplayredline
+                  v-if="item.imgArr[indexI] && item.imgArr[indexI] === 'true'"
+                  :play-storkes="false"
+                  :book-text="item.con"
+                  :target-div="'write-praT' + item.con + itemI + Math.random().toString(36).substring(2, 10)"
+                  :book-strokes="itemI.strokes"
+                  :class="[
+                    'strock-chinese',
+                    'strock-chinese' + ((indexI + itemI.length + 1) % writer_number_yuan),
+                    (indexI + itemI.length + 1) % writer_number_yuan !== 0 && indexI !== miao_arr[index].length - 1
+                      ? 'border-right-none'
+                      : '',
+                  ]"
+                />
+                <Strockplayredline
+                  v-else
+                  :play-storkes="false"
+                  :book-text="item.con"
+                  :target-div="'write-praT' + item.con + itemI + Math.random().toString(36).substring(2, 10)"
+                  :book-strokes="itemI.strokes"
+                  :strokeColor="'#ddd'"
+                  :class="[
+                    'strock-chinese',
+                    'strock-chinese' + ((indexI + itemI.length + 1) % writer_number_yuan),
+                    (indexI + itemI.length + 1) % writer_number_yuan !== 0 && indexI !== miao_arr[index].length - 1
+                      ? 'border-right-none'
+                      : '',
+                  ]"
+                />
+              </div>
+              <!-- 书写 -->
+              <div v-for="(items, indexs) in item.imgArr" :key="indexs" class="con-box">
+                <div
+                  :class="[
+                    'strockplay-newWord',
+                    (indexs + item.hz_strokes_list.length) % writer_number_yuan !== 0 ? 'border-left-none' : '',
+                  ]"
+                  @click="freeWrite(items ? JSON.parse(items) : null, index, indexs, item.mark)"
+                >
+                  <SvgIcon icon-class="hanzi-writer-bg" class="character-target-bg" />
+                  <img
+                    v-if="!play_status && items && JSON.parse(items).strokes_image"
+                    class="hanzi-writer-img"
+                    :src="JSON.parse(items).strokes_image"
+                    alt=""
+                  />
+                </div>
+              </div>
+            </div>
+          </template>
+        </div>
+      </div>
+      <div v-if="if_free_show" class="practiceBox practice-box-strock">
+        <FreewriteLettle
+          ref="freePaint"
+          :current-tree-i-d="'1234456'"
+          :current-hz="current_hz"
+          :curren-hz-data="current_hz_data"
+          :row-index="active_index"
+          :col-index="active_col_index"
+          :disabled="disabled"
+          @closeIfFreeShow="closeIfFreeShow"
+          @changePraShow="changePraShow"
+          @changeCurQue="changeCurQue"
+          @deleteWriteRecord="deleteWriteRecord"
+        />
+      </div>
+      <div v-if="if_miao_show" class="practiceBox practice-box-strock">
+        <div class="miao-box">
+          <i class="el-icon-close close-icon" @click="if_miao_show = false"></i>
+          <Strockplayredline
+            v-if="
+              (data.answer.answer_list[active_index].strokes_content_list[active_col_index] &&
+                data.answer.answer_list[active_index].strokes_content_list[active_col_index] === 'true') ||
+              disabled
+            "
+            :play-storkes="false"
+            :book-text="current_hz"
+            :target-div="'write-praT' + current_hz + active_col_index + Math.random().toString(36).substring(2, 10)"
+            :book-strokes="current_hz_data"
+            :strokeColor="
+              disabled &&
+              (!data.answer.answer_list[active_index].strokes_content_list[active_col_index] ||
+                (data.answer.answer_list[active_index].strokes_content_list[active_col_index] &&
+                  data.answer.answer_list[active_index].strokes_content_list[active_col_index] === 'false'))
+                ? '#ddd'
+                : ''
+            "
+          />
+          <Strockred
+            :class="[
+              'strock-red',
+              data.answer.answer_list[active_index].strokes_content_list[active_col_index] &&
+              data.answer.answer_list[active_index].strokes_content_list[active_col_index] === 'true'
+                ? 'strock-red-zindex'
+                : '',
+            ]"
+            :book-text="current_hz"
+            :hanzi-color="hanzi_color"
+            :target-div="'write-praT' + current_hz + active_col_index + Math.random().toString(36).substring(2, 10)"
+            :book-strokes="current_hz_data"
+            :is-answer.sync="data.answer.answer_list[active_index].strokes_content_list[active_col_index]"
+            ref="strockRed"
+          />
+          <div v-if="!disabled" :class="['reset-box']" @click="resetHanzi">
+            <SvgIcon icon-class="reset" class="reset-btn" />
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script>
-import { getWriteData, fillFontList, arrangeTypeList, audioPositionList } from '@/views/book/courseware/data/write';
+import { getWriteData } from '@/views/book/courseware/data/write';
 
 import PreviewMixin from '../common/PreviewMixin';
-import AudioFill from '../fill/components/AudioFillPlay.vue';
-import SoundRecord from '../../common/SoundRecord.vue';
+import AudioPlay from '../character_base/components/AudioPlay.vue';
+import Strockplayredline from '../character_base/components/Strockplayredline.vue';
+import Strockred from '../character_base/components/Strockred.vue';
+import FreewriteLettle from '../character_base/components/FreewriteLettle.vue';
 
 export default {
   name: 'WritePreview',
   components: {
-    AudioFill,
-    SoundRecord,
+    AudioPlay,
+    Strockplayredline,
+    Strockred,
+    FreewriteLettle,
   },
   mixins: [PreviewMixin],
   data() {
     return {
       data: getWriteData(),
+      hanzi_color: '#404040', // 描红汉字底色
+      writer_number_yuan: 15,
+      writer_number: null, // 书写个数
+      miao_number: null, // 描红个数
+      if_free_show: false,
+      free_img: [],
+      active_index: null,
+      active_col_index: null,
+      current_hz: '', // 当前汉字
+      current_hz_data: null, // 当前汉字数据
+      play_status: false, // 播放状态
+      active_mark: '',
+      option_list: [],
+      show_preview: false,
+      miao_arr: [],
+      if_miao_show: false, // 描红模块
     };
   },
-  computed: {
-    fontFamily() {
-      return fillFontList.find(({ value }) => this.data.property.fill_font === value).font;
+  watch: {
+    'data.content': {
+      handler(val) {
+        if (val) {
+          this.handleData();
+        }
+      },
+      deep: true,
+      immediate: true,
+    },
+    isJudgingRightWrong: {
+      handler(val) {
+        if (!val) return;
+        this.handleData();
+      },
+      immediate: true,
     },
   },
-  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();
-  },
+  computed: {},
+  created() {},
   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;
+    handleData() {
+      if (!this.isJudgingRightWrong) {
+        this.data.answer.answer_list = [];
+      }
+      this.writer_number = this.data.property.write_number ? Number(this.data.property.write_number) : 5;
+      this.miao_number = this.data.property.miao_number ? Number(this.data.property.miao_number) : 5;
+      let option_list = JSON.parse(JSON.stringify(this.data)).content_list;
+
+      this.miao_arr = [];
+      option_list.forEach((item, index) => {
+        let arr = [];
+        this.miao_arr.push([]);
+        if (item.con.trim()) {
+          for (let i = 0; i < this.writer_number; i++) {
+            item.hz_strokes_list.forEach((items) => {
+              arr.push(null);
+            });
+          }
+          for (let i = 0; i < this.miao_number; i++) {
+            item.hz_strokes_list.forEach((items) => {
+              this.miao_arr[index].push({
+                strokes: items && items.strokes ? items.strokes : null,
+                length: item.hz_strokes_list.length,
+              });
+            });
+          }
+          item.imgArr = arr;
+          if (this.isJudgingRightWrong) {
+            item.imgArr = this.data.answer.answer_list.find((items) => items.mark === item.mark)?.strokes_content_list;
+          }
+        }
+        let obj = {
+          mark: item.mark,
+          strokes_content_list: arr,
+        };
+        if (!this.isJudgingRightWrong) {
+          this.data.answer.answer_list.push(obj);
+        }
+      });
+      this.option_list = option_list;
+      setTimeout(() => {
+        if (document.getElementsByClassName('chinese-preview').length > 0) {
+          this.writer_number_yuan = Math.floor(
+            (document.getElementsByClassName('chinese-preview')[0].clientWidth - 128) / 64,
+          );
+        }
+      }, 10);
+    },
+    changePraShow() {
+      this.if_free_show = false;
+    },
+    closeIfFreeShow(data, rowIndex, colIndex, mark) {
+      this.option_list[rowIndex].imgArr[colIndex] = JSON.stringify(data);
+      this.if_free_show = false;
+      this.freeWrite(data, rowIndex, colIndex, mark);
+      this.$forceUpdate();
+    },
+    freeWrite(imgUrl, index, indexs, mark) {
+      this.if_free_show = true;
+      this.active_index = index;
+      this.active_col_index = indexs;
+      this.active_mark = mark;
+
+      this.current_hz = this.option_list[index].content;
+      this.current_hz_data = imgUrl;
+    },
+    // 删除记录
+    deleteWriteRecord(rowIndex, colIndex) {
+      this.$set(this.option_list[rowIndex].imgArr, colIndex, JSON.stringify({}));
+      this.changeCurQue(null, colIndex, this.active_mark);
+      this.current_hz_data = null;
+      this.active_mark = '';
+      this.$forceUpdate();
+    },
+    changeCurQue(answer, colIndex, mark) {
+      if (answer) {
+        let write_model = [];
+        this.answer.answer_list.forEach((itema) => {
+          if (itema.mark === mark) {
+            write_model = itema.strokes_content_list;
+          }
+        });
+        write_model[colIndex] = JSON.stringify(answer);
+      }
+    },
+    // 点击描红模块
+    miaoStorkes(index, indexs, mark, content, storkes) {
+      this.if_miao_show = true;
+      this.active_index = index;
+      this.active_col_index = indexs;
+      this.active_mark = mark;
+      this.current_hz = content;
+      this.current_hz_data = storkes;
+    },
+    // 保存描红
+    saveComplete(flag) {
+      this.answer.answer_list[this.active_index].strokes_content_list[this.active_col_index] = flag;
+    },
+    resetHanzi() {
+      this.$refs.strockRed.resetHanzi();
+    },
+    updateColor(color) {
+      this.writer.updateColor('strokeColor', color);
+      this.writer.updateColor('drawingColor', color);
     },
   },
 };
@@ -77,59 +330,248 @@ export default {
 <style lang="scss" scoped>
 @use '@/styles/mixin.scss' as *;
 
-.select-preview {
+.chinese-preview {
   @include preview-base;
 
-  .main {
-    display: grid;
-    align-items: center;
+  .audio-wrapper-nobg {
+    height: 16px;
+
+    :deep .audio-play {
+      width: 16px;
+      height: 16px;
+      color: #000;
+      background-color: initial;
+    }
+
+    :deep .audio-play.not-url {
+      color: #a1a1a1;
+    }
+
+    :deep .voice-play {
+      width: 16px;
+      height: 16px;
+    }
   }
 
-  .fill-wrapper {
-    grid-area: fill;
-    font-size: 16pt;
+  .words-box {
+    .words-item {
+      // display: flex;
+      // flex-wrap: wrap;
+      min-width: 64px;
+      margin-bottom: 24px;
+    }
+
+    .words-top {
+      display: flex;
+      width: 100%;
+      min-height: 30px;
+      border: 1px solid #e81b1b;
+
+      .words-left {
+        box-sizing: border-box;
+        display: flex;
+        column-gap: 4px;
+        align-items: center;
+        justify-content: center;
+        width: 64px;
+      }
+
+      .words-right {
+        padding: 8px 4px;
+        margin: 0;
+        font-size: 14px;
+        line-height: 14px;
+        color: #000;
+      }
+    }
+
+    .audio-wrapper {
+      height: 16px;
+
+      :deep .audio-play {
+        width: 16px;
+        height: 16px;
+        color: #000;
+        background-color: initial;
+      }
+
+      :deep .audio-play.not-url {
+        color: #a1a1a1;
+      }
 
-    p {
-      margin: 0;
+      :deep .voice-play {
+        width: 16px;
+        height: 16px;
+      }
     }
 
-    .el-input {
-      display: inline-flex;
+    .words-info {
+      display: flex;
+      column-gap: 4px;
       align-items: center;
-      width: 120px;
-      margin: 0 2px;
+      justify-content: center;
+      width: max-content;
+      padding: 5px 16px;
+      margin: 0 auto 8px;
+      font-size: 14px;
+      line-height: 22px;
+      color: #fff;
+      background: #165dff;
+      border-radius: 20px;
 
-      &.pinyin :deep input.el-input__inner {
-        font-family: 'PINYIN-B', sans-serif;
+      .pinyin {
+        font-size: 14px;
+        color: #fff;
       }
 
-      &.chinese :deep input.el-input__inner {
-        font-family: 'arial', sans-serif;
+      .audio-wrapper {
+        :deep .audio-play {
+          color: #fff;
+        }
+
+        :deep .audio-play.not-url {
+          color: #a1a1a1;
+        }
       }
+    }
 
-      &.english :deep input.el-input__inner {
-        font-family: 'arial', sans-serif;
+    .card-box {
+      display: flex;
+      flex-wrap: wrap;
+    }
+
+    .pinyin {
+      font-family: 'League';
+      font-size: 12px;
+      font-weight: 500;
+      color: #000;
+    }
+
+    .strock-chinese {
+      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;
 
-      :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;
+      .words-item {
+        display: block;
       }
     }
   }
 
-  .record-box {
-    padding: 6px 12px;
-    background-color: $fill-color;
+  .words-dic-item {
+    .strockplay-newWord {
+      border-top: 1px solid #e81b1b;
+    }
+  }
+
+  .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;
+    }
+
+    .miao-box {
+      position: relative;
+      width: 332px;
+      height: 360px;
+      padding: 30px 16px;
+      margin: 0 auto;
+      background: #f3f3f3;
+      border-radius: 8px;
+      box-shadow: 0 4px 16px rgba(0, 0, 0, 15%);
+
+      .close-icon {
+        position: absolute;
+        top: 0;
+        right: 8px;
+        z-index: 2;
+        width: 32px;
+        height: 32px;
+        padding: 8px;
+        cursor: pointer;
+      }
+
+      .strockredBox,
+      .strockplay-redInner {
+        width: 300px;
+        height: 300px;
+        margin: 0 auto;
+      }
+
+      .strock-red {
+        position: absolute;
+        top: 30px;
+        left: 16px;
 
-    :deep .record-time {
-      width: 100px;
+        &-zindex {
+          z-index: -1;
+        }
+      }
+
+      .reset-box {
+        position: absolute;
+        right: 22px;
+        bottom: 22px;
+        z-index: 100;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 11px;
+        height: 11px;
+        color: $text-color;
+        cursor: pointer;
+
+        &:hover {
+          color: #000;
+        }
+      }
     }
   }
 }