Browse Source

Merge branch 'master' into lhd

natasha 1 week ago
parent
commit
37d458d14c

+ 247 - 0
src/views/book/components/MultilingualFill.vue

@@ -0,0 +1,247 @@
+<template>
+  <el-dialog
+    :visible="visible"
+    width="800px"
+    custom-class="multilingual-fill-dialog"
+    :close-on-click-modal="false"
+    @close="closeDialog"
+  >
+    <div class="multilingual-fill">
+      <div class="left-menu">
+        <span class="title">多语言</span>
+        <ul class="lang-list">
+          <li
+            v-for="{ code } in selectedLangList"
+            :key="code"
+            :class="['lang-item', { active: curLang === code }]"
+            @click="curLang = code"
+          >
+            {{ langList.find((item) => item.code === code).name }}
+          </li>
+          <li class="lang-item" @click="showAddLang">
+            <i class="el-icon-plus"></i>
+            <span>增加语种</span>
+          </li>
+        </ul>
+      </div>
+
+      <div class="right-content">
+        <div class="operator">
+          <div class="operator-left">
+            <span class="btn" @click="isShowOriginal = !isShowOriginal">
+              <i v-show="!isShowOriginal" class="el-icon-view"></i>
+              <SvgIcon v-show="isShowOriginal" :size="12" icon-class="eye-invisible" />
+              <span>{{ isShowOriginal ? '隐藏原文' : '显示原文' }}</span>
+            </span>
+            <span class="btn primary" @click="submitTranslation">
+              <span>提交译文</span>
+            </span>
+          </div>
+          <i class="el-icon-close" @click="closeDialog"></i>
+        </div>
+        <div class="content">
+          <el-input v-show="isShowOriginal" :value="text" type="textarea" :rows="27" resize="none" :readonly="true" />
+          <el-input
+            v-for="lang in selectedLangList"
+            v-show="curLang === lang.code"
+            :key="lang.code"
+            v-model="lang.translation"
+            type="textarea"
+            :rows="27"
+            resize="none"
+            placeholder="输入译文"
+          />
+        </div>
+      </div>
+    </div>
+
+    <UpdateLang :visible.sync="langVisible" :selected-langs="selectedLangList" @update-langs="handleUpdateLangs" />
+  </el-dialog>
+</template>
+
+<script>
+import UpdateLang from './UpdateLang.vue';
+
+import { langList } from '@/views/book/courseware/data/common';
+
+export default {
+  name: 'MultilingualFill',
+  components: {
+    UpdateLang,
+  },
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    text: {
+      type: String,
+      default: '',
+    },
+    translations: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      langList,
+      selectedLangList: [
+        { code: 'en', translation: '' },
+        { code: 'fr', translation: '' },
+        { code: 'de', translation: '' },
+        { code: 'es', translation: '' },
+        { code: 'it', translation: '' },
+        { code: 'pt', translation: '' },
+        { code: 'ko', translation: '' },
+        { code: 'ja', translation: '' },
+      ],
+      noSelectedLangList: ['ru', 'ar', 'tr', 'nl', 'pl', 'sv', 'el'],
+      curLang: 'en',
+      isShowOriginal: true, // 是否显示原文
+      langVisible: false,
+    };
+  },
+  watch: {
+    translations: {
+      handler(newVal) {
+        if (!newVal || !Array.isArray(newVal) || newVal.length === 0) return;
+        this.selectedLangList = newVal.map(({ code, translation }) => ({
+          code,
+          translation,
+        }));
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    closeDialog() {
+      this.$emit('update:visible', false);
+    },
+    showAddLang() {
+      this.langVisible = true;
+    },
+    /**
+     * 处理语言更新
+     * @param {Array} langs
+     */
+    handleUpdateLangs(langs) {
+      const newLangs = langs.filter((item) => !this.selectedLangList.map((i) => i.code).includes(item));
+      const removedLangs = this.selectedLangList.map((i) => i.code).filter((item) => !langs.includes(item));
+
+      this.selectedLangList = [
+        ...this.selectedLangList.filter((item) => !removedLangs.includes(item.code)),
+        ...newLangs.map((item) => ({ code: item, translation: '' })),
+      ];
+    },
+    submitTranslation() {
+      this.$emit('submit-translation', this.selectedLangList);
+      this.closeDialog();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use 'sass:color';
+
+.el-dialog__wrapper {
+  :deep .multilingual-fill-dialog {
+    > .el-dialog__header {
+      display: none;
+    }
+
+    > .el-dialog__body {
+      height: 650px;
+      padding: 12px 16px;
+    }
+  }
+}
+
+.multilingual-fill {
+  display: flex;
+  column-gap: 8px;
+
+  .left-menu {
+    width: 90px;
+
+    .title {
+      font-size: 16px;
+      font-weight: bold;
+      color: #333;
+    }
+
+    .lang-list {
+      display: flex;
+      flex-direction: column;
+      row-gap: 8px;
+      height: 585px;
+      margin-top: 12px;
+      overflow: auto;
+
+      .lang-item {
+        padding: 4px 8px;
+        text-align: center;
+        cursor: pointer;
+        background-color: #f5f5f5;
+        border-radius: 4px;
+
+        &:hover {
+          background-color: #e0e0e0;
+        }
+
+        &.active {
+          color: #fff;
+          background-color: $main-color;
+        }
+      }
+    }
+  }
+
+  .right-content {
+    flex: 1;
+
+    .operator {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 8px;
+
+      &-left {
+        display: flex;
+        column-gap: 8px;
+        align-items: center;
+
+        .btn {
+          display: flex;
+          column-gap: 4px;
+          align-items: center;
+          padding: 6px 12px;
+          font-size: 12px;
+          cursor: pointer;
+          background-color: #f5f5f5;
+          border-radius: 4px;
+
+          &.primary {
+            color: #fff;
+            background-color: $main-color;
+
+            &:hover {
+              background-color: color.adjust($main-color, $lightness: -5%);
+            }
+          }
+        }
+      }
+
+      i {
+        font-weight: bold;
+        cursor: pointer;
+      }
+    }
+
+    .content {
+      display: flex;
+      column-gap: 8px;
+    }
+  }
+}
+</style>

+ 67 - 0
src/views/book/components/UpdateLang.vue

@@ -0,0 +1,67 @@
+<template>
+  <el-dialog
+    :visible="visible"
+    width="610px"
+    :close-on-click-modal="false"
+    :append-to-body="true"
+    title="增加语言"
+    @close="dialogLangClose"
+  >
+    <el-transfer
+      v-model="langs"
+      :titles="['可选', '已选择']"
+      :props="{ key: 'code', label: 'name' }"
+      :data="langList"
+    />
+
+    <div slot="footer">
+      <el-button @click="dialogLangClose">取消</el-button>
+      <el-button type="primary" @click="updateLangs">确定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { langList } from '@/views/book/courseware/data/common';
+
+export default {
+  name: 'UpdateLang',
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    selectedLangs: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      langList: langList.map((item) => ({ ...item, disabled: false })),
+      langs: [],
+    };
+  },
+  watch: {
+    selectedLangs: {
+      handler(val) {
+        if (!val || !Array.isArray(val)) return;
+
+        this.langs = val.map((item) => item.code);
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    dialogLangClose() {
+      this.$emit('update:visible', false);
+    },
+    updateLangs() {
+      this.$emit('update-langs', this.langs);
+      this.dialogLangClose();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -10,7 +10,7 @@
     <span class="tone-pinyin">{{ tonePinyin }}</span>
     <span class="content-text">{{ selectContent }}</span>
     <el-input v-model="numberPinyin" autocomplete="off" placeholder="请输入正确的拼音" @blur="convertTonePinyin" />
-    <span class="tips">一到四声分别用数字1-4表示,轻声用0表示。</span>
+    <span class="tips">一到四声分别用数字1-4表示,轻声用0表示,拼音间用空格隔开。</span>
     <template slot="footer">
       <el-button size="medium" @click="dialogClose">取消</el-button>
       <el-button type="primary" size="medium" @click="confirm">保存</el-button>

+ 0 - 1
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -142,7 +142,6 @@ export default {
     },
     // 打开弹窗,接收子组件的值 { visible: true, noteId: start.id }
     viewExplanatoryNote(val) {
-      debugger;
       this.isViewExplanatoryNoteDialog = val.visible;
       let id = val.noteId;
       if (this.data.note_list.some((p) => p.id === id)) {

+ 11 - 1
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -39,7 +39,12 @@
             <SoundRecord v-else :wav-blob.sync="data.audio_file_id" />
           </div>
         </template>
-        <el-button @click="identifyText">识别</el-button>
+
+        <div>
+          <el-button @click="identifyText">识别</el-button>
+          <el-button @click="multilingualVisible = true">多语言</el-button>
+        </div>
+
         <div class="correct-answer">
           <el-input
             v-for="(item, i) in data.answer.answer_list.filter(({ type }) => type === 'any_one')"
@@ -51,6 +56,8 @@
           </el-input>
         </div>
       </div>
+
+      <MultilingualFill :visible.sync="multilingualVisible" :text="data.content" />
     </template>
   </ModuleBase>
 </template>
@@ -59,6 +66,7 @@
 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 MultilingualFill from '@/views/book/components/MultilingualFill.vue';
 
 import { getFillData, arrangeTypeList, fillFontList, fillTypeList } from '@/views/book/courseware/data/fill';
 import { addTone, handleToneValue } from '@/views/book/courseware/data/common';
@@ -70,12 +78,14 @@ export default {
   components: {
     SoundRecord,
     UploadAudio,
+    MultilingualFill,
   },
   mixins: [ModuleMixin],
   data() {
     return {
       data: getFillData(),
       fillTypeList,
+      multilingualVisible: false,
     };
   },
   watch: {

+ 18 - 0
src/views/book/courseware/data/common.js

@@ -184,3 +184,21 @@ export const reversedComputeOptionMethods = {
   [serialNumberTypeList[2].value]: (i) => i.charCodeAt(0) - 97 + 1, // 小写
   [serialNumberTypeList[3].value]: (i) => i.charCodeAt(0) - 65 + 1,
 };
+
+export const langList = [
+  { name: '英语', code: 'en' },
+  { name: '法语', code: 'fr' },
+  { name: '德语', code: 'de' },
+  { name: '意大利语', code: 'it' },
+  { name: '西班牙语', code: 'es' },
+  { name: '葡萄牙语', code: 'pt' },
+  { name: '韩语', code: 'ko' },
+  { name: '日语', code: 'ja' },
+  { name: '俄语', code: 'ru' },
+  { name: '阿拉伯语', code: 'ar' },
+  { name: '土耳其语', code: 'tr' },
+  { name: '荷兰语', code: 'nl' },
+  { name: '波兰语', code: 'pl' },
+  { name: '瑞典语', code: 'sv' },
+  { name: '希腊语', code: 'el' },
+];

+ 41 - 5
src/views/book/courseware/preview/components/rich_text/RichTextPreview.vue

@@ -3,19 +3,19 @@
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
 
     <div class="main">
-      <div :style="{ width: data.note_list.length > 0 ? '' : '100%' }">
+      <div ref="leftDiv" :style="{ width: data.note_list.length > 0 ? '' : '100%' }">
         <PinyinText
           v-if="isEnable(data.property.view_pinyin)"
           :paragraph-list="data.paragraph_list"
           :pinyin-position="data.property.pinyin_position"
           :is-preview="isPreview"
         />
-        <span v-else class="rich-text" v-html="sanitizeHTML(data.content)" @click="handleRichFillClick"></span>
+        <span v-else class="rich-text" @click="handleRichFillClick" v-html="sanitizeHTML(data.content)"></span>
       </div>
-      <div v-if="data.note_list.length > 0" class="note-list">
+      <div v-if="data.note_list.length > 0" ref="rightDiv" class="note-list" :style="{ height: divHeight + 'px' }">
         <span>注释</span>
         <ul>
-          <li v-for="note in data.note_list" :key="note.id">
+          <li v-for="note in data.note_list" :key="note.id" :ref="note.id">
             <p :style="{ 'background-color': selectedNoteId == note.id ? '#FFF2C9' : '#CCC' }">
               {{ note.selectText }}
             </p>
@@ -43,8 +43,19 @@ export default {
       data: getRichTextData(),
       selectedNoteId: '',
       isPreview: true,
+      divHeight: 'auto',
+      observer: null,
     };
   },
+  mounted() {
+    this.observer = new ResizeObserver(() => {
+      this.updateHeight();
+    });
+    this.observer.observe(this.$refs.leftDiv.closest('.describe-preview'));
+  },
+  beforeDestroy() {
+    this.observer.disconnect();
+  },
   methods: {
     handleRichFillClick(event) {
       // 检查点击的元素是否是 rich-fill 或者其子元素
@@ -52,11 +63,34 @@ export default {
       if (richFillElement) {
         // 处理点击事件
         this.selectedNoteId = richFillElement.dataset.annotationId;
-        // 这里可以添加你的业务逻辑
+
+        const sectionRef = this.$refs[`${this.selectedNoteId}`][0];
+        const rightDiv = this.$refs.rightDiv;
+
+        // 计算滚动位置
+        const rightDivTop = rightDiv.getBoundingClientRect().top;
+        const sectionTop = sectionRef.getBoundingClientRect().top;
+        const scrollPosition = sectionTop - rightDivTop + rightDiv.scrollTop;
+
+        // 平滑滚动
+        rightDiv.scrollTo({
+          top: scrollPosition,
+          behavior: 'smooth',
+        });
       } else {
         this.selectedNoteId = '';
       }
     },
+    updateHeight() {
+      this.$nextTick(() => {
+        const target = this.$refs.leftDiv;
+        const parent = target.closest('.describe-preview');
+        if (!parent) return;
+        setTimeout(() => {
+          this.divHeight = parent.offsetHeight - 16;
+        }, 800);
+      });
+    },
   },
 };
 </script>
@@ -76,6 +110,8 @@ export default {
       display: flex;
       flex-direction: column;
       width: 20%;
+      min-height: 150px;
+      overflow: auto;
 
       > span {
         font-weight: bold;