Ver código fonte

Merge branch 'master' of http://gcls-git.helxsoft.cn/GCLS/eep_page

dusenyao 1 mês atrás
pai
commit
2df8ca1568

+ 28 - 0
src/api/article.js

@@ -0,0 +1,28 @@
+import { http } from '@/utils/http';
+import store from '@/store';
+import { app } from '@/store/mutation-types';
+
+//分句
+export function segSentences(data) {
+  return http.post(`/OtherSysTool/GCLSHMToolsServer/api/nlp/chinese/text2Sentences`, data)
+}
+
+//批量分词
+export function BatchSegContent(data) {
+  return http.post( `/OtherSysTool/GCLSHMToolsServer/api/nlp/chinese/text2word`, data)
+}
+
+//音频预处理
+export function prepareTranscribe(data) {
+  return http.post( `/OtherSysTool/GCLSHMToolsServer/api/xunfei/speech/prepareTranscribe`,data)
+}
+
+//音频识别结果获取
+export function getWordTime(data) {
+  return http.post(`/OtherSysTool/GCLSHMToolsServer/api/xunfei/speech/getResult`,data)
+}
+
+// 文本分析 
+export function analysSubmit(data) {
+  return http.post(`/OtherSysTool/TeachingServer/TextAnalyser/Analyse`,data)
+}

+ 1 - 1
src/views/book/courseware/create/components/base/common/UploadFile.vue

@@ -220,7 +220,7 @@ export default {
       if (this.type === 'audio') {
         fileType = ['mp3', 'acc', 'wma', 'wav'];
         typeTip = '音频文件只能是 mp3、acc、wma、wav格式!';
-      } else if (this.type === 'picture') {
+      } else if (this.type === 'picture' || this.type === 'image_text') {
         fileType = ['jpg', 'png', 'jpeg'];
         typeTip = '图片文件只能是 jpg、png、jpeg 格式!';
       } else if (this.type === 'video') {

+ 73 - 2
src/views/book/courseware/create/components/base/pinyin_base/PinyinBase.vue

@@ -47,6 +47,17 @@
             <SoundRecord v-else :wav-blob.sync="data.audio_file_id" />
           </div>
         </template>
+        <el-button @click="identifyText" v-if="data.property.fun_type === 'input'">识别</el-button>
+        <div class="correct-answer" v-if="data.property.fun_type === 'input'">
+          <el-input
+            v-for="(item, i) in data.answer.answer_list"
+            :key="item.mark"
+            v-model="item.con"
+            @blur="handleTone(item.con, i)"
+          >
+            <span slot="prefix">{{ i + 1 }}.</span>
+          </el-input>
+        </div>
       </div>
     </template>
   </ModuleBase>
@@ -173,7 +184,10 @@ export default {
       this.res_arr = [];
       this.$set(this.matically_pinyin_obj, item.mark, []);
       this.$set(this.matically_pinyin_str, item.mark, '');
-      this.data.answer.answer_list = [];
+      if (this.data.property.fun_type !== 'input') {
+        this.data.answer.answer_list = [];
+      }
+
       content_arr.forEach((items, index) => {
         let items_trim = items.trim();
         if (items_trim) {
@@ -212,8 +226,10 @@ export default {
         mark: item.mark,
         value: select_item.split(' '),
       };
-      this.data.answer.answer_list.push(obj);
+
       item.content_view = content_preview.trim().split(' ');
+      if (this.data.property.fun_type === 'input') return;
+      this.data.answer.answer_list.push(obj);
       // item.matically_pinyin = matically_pinyin.trim().split(' ').join(',');
     },
     // 改变类型
@@ -226,6 +242,45 @@ export default {
         item.audio_file_id = '';
       }
     },
+    // 识别文本
+    identifyText() {
+      this.data.content_view = [];
+      this.data.answer.answer_list = [];
+      let arr = this.matically_pinyin_str[this.data.mark].split(/_{3,}/g);
+      let inputIndex = 0;
+      arr.forEach((item, index) => {
+        let obj = {
+          con: item,
+          type: 'text',
+        };
+        this.data.content_view.push(obj);
+        if (index !== arr.length - 1) {
+          let objs = {
+            con: '',
+            type: 'input',
+            inputIndex: inputIndex,
+            mark: getRandomNumber(),
+          };
+          this.data.content_view.push(objs);
+          this.data.answer.answer_list.push(JSON.parse(JSON.stringify(objs)));
+          inputIndex++;
+        }
+      });
+    },
+    handleTone(value, i) {
+      if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
+      this.data.answer.answer_list[i].con = value
+        .trim()
+        .split(/\s+/)
+        .map((item) => {
+          return handleToneValue(item);
+        })
+        .map((item) =>
+          item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
+        )
+        .filter((item) => item.length > 0)
+        .join(' ');
+    },
   },
 };
 </script>
@@ -311,4 +366,20 @@ export default {
   display: flex;
   column-gap: 8px;
 }
+
+.correct-answer {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+
+  .el-input {
+    width: 180px;
+
+    :deep &__prefix {
+      display: flex;
+      align-items: center;
+      color: $text-color;
+    }
+  }
+}
 </style>

+ 1 - 1
src/views/book/courseware/create/components/question/article/Article.vue

@@ -82,7 +82,7 @@ import NewWord from './NewWord.vue';
 import Notes from './Notes.vue';
 
 import { getArticleData } from '@/views/book/courseware/data/article';
-import { segSentences, BatchSegContent, GetStaticResources, getWordTime, prepareTranscribe } from '@/api/app';
+import { segSentences, BatchSegContent, GetStaticResources, getWordTime, prepareTranscribe } from '@/api/article';
 const Base64 = require('js-base64').Base64;
 import cnchar from 'cnchar';
 

+ 400 - 0
src/views/book/courseware/create/components/question/image_text/ImageText.vue

@@ -0,0 +1,400 @@
+<template>
+  <ModuleBase :type="data.type">
+    <template #content>
+      <UploadFile
+        key="upload_image"
+        :courseware-id="courseware_id"
+        :component-id="id"
+        :type="data.type"
+        :total-size="data.total_size"
+        :file-list="data.image_list"
+        :file-id-list="data.image_id_list"
+        :file-info-list="data.image_info_list"
+        :label-text="labelText"
+        :accept-file-type="acceptFileType"
+        :icon-class="iconClass"
+        :limit="1"
+        :singleSize="500"
+        @updateFileList="updateFileList"
+      />
+      <div class="image-size">
+        <span>图片大小</span
+        ><el-input-number v-model="data.image_width" :step="100" :min="200" :max="800"></el-input-number
+        ><el-input-number v-model="data.image_height" :step="100" :min="200" :max="800"></el-input-number>
+      </div>
+      <SelectUpload label="音频" type="audio" width="500px" @uploadSuccess="uploadAudioSuccess" />
+      <div v-if="data.mp3_list.length > 0" class="upload-file">
+        <div class="file-name">
+          <span>
+            <SvgIcon icon-class="mp3" size="12" />
+            <span>{{ data.mp3_list[0].name }}</span>
+          </span>
+          <span> 完成 </span>
+        </div>
+        <SvgIcon icon-class="delete-black" size="12" @click="removeFile" />
+      </div>
+      <el-radio-group v-model="isText" style="width: 100%; margin: 20px 0; text-align: center">
+        <el-radio-button :label="true">文字框</el-radio-button>
+        <el-radio-button :label="false">输入框</el-radio-button>
+      </el-radio-group>
+      <div
+        v-if="data.image_list[0]"
+        id="selectableArea"
+        @mousedown="startSelection"
+        @mousemove="updateSelection"
+        @mouseup="endSelection"
+        @mouseleave="endSelection"
+        :style="{
+          width: data.image_width + 'px',
+          height: data.image_height + 'px',
+          background: 'url(' + data.image_list[0].url + ') center / contain no-repeat',
+          position: 'relative',
+        }"
+      >
+        <div
+          v-if="isSelecting"
+          :style="{
+            position: 'absolute',
+            top: `${startY}px`,
+            left: `${startX}px`,
+            width: `${endX - startX}px`,
+            height: `${endY - startY}px`,
+            border: isText ? '2px solid #165DFF' : '2px solid #f90',
+          }"
+        ></div>
+        <template v-for="(itemh, indexh) in data.text_list">
+          <div
+            v-if="indexh === hotspotsActiveIndex"
+            :key="indexh"
+            :style="{
+              position: 'absolute',
+              top: `${itemh.y}`,
+              left: `${itemh.x}`,
+              width: `${itemh.width}`,
+              height: `${itemh.height}`,
+              border: '2px solid #165DFF',
+            }"
+          >
+            <label
+              :style="{
+                position: 'absolute',
+                top: '-13px',
+                right: '-13px',
+                width: '26px',
+                height: '26px',
+                border: '2px solid #165DFF',
+                textAlign: 'center',
+                borderRadius: '50%',
+                boxShadow: ' 0px 5px 5px -3px #0000001A',
+                background: '#fff',
+                color: '#165DFF',
+              }"
+              >{{ indexh + 1 }}</label
+            >
+          </div>
+        </template>
+        <template v-for="(itemh, indexhs) in data.input_list">
+          <div
+            v-if="indexhs === inputActiveIndex"
+            :key="indexhs"
+            :style="{
+              position: 'absolute',
+              top: `${itemh.y}`,
+              left: `${itemh.x}`,
+              width: `${itemh.width}`,
+              height: `${itemh.height}`,
+              border: '2px solid #f90',
+            }"
+          >
+            <label
+              :style="{
+                position: 'absolute',
+                top: '-13px',
+                right: '-13px',
+                width: '26px',
+                height: '26px',
+                border: '2px solid #f90',
+                textAlign: 'center',
+                borderRadius: '50%',
+                boxShadow: ' 0px 5px 5px -3px #0000001A',
+                background: '#fff',
+                color: '#f90',
+              }"
+              >{{ indexhs + 1 }}</label
+            >
+          </div>
+        </template>
+      </div>
+      <template v-if="data.image_list[0] && data.text_list.length > 0">
+        <h4>文本框内容</h4>
+        <ul class="hotspots-list">
+          <li v-for="(itemh, indexh) in data.text_list" :key="indexh">
+            <span>{{ indexh + 1 }}</span>
+            <div class="content">
+              <el-input
+                v-if="indexh === hotspotsActiveIndex"
+                type="textarea"
+                :rows="2"
+                placeholder="请输入"
+                v-model="itemh.text"
+                maxlength="500"
+                show-word-limit
+              >
+              </el-input>
+              <p v-else>{{ itemh.text }}</p>
+            </div>
+            <el-button
+              v-if="hotspotsActiveIndex !== indexh"
+              type="primary"
+              size="small"
+              @click="hotspotsActiveIndex = indexh"
+              >编辑</el-button
+            >
+            <el-button v-else type="primary" size="small" @click="hotspotsActiveIndex = null">保存</el-button>
+            <el-button type="danger" size="small" @click="deletehotspots(indexh)">删除</el-button>
+          </li>
+        </ul>
+      </template>
+      <template v-if="data.image_list[0] && data.input_list.length > 0">
+        <h4>输入框答案</h4>
+        <ul class="hotspots-list">
+          <li v-for="(itemh, indexh) in data.input_list" :key="indexh">
+            <span>{{ indexh + 1 }}</span>
+            <div class="content">
+              <el-input
+                v-if="indexh === inputActiveIndex"
+                type="textarea"
+                :rows="2"
+                placeholder="请输入"
+                v-model="itemh.text"
+                maxlength="500"
+                show-word-limit
+              >
+              </el-input>
+              <p v-else>{{ itemh.text }}</p>
+            </div>
+            <el-button v-if="inputActiveIndex !== indexh" type="primary" size="small" @click="inputActiveIndex = indexh"
+              >编辑</el-button
+            >
+            <el-button v-else type="primary" size="small" @click="inputActiveIndex = null">保存</el-button>
+            <el-button type="danger" size="small" @click="deletehotspotsInput(indexh)">删除</el-button>
+          </li>
+        </ul>
+      </template>
+    </template>
+  </ModuleBase>
+</template>
+
+<script>
+import ModuleMixin from '../../common/ModuleMixin';
+import UploadFile from '../../base/common/UploadFile.vue';
+import { getImageTextData } from '@/views/book/courseware/data/imageText';
+import SelectUpload from '@/views/book/courseware/create/components/common/SelectUpload.vue';
+import { GetFileURLMap } from '@/api/app';
+
+export default {
+  name: 'ImageTextPage',
+  components: { UploadFile, SelectUpload },
+  mixins: [ModuleMixin],
+  data() {
+    return {
+      data: getImageTextData(),
+      labelText: '背景图',
+      acceptFileType: '.jpg,.png,.jpeg',
+      iconClass: 'picture',
+      isSelecting: false,
+      startX: 0,
+      startY: 0,
+      endX: 0,
+      endY: 0,
+      hotspotsActiveIndex: null, // 当前编辑文本框热区索引
+      genloading: false, // 字幕节点loading
+      isText: true, // 框选是文本还是输入框
+      inputActiveIndex: null, // 当前编辑输入框热区索引
+    };
+  },
+  watch: {
+    'data.image_list': {
+      handler(val) {
+        if (val.length > 0) {
+          this.handleData();
+        }
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    updateFileList({ file_list, file_id_list, file_info_list }) {
+      this.data.image_list = file_list;
+      this.data.image_id_list = file_id_list;
+      this.data.image_info_list = file_info_list;
+    },
+    uploadAudioSuccess(fileList) {
+      if (fileList.length > 0) {
+        const { file_name: name, file_url: temporary_url, file_id, media_duration } = fileList[0];
+        this.data.mp3_list = [
+          {
+            name,
+            media_duration,
+            temporary_url,
+            url: file_id,
+            file_id,
+          },
+        ];
+      }
+    },
+    removeFile() {
+      this.data.mp3_list = [];
+    },
+    startSelection(event) {
+      this.isSelecting = true;
+      let clientRect = document.getElementById('selectableArea').getBoundingClientRect();
+
+      this.startX = event.clientX - clientRect.left;
+      this.startY = event.clientY - clientRect.top;
+    },
+    updateSelection(event) {
+      if (!this.isSelecting) return;
+      let clientRect = document.getElementById('selectableArea').getBoundingClientRect();
+
+      this.endX = event.clientX - clientRect.left;
+      this.endY = event.clientY - clientRect.top;
+    },
+    endSelection() {
+      this.isSelecting = false;
+      const width = Math.abs(this.endX - this.startX);
+      const height = Math.abs(this.endY - this.startY);
+      const x = this.endX > this.startX ? this.startX + 'px' : this.endX + 'px';
+      const y = this.endY > this.startY ? this.startY + 'px' : this.endY + 'px';
+      let obj = {
+        id: Math.random().toString(36).substring(2, 10),
+        width: width + 'px',
+        height: height + 'px',
+        x: x,
+        y: y,
+        text: '',
+      };
+      this.startX = 0;
+      this.endX = 0;
+      this.startY = 0;
+      this.endY = 0;
+      if (width && height && this.isText) {
+        this.data.text_list.push(obj);
+
+        this.hotspotsActiveIndex = this.data.text_list.length - 1;
+      } else if (width && height && !this.isText) {
+        this.data.input_list.push(obj);
+
+        this.inputActiveIndex = this.data.input_list.length - 1;
+      }
+    },
+    handleData() {
+      this.data.image_list.forEach((item) => {
+        GetFileURLMap({ file_id_list: [item.file_id] }).then(({ url_map }) => {
+          this.$set(item, 'url', url_map[item.file_id]);
+        });
+      });
+    },
+    // 删除文本框热区
+    deletehotspots(index) {
+      this.data.text_list.splice(index, 1);
+      if (this.hotspotsActiveIndex === index) {
+        this.hotspotsActiveIndex = null;
+      }
+    },
+    // 删除输入框热区
+    deletehotspotsInput(index) {
+      this.data.input_list.splice(index, 1);
+      if (this.inputActiveIndex === index) {
+        this.inputActiveIndex = null;
+      }
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+.upload-file {
+  display: flex;
+  column-gap: 12px;
+  align-items: center;
+  margin: 8px 0;
+
+  .file-name {
+    display: flex;
+    column-gap: 14px;
+    align-items: center;
+    justify-content: space-between;
+    max-width: 360px;
+    padding: 8px 12px;
+    font-size: 14px;
+    color: #1d2129;
+    background-color: #f7f8fa;
+
+    span {
+      display: flex;
+      column-gap: 14px;
+      align-items: center;
+    }
+  }
+
+  .svg-icon {
+    cursor: pointer;
+  }
+}
+
+.image-size {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  padding: 20px 0;
+}
+
+.hotspots-list {
+  padding: 0;
+  margin: 0;
+  list-style: none;
+
+  li {
+    display: flex;
+    column-gap: 8px;
+    width: 100%;
+    padding: 16px;
+    margin-bottom: 4px;
+    border: 1px solid rgba(0, 0, 0, 8%);
+  }
+
+  span {
+    display: block;
+    width: 24px;
+    height: 24px;
+    font-size: 14px;
+    font-weight: 600;
+    line-height: 24px;
+    color: #fff;
+    text-align: center;
+    background: #004eff;
+  }
+
+  .el-button--danger,
+  .el-button--primary {
+    height: 32px;
+    margin-left: 0;
+  }
+
+  .content {
+    flex: 1;
+
+    .el-textarea {
+      width: 100%;
+    }
+
+    p {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 400;
+      line-height: 24px;
+      color: #000;
+    }
+  }
+}
+</style>

+ 57 - 0
src/views/book/courseware/create/components/question/image_text/ImageTextSetting.vue

@@ -0,0 +1,57 @@
+<template>
+  <div>
+    <el-form :model="property" label-width="72px" label-position="left">
+      <SerailNumber :property="property" />
+
+      <el-form-item label="拼音">
+        <el-switch v-model="property.view_pinyin" active-value="true" inactive-value="false" />
+      </el-form-item>
+      <el-form-item label="拼音位置">
+        <el-radio
+          v-for="{ value, label } in pinyinPositionList"
+          :key="value"
+          v-model="property.pinyin_position"
+          :label="value"
+          :disabled="!isEnable(property.view_pinyin)"
+        >
+          {{ label }}
+        </el-radio>
+      </el-form-item>
+      <el-form-item label="">
+        <el-checkbox
+          v-model="property.is_first_sentence_first_hz_pinyin_first_char_upper_case"
+          :disabled="!isEnable(property.view_pinyin)"
+          >句首大写</el-checkbox
+        >
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
+import { getImageTextProperty } from '@/views/book/courseware/data/imageText';
+import { isEnable, pinyinPositionList } from '@/views/book/courseware/data/common';
+
+export default {
+  name: 'NewWordSetting',
+  mixins: [SettingMixin],
+  data() {
+    return {
+      isEnable,
+      property: getImageTextProperty(),
+      pinyinPositionList,
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.el-form {
+  @include setting-base;
+}
+</style>

+ 11 - 0
src/views/book/courseware/data/bookType.js

@@ -55,6 +55,8 @@ import Judge from '../create/components/question/judge/Judge.vue';
 import JudgeSetting from '../create/components/question/judge/JudgeSetting.vue';
 import WriteBase from '../create/components/base/write_base/WriteBase.vue';
 import WriteBaseSetting from '../create/components/base/write_base/WriteBaseSetting.vue';
+import ImageText from '../create/components/question/image_text/ImageText.vue';
+import ImageTextSetting from '../create/components/question/image_text/ImageTextSetting.vue';
 
 // 预览组件页面列表
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
@@ -86,6 +88,7 @@ import InputPreview from '../preview/components/input/InputPreview.vue';
 
 import JudgePreview from '../preview/components/judge/JudgePreview.vue';
 import WriteBasePreview from '../preview/components/write_base/WriteBasePreview.vue';
+import ImageTextPreview from '../preview/components/image_text/ImageTextPreview.vue';
 
 export const bookTypeOption = [
   {
@@ -323,6 +326,14 @@ export const bookTypeOption = [
         set: JudgeSetting,
         preview: JudgePreview,
       },
+      {
+        value: 'image_text',
+        label: '图片文本融合',
+        icon: '',
+        component: ImageText,
+        set: ImageTextSetting,
+        preview: ImageTextPreview,
+      },
     ],
   },
 ];

+ 43 - 0
src/views/book/courseware/data/imageText.js

@@ -0,0 +1,43 @@
+import {
+  displayList,
+  serialNumberTypeList,
+  serialNumberPositionList,
+  isEnable,
+  pinyinPositionList
+} from '@/views/book/courseware/data/common';
+
+export {  isEnable };
+
+
+export function getImageTextProperty() {
+  return {
+    serial_number: 1,
+    sn_type: serialNumberTypeList[0].value,
+    sn_position: serialNumberPositionList[0].value,
+    sn_display_mode: displayList[0].value,
+
+    view_pinyin: 'true', // 显示拼音
+    pinyin_position: pinyinPositionList[0].value, // top bottom
+    is_first_sentence_first_hz_pinyin_first_char_upper_case: 'false', // 句首大写
+  };
+}
+
+export function getImageTextData() {
+  return {
+    type: 'image_text',
+    title: '图片文本融合',
+    property: getImageTextProperty(),
+    total_size: 500, // 单位MB
+    mp3_list: [], // 音频列表
+    image_list: [], // 图片列表
+    image_info_list: [],
+    image_id_list: [], // 文件 id
+    image_width: 500, // 图片宽度px
+    image_height: 500, // 图片高度px
+    text_list: [], // 文字框列表
+    input_list:[], // 输入框列表
+    answer: {
+      answer_list: [],
+    },
+  };
+}

+ 4 - 4
src/views/book/courseware/data/pinyinBase.js

@@ -41,10 +41,10 @@ export const funList = [
     value: 'show',
     label: '拼音展示',
   },
-  // {
-  //   value: 'input',
-  //   label: '输入',
-  // },
+  {
+    value: 'input',
+    label: '拼音输入',
+  },
 ];
 
 // 标声调类型

+ 181 - 0
src/views/book/courseware/preview/components/image_text/ImageTextPreview.vue

@@ -0,0 +1,181 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="imageText-preview" :style="getAreaStyle()">
+    <template v-if="data.mp3_list && data.mp3_list.length > 0 && mp3_url">
+      <AudioLine
+        audioId="Audio"
+        :mp3="mp3_url"
+        :getCurTime="getCurTime"
+        :duration="data.mp3_list[0].media_duration"
+        :mp3Source="data.mp3_list[0].source"
+        :width="audio_width"
+        :ed="ed"
+        type="audioLine"
+        ref="audioLine"
+        @emptyEd="emptyEd"
+      />
+    </template>
+    <div
+      class="img-box"
+      :style="{
+        background: 'url(' + image_url + ') center / contain no-repeat',
+        width: data.image_width + 'px',
+        height: data.image_height + 'px',
+      }"
+      v-if="image_url"
+    >
+      <div
+        v-for="(itemP, indexP) in data.text_list"
+        :key="indexP"
+        :class="['position-item', mageazineDetailIndex === indexP ? 'active' : '']"
+        :style="{
+          width: itemP.width,
+          height: itemP.height,
+          left: itemP.x,
+          top: itemP.y,
+        }"
+        @click="handleChangePosition(indexP)"
+      ></div>
+      <div
+        v-for="(itemP, indexP) in data.input_list"
+        :key="indexP"
+        :class="['position-item position-item-input', 'active']"
+        :style="{
+          width: itemP.width,
+          height: itemP.height,
+          left: itemP.x,
+          top: itemP.y,
+        }"
+      >
+        <el-input
+          type="textarea"
+          v-model="answer.answer_list[indexP].text"
+          style="height: 100%"
+          placeholder="请输入"
+        ></el-input>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import PreviewMixin from '../common/PreviewMixin';
+import AudioLine from '../voice_matrix/components/AudioLine.vue';
+import { getImageTextData } from '@/views/book/courseware/data/imageText';
+import { GetFileURLMap } from '@/api/app';
+export default {
+  name: 'ImageTextPreview',
+
+  components: { AudioLine },
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      data: getImageTextData(),
+      curTime: 0,
+      paraIndex: -1, //段落索引
+      sentIndex: -1, // 句子索引
+      ed: undefined,
+      mp3_url: '',
+      image_url: '',
+      audio_width: 0,
+      mageazineDetailIndex: null, // 当前高亮第几个
+      mageazineDetailShow: false,
+      inputIndex: null,
+    };
+  },
+  methods: {
+    initData() {
+      if (!this.isJudgingRightWrong) {
+        this.answer.answer_list = [];
+        this.data.input_list.forEach((item, index) => {
+          let obj = {
+            text: '',
+            answer: item.text,
+            id: item.id,
+          };
+          this.answer.answer_list.push(obj);
+        });
+      }
+
+      this.data.image_list.forEach((item) => {
+        GetFileURLMap({ file_id_list: [item.file_id] }).then(({ url_map }) => {
+          this.image_url = url_map[item.file_id];
+        });
+      });
+      this.data.mp3_list.forEach((item) => {
+        GetFileURLMap({ file_id_list: [item.file_id] }).then(({ url_map }) => {
+          this.mp3_url = url_map[item.file_id];
+        });
+      });
+    },
+    getCurTime(curTime) {
+      this.curTime = curTime * 1000;
+      this.getSentIndex(this.curTime);
+    },
+    getSentIndex(curTime) {
+      // for (let i = 0; i < this.curQue.wordTime.length; i++) {
+      //   let bg = this.curQue.wordTime[i].bg;
+      //   let ed = this.curQue.wordTime[i].ed;
+      //   if (curTime >= bg && curTime <= ed) {
+      //     this.sentIndex = i;
+      //     break;
+      //   }
+      // }
+    },
+    emptyEd() {
+      this.ed = undefined;
+    },
+    // 切换画刊里面的卡片
+    handleChangePosition(index) {
+      if (this.$refs.audioLine.audio.playing) {
+        this.$refs.audioLine.PlayAudio();
+      }
+      this.mageazineDetailIndex = index;
+      this.mageazineDetailShow = true;
+    },
+  },
+  created() {
+    this.initData();
+  },
+  mounted() {
+    this.audio_width = document.getElementsByClassName('imageText-preview')[0].clientWidth - 150;
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.position-item {
+  position: absolute;
+  z-index: 1;
+  cursor: pointer;
+  border: 3px solid transparent;
+
+  &.active {
+    border-color: #ff1616;
+  }
+
+  &:hover {
+    border-color: #ff1616;
+  }
+
+  &.position-item-input {
+    border-color: #f90;
+  }
+
+  :deep .el-textarea__inner {
+    height: 100%;
+    font-family: 'League', '楷体';
+    text-align: center;
+    resize: none;
+    background: transparent;
+    border: none;
+  }
+}
+
+.img-box {
+  position: relative;
+  margin: 20px auto;
+}
+</style>

+ 0 - 1
src/views/book/courseware/preview/components/notes/NotesPreview.vue

@@ -80,7 +80,6 @@ export default {
   @include preview-base;
 
   .NPC-zhedie {
-    width: 780px;
     margin-bottom: 24px;
 
     .topTitle {

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

@@ -18,7 +18,30 @@
             :class="[isJudgingRightWrong ? (con_preview[0].all_right ? 'all-right' : 'has-error') : '']"
           >
             <span v-if="data.content_hz" class="items-hz">{{ data.content_hz }}</span>
-            <template v-if="data.property.answer_mode === 'select'">
+            <!-- 拼音输入 -->
+            <template v-if="data.property.fun_type === 'input'">
+              <span
+                v-for="(itemc, indexc) in con_preview"
+                :key="indexc"
+                :class="[
+                  'item-con',
+                  isJudgingRightWrong && itemc.type === 'input' && item.answer && item.con !== item.answer
+                    ? 'error'
+                    : '',
+                ]"
+              >
+                <el-input
+                  v-model="itemc.con"
+                  :disabled="disabled"
+                  :style="[{ width: Math.max(20, itemc.con.length * 10) + 'px' }]"
+                  class="item-input"
+                  v-if="itemc.type === 'input'"
+                  @blur="onInputChange(itemc)"
+                />
+                <span v-else>{{ itemc.con }}</span>
+              </span>
+            </template>
+            <template v-else-if="data.property.answer_mode === 'select'">
               <span
                 v-for="(itemc, indexc) in con_preview[0].item_con"
                 :key="indexc"
@@ -103,7 +126,7 @@
             <SvgIcon :icon-class="img" />
           </span>
         </div>
-        <template v-else-if="data.property.fun_type === 'show' && isEnable(data.property.is_enable_voice_answer)">
+        <template v-else-if="data.property.fun_type !== 'mark' && isEnable(data.property.is_enable_voice_answer)">
           <SoundRecord
             ref="record"
             type="normal"
@@ -124,6 +147,7 @@ import { getPinyinBaseData, arrangeTypeList, audioPositionList } from '@/views/b
 import PreviewMixin from '../common/PreviewMixin';
 import AudioPlay from '../character_base/components/AudioPlay.vue';
 import SoundRecord from '../../common/SoundRecord.vue';
+import { addTone, handleToneValue } from '@/utils/common';
 
 export default {
   name: 'PinyinBasePreview',
@@ -250,35 +274,57 @@ export default {
       if (!this.isJudgingRightWrong) {
         this.answer.answer_list = [];
       }
-      // this.data.option_list.forEach((item) => {
-      let con_arr = JSON.parse(JSON.stringify(this.data.content_view));
-      let user_answer = [];
-      let user_submit = []; // 用户提交答案
-      con_arr.forEach((items) => {
-        user_answer.push({
-          select_tone: null,
-          select_letter: '',
-          select_index: '',
+      if (this.data.property.fun_type === 'input') {
+        let arr = this.data.content.split(/_{3,}/g);
+        let inputIndex = 0;
+        arr.forEach((item, index) => {
+          let obj = {
+            con: item,
+            type: 'text',
+          };
+          if (index !== arr.length - 1) {
+            let objs = {
+              con: '',
+              type: 'input',
+              inputIndex: inputIndex,
+              answer: this.data.answer.answer_list[inputIndex] ? this.data.answer.answer_list[inputIndex].con : '',
+            };
+            this.con_preview.push(obj);
+            this.con_preview.push(JSON.parse(JSON.stringify(objs)));
+            inputIndex++;
+          }
+        });
+      } else {
+        // this.data.option_list.forEach((item) => {
+        let con_arr = JSON.parse(JSON.stringify(this.data.content_view));
+        let user_answer = [];
+        let user_submit = []; // 用户提交答案
+        con_arr.forEach((items) => {
+          user_answer.push({
+            select_tone: null,
+            select_letter: '',
+            select_index: '',
+          });
+          user_submit.push(this.data.property.answer_mode === 'label' ? items : '');
         });
-        user_submit.push(this.data.property.answer_mode === 'label' ? items : '');
-      });
-      let obj = {
-        item_con: con_arr,
-        item_con_yuan: JSON.parse(JSON.stringify(con_arr)),
-        mark: this.data.mark,
-        user_answer,
-        item_active_index: 0,
-        active_letter: '',
-      };
-      if (!this.isJudgingRightWrong) {
         let obj = {
+          item_con: con_arr,
+          item_con_yuan: JSON.parse(JSON.stringify(con_arr)),
           mark: this.data.mark,
-          value: user_submit,
+          user_answer,
+          item_active_index: 0,
+          active_letter: '',
         };
-        this.answer.answer_list.push(obj);
+        if (!this.isJudgingRightWrong) {
+          let obj = {
+            mark: this.data.mark,
+            value: user_submit,
+          };
+          this.answer.answer_list.push(obj);
+        }
+        this.con_preview.push(obj);
+        // });
       }
-      this.con_preview.push(obj);
-      // });
       this.show_preview = true;
     },
     handleReplaceTone(e, arr, index, resArr) {
@@ -406,7 +452,6 @@ export default {
       return cons;
     },
     handleSelectItemTone(i, indexc, indexi, itemi) {
-      console.log(this.data.property.fun_type);
       if (this.data.property.fun_type === 'show') {
         return;
       }
@@ -482,6 +527,22 @@ export default {
     handleWav(data) {
       this.data.record_list = data;
     },
+    onInputChange(item) {
+      let answer = item.con;
+
+      answer = answer
+        .trim()
+        .split(/\s+/)
+        .map((item) => {
+          return handleToneValue(item);
+        })
+        .map((item) =>
+          item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
+        )
+        .filter((item) => item.length > 0)
+        .join(' ');
+      item.con = answer;
+    },
   },
 };
 </script>
@@ -595,5 +656,19 @@ export default {
     background: #f2f3f5;
     border-radius: 2px;
   }
+
+  .item-input {
+    :deep .el-input__inner {
+      padding: 0;
+      font-family: 'League';
+      font-size: 16px;
+      color: #1d2129;
+      text-align: center;
+      background-color: transparent;
+      border-width: 0;
+      border-bottom: 1px solid #1d2129;
+      border-radius: 0;
+    }
+  }
 }
 </style>

+ 2 - 2
src/views/book/courseware/preview/components/voice_matrix/components/AudioLine.vue

@@ -445,7 +445,7 @@ export default {
     top: 12px;
     width: 8px;
     height: 8px;
-    background: var(--slider-color);
+    background: #165dff;
     border: none;
   }
 
@@ -463,7 +463,7 @@ export default {
 
   .el-slider__bar {
     height: 2px;
-    background: var(--slider-color);
+    background: #165dff;
   }
 }
 </style>