natasha 10 mesi fa
parent
commit
7b2fd168c9

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

@@ -0,0 +1,87 @@
+<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="选择类型" v-if="property.fun_type === 'mark'">
+        <el-radio-group v-model="property.mark_type">
+          <el-radio v-for="{ value, label } in markList" :key="value" :label="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="排列">
+        <el-radio-group v-model="property.arrange_type">
+          <el-radio v-for="{ value, label } in arrangeTypeList" :key="value" :label="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-divider />
+      <el-form-item label="读音">
+        <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-form-item label="读音位置">
+        <el-radio-group v-model="property.audio_position">
+          <el-radio v-for="{ value, label } in audioPositionList" :key="value" :label="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-divider v-if="property.fun_type !== 'mark'" />
+      <el-form-item label="录音作答" v-if="property.fun_type !== 'mark'">
+        <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-form>
+  </div>
+</template>
+
+<script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
+import {
+  getPinyinBaseProperty,
+  arrangeTypeList,
+  audioPositionList,
+  audioGenerationMethodList,
+  switchOption,
+  funList,
+  markList,
+} from '@/views/book/courseware/data/pinyinBase';
+
+export default {
+  name: 'FillSetting',
+  mixins: [SettingMixin],
+  data() {
+    return {
+      property: getPinyinBaseProperty(),
+      arrangeTypeList,
+      audioPositionList,
+      audioGenerationMethodList,
+      switchOption,
+      funList,
+      markList,
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.el-form {
+  @include setting-base;
+}
+</style>

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

@@ -0,0 +1,225 @@
+<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"
+        />
+        <div v-if="data.audio_file_id">
+          <SoundRecord :wav-blob.sync="data.audio_file_id" />
+        </div>
+        <template v-else>
+          <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>
+      </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 { getPinyinBaseData } from '@/views/book/courseware/data/pinyinBase';
+import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
+import { getRandomNumber } from '@/utils';
+import { GetStaticResources } from '@/api/app';
+
+export default {
+  name: 'PinyinBasePage',
+  components: {
+    SoundRecord,
+    UploadAudio,
+  },
+  mixins: [ModuleMixin],
+  data() {
+    return {
+      data: getPinyinBaseData(),
+    };
+  },
+  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>

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

@@ -30,6 +30,8 @@ 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 PinyinBaseSetting from '../create/components/base/pinyin_base/PinyinBaseSetting.vue';
 
 
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
@@ -48,6 +50,7 @@ import FillPreview from '@/views/book/courseware/preview/components/fill/FillPre
 import RecordInputPreview from '../preview/components/record_input/RecordInputPreview.vue';
 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';
 
 export const bookTypeOption = [
   {
@@ -135,6 +138,14 @@ export const bookTypeOption = [
         set: SpacingSetting,
         preview: SpacingPreview,
       },
+      {
+        value: 'pinyin_base',
+        label: '拼音',
+        icon: 'pinyin',
+        component: PinyinBse,
+        set: PinyinBaseSetting,
+        preview: PinyinBasePreview,
+      },
     ],
   },
   {

+ 89 - 0
src/views/book/courseware/data/pinyinBase.js

@@ -0,0 +1,89 @@
+import {
+  displayList,
+  serialNumberTypeList,
+  serialNumberPositionList,
+  arrangeTypeList,
+  switchOption,
+} from '@/views/book/courseware/data/common';
+
+export { arrangeTypeList, switchOption };
+
+// 音频位置
+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: 'mark',
+    label: '标声调',
+  },
+  {
+    value: 'show',
+    label: '拼音展示',
+  },
+  {
+    value: 'input',
+    label: '输入',
+  },
+]
+
+// 标声调类型
+export const markList = [
+  {
+    value: 'select',
+    label: '选择声调',
+  },
+  {
+    value: 'label',
+    label: '标注声调',
+  },
+]
+
+export function getPinyinBaseProperty() {
+  return {
+    serial_number: 1,
+    sn_type: serialNumberTypeList[0].value,
+    sn_position: serialNumberPositionList[0].value,
+    sn_display_mode: displayList[0].value,
+    arrange_type: arrangeTypeList[0].value,
+    audio_position: audioPositionList[0].value,
+    audio_generation_method: audioGenerationMethodList[0].value,
+    is_enable_voice_answer: switchOption[0].value,
+
+    fun_type: 'mark',
+    mark_type: 'select',
+  };
+}
+
+export function getPinyinBaseData() {
+  return {
+    type: 'pinyin_base',
+    title: '拼音',
+    property: getPinyinBaseProperty(),
+    content: '',
+    audio_file_id: '',
+    model_essay: [],
+    answer: {
+      answer_list: [],
+    },
+  };
+}

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