Browse Source

基础-汉字

natasha 1 year ago
parent
commit
81ad79b127

+ 233 - 0
src/views/book/courseware/create/components/base/character_base/CharacterBase.vue

@@ -0,0 +1,233 @@
+<template>
+  <ModuleBase :type="data.type">
+    <template #content>
+      <!-- eslint-disable max-len -->
+      <div class="fill-wrapper">
+        <RichText
+          v-model="data.content"
+          toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
+          :wordlimit-num="false"
+        />
+        <el-input
+          v-model="data.pinyin"
+          placeholder="输入拼音"
+          v-if="isEnable(data.property.is_enable_pinyin)"
+        ></el-input>
+        <el-input v-model="data.definition" placeholder="输入释义或描述" type="textarea"></el-input>
+        <div v-if="data.audio_file_id">
+          <SoundRecord :wav-blob.sync="data.audio_file_id" />
+        </div>
+        <template v-else>
+          <template v-if="isEnable(data.property.is_enable_voice)">
+            <div :class="['upload-audio-play']">
+              <UploadAudio
+                v-if="data.property.audio_generation_method === 'upload'"
+                :file-id="data.audio_file_id"
+                :show-upload="!data.audio_file_id"
+                @upload="uploads"
+                @deleteFile="deleteFiles"
+              />
+              <div v-else-if="data.property.audio_generation_method === 'auto'" class="auto-matic" @click="handleMatic">
+                <SvgIcon icon-class="voiceprint-line" class="record" />
+                <span class="auto-btn">{{ data.audio_file_id ? '已生成' : '生成音频' }}</span
+                >{{ data.audio_file_id ? '成功' : '' }}
+              </div>
+              <SoundRecord v-else :wav-blob.sync="data.audio_file_id" />
+            </div>
+          </template>
+        </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 { getCharacterBaseData } from '@/views/book/courseware/data/characterBase';
+import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
+import { getRandomNumber } from '@/utils';
+import { GetStaticResources } from '@/api/app';
+
+export default {
+  name: 'CharacterBasePage',
+  components: {
+    SoundRecord,
+    UploadAudio,
+  },
+  mixins: [ModuleMixin],
+  data() {
+    return {
+      data: getCharacterBaseData(),
+    };
+  },
+  methods: {
+    // 识别文本
+    identifyText() {
+      this.data.model_essay = [];
+      this.data.answer.answer_list = [];
+
+      this.data.content
+        .split(/<(p|div)[^>]*>(.*?)<\/(p|div)>/g)
+        .filter((s) => s && !s.match(/^(p|div)$/))
+        .forEach((item) => {
+          if (item.charCodeAt() === 10) return;
+          let str = item
+            // 去除所有的 font-size 样式
+            .replace(/font-size:\s*\d+(\.\d+)?px;/gi, '')
+            // 匹配 class 名为 rich-fill 的 span 标签和三个以上的_,并将它们组成数组
+            .replace(/<span class="rich-fill".*?>(.*?)<\/span>|([_]{3,})/gi, '###$1$2###');
+          this.data.model_essay.push(this.splitRichText(str));
+        });
+    },
+    // 分割富文本
+    splitRichText(str) {
+      let _str = str;
+      let start = 0;
+      let index = 0;
+      let arr = [];
+      let matchNum = 0;
+      while (index !== -1) {
+        index = _str.indexOf('###', start);
+        if (index === -1) break;
+        matchNum += 1;
+        arr.push({ content: _str.slice(start, index), type: 'text' });
+        if (matchNum % 2 === 0 && arr.length > 0) {
+          arr[arr.length - 1].type = 'input';
+          let mark = getRandomNumber();
+          arr[arr.length - 1].mark = mark;
+          let content = arr[arr.length - 1].content;
+          // 设置答案数组
+          let isUnderline = /^_{3,}$/.test(content);
+          this.data.answer.answer_list.push({
+            value: isUnderline ? '' : content,
+            mark,
+            type: isUnderline ? 'any_one' : 'only_one',
+          });
+
+          // 将 content 设置为空,为预览准备
+          arr[arr.length - 1].content = '';
+        }
+        start = index + 3;
+      }
+      let last = _str.slice(start);
+      if (last) {
+        arr.push({ content: last, type: 'text' });
+      }
+      return arr;
+    },
+    handleTone(value, i) {
+      if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
+      this.data.answer.answer_list[i].value = 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(' ');
+    },
+    uploads(file_id) {
+      this.data.audio_file_id = file_id;
+    },
+    deleteFiles() {
+      this.data.audio_file_id = '';
+    },
+    // 自动生成音频
+    handleMatic() {
+      GetStaticResources('tool-TextToVoiceFile', {
+        text: this.data.content.replace(/<[^>]+>/g, ''),
+      })
+        .then(({ status, file_id }) => {
+          if (status === 1) {
+            this.data.audio_file_id = file_id;
+          }
+        })
+        .catch(() => {});
+    },
+  },
+};
+</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>

+ 120 - 0
src/views/book/courseware/create/components/base/character_base/CharacterBaseSetting.vue

@@ -0,0 +1,120 @@
+<template>
+  <div>
+    <el-form :model="property" label-width="72px" label-position="left">
+      <SerailNumber :property="property" />
+      <el-form-item label="功能">
+        <el-select v-model="property.fun_type" placeholder="请选择">
+          <el-option v-for="{ value, label } in funList" :key="value" :label="label" :value="value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="拼音">
+        <el-radio-group v-model="property.is_enable_pinyin">
+          <el-radio v-for="{ value, label } in showList" :key="value" :label="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-divider />
+      <el-form-item label="读音">
+        <el-radio-group v-model="property.is_enable_voice">
+          <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="读音" v-if="isEnable(property.is_enable_voice)">
+        <el-select v-model="property.audio_generation_method" placeholder="请选择">
+          <el-option v-for="{ value, label } in audioGenerationMethodList" :key="value" :label="label" :value="value" />
+        </el-select>
+      </el-form-item>
+
+      <el-divider />
+      <el-form-item label="录音作答">
+        <el-radio-group v-model="property.is_enable_voice_answer">
+          <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value" :value="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-divider />
+      <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-color-picker v-model="property.frame_color"></el-color-picker>
+      </el-form-item>
+      <template v-if="property.fun_type === 'write'">
+        <el-divider />
+        <el-form-item label="描红">
+          <el-radio-group v-model="property.is_enable_miao">
+            <el-radio v-for="{ value, label } in switchOption" :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.is_enable_play_back">
+            <el-radio v-for="{ value, label } in switchOption" :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.is_enable_error">
+            <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value" :value="value">
+              {{ label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </template>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
+import {
+  getCharacterBaseProperty,
+  arrangeTypeList,
+  audioPositionList,
+  audioGenerationMethodList,
+  switchOption,
+  funList,
+  showList,
+  isEnable,
+  frameList,
+} from '@/views/book/courseware/data/characterBase';
+
+export default {
+  name: 'CharacterBaseSetting',
+  mixins: [SettingMixin],
+  data() {
+    return {
+      property: getCharacterBaseProperty(),
+      arrangeTypeList,
+      audioPositionList,
+      audioGenerationMethodList,
+      switchOption,
+      isEnable,
+      funList,
+      showList,
+      frameList,
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.el-form {
+  @include setting-base;
+}
+</style>

+ 0 - 0
src/views/book/courseware/create/components/base/pinyin_base/PinyinBse.vue → src/views/book/courseware/create/components/base/pinyin_base/PinyinBase.vue


+ 1 - 1
src/views/book/courseware/create/components/base/pinyin_base/PinyinBaseSetting.vue

@@ -61,7 +61,7 @@ import {
 } from '@/views/book/courseware/data/pinyinBase';
 
 export default {
-  name: 'FillSetting',
+  name: 'PinyinBaseSetting',
   mixins: [SettingMixin],
   data() {
     return {

+ 13 - 2
src/views/book/courseware/data/bookType.js

@@ -30,8 +30,10 @@ import UploadControl from '../create/components/base/upload_control/UploadContro
 import UploadControlSetting from '../create/components/base/upload_control/UploadControlSetting.vue';
 import UploadPreview from '../create/components/base/upload_preview/UploadPreview.vue';
 import UploadRreviewSetting from '../create/components/base/upload_preview/UploadRreviewSetting.vue';
-import PinyinBse from '../create/components/base/pinyin_base/PinyinBse.vue';
+import PinyinBase from '../create/components/base/pinyin_base/PinyinBase.vue';
 import PinyinBaseSetting from '../create/components/base/pinyin_base/PinyinBaseSetting.vue';
+import CharacterBase from '../create/components/base/character_base/CharacterBase.vue';
+import CharacterBaseSetting from '../create/components/base/character_base/CharacterBaseSetting.vue';
 
 
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
@@ -51,6 +53,7 @@ import RecordInputPreview from '../preview/components/record_input/RecordInputPr
 import UploadControlPreview from '../preview/components/upload_control/UploadControlPreview.vue';
 import UploadPreviewPreview from '../preview/components/upload_preview/UploadPreviewPreview.vue';
 import PinyinBasePreview from '../preview/components/pinyin_base/PinyinBasePreview.vue';
+import CharacterBasePreview from '../preview/components/character_base/CharacterBasePreview.vue';
 
 export const bookTypeOption = [
   {
@@ -142,10 +145,18 @@ export const bookTypeOption = [
         value: 'pinyin_base',
         label: '拼音',
         icon: 'pinyin',
-        component: PinyinBse,
+        component: PinyinBase,
         set: PinyinBaseSetting,
         preview: PinyinBasePreview,
       },
+      {
+        value: 'character_base',
+        label: '汉字',
+        icon: 'chineseCharacter',
+        component: CharacterBase,
+        set: CharacterBaseSetting,
+        preview: CharacterBasePreview,
+      },
     ],
   },
   {

+ 108 - 0
src/views/book/courseware/data/characterBase.js

@@ -0,0 +1,108 @@
+import {
+  displayList,
+  serialNumberTypeList,
+  serialNumberPositionList,
+  arrangeTypeList,
+  switchOption,
+  isEnable
+} from '@/views/book/courseware/data/common';
+
+export { arrangeTypeList, switchOption,isEnable };
+
+// 音频位置
+export const audioPositionList = [
+  { value: 'front', label: '拼音前' },
+  { value: 'back', label: '拼音后' },
+];
+
+// 读音生成方式
+export const audioGenerationMethodList = [
+  {
+    value: 'upload',
+    label: '上传',
+  },
+  {
+    value: 'auto',
+    label: '自动生成',
+  },
+  {
+    value: 'record',
+    label: '录音',
+  },
+];
+
+// 功能
+export const funList = [
+  {
+    value: 'show',
+    label: '汉字展示',
+  },
+  {
+    value: 'write',
+    label: '汉字书写',
+  },
+]
+
+// 显示
+export const showList = [
+  {
+    value: 'true',
+    label: '显示',
+  },
+  {
+    value: 'false',
+    label: '不显示',
+  },
+]
+
+// 汉字框
+export const frameList = [
+  {
+    value: 'tian',
+    label: '田字格',
+  },
+  {
+    value: 'fang',
+    label: '方框',
+  },
+  {
+    value: 'none',
+    label: '无',
+  },
+]
+
+export function getCharacterBaseProperty() {
+  return {
+    serial_number: 1,
+    sn_type: serialNumberTypeList[0].value,
+    sn_position: serialNumberPositionList[0].value,
+    sn_display_mode: displayList[0].value,
+    audio_generation_method: audioGenerationMethodList[0].value,
+    is_enable_voice_answer: switchOption[0].value,
+
+    is_enable_pinyin: showList[0].value,
+    fun_type: 'show',
+    is_enable_voice: showList[0].value,
+    frame_type: 'tian',
+    frame_color: '#F13232',
+    is_enable_error: showList[0].value,
+    is_enable_play_back: showList[0].value,
+    is_enable_miao: showList[0].value,
+  };
+}
+
+export function getCharacterBaseData() {
+  return {
+    type: 'character_base',
+    title: '拼音',
+    property: getCharacterBaseProperty(),
+    content: '',
+    pinyin: '',
+    definition:'',
+    audio_file_id: '',
+    model_essay: [],
+    answer: {
+      answer_list: [],
+    },
+  };
+}

+ 141 - 0
src/views/book/courseware/preview/components/character_base/CharacterBasePreview.vue

@@ -0,0 +1,141 @@
+<!-- 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 {
+  getCharacterBaseData,
+  fillFontList,
+  arrangeTypeList,
+  audioPositionList,
+} from '@/views/book/courseware/data/characterBase';
+
+import PreviewMixin from '../common/PreviewMixin';
+import AudioFill from '../fill/components/AudioFillPlay.vue';
+import SoundRecord from '../../common/SoundRecord.vue';
+
+export default {
+  name: 'CharacterBasePreview',
+  components: {
+    AudioFill,
+    SoundRecord,
+  },
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      data: getCharacterBaseData(),
+    };
+  },
+  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>