Ver Fonte

书写组件

natasha há 10 meses atrás
pai
commit
747cc1149e

Diff do ficheiro suprimidas por serem muito extensas
+ 842 - 1171
package-lock.json


+ 2 - 1
package.json

@@ -20,11 +20,12 @@
   "dependencies": {
     "@tinymce/tinymce-vue": "^3.2.8",
     "axios": "^1.7.2",
+    "cnchar": "^3.2.6",
     "core-js": "^3.37.1",
     "dompurify": "^3.1.5",
     "element-ui": "^2.15.14",
-    "js-cookie": "^3.0.5",
     "js-audio-recorder": "^1.0.7",
+    "js-cookie": "^3.0.5",
     "md5": "^2.3.0",
     "nprogress": "^0.2.0",
     "tinymce": "^5.10.9",

+ 24 - 9
src/views/book/courseware/create/components/question/character/Character.vue

@@ -3,7 +3,7 @@
     <template #content>
       <!-- eslint-disable max-len -->
       <div class="fill-wrapper">
-        <el-input v-model="data.content" placeholder="输入" type="textarea"></el-input>
+        <el-input v-model="data.content" placeholder="输入" type="textarea" @change="handleChangeContent"></el-input>
         <span class="tips">输入字或词请一字一行</span>
       </div>
     </template>
@@ -17,6 +17,7 @@ import UploadAudio from '@/views/book/courseware/create/components/question/fill
 
 import { getCharacterData } from '@/views/book/courseware/data/character';
 import { GetStaticResources } from '@/api/app';
+import cnchar from 'cnchar';
 
 export default {
   name: 'CharacterPage',
@@ -31,20 +32,34 @@ export default {
     };
   },
   methods: {
-    uploads(file_id) {
-      this.data.audio_file_id = file_id;
-    },
-    deleteFiles() {
-      this.data.audio_file_id = '';
+    // 解析输入内容
+    handleChangeContent() {
+      if (this.data.content.trim()) {
+        let contentArr = this.data.content.split('\n');
+        let contentList = [];
+        contentArr.forEach((item, index) => {
+          if (item.trim()) {
+            contentList.push({
+              con: item.trim(),
+              pinyin: cnchar.spell(item.trim(), 'array', 'low', 'tone').join(' '),
+              audio_file_id: '',
+            });
+            this.handleMatic(item.trim(), contentList.length - 1);
+          }
+        });
+        this.data.content_list = contentList;
+      } else {
+        this.data.content_list = [];
+      }
     },
     // 自动生成音频
-    handleMatic() {
+    handleMatic(con, index) {
       GetStaticResources('tool-TextToVoiceFile', {
-        text: this.data.content.replace(/<[^>]+>/g, ''),
+        text: con.replace(/<[^>]+>/g, ''),
       })
         .then(({ status, file_id }) => {
           if (status === 1) {
-            this.data.audio_file_id = file_id;
+            this.data.content_list[index].audio_file_id = file_id;
           }
         })
         .catch(() => {});

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

@@ -0,0 +1,178 @@
+<template>
+  <ModuleBase :type="data.type">
+    <template #content>
+      <!-- eslint-disable max-len -->
+      <div class="fill-wrapper">
+        <template v-if="data.property.content_type === 'con'">
+          <el-input v-model="data.content" placeholder="输入" type="textarea" @change="handleChangeContent"></el-input>
+          <span class="tips">输入字或词请一字一行</span>
+        </template>
+        <template v-else>
+          <UploadFile
+            :courseware-id="courseware_id"
+            :component-id="id"
+            :type="data.type"
+            :single-size="data.single_size"
+            :total-size="data.total_size"
+            :file-list="data.file_list"
+            :file-id-list="data.file_id_list"
+            :file-info-list="data.file_info_list"
+            :label-text="labelText"
+            :accept-file-type="acceptFileType"
+            :upload-tip="uploadTip"
+            :icon-class="iconClass"
+            @updateFileList="updateFileList"
+          />
+        </template>
+      </div>
+    </template>
+  </ModuleBase>
+</template>
+
+<script>
+import ModuleMixin from '../../common/ModuleMixin';
+import SoundRecord from '@/views/book/courseware/create/components/question/fill/components/SoundRecord.vue';
+import UploadAudio from '@/views/book/courseware/create/components/question/fill/components/UploadAudio.vue';
+import UploadFile from '@/views/book/courseware/create/components/base/common/UploadFile.vue';
+
+import { getWriteData } from '@/views/book/courseware/data/write';
+import { GetStaticResources } from '@/api/app';
+import cnchar from 'cnchar';
+
+export default {
+  name: 'WritePage',
+  components: {
+    SoundRecord,
+    UploadAudio,
+    UploadFile,
+  },
+  mixins: [ModuleMixin],
+  data() {
+    return {
+      data: getWriteData(),
+      labelText: '图片',
+      acceptFileType: '.png,.jpg',
+      uploadTip:
+        'The size of the uploaded image should not exceed 2MB, the size of the uploaded audio file, pdf file, and excel file should not exceed 20MB, and the size of the uploaded audio file should not exceed 20MB',
+      iconClass: 'picture',
+    };
+  },
+  methods: {
+    // 解析输入内容
+    handleChangeContent() {
+      if (this.data.content.trim()) {
+        let contentArr = this.data.content.split('\n');
+        let contentList = [];
+        contentArr.forEach((item, index) => {
+          if (item.trim()) {
+            contentList.push({
+              con: item.trim(),
+              pinyin: cnchar.spell(item.trim(), 'array', 'low', 'tone').join(' '),
+              audio_file_id: '',
+            });
+            this.handleMatic(item.trim(), contentList.length - 1);
+          }
+        });
+        this.data.content_list = contentList;
+      } else {
+        this.data.content_list = [];
+      }
+    },
+    // 自动生成音频
+    handleMatic(con, index) {
+      GetStaticResources('tool-TextToVoiceFile', {
+        text: con.replace(/<[^>]+>/g, ''),
+      })
+        .then(({ status, file_id }) => {
+          if (status === 1) {
+            this.data.content_list[index].audio_file_id = file_id;
+          }
+        })
+        .catch(() => {});
+    },
+    updateFileList({ file_list, file_id_list, file_info_list }) {
+      this.data.file_list = file_list;
+      this.data.file_id_list = file_id_list;
+      this.data.file_info_list = file_info_list;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.fill-wrapper {
+  display: flex;
+  flex-direction: column;
+  row-gap: 16px;
+  align-items: flex-start;
+
+  :deep .rich-wrapper {
+    width: 100%;
+  }
+
+  .tips {
+    font-size: 12px;
+    color: #999;
+  }
+
+  .auto-matic,
+  .upload-audio-play {
+    :deep .upload-wrapper {
+      margin-top: 0;
+    }
+
+    .audio-wrapper {
+      :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;
+      }
+    }
+  }
+
+  .auto-matic {
+    display: flex;
+    flex-shrink: 0;
+    column-gap: 12px;
+    align-items: center;
+    width: 200px;
+    padding: 5px 12px;
+    background-color: $fill-color;
+    border-radius: 2px;
+
+    .auto-btn {
+      font-size: 16px;
+      font-weight: 400;
+      line-height: 22px;
+      color: #1d2129;
+      cursor: pointer;
+    }
+  }
+
+  .correct-answer {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+
+    .el-input {
+      width: 180px;
+
+      :deep &__prefix {
+        display: flex;
+        align-items: center;
+        color: $text-color;
+      }
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,86 @@
+<template>
+  <div>
+    <el-form :model="property" label-width="72px" label-position="left">
+      <SerailNumber :property="property" />
+      <el-form-item label="汉字框">
+        <el-radio-group v-model="property.frame_type">
+          <el-radio v-for="{ value, label } in frameList" :key="value" :label="value" :value="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="汉字内容">
+        <el-radio-group v-model="property.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 label="框颜色">
+        <el-color-picker v-model="property.frame_color"></el-color-picker>
+      </el-form-item>
+
+      <el-divider />
+      <el-form-item label="描红格" v-if="property.content_type === 'con'">
+        <el-input-number v-model="property.miao_number" :min="0" :step="1" />
+      </el-form-item>
+      <el-form-item label="书写格">
+        <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-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 label="笔迹回放">
+        <el-radio-group v-model="property.is_enable_play_back">
+          <el-radio v-for="{ value, label } in showList" :key="value" :label="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
+import {
+  getWriteProperty,
+  switchOption,
+  funList,
+  showList,
+  isEnable,
+  frameList,
+  conList,
+} from '@/views/book/courseware/data/write';
+
+export default {
+  name: 'WriteSetting',
+  mixins: [SettingMixin],
+  data() {
+    return {
+      property: getWriteProperty(),
+      switchOption,
+      isEnable,
+      funList,
+      showList,
+      frameList,
+      conList,
+    };
+  },
+  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

@@ -36,6 +36,8 @@ import CharacterBase from '../create/components/base/character_base/CharacterBas
 import CharacterBaseSetting from '../create/components/base/character_base/CharacterBaseSetting.vue';
 import Character from '../create/components/question/character/Character.vue';
 import CharacterSetting from '../create/components/question/character/CharacterSetting.vue';
+import Write from '../create/components/question/write/Write.vue';
+import WriteSetting from '../create/components/question/write/WriteSetting.vue';
 
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
 import DividerPreview from '@/views/book/courseware/preview/components/divider/DividerPreview.vue';
@@ -56,6 +58,7 @@ import UploadPreviewPreview from '../preview/components/upload_preview/UploadPre
 import PinyinBasePreview from '../preview/components/pinyin_base/PinyinBasePreview.vue';
 import CharacterBasePreview from '../preview/components/character_base/CharacterBasePreview.vue';
 import CharacterPreview from '../preview/components/character/CharacterPreview.vue';
+import WritePreview from '../preview/components/write/WritePreview.vue';
 
 export const bookTypeOption = [
   {
@@ -229,6 +232,14 @@ export const bookTypeOption = [
         set: CharacterSetting,
         preview: CharacterPreview,
       },
+      {
+        value: 'write',
+        label: '书写组件',
+        icon: '',
+        component: Write,
+        set: WriteSetting,
+        preview: WritePreview,
+      },
     ],
   },
 ];

+ 1 - 2
src/views/book/courseware/data/character.js

@@ -60,8 +60,7 @@ export function getCharacterData() {
     title: '汉字',
     property: getCharacterProperty(),
     content: '',
-    contentList: [],
-    audio_file_id: '',
+    content_list: [],
     answer: {
       answer_list: [],
     },

+ 87 - 0
src/views/book/courseware/data/write.js

@@ -0,0 +1,87 @@
+import {
+  displayList,
+  serialNumberTypeList,
+  serialNumberPositionList,
+  arrangeTypeList,
+  switchOption,
+  isEnable,
+} from '@/views/book/courseware/data/common';
+
+export { arrangeTypeList, switchOption, isEnable };
+
+// 汉字内容
+export const conList = [
+  {
+    value: 'con',
+    label: '手动录入'
+  },
+  {
+    value: 'upload',
+    label: '上传图片'
+  },
+]
+
+// 显示
+export const showList = [
+  {
+    value: 'true',
+    label: '显示',
+  },
+  {
+    value: 'false',
+    label: '不显示',
+  },
+];
+
+// 汉字框
+export const frameList = [
+  {
+    value: 'tian',
+    label: '田字格',
+  },
+  {
+    value: 'fang',
+    label: '方框',
+  },
+  {
+    value: 'none',
+    label: '无',
+  },
+];
+
+export function getWriteProperty() {
+  return {
+    serial_number: 1,
+    sn_type: serialNumberTypeList[0].value,
+    sn_position: serialNumberPositionList[0].value,
+    sn_display_mode: displayList[0].value,
+    frame_type: 'tian',
+    frame_color: '#F13232',
+    content_type: 'con',
+    miao_number: 5,
+    write_number: 5,
+    is_enable_play_back: showList[0].value,
+    is_enable_error: showList[0].value,
+  };
+}
+
+export function getWriteData() {
+  return {
+    type: 'write',
+    title: '书写组件',
+    property: getWriteProperty(),
+    content: '',
+    content_list: [],
+    single_size: 20, // 单位MB
+    total_size: 100, // 单位MB
+    min_width: '144', // 大于等于最小缩略图宽度
+    min_height: '306', // 大于等于2倍缩略图宽度加间隙高度
+    file_info_list: [],
+    file_id_list: [], // 文件 id['20032-121212', '20032-121216']
+    // 内容中包含的文件列表,
+    file_list: [],
+    answer: {
+      answer_list: [],
+    },
+  };
+}

+ 136 - 0
src/views/book/courseware/preview/components/write/WritePreview.vue

@@ -0,0 +1,136 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="select-preview" :style="getAreaStyle()">
+    <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
+
+    <div class="main" :style="getMainStyle()">预览开发中</div>
+  </div>
+</template>
+
+<script>
+import { getWriteData, fillFontList, arrangeTypeList, audioPositionList } from '@/views/book/courseware/data/write';
+
+import PreviewMixin from '../common/PreviewMixin';
+import AudioFill from '../fill/components/AudioFillPlay.vue';
+import SoundRecord from '../../common/SoundRecord.vue';
+
+export default {
+  name: 'WritePreview',
+  components: {
+    AudioFill,
+    SoundRecord,
+  },
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      data: getWriteData(),
+    };
+  },
+  computed: {
+    fontFamily() {
+      return fillFontList.find(({ value }) => this.data.property.fill_font === value).font;
+    },
+  },
+  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();
+  },
+  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;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.select-preview {
+  @include preview-base;
+
+  .main {
+    display: grid;
+    align-items: center;
+  }
+
+  .fill-wrapper {
+    grid-area: fill;
+    font-size: 16pt;
+
+    p {
+      margin: 0;
+    }
+
+    .el-input {
+      display: inline-flex;
+      align-items: center;
+      width: 120px;
+      margin: 0 2px;
+
+      &.pinyin :deep input.el-input__inner {
+        font-family: 'PINYIN-B', sans-serif;
+      }
+
+      &.chinese :deep input.el-input__inner {
+        font-family: 'arial', sans-serif;
+      }
+
+      &.english :deep input.el-input__inner {
+        font-family: 'arial', sans-serif;
+      }
+
+      :deep input.el-input__inner {
+        padding: 0;
+        font-size: 16pt;
+        color: $font-color;
+        text-align: center;
+        background-color: #fff;
+        border-width: 0;
+        border-bottom: 1px solid $font-color;
+        border-radius: 0;
+      }
+    }
+  }
+
+  .record-box {
+    padding: 6px 12px;
+    background-color: $fill-color;
+
+    :deep .record-time {
+      width: 100px;
+    }
+  }
+}
+</style>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff