2 Commits a66906baf4 ... a81f9fd6e2

Author SHA1 Message Date
  dsy a81f9fd6e2 Merge branch 'master' of http://60.205.254.193:3000/GCLS/eep_page 1 month ago
  dsy 1a41e0b12f 填空的简繁体和多语言 1 month ago

+ 11 - 0
package-lock.json

@@ -21,6 +21,7 @@
         "mathjax": "^3.2.2",
         "md5": "^2.3.0",
         "nprogress": "^0.2.0",
+        "opencc-js": "^1.0.5",
         "simple-mind-map": "^0.14.0-fix.1",
         "three": "^0.178.0",
         "tinymce": "^5.10.9",
@@ -14273,6 +14274,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/opencc-js": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.0.5.tgz",
+      "integrity": "sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg=="
+    },
     "node_modules/opener": {
       "version": "1.5.2",
       "resolved": "https://registry.npmmirror.com/opener/-/opener-1.5.2.tgz",
@@ -31666,6 +31672,11 @@
         "is-wsl": "^2.2.0"
       }
     },
+    "opencc-js": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.0.5.tgz",
+      "integrity": "sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg=="
+    },
     "opener": {
       "version": "1.5.2",
       "resolved": "https://registry.npmmirror.com/opener/-/opener-1.5.2.tgz",

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "mathjax": "^3.2.2",
     "md5": "^2.3.0",
     "nprogress": "^0.2.0",
+    "opencc-js": "^1.0.5",
     "simple-mind-map": "^0.14.0-fix.1",
     "three": "^0.178.0",
     "tinymce": "^5.10.9",

+ 11 - 0
src/utils/common.js

@@ -1,3 +1,5 @@
+import DOMPurify from 'dompurify';
+
 /**
  * 换算数据大小
  * @param {number} size
@@ -136,3 +138,12 @@ export function getArrayDepth(arr) {
 
   return 1 + Math.max(...arr.map((item) => (Array.isArray(item) ? getArrayDepth(item) : 0)));
 }
+
+/**
+ * 过滤 html,防止 xss 攻击
+ * @param {string} html 需要过滤的html
+ * @returns {string} 过滤后的html
+ */
+export function sanitizeHTML(html) {
+  return DOMPurify.sanitize(html);
+}

+ 19 - 2
src/views/book/components/MultilingualFill.vue

@@ -40,7 +40,8 @@
           <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" />
+          <!-- eslint-disable-next-line vue/no-v-html -->
+          <div v-show="isShowOriginal" class="original-text" v-html="sanitizeHTML(text)"></div>
           <el-input
             v-for="lang in selectedLangList"
             v-show="curLang === lang.code"
@@ -63,6 +64,7 @@
 import UpdateLang from './UpdateLang.vue';
 
 import { langList } from '@/views/book/courseware/data/common';
+import { sanitizeHTML } from '@/utils/common';
 
 export default {
   name: 'MultilingualFill',
@@ -86,6 +88,7 @@ export default {
   data() {
     return {
       langList,
+      sanitizeHTML,
       selectedLangList: [
         { code: 'en', translation: '' },
         { code: 'fr', translation: '' },
@@ -135,7 +138,7 @@ export default {
       ];
     },
     submitTranslation() {
-      this.$emit('submit-translation', this.selectedLangList);
+      this.$emit('SubmitTranslation', this.selectedLangList);
       this.closeDialog();
     },
   },
@@ -241,6 +244,20 @@ export default {
     .content {
       display: flex;
       column-gap: 8px;
+
+      .original-text {
+        width: 100%;
+        height: 579px;
+        padding: 5px 15px;
+        overflow: auto;
+        background-color: #f2f3f5;
+        border: 1px solid #f2f3f5;
+        border-radius: 4px;
+
+        :deep p {
+          margin: 0;
+        }
+      }
     }
   }
 }

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

@@ -63,5 +63,3 @@ export default {
   },
 };
 </script>
-
-<style lang="scss" scoped></style>

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

@@ -57,7 +57,12 @@
         </div>
       </div>
 
-      <MultilingualFill :visible.sync="multilingualVisible" :text="data.content" />
+      <MultilingualFill
+        :visible.sync="multilingualVisible"
+        :text="data.content"
+        :translations="data.multilingual"
+        @SubmitTranslation="handleMultilingualTranslation"
+      />
     </template>
   </ModuleBase>
 </template>
@@ -208,6 +213,13 @@ export default {
         },
       ];
     },
+    /**
+     * @description 提交多语言翻译
+     * @param {Array} multilingual
+     */
+    handleMultilingualTranslation(multilingual) {
+      this.data.multilingual = multilingual;
+    },
   },
 };
 </script>

+ 1 - 0
src/views/book/courseware/data/fill.js

@@ -78,5 +78,6 @@ export function getFillData() {
     mind_map: {
       node_list: [{ name: '横排中文填空组件' }],
     },
+    multilingual: [], // 多语言
   };
 }

+ 8 - 9
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -1,12 +1,13 @@
 import SerialNumberPosition from './SerialNumberPosition.vue';
-import DOMPurify from 'dompurify';
 
 import { isEnable } from '@/views/book/courseware/data/common';
 import { ContentGetCoursewareComponentContent } from '@/api/book';
+import { sanitizeHTML } from '@/utils/common';
 
 const mixin = {
   data() {
     return {
+      sanitizeHTML,
       answer: { answer_list: [] }, // 答案
       isJudgingRightWrong: false, // 是否判断对错
       isShowRightAnswer: false, // 是否显示正确答案
@@ -15,6 +16,7 @@ const mixin = {
       loader: false,
     };
   },
+  inject: ['getLang', 'getChinese', 'convertText'],
   props: {
     id: {
       type: String,
@@ -33,6 +35,11 @@ const mixin = {
       default: '',
     },
   },
+  computed: {
+    showLang() {
+      return this.getLang() !== 'zh';
+    },
+  },
   watch: {
     content: {
       handler(newVal) {
@@ -112,14 +119,6 @@ const mixin = {
         grid,
       };
     },
-    /**
-     * 过滤 html,防止 xss 攻击
-     * @param {string} html 需要过滤的html
-     * @returns {string} 过滤后的html
-     */
-    sanitizeHTML(html) {
-      return DOMPurify.sanitize(html);
-    },
   },
 };
 

+ 24 - 5
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -8,7 +8,7 @@
       <div class="fill-wrapper">
         <p v-for="(item, i) in modelEssay" :key="i">
           <template v-for="(li, j) in item">
-            <span v-if="li.type === 'text'" :key="j" v-html="sanitizeHTML(li.content)"></span>
+            <span v-if="li.type === 'text'" :key="j" v-html="convertText(sanitizeHTML(li.content))"></span>
             <template v-if="li.type === 'input'">
               <template v-if="data.property.fill_type === fillTypeList[0].value">
                 <el-input
@@ -84,6 +84,9 @@
         :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
         @handleWav="handleWav"
       />
+      <div v-if="showLang" class="lang">
+        {{ data.multilingual.find((item) => item.code === getLang())?.translation }}
+      </div>
     </div>
 
     <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
@@ -234,14 +237,26 @@ export default {
       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 gridArea = '';
+      let gridTemplateRows = '';
+      let gridTemplateColumns = '';
+      if (isRow) {
+        gridArea = `"${_list[0].name} ${_list[1].name}${isEnableVoice ? ' record' : ''}" ${this.showLang ? ' "lang lang lang"' : ''}`;
+        gridTemplateRows = `auto ${this.showLang ? 'auto' : ''}`;
+        gridTemplateColumns = `${_list[0].value} ${_list[1].value}${isEnableVoice ? ' 160px' : ''}`;
+      } else {
+        gridArea = `"${_list[0].name}" "${_list[1].name}" ${isEnableVoice ? `" record" ` : ''} ${this.showLang ? ' "lang"' : ''}`;
+        gridTemplateRows = `${_list[0].value} ${_list[1].value} ${isEnableVoice ? ' 32px' : ''} ${this.showLang ? 'auto' : ''}`;
+        gridTemplateColumns = '1fr';
+      }
+
       let style = {
         'grid-auto-flow': isRow ? 'column' : 'row',
         'column-gap': isRow ? '16px' : undefined,
         'row-gap': isRow ? undefined : '8px',
-        grid,
+        'grid-template-areas': gridArea,
+        'grid-template-rows': gridTemplateRows,
+        'grid-template-columns': gridTemplateColumns,
       };
       return style;
     },
@@ -299,6 +314,10 @@ export default {
   .main {
     display: grid;
     align-items: center;
+
+    .lang {
+      grid-area: lang;
+    }
   }
 
   .fill-wrapper {

+ 60 - 0
src/views/personal_workbench/edit_task/edit/index.vue

@@ -13,6 +13,21 @@
           <span :class="['link', { active: !isEdit }]" @click="toggleEditMode(false)">位置调整</span>
         </div>
         <div class="operator">
+          <el-switch
+            v-show="!isEdit"
+            v-model="chinese"
+            active-value="zh-Hant"
+            inactive-value="zh-Hans"
+            active-color="#ff4949"
+            inactive-color="#165dff"
+            active-text="繁"
+            inactive-text="简"
+          />
+          <span v-if="!isEdit" class="link">
+            <el-select v-model="lang" placeholder="请选择语言" size="mini" class="lang-select">
+              <el-option v-for="item in langList" :key="item.code" :label="item.name" :value="item.code" />
+            </el-select>
+          </span>
           <span class="link" @click="showSetBackground">背景图</span>
           <span class="link" @click="saveCoursewareContent('quit')">退出编辑</span>
           <span class="link" @click="saveCoursewareContent">保存</span>
@@ -26,8 +41,12 @@
 </template>
 
 <script>
+import Vue from 'vue';
+
 import CreatePage from '@/views/book/courseware/create/index.vue';
 import MenuPage from '@/views/personal_workbench/common/menu.vue';
+import * as OpenCC from 'opencc-js';
+import { langList } from '@/views/book/courseware/data/common';
 
 import { GetBookCoursewareInfo, GetMyBookCoursewareTaskList } from '@/api/project';
 
@@ -37,6 +56,14 @@ export default {
     CreatePage,
     MenuPage,
   },
+  // 将 lang、chinese 提供给子组件,并让这几个属性可响应式
+  provide() {
+    return {
+      getLang: () => this.lang,
+      getChinese: () => this.chinese,
+      convertText: this.convertText,
+    };
+  },
   data() {
     return {
       id: this.$route.params.courseware_id,
@@ -44,6 +71,10 @@ export default {
       courseware_info: {},
       courseware_list: [],
       isEdit: true, // 是否编辑状态
+      opencc: OpenCC.Converter({ from: 'cn', to: 'tw' }),
+      langList: [{ code: 'zh', name: '中文' }, ...langList],
+      lang: 'zh',
+      chinese: 'zh-Hans',
     };
   },
   created() {
@@ -98,6 +129,17 @@ export default {
         this.courseware_list = courseware_list;
       });
     },
+    /**
+     * 文本转换
+     * @param {string} text - 要转换的文本
+     * @returns {string} - 转换后的文本
+     */
+    convertText(text) {
+      if (this.chinese === 'zh-Hant' && this.opencc) {
+        return this.opencc(text);
+      }
+      return text;
+    },
   },
 };
 </script>
@@ -144,6 +186,24 @@ export default {
           }
         }
       }
+
+      .operator {
+        .lang-select {
+          :deep .el-input {
+            width: 100px;
+          }
+
+          :deep .el-input__inner {
+            height: 24px;
+            line-height: 24px;
+            background-color: #fff;
+          }
+
+          :deep .el-input__icon {
+            line-height: 24px;
+          }
+        }
+      }
     }
   }