浏览代码

生词预览优化

natasha 1 月之前
父节点
当前提交
4dcd4918e8
共有 21 个文件被更改,包括 1036 次插入135 次删除
  1. 二进制
      src/assets/voice-play.png
  2. 二进制
      src/assets/voice-white.png
  3. 1 0
      src/icons/svg/mi.svg
  4. 4 0
      src/icons/svg/playStorkes.svg
  5. 3 0
      src/icons/svg/tian.svg
  6. 36 1
      src/views/book/courseware/create/components/question/new_word/NewWord.vue
  7. 3 2
      src/views/book/courseware/data/newWord.js
  8. 4 4
      src/views/book/courseware/preview/components/article/NormalModelChs.vue
  9. 4 4
      src/views/book/courseware/preview/components/article/PhraseModelChs.vue
  10. 2 2
      src/views/book/courseware/preview/components/article/Practicechs.vue
  11. 2 2
      src/views/book/courseware/preview/components/article/Voicefullscreen.vue
  12. 4 4
      src/views/book/courseware/preview/components/article/WordModelChs.vue
  13. 1 1
      src/views/book/courseware/preview/components/article/components/FreewriteLettle.vue
  14. 1 1
      src/views/book/courseware/preview/components/article/components/Practice.vue
  15. 1 1
      src/views/book/courseware/preview/components/article/components/Strockplayredline.vue
  16. 4 4
      src/views/book/courseware/preview/components/article/components/WordPhraseDetail.vue
  17. 1 1
      src/views/book/courseware/preview/components/article/components/Wordcard.vue
  18. 2 1
      src/views/book/courseware/preview/components/article/index.vue
  19. 686 107
      src/views/book/courseware/preview/components/new_word/NewWordPreview.vue
  20. 105 0
      src/views/book/courseware/preview/components/new_word/components/AudioPlay.vue
  21. 172 0
      src/views/book/courseware/preview/components/new_word/components/Strockplay.vue

二进制
src/assets/voice-play.png


二进制
src/assets/voice-white.png


文件差异内容过多而无法显示
+ 1 - 0
src/icons/svg/mi.svg


+ 4 - 0
src/icons/svg/playStorkes.svg

@@ -0,0 +1,4 @@
+<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 0H11V11C5.50879 9.77973 1.22027 5.49121 0 0Z" fill="currentColor"/>
+<path 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" fill-opacity="0.85"/>
+</svg>

+ 3 - 0
src/icons/svg/tian.svg

@@ -0,0 +1,3 @@
+<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 31H62M31 62V0" stroke="currentColor" stroke-dasharray="3 6"/>
+</svg>

+ 36 - 1
src/views/book/courseware/create/components/question/new_word/NewWord.vue

@@ -18,7 +18,7 @@
         </el-table-column>
         <el-table-column fixed prop="new_word" label="生词/短语" width="110">
           <template slot-scope="scope">
-            <el-input v-model="scope.row.new_word"></el-input>
+            <el-input v-model="scope.row.new_word" @blur="handleBlurCon(scope.row)"></el-input>
           </template>
         </el-table-column>
         <el-table-column prop="mp3_list" label="读音" width="200">
@@ -156,6 +156,7 @@ import UploadPicture from './components/UploadPicture.vue';
 import { getNewWordData, getOption } from '@/views/book/courseware/data/newWord';
 import SelectUpload from '@/views/book/courseware/create/components/common/SelectUpload.vue';
 import { GetStaticResources } from '@/api/app';
+import cnchar from 'cnchar';
 
 export default {
   name: 'NewWordPage',
@@ -286,6 +287,40 @@ export default {
     addElement() {
       this.data.option.push(getOption());
     },
+    // 获取数据
+    handleBlurCon(row) {
+      let cons = row.new_word.trim();
+      let MethodName = 'hz_resource_manager-GetMultHZStrokesContent';
+      let data = {
+        hz_str: cons,
+      };
+      GetStaticResources(MethodName, data)
+        .then((res) => {
+          for (let key in res) {
+            if (key != 'status' && key != ',' && res[key]) {
+              res[key] = JSON.parse(res[key]);
+            }
+          }
+          let hzDetailList = res;
+          let hz_list = [];
+          cons.split('').forEach((items) => {
+            let res = JSON.parse(JSON.stringify(hzDetailList[items]));
+            let obj = {
+              con: items,
+              hzDetail: {
+                hz_json: res,
+              },
+            };
+            hz_list.push(obj);
+          });
+          row.hz_info = hz_list;
+        })
+        .catch(() => {
+          this.loading = false;
+        });
+
+      row.pinyin = cnchar.spell(cons, 'array', 'low', 'tone').join(' ');
+    },
   },
 };
 </script>

+ 3 - 2
src/views/book/courseware/data/newWord.js

@@ -48,14 +48,15 @@ export function getOption() {
     number: '',
     new_word: '',
     cixing: '', // 词性
-    definition_list: '', // 需要增加词性
+    definition_list: '', // 释义
     pinyin: '',
     mp3_list: '',
     collocation: '', // 搭配
     liju_list: '', // 例句
     file_list: [''], // 图片
     header_con: '',//页眉
-    label:'',// 标签
+    label: '',// 标签
+    hz_info: []
   };
 }
 export function getNewWordProperty() {

+ 4 - 4
src/views/book/courseware/preview/components/article/NormalModelChs.vue

@@ -1336,7 +1336,7 @@ export default {
           &.NNPE-chs {
             display: flex;
             flex-flow: wrap;
-            font-family: 'FZJCGFKTK';
+            font-family: '楷体';
             font-size: 20px;
             line-height: 28px;
             color: #000;
@@ -1403,7 +1403,7 @@ export default {
         &.NNPE-chs {
           display: flex;
           flex-flow: wrap;
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 20px;
           line-height: 28px;
           color: #000;
@@ -1547,7 +1547,7 @@ export default {
           &.NNPE-chs {
             display: flex;
             flex-flow: wrap;
-            font-family: 'FZJCGFKTK';
+            font-family: '楷体';
             font-size: 20px;
             line-height: 28px;
             color: rgba(0, 0, 0, 100%);
@@ -1606,7 +1606,7 @@ export default {
         &.NNPE-chs {
           display: flex;
           flex-flow: wrap;
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 20px;
           line-height: 28px;
           color: rgba(0, 0, 0, 100%);

+ 4 - 4
src/views/book/courseware/preview/components/article/PhraseModelChs.vue

@@ -1268,7 +1268,7 @@ export default {
           }
 
           &.NNPE-chs {
-            font-family: 'FZJCGFKTK';
+            font-family: '楷体';
             font-size: 20px;
             line-height: 28px;
 
@@ -1315,7 +1315,7 @@ export default {
         }
 
         &.NNPE-chs {
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 20px;
           line-height: 28px;
 
@@ -1445,7 +1445,7 @@ export default {
           }
 
           &.NNPE-chs {
-            font-family: 'FZJCGFKTK';
+            font-family: '楷体';
             font-size: 20px;
             line-height: 28px;
             color: rgba(0, 0, 0, 65%);
@@ -1502,7 +1502,7 @@ export default {
         }
 
         &.NNPE-chs {
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 20px;
           line-height: 28px;
           color: rgba(0, 0, 0, 65%);

+ 2 - 2
src/views/book/courseware/preview/components/article/Practicechs.vue

@@ -892,7 +892,7 @@ export default {
           &.NNPE-chs {
             display: flex;
             flex-flow: wrap;
-            font-family: 'FZJCGFKTK';
+            font-family: '楷体';
             font-size: 20px;
             line-height: 28px;
             color: rgba(0, 0, 0, 45%);
@@ -951,7 +951,7 @@ export default {
         &.NNPE-chs {
           display: flex;
           flex-flow: wrap;
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 20px;
           line-height: 28px;
           color: rgba(0, 0, 0, 45%);

+ 2 - 2
src/views/book/courseware/preview/components/article/Voicefullscreen.vue

@@ -1733,7 +1733,7 @@ export default {
           &.NNPE-chs {
             display: flex;
             flex-flow: wrap; // 兼容不是汉字的内容
-            font-family: 'FZJCGFKTK';
+            font-family: '楷体';
             font-size: 48px;
             line-height: 1.17;
             color: rgba(0, 0, 0, 85%);
@@ -1820,7 +1820,7 @@ export default {
         &.NNPE-chs {
           display: flex;
           flex-flow: wrap; // 兼容不是汉字的内容
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 48px;
           line-height: 1.17;
           color: rgba(0, 0, 0, 85%);

+ 4 - 4
src/views/book/courseware/preview/components/article/WordModelChs.vue

@@ -1027,7 +1027,7 @@ export default {
           }
 
           &.NNPE-chs {
-            font-family: 'FZJCGFKTK';
+            font-family: '楷体';
             font-size: 20px;
             line-height: 28px;
 
@@ -1078,7 +1078,7 @@ export default {
         }
 
         &.NNPE-chs {
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 20px;
           line-height: 28px;
 
@@ -1213,7 +1213,7 @@ export default {
         }
 
         &.NNPE-chs {
-          font-family: 'FZJCGFKTK';
+          font-family: '楷体';
           font-size: 20px;
           line-height: 28px;
           color: rgba(0, 0, 0, 85%);
@@ -1270,7 +1270,7 @@ export default {
       }
 
       &.NNPE-chs {
-        font-family: 'FZJCGFKTK';
+        font-family: '楷体';
         font-size: 20px;
         line-height: 28px;
         color: rgba(0, 0, 0, 85%);

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

@@ -572,7 +572,7 @@ export default {
 
   > span {
     margin-bottom: 9px;
-    font-family: 'FZJCGFKTK';
+    font-family: '楷体';
     font-size: 24px;
     font-weight: 600;
     line-height: 34px;

+ 1 - 1
src/views/book/courseware/preview/components/article/components/Practice.vue

@@ -673,7 +673,7 @@ export default {
 
   > span {
     margin-bottom: 9px;
-    font-family: 'FZJCGFKTK';
+    font-family: '楷体';
     font-size: 24px;
     font-weight: 600;
     line-height: 34px;

+ 1 - 1
src/views/book/courseware/preview/components/article/components/Strockplayredline.vue

@@ -106,7 +106,7 @@ export default {
   justify-content: center;
   width: 100%;
   height: 100%;
-  font-family: 'FZJCGFKTK';
+  font-family: '楷体';
   background: #fff url('@/assets/chinaTianRed.png') center no-repeat;
   background-size: 100% 100%;
 }

+ 4 - 4
src/views/book/courseware/preview/components/article/components/WordPhraseDetail.vue

@@ -899,7 +899,7 @@ export default {
       .bwc-tolength {
         padding: 40px 0;
         margin: 0 37px 16px 0;
-        font-family: 'FZJCGFKTK';
+        font-family: '楷体';
         font-size: 30px;
         line-height: 1.5;
         color: #404040;
@@ -1031,14 +1031,14 @@ export default {
 
             > :nth-child(1) {
               margin-right: 6px;
-              font-family: 'FZJCGFKTK';
+              font-family: '楷体';
               line-height: 24px;
               text-align: right;
             }
 
             p {
               margin: 0;
-              font-family: 'FZJCGFKTK';
+              font-family: '楷体';
               font-size: 16px;
               line-height: 24px;
               color: rgba(0, 0, 0, 85%);
@@ -1192,7 +1192,7 @@ export default {
                 height: 22px;
                 margin-left: 16px;
                 overflow: hidden;
-                font-family: 'FZJCGFKTK';
+                font-family: '楷体';
                 font-size: 14px;
                 font-weight: 400;
                 line-height: 22px;

+ 1 - 1
src/views/book/courseware/preview/components/article/components/Wordcard.vue

@@ -549,7 +549,7 @@ export default {
   .bwc-tolength {
     padding: 40px 0;
     margin: 0 0 16px;
-    font-family: 'FZJCGFKTK';
+    font-family: '楷体';
     font-size: 30px;
     line-height: 1.5;
     color: #404040;

+ 2 - 1
src/views/book/courseware/preview/components/article/index.vue

@@ -189,7 +189,7 @@ export default {
     NewWordPreview,
   },
   mixins: [PreviewMixin],
-  inject: ['bookInfo', 'courseware_id'],
+  inject: ['bookInfo'],
   watch: {
     'data.content': {
       handler(val) {
@@ -263,6 +263,7 @@ export default {
       NNPENewWordList: [],
       NpcNewWordMp3: [],
       colLength: 1,
+      courseware_id: this.$route.params.id,
     };
   },
   computed: {},

+ 686 - 107
src/views/book/courseware/preview/components/new_word/NewWordPreview.vue

@@ -9,132 +9,293 @@
           <div class="NPC-top-left">
             <span class="NPC-topTitle-text" v-html="data.title_con"></span>
           </div>
-          <div class="NPC-top-right" @click="handleChangeTab">
-            <span class="NPC-top-right-text">{{ wordShow ? '收起' : '展开' }}</span>
-            <img v-if="wordShow" src="@/assets/down.png" alt="" />
-            <img v-else class="rotate" src="@/assets/down.png" alt="" />
+
+          <div class="NPC-top-right">
+            <img v-if="is_list" src="@/assets/newWord_list.png" alt="" @click="is_list = false" />
+            <img v-else src="@/assets/newWord_tile.png" alt="" @click="is_list = true" />
+            <span class="NPC-top-right-text" @click="handleChangeTab">{{ wordShow ? '收起' : '展开' }}</span>
+            <img v-if="wordShow" src="@/assets/down.png" alt="" @click="handleChangeTab" />
+            <img v-else class="rotate" src="@/assets/down.png" alt="" @click="handleChangeTab" />
           </div>
         </div>
         <el-collapse-transition>
-          <div v-if="data.option && data.option.length > 0" v-show="wordShow" class="NPC-word-list">
-            <div v-if="data.audio_data.file_id" class="aduioLine-box">
-              <AudioLine
-                ref="audioLine"
-                :audio-id="'newWordAudio' + indexStr"
-                :mp3="data.audio_data.url"
-                :get-cur-time="getCurTime"
-                :ed="ed"
-                type="audioLine"
-                @handleListenRead="handleListenRead"
-              />
-            </div>
-            <ul class="NPC-word-table" cellspacing="0" border="0" cellpadding="0">
-              <li v-for="(item, index) in data.option_list" :key="'curQue.option' + index" class="NPC-word-tr">
-                <div
-                  v-for="(sItem, sIndex) in item"
-                  :key="'curQue.option.child' + sIndex"
-                  :class="[
-                    'NPC-word-row',
-                    playClass && mp3_index == sItem.sIndex ? 'active' : '',
-                    curTime >= sItem.bg && curTime < sItem.ed && stopAudioS ? 'active' : '',
-                  ]"
-                >
-                  <template v-if="sItem.bg || sItem.ed">
-                    <a
-                      :class="['play-btn', curTime >= sItem.bg && curTime < sItem.ed && stopAudioS ? 'active' : '']"
-                      @click="handleChangeTime(sItem.bg, sItem.ed)"
-                    ></a>
-                  </template>
-                  <template v-else-if="sItem.mp3_list">
-                    <span
-                      :class="['play-btn', playClass && mp3_index === sItem.sIndex ? 'active' : '']"
-                      @click="palyAudio(sItem.mp3_list_url, sItem.sIndex)"
-                    ></span>
-                    <audio :id="'word' + indexStr + sItem.sIndex" :src="sItem.mp3_list_url"></audio>
-                  </template>
-
-                  <template v-else>
-                    <span style="width: 16px; height: 16px"></span>
-                  </template>
-                  <div class="tabNum-box">
-                    <template v-if="sItem.mIndex == 0">
-                      <b class="tabNum" :style="{ backgroundColor: bookInfo.theme_color }">{{ index + 1 }}</b>
+          <template v-if="is_list">
+            <div v-if="data.option && data.option.length > 0" v-show="wordShow" class="NPC-word-list">
+              <div v-if="data.audio_data.file_id" class="aduioLine-box">
+                <AudioLine
+                  ref="audioLine"
+                  :audio-id="'newWordAudio' + indexStr"
+                  :mp3="data.audio_data.url"
+                  :get-cur-time="getCurTime"
+                  :ed="ed"
+                  type="audioLine"
+                  @handleListenRead="handleListenRead"
+                />
+              </div>
+              <ul class="NPC-word-table" cellspacing="0" border="0" cellpadding="0">
+                <li v-for="(item, index) in data.option_list" :key="'curQue.option' + index" class="NPC-word-tr">
+                  <div
+                    v-for="(sItem, sIndex) in item"
+                    :key="'curQue.option.child' + sIndex"
+                    :class="[
+                      'NPC-word-row',
+                      playClass && mp3_index == sItem.sIndex ? 'active' : '',
+                      curTime >= sItem.bg && curTime < sItem.ed && stopAudioS ? 'active' : '',
+                    ]"
+                  >
+                    <template v-if="sItem.bg || sItem.ed">
+                      <a
+                        :class="['play-btn', curTime >= sItem.bg && curTime < sItem.ed && stopAudioS ? 'active' : '']"
+                        @click="handleChangeTime(sItem.bg, sItem.ed)"
+                      ></a>
                     </template>
-                    <div v-else style="width: 16px; height: 16px; margin-left: 8px"></div>
-                  </div>
-                  <template v-if="sItem.pinyin_site && (sItem.pinyin_site == 'top' || sItem.pinyin_site == 'bottom')">
-                    <div class="NPC-word-tab-box">
+                    <template v-else-if="sItem.mp3_list">
                       <span
-                        v-if="sItem.pinyin_site == 'top'"
+                        :class="['play-btn', playClass && mp3_index === sItem.sIndex ? 'active' : '']"
+                        @click="palyAudio(sItem.mp3_list_url, sItem.sIndex)"
+                      ></span>
+                      <audio :id="'word' + indexStr + sItem.sIndex" :src="sItem.mp3_list_url"></audio>
+                    </template>
+
+                    <template v-else>
+                      <span style="width: 16px; height: 16px"></span>
+                    </template>
+                    <div class="tabNum-box">
+                      <template v-if="sItem.mIndex == 0">
+                        <b class="tabNum" :style="{ backgroundColor: bookInfo.theme_color }">{{ index + 1 }}</b>
+                      </template>
+                      <div v-else style="width: 16px; height: 16px; margin-left: 8px"></div>
+                    </div>
+                    <template v-if="sItem.pinyin_site && (sItem.pinyin_site == 'top' || sItem.pinyin_site == 'bottom')">
+                      <div class="NPC-word-tab-box">
+                        <span
+                          v-if="sItem.pinyin_site == 'top'"
+                          class="NPC-word-tab-common NPC-word-tab-pinyin"
+                          v-html="sItem.pinyin"
+                        >
+                        </span>
+                        <span class="NPC-word-tab-common NPC-word-tab-word" v-html="sItem.new_word"> </span>
+                        <span
+                          v-if="sItem.pinyin_site == 'bottom'"
+                          class="NPC-word-tab-common NPC-word-tab-pinyin"
+                          v-html="sItem.pinyin"
+                        >
+                        </span>
+                      </div>
+                      <span
+                        class="NPC-word-tab-common NPC-word-tab-cixing"
+                        :class="[/[\u4E00-\u9FA5\uF900-\uFA2D]/.test(sItem.cixing) ? 'hasCn' : '']"
+                        v-html="sItem.cixing"
+                      ></span>
+                      <span class="NPC-word-tab-common NPC-word-tab-def" v-html="sItem.def_str"></span>
+                    </template>
+                    <template v-else>
+                      <span
+                        v-if="!sItem.pinyin_site || sItem.pinyin_site == 'first'"
                         class="NPC-word-tab-common NPC-word-tab-pinyin"
                         v-html="sItem.pinyin"
                       >
                       </span>
                       <span class="NPC-word-tab-common NPC-word-tab-word" v-html="sItem.new_word"> </span>
                       <span
-                        v-if="sItem.pinyin_site == 'bottom'"
+                        v-if="sItem.pinyin_site == 'last'"
                         class="NPC-word-tab-common NPC-word-tab-pinyin"
                         v-html="sItem.pinyin"
                       >
                       </span>
-                    </div>
-                    <span
-                      class="NPC-word-tab-common NPC-word-tab-cixing"
-                      :class="[/[\u4E00-\u9FA5\uF900-\uFA2D]/.test(sItem.cixing) ? 'hasCn' : '']"
-                      v-html="sItem.cixing"
-                    ></span>
-                    <span class="NPC-word-tab-common NPC-word-tab-def" v-html="sItem.def_str"></span>
-                  </template>
-                  <template v-else>
-                    <span
-                      v-if="!sItem.pinyin_site || sItem.pinyin_site == 'first'"
-                      class="NPC-word-tab-common NPC-word-tab-pinyin"
-                      v-html="sItem.pinyin"
-                    >
+                      <span
+                        class="NPC-word-tab-common NPC-word-tab-cixing"
+                        :class="[/[\u4E00-\u9FA5\uF900-\uFA2D]/.test(sItem.cixing) ? 'hasCn' : '']"
+                        v-html="sItem.cixing"
+                      ></span>
+                      <span class="NPC-word-tab-common NPC-word-tab-def" v-html="sItem.def_str"></span>
+                    </template>
+                    <span>
+                      <!-- :answerRecordList="data.answer.answer_list[index][sIndex].recordList" -->
+                      <SoundRecord
+                        :tm-index="index"
+                        :tms-index="sIndex"
+                        :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
+                        :answer-record-list="[]"
+                        type="mini"
+                        class="luyin-box-wordphrase"
+                        :style="{ marginLeft: '8px' }"
+                        @handleWav="handleWav"
+                      />
                     </span>
-                    <span class="NPC-word-tab-common NPC-word-tab-word" v-html="sItem.new_word"> </span>
-                    <span
-                      v-if="sItem.pinyin_site == 'last'"
-                      class="NPC-word-tab-common NPC-word-tab-pinyin"
-                      v-html="sItem.pinyin"
-                    >
+                    <span v-if="isEnable(data.property.is_has_infor)">
+                      <img src="@/assets/detail-icon.png" class="detail-icon" @click="showDetail(sItem)" />
                     </span>
-                    <span
-                      class="NPC-word-tab-common NPC-word-tab-cixing"
-                      :class="[/[\u4E00-\u9FA5\uF900-\uFA2D]/.test(sItem.cixing) ? 'hasCn' : '']"
-                      v-html="sItem.cixing"
-                    ></span>
-                    <span class="NPC-word-tab-common NPC-word-tab-def" v-html="sItem.def_str"></span>
-                  </template>
-                  <span>
-                    <!-- :answerRecordList="data.answer.answer_list[index][sIndex].recordList" -->
-                    <SoundRecord
-                      :tm-index="index"
-                      :tms-index="sIndex"
-                      :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
-                      :answer-record-list="[]"
-                      type="mini"
-                      class="luyin-box-wordphrase"
-                      :style="{ marginLeft: '8px' }"
-                      @handleWav="handleWav"
-                    />
-                  </span>
-                  <span v-if="isEnable(data.property.is_has_infor)">
-                    <img src="@/assets/detail-icon.png" class="detail-icon" @click="showDetail(sItem)" />
-                  </span>
-                  <div v-if="sItem.collocation" class="collocation">
-                    <span>搭配:</span><b v-html="sItem.collocation"></b>
+                    <div v-if="sItem.collocation" class="collocation">
+                      <span>搭配:</span><b v-html="sItem.collocation"></b>
+                    </div>
+                    <div v-if="sItem.liju_list" class="collocation">
+                      <span>例句:</span>
+                      <div>
+                        <b v-html="sItem.liju_list"></b>
+                      </div>
+                    </div>
+                  </div>
+                </li>
+              </ul>
+            </div>
+          </template>
+          <template v-else>
+            <div v-if="data.option && data.option.length > 0" v-show="wordShow" class="NPC-word-tile">
+              <div v-for="(items, index) in data.option_list" :key="index" class="NPC-word-tile-item">
+                <div
+                  v-for="(item, indexs) in items"
+                  :key="indexs"
+                  class="writeTop"
+                  v-bind:class="{ flipped: item.isFlipped }"
+                >
+                  <div
+                    class="left left-preview"
+                    :class="[item.file_list[0] ? '' : 'left-big']"
+                    v-if="item.show_left"
+                    :style="{
+                      borderColor: '#f44444',
+                      padding:
+                        item.new_word && (item.header_con || item.label)
+                          ? ''
+                          : !item.new_word && (item.header_con || item.label)
+                            ? '40px 12px 0 12px'
+                            : '12px',
+                    }"
+                  >
+                    <div class="header-info-preview">
+                      <h5 :style="{ textAlign: 'left' }">{{ item.header_con }}</h5>
+                      <label :style="{ background: '#f44444' }">{{ item.label }}</label>
+                    </div>
+                    <div class="item-image" v-if="item.file_list[0]">
+                      <el-image
+                        :style="{
+                          width: '368px',
+                          height:
+                            item.new_word && (item.header_con || item.label)
+                              ? '220px'
+                              : !item.new_word && (item.header_con || item.label)
+                                ? '240px'
+                                : '294px',
+                        }"
+                        :src="item.pic_url"
+                        :preview-src-list="[item.pic_url]"
+                        fit="contain"
+                      />
+                    </div>
+                    <h2 :class="['con-preview', item.file_list[0] ? '' : 'con-preview-big']" v-if="item.new_word">
+                      {{ item.new_word }}
+                    </h2>
+
+                    <a class="overturn-btn" @click="changeShowLeft(item)"><i class="el-icon-refresh"></i></a>
                   </div>
-                  <div v-if="sItem.liju_list" class="collocation">
-                    <span>例句:</span>
-                    <div>
-                      <b v-html="sItem.liju_list"></b>
+                  <div
+                    class="right right-preview left-preview right-preview-rota"
+                    v-else
+                    :style="{
+                      borderColor: '#f44444',
+                      paddingTop: item.collocation || item.liju_list || item.definition_list ? '' : '50px',
+                    }"
+                  >
+                    <div class="header-info-preview">
+                      <h5 :style="{ textAlign: 'left' }">{{ item.header_con }}</h5>
+                      <label :style="{ background: '#f44444' }">{{ item.label }}</label>
+                    </div>
+                    <div
+                      :style="{
+                        display: 'flex',
+                        justifyContent:
+                          !(item.collocation && item.liju_list) && item.new_word.length < 4 ? 'center' : 'auto',
+                        columnGap: '16px',
+                      }"
+                    >
+                      <div style="width: max-content" v-if="item.hz_info.length > 0">
+                        <AudioPlay :style="{ background: '#f44444' }" :file-id="item.mp3_list" v-if="item.mp3_list" />
+                        <p
+                          :style="{ color: '#f44444' }"
+                          v-if="item.pinyin && item.pinyin.split(' ').length === 1"
+                          class="pinyin-box"
+                        >
+                          {{ item.pinyin }}
+                        </p>
+                        <div class="hz-box">
+                          <div class="hz-item" v-for="(itemh, indexh) in item.hz_info" :key="indexh">
+                            <p :style="{ color: '#f44444' }" v-if="item.pinyin && item.pinyin.split(' ').length > 1">
+                              {{ item.pinyin.split(' ')[indexh] ? item.pinyin.split(' ')[indexh] : '' }}
+                            </p>
+                            <Strockplay
+                              className="adult-strockplay"
+                              :Book_text="itemh.con"
+                              :playStorkes="true"
+                              :strokePlayColor="'#f44444'"
+                              :strokeColor="'#000000'"
+                              :palyWidth="'18px'"
+                              :BoxbgType="'0'"
+                              :curItem="itemh.hzDetail.hz_json"
+                              :targetDiv="'writeTops-item-' + indexh + '-' + itemh.con + '-' + index + '-' + indexs"
+                              :class="[indexh !== 0 ? 'writeTop-item-noLeft' : '']"
+                              class="writeTop-item"
+                              :style="{ borderColor: '#f44444' }"
+                            />
+                          </div>
+                        </div>
+                      </div>
+
+                      <div
+                        class="definition_list-box"
+                        v-if="(item.collocation || item.liju_list) && item.new_word.length < 4"
+                        :style="{
+                          flex: '1',
+                          marginTop: item.mp3_list ? '85px' : '27px',
+                        }"
+                      >
+                        <div v-if="item.cixing">
+                          <label class="card-label">词性:</label>
+                          <p v-html="item.cixing"></p>
+                        </div>
+                        <div v-if="item.definition_list">
+                          <label class="card-label">释义:</label>
+                          <p v-html="item.definition_list"></p>
+                        </div>
+                      </div>
                     </div>
+                    <div
+                      class="definition_list-box"
+                      :style="{
+                        width:
+                          !(item.collocation || item.liju_list) && item.new_word.length < 4
+                            ? item.hz_info.length * 86 + 'px'
+                            : '',
+                        margin:
+                          !(item.collocation || item.liju_list) && item.new_word.length < 4 ? '16px auto 0 auto' : '',
+                      }"
+                      v-if="item.collocation || item.liju_list || item.definition_list || item.cixing"
+                    >
+                      <template v-if="!(item.collocation || item.liju_list) || item.new_word.length >= 4">
+                        <div v-if="item.cixing">
+                          <label class="card-label">词性:</label>
+                          <p v-html="item.cixing"></p>
+                        </div>
+                        <div v-if="item.definition_list">
+                          <label class="card-label">释义:</label>
+                          <p v-html="item.definition_list"></p>
+                        </div>
+                      </template>
+                      <div v-if="item.collocation">
+                        <label class="card-label">搭配:</label>
+                        <p v-html="item.collocation"></p>
+                      </div>
+                      <div v-if="item.liju_list">
+                        <label class="card-label">例句:</label>
+                        <p v-html="item.liju_list"></p>
+                      </div>
+                    </div>
+                    <a class="overturn-btn" @click="changeShowLeft(item)"><i class="el-icon-refresh"></i></a>
                   </div>
                 </div>
-              </li>
-            </ul>
-          </div>
+              </div>
+            </div>
+          </template>
         </el-collapse-transition>
         <div class="practiceBox" v-if="detailShow">
           <WordPhraseDetail
@@ -164,16 +325,21 @@ import SoundRecord from '../../common/SoundRecord.vue';
 import AudioLine from '../voice_matrix/components/AudioLine.vue';
 import { GetFileURLMap } from '@/api/app';
 import WordPhraseDetail from './components/WordPhraseDetail.vue';
+import AudioPlay from './components/AudioPlay.vue';
+import Strockplay from './components/Strockplay.vue';
 
 export default {
   name: 'NewWordPreview',
+
   components: {
     SoundRecord,
     AudioLine,
     WordPhraseDetail,
+    AudioPlay,
+    Strockplay,
   },
   mixins: [PreviewMixin],
-  inject: ['courseware_id', 'bookInfo'],
+  inject: ['bookInfo'],
   data() {
     return {
       data: getNewWordData(),
@@ -194,6 +360,8 @@ export default {
       ed: null,
       indexStr: Math.random().toString(36).substring(2, 10),
       is_change: false,
+      is_list: true, // 列表还是卡片展示
+      courseware_id: this.$route.params.id,
     };
   },
   watch: {
@@ -338,6 +506,13 @@ export default {
             this.$set(item, 'mp3_list_url', url_map[item.mp3_list]);
           });
         }
+        if (item.file_list && item.file_list[0]) {
+          GetFileURLMap({ file_id_list: item.file_list }).then(({ url_map }) => {
+            this.$set(item, 'pic_url', url_map[item.file_list[0]]);
+          });
+        }
+        this.$set(item, 'show_left', true);
+        this.$set(item, 'isFlipped', false);
       });
       option_list.forEach((item, index) => {
         optionRes = optionRes.concat(item);
@@ -387,6 +562,11 @@ export default {
       tmsIndex = tmsIndex || 0;
       this.$set(this.data.answer.answer_list[tmIndex][tmsIndex], 'recordList', list);
     },
+    // 翻面
+    changeShowLeft(item) {
+      item.show_left = !item.show_left;
+      item.isFlipped = !item.isFlipped;
+    },
   },
 };
 </script>
@@ -670,6 +850,405 @@ export default {
   .luyin-box-wordphrase {
     height: 24px;
   }
+
+  .NPC-word-tile {
+    display: flex;
+    flex-flow: wrap;
+    gap: 20px;
+    padding: 20px 0;
+  }
+
+  .writeTop {
+    position: relative;
+    display: flex;
+    column-gap: 8px;
+    width: 400px;
+
+    .left,
+    .right {
+      position: relative;
+      box-sizing: border-box;
+      width: 100%;
+      min-height: 270px;
+      padding: 8px 12px 18px;
+      overflow: hidden;
+      background: #fff;
+      border: 4px solid #fff;
+      border-radius: 24px;
+
+      .header-info {
+        display: flex;
+        justify-content: space-between;
+        width: 100%;
+        margin-bottom: 12px;
+
+        :deep .el-input__inner {
+          height: 24px;
+          padding: 0;
+          font-size: 24px;
+          font-weight: 400;
+          line-height: 100%;
+          color: rgba(0, 0, 0, 100%);
+          border: none;
+        }
+
+        .label {
+          :deep .el-input__inner {
+            text-align: right;
+          }
+        }
+      }
+    }
+
+    min-height: 332px;
+    transition: 0.6s;
+    perspective: 1000px;
+    transform-style: preserve-3d;
+
+    .left-preview {
+      padding-top: 40px;
+
+      // padding-bottom: 32px;
+      // position: absolute;
+      backface-visibility: hidden;
+    }
+
+    .header-info-preview {
+      position: absolute;
+      top: 0;
+      left: 0;
+      z-index: 1;
+      width: 100%;
+
+      h5 {
+        padding: 0 12px;
+        margin: 0;
+        font-size: 20px;
+        font-weight: 400;
+        line-height: 32px;
+        color: #000;
+      }
+
+      label {
+        position: absolute;
+        top: -4px;
+        right: -4px;
+        padding: 0 16px 0 8px;
+        font-size: 20px;
+        font-weight: 500;
+        line-height: 150%;
+        color: #fff;
+        background: #fff;
+        border-radius: 0 8px;
+      }
+    }
+
+    .left-big {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .del-btn {
+      position: absolute;
+      right: 8px;
+      bottom: 8px;
+      padding: 5px 8px;
+      font-size: 24px;
+      color: #fff;
+      cursor: pointer;
+      background: #f56767;
+      border-radius: 40px;
+    }
+
+    .overturn-btn {
+      position: absolute;
+      right: 8px;
+      bottom: 8px;
+      width: 40px;
+      height: 40px;
+      padding: 8px;
+      font-size: 24px;
+      line-height: 1;
+      color: #fff;
+      cursor: pointer;
+      background: #e0e0e0;
+      border-radius: 8px;
+    }
+
+    .el-icon-zoom-in,
+    .filt-check {
+      position: absolute;
+      bottom: 8px;
+      left: 8px;
+      width: 30px;
+      height: 30px;
+      font-size: 24px;
+      cursor: pointer;
+
+      :deep .el-checkbox__inner {
+        width: 24px;
+        height: 24px;
+      }
+
+      :deep .el-checkbox__inner::after {
+        left: 8px;
+        width: 6px;
+        height: 14px;
+      }
+    }
+
+    .right {
+      display: flex;
+      flex-flow: wrap;
+      row-gap: 8px;
+      align-items: center;
+      padding: 16px 24px 26px;
+
+      .card-label {
+        width: 100%;
+        height: 22px;
+        font-size: 14px;
+        font-weight: 400;
+        line-height: 22px;
+        color: #4e5969;
+      }
+
+      :deep .el-textarea {
+        height: 64px;
+      }
+
+      .config-box {
+        display: flex;
+        align-items: center;
+        width: 100%;
+
+        span {
+          margin-right: 8px;
+          font-size: 14px;
+          line-height: 20px;
+          color: #000;
+        }
+
+        .el-color-picker {
+          height: 32px;
+        }
+
+        :deep .el-color-picker__trigger {
+          height: 32px;
+        }
+
+        .el-radio {
+          margin-right: 8px;
+        }
+
+        .el-radio-group {
+          display: flex;
+        }
+
+        :deep .el-radio__input.is-checked .el-radio__inner {
+          background: #000;
+          border-color: #000;
+        }
+
+        :deep .el-radio__input.is-checked + .el-radio__label {
+          color: #000;
+        }
+      }
+    }
+
+    .right-preview {
+      display: block;
+      padding: 36px;
+
+      .pinyin-box {
+        margin-bottom: 8px;
+        font-family: 'League';
+        font-feature-settings: 'cv01' on;
+
+        // font-size: 18px;
+        line-height: 120%;
+        color: #de4444;
+        text-align: center;
+      }
+
+      .hz-box {
+        width: 100%;
+
+        .hz-item {
+          text-align: center;
+
+          :deep .strockplayInner {
+            width: 76px;
+            height: 76px;
+          }
+
+          p {
+            margin: 0 0 8px;
+            font-family: 'League';
+            font-size: 18px;
+            font-feature-settings: 'cv01' on;
+            line-height: 120%;
+            color: #de4444;
+          }
+        }
+      }
+
+      :deep .audio-wrapper {
+        box-sizing: border-box;
+        width: 50px;
+        height: 50px;
+        padding: 13px;
+        margin: 0 auto 8px;
+        cursor: pointer;
+        background: #f3f3f3;
+        border-radius: 40px;
+
+        .voice-play {
+          width: 24px;
+          height: 24px;
+        }
+      }
+
+      .definition_list-box {
+        margin-top: 16px;
+        white-space: pre;
+
+        > div {
+          display: flex;
+          margin-bottom: 8px;
+
+          label,
+          p {
+            width: 40px;
+            font-size: 14px;
+            font-weight: 400;
+            line-height: 150%;
+            color: #000;
+          }
+
+          label {
+            width: 47px;
+          }
+
+          p {
+            flex: 1;
+            line-height: 0;
+            word-break: break-word;
+            white-space: pre-wrap;
+          }
+        }
+      }
+
+      :deep p {
+        margin: 0;
+        line-height: 1.5;
+      }
+    }
+
+    .right-preview-rota {
+      transform: rotateY(180deg);
+    }
+
+    .item-image {
+      position: relative;
+      overflow: hidden;
+      font-size: 0;
+
+      // background: #f2f3f5;
+      border-radius: 8px;
+
+      .item-image-del {
+        position: absolute;
+        top: 8px;
+        right: 8px;
+        display: block;
+        width: 16px;
+        height: 16px;
+        padding: 8px;
+        font-size: 16px;
+        color: #ee3232;
+        cursor: pointer;
+        background-color: #fff;
+        border-radius: 50%;
+        box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 25%);
+      }
+    }
+
+    .item-con {
+      display: flex;
+      align-items: center;
+      width: 50%;
+      margin-top: 16px;
+
+      label {
+        width: 44px;
+        font-size: 14px;
+        font-weight: 400;
+        line-height: 22px;
+        color: #4e5969;
+      }
+
+      :deep .el-input__inner {
+        width: 235px;
+        height: 32px;
+        font-family: '楷体';
+        font-size: 14px;
+        font-weight: 400;
+        line-height: 22px;
+        background: #f2f3f5;
+        border: none;
+        border-radius: 2px;
+      }
+
+      .pinyin {
+        :deep .el-input__inner {
+          font-family: 'League';
+        }
+      }
+    }
+
+    .con-preview {
+      margin-top: 8px;
+      font-family: '楷体';
+      font-size: 38px;
+      font-weight: 400;
+      line-height: 100%;
+      color: #000;
+      text-align: center;
+
+      &-big {
+        margin: 0;
+        font-size: 86px;
+      }
+    }
+
+    .writeTop-row {
+      display: flex;
+      justify-content: center;
+    }
+  }
+
+  .flipped {
+    transform: rotateY(180deg);
+  }
+
+  .flipped-back {
+    transform: rotateY(180deg);
+  }
+
+  .hz-box {
+    display: flex;
+    width: max-content;
+  }
+
+  .writeTop-item {
+    border: 1px solid #de4444;
+  }
+
+  .writeTop-item-noLeft {
+    border-left: none;
+  }
 }
 </style>
 <style lang="scss">
@@ -703,6 +1282,7 @@ export default {
 
     .NPC-top-right {
       display: flex;
+      gap: 4px;
       align-items: center;
       justify-content: flex-start;
       cursor: pointer;
@@ -717,7 +1297,6 @@ export default {
       img {
         width: 16px;
         height: 16px;
-        margin-left: 4px;
       }
     }
 

+ 105 - 0
src/views/book/courseware/preview/components/new_word/components/AudioPlay.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="audio-wrapper" @click="playAudio">
+    <span :class="[url ? 'audio-play' : 'audio-play not-url']">
+      <img
+        :src="audio.paused ? require('@/assets/voice-white.png') : require('@/assets/voice-play.png')"
+        class="voice-play"
+      />
+    </span>
+    <audio :id="fileId" :ref="fileId" :src="url" preload="metadata"></audio>
+  </div>
+</template>
+
+<script>
+import { GetFileURLMap } from '@/api/app';
+
+export default {
+  name: 'AudioPlay',
+  props: {
+    fileId: {
+      type: String,
+      required: true,
+    },
+    themeColor: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      url: '',
+      audio: {
+        paused: true,
+      },
+    };
+  },
+  computed: {},
+  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();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.audio-wrapper {
+  .audio-play {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 24px;
+    height: 24px;
+    color: #fff;
+    cursor: pointer;
+    border-radius: 50%;
+
+    &.not-url {
+      color: #a1a1a1;
+      cursor: not-allowed;
+    }
+
+    .voice-play {
+      width: 20px;
+      height: 20px;
+    }
+  }
+}
+</style>

+ 172 - 0
src/views/book/courseware/preview/components/new_word/components/Strockplay.vue

@@ -0,0 +1,172 @@
+<!--  -->
+<template>
+  <div class="strockplayInner" :class="className">
+    <div @click="playHanzi" class="strock-play-box" :style="{ width: palyWidth, height: palyWidth }" v-if="playStorkes">
+      <svg-icon
+        icon-class="playStorkes"
+        className="playStorkes-btn"
+        v-if="playStorkes"
+        @click="playHanzi"
+        :style="{ color: strokePlayColor, width: palyWidth, height: palyWidth }"
+      />
+    </div>
+    <div :class="wordNum == '2' ? 'morewords' : ''" :id="targetDiv" class="character-target-div">
+      <svg-icon icon-class="tian" className="tian-bg" v-if="BoxbgType == 0" :style="{ color: '#DEDEDE' }" />
+      <svg-icon icon-class="mi" className="tian-bg" v-if="BoxbgType == 1" :style="{ color: '#DEDEDE' }" />
+    </div>
+  </div>
+</template>
+
+<script>
+const HanziWriter = require('hanzi-writer');
+export default {
+  components: {},
+  props: [
+    'targetDiv',
+    'Book_text',
+    'playStorkes',
+    'strokeColor',
+    'wordNum',
+    'className',
+    'strokePlayColor',
+    'palyWidth',
+    'BoxbgType',
+    'curItem',
+  ],
+  data() {
+    return {
+      writer: null,
+    };
+  },
+  computed: {},
+  watch: {
+    targetDiv: {
+      handler: function (val, oldVal) {
+        if (val != oldVal) {
+          let _this = this;
+          _this.$nextTick(() => {
+            _this.initHanziwrite();
+          });
+        }
+      },
+      // 深度观察监听
+      deep: true,
+    },
+    strokeColor: {
+      handler: function (val, oldVal) {
+        if (val != oldVal) {
+          let _this = this;
+          _this.$nextTick(() => {
+            _this.initHanziwrite();
+          });
+        }
+      },
+      // 深度观察监听
+      deep: true,
+    },
+  },
+  //方法集合
+  methods: {
+    initHanziwrite() {
+      let _this = this;
+      let node = document.getElementById(`${_this.targetDiv}`);
+      if (node.children.length > 1) {
+        node.removeChild(node.children[1]);
+      }
+      //var ren = require("hanzi-writer-data/国");
+      _this.writer = HanziWriter.default.create(_this.targetDiv, _this.Book_text, {
+        charDataLoader: function (char, onComplete) {
+          let charData = _this.handleData();
+          onComplete(charData);
+        },
+        padding: 5,
+        showOutline: true,
+        strokeColor: _this.strokeColor ? _this.strokeColor : '#333',
+      });
+    },
+    playHanzi() {
+      let _this = this;
+      _this.writer.animateCharacter();
+    },
+    handleData() {
+      if (this.curItem) {
+        let charData = JSON.parse(JSON.stringify(this.curItem));
+        return charData;
+      }
+    },
+  },
+  //生命周期 - 创建完成(可以访问当前this实例)
+  created() {},
+  //生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {
+    let _this = this;
+    _this.$nextTick(() => {
+      _this.initHanziwrite();
+    });
+  },
+  beforeCreate() {}, //生命周期 - 创建之前
+  beforeMount() {}, //生命周期 - 挂载之前
+  beforeUpdate() {}, //生命周期 - 更新之前
+  updated() {}, //生命周期 - 更新之后
+  beforeDestroy() {}, //生命周期 - 销毁之前
+  destroyed() {}, //生命周期 - 销毁完成
+  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+};
+</script>
+<style lang="scss" scoped>
+//@import url(); 引入公共css类
+.strockplayInner {
+  position: relative;
+  width: 100%; //444px
+  height: 100%; //480px
+  margin: 0 auto;
+
+  .strock-play-box {
+    position: absolute;
+    top: 0;
+    right: 0;
+    z-index: 999;
+    cursor: pointer;
+  }
+}
+
+.character-target-div {
+  position: relative;
+  z-index: 998;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%; //444px
+  height: 100%; //480px
+  //   background: #fff url("../../../../assets/NPC/chinaTianRed.png") center
+  //     no-repeat;
+  background-size: 100% 100%;
+  border-radius: 24px;
+
+  &.morewords {
+    // background: url("../../../../assets/NPC/chinaTianRed.png") center no-repeat;
+    background-size: 100% 100%;
+  }
+}
+
+.tian-bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: -1;
+  width: 100%;
+  height: 100%;
+}
+
+.animate-butto {
+  width: 240px;
+  height: 160px;
+  font-size: 28px;
+}
+
+.playStorkes-btn {
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+</style>

部分文件因为文件数量过多而无法显示