Browse Source

Merge branch 'master' of http://60.205.254.193:3000/GCLS/GCLS_Page_Textbook

# Conflicts:
#	package-lock.json
zq 1 year ago
parent
commit
db5be142dc

File diff suppressed because it is too large
+ 215 - 154
package-lock.json


+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     "dev": "vue-cli-service serve",
     "build": "vue-cli-service build",
     "lint": "vue-cli-service lint",
+    "postinstall": "patch-package",
     "builder": "electron-builder",
     "builder:mac": "electron-builder --mac"
   },

+ 178 - 0
patches/tinymce+5.10.9.patch

@@ -0,0 +1,178 @@
+diff --git a/node_modules/tinymce/plugins/ax_wordlimit/index.js b/node_modules/tinymce/plugins/ax_wordlimit/index.js
+new file mode 100644
+index 0000000..02edc4d
+--- /dev/null
++++ b/node_modules/tinymce/plugins/ax_wordlimit/index.js
+@@ -0,0 +1,7 @@
++// Exports the "ax_wordlimit" plugin for usage with module loaders
++// Usage:
++//   CommonJS:
++//     require('tinymce/plugins/ax_wordlimit')
++//   ES2015:
++//     import 'tinymce/plugins/ax_wordlimit'
++require('./plugin.js');
+diff --git a/node_modules/tinymce/plugins/ax_wordlimit/plugin.js b/node_modules/tinymce/plugins/ax_wordlimit/plugin.js
+new file mode 100644
+index 0000000..bd4704d
+--- /dev/null
++++ b/node_modules/tinymce/plugins/ax_wordlimit/plugin.js
+@@ -0,0 +1,55 @@
++tinymce.PluginManager.add('ax_wordlimit', function(editor) {
++    var pluginName='字数限制';
++    var global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools');
++    var global$2 = tinymce.util.Tools.resolve('tinymce.util.Delay');
++    var ax_wordlimit_type = editor.getParam('ax_wordlimit_type', 'letter' );
++    var ax_wordlimit_num = editor.getParam('ax_wordlimit_num', false );
++    var ax_wordlimit_delay = editor.getParam('ax_wordlimit_delay', 500 );
++    var ax_wordlimit_callback = editor.getParam('ax_wordlimit_callback', function(){} );
++    var ax_wordlimit_event = editor.getParam('ax_wordlimit_event', 'SetContent Undo Redo Keyup' );
++    var onsign=1;
++    //统计方法1:计算总字符数
++    var sumLetter = function(){
++        var html = editor.getContent();
++        var re1 = new RegExp("<.+?>","g");
++        var txt = html.replace(re1,'');
++        txt = txt.replace(/\n/g,'');
++        txt = txt.replace(/&nbsp;/g,' ');
++        var num=txt.length;
++        return {txt:txt,num:num}
++    }
++    var onAct = function(){
++        if(onsign){
++            onsign=0;
++            //此处预留更多统计方法
++            switch(ax_wordlimit_type){
++                case 'letter':
++                default:
++                    var res = sumLetter();
++            }
++            if( res.num > ax_wordlimit_num ){
++                ax_wordlimit_callback(editor, res.txt, ax_wordlimit_num);
++            }
++            setTimeout(function(){onsign=1}, ax_wordlimit_delay);
++        }
++
++    }
++    var setup = function(){
++        if( ax_wordlimit_num>0 ){
++            global$2.setEditorTimeout(editor, function(){
++                var doth = editor.on(ax_wordlimit_event, onAct);
++            }, 300);
++        }
++    };
++
++    setup();
++
++    return {
++        getMetadata: function () {
++            return  {
++                name: pluginName,
++                url: "http://tinymce.ax-z.cn/more-plugins/ax_wordlimit.php",
++            };
++        }
++    };
++});
+diff --git a/node_modules/tinymce/plugins/ax_wordlimit/plugin.min.js b/node_modules/tinymce/plugins/ax_wordlimit/plugin.min.js
+new file mode 100644
+index 0000000..143777e
+--- /dev/null
++++ b/node_modules/tinymce/plugins/ax_wordlimit/plugin.min.js
+@@ -0,0 +1,55 @@
++tinymce.PluginManager.add('ax_wordlimit', function(editor) {
++    var pluginName='字数限制';
++    var global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools');
++    var global$2 = tinymce.util.Tools.resolve('tinymce.util.Delay');
++    var ax_wordlimit_type = editor.getParam('ax_wordlimit_type', 'letter' );
++    var ax_wordlimit_num = editor.getParam('ax_wordlimit_num', false );
++    var ax_wordlimit_delay = editor.getParam('ax_wordlimit_delay', 500 );
++    var ax_wordlimit_callback = editor.getParam('ax_wordlimit_callback', function(){} );
++    var ax_wordlimit_event = editor.getParam('ax_wordlimit_event', 'SetContent Undo Redo Keyup' );
++    var onsign=1;
++    //统计方法1:计算总字符数
++    var sumLetter = function(){
++        var html = editor.getContent();
++        var re1 = new RegExp("<.+?>","g");
++        var txt = html.replace(re1,'');
++        txt = txt.replace(/\n/g,'');
++        txt = txt.replace(/&nbsp;/g,' ');
++        var num=txt.length;
++        return {txt:txt,num:num}
++    }
++    var onAct = function(){
++        if(onsign){
++            onsign=0;
++            //此处预留更多统计方法
++            switch(ax_wordlimit_type){
++                case 'letter':
++                default:
++                    var res = sumLetter();
++            }
++            if( res.num > ax_wordlimit_num ){
++                ax_wordlimit_callback(editor, res.txt, ax_wordlimit_num);
++            }
++            setTimeout(function(){onsign=1}, ax_wordlimit_delay);
++        }
++        
++    }
++    var setup = function(){
++        if( ax_wordlimit_num>0 ){
++            global$2.setEditorTimeout(editor, function(){
++                var doth = editor.on(ax_wordlimit_event, onAct);
++            }, 300);
++        }
++    };
++
++    setup();
++
++    return {
++        getMetadata: function () {
++            return  {
++                name: pluginName,
++                url: "http://tinymce.ax-z.cn/more-plugins/ax_wordlimit.php",
++            };
++        }
++    };
++});
+diff --git a/node_modules/tinymce/plugins/image/plugin.js b/node_modules/tinymce/plugins/image/plugin.js
+index 1691b27..5255b8c 100644
+--- a/node_modules/tinymce/plugins/image/plugin.js
++++ b/node_modules/tinymce/plugins/image/plugin.js
+@@ -8,7 +8,6 @@
+  */
+ (function () {
+     'use strict';
+-
+     var global$6 = tinymce.util.Tools.resolve('tinymce.PluginManager');
+ 
+     var __assign = function () {
+@@ -1407,7 +1406,7 @@
+               meta: {}
+             }
+           });
+-          api.showTab('general');
++          // api.showTab('general'); 切换 tab 为普通
+           changeSrc(helpers, info, state, api);
+         };
+         blobToDataUri(file).then(function (dataUrl) {
+@@ -1416,6 +1415,8 @@
+             helpers.uploadImage(blobInfo).then(function (result) {
+               updateSrcAndSwitchTab(result.url);
+               finalize();
++              helpers.onSubmit(info)(api); // 上传成功后,触发保存事件
++              updateSrcAndSwitchTab(""); // 保存事件成功后,清空 src
+             }).catch(function (err) {
+               finalize();
+               helpers.alertErr(err);
+diff --git a/node_modules/tinymce/tinymce.js b/node_modules/tinymce/tinymce.js
+index 3955a24..2988ecd 100644
+--- a/node_modules/tinymce/tinymce.js
++++ b/node_modules/tinymce/tinymce.js
+@@ -810,7 +810,7 @@
+     var lTrim = blank(/^\s+/g);
+     var rTrim = blank(/\s+$/g);
+     var isNotEmpty = function (s) {
+-      return s.length > 0;
++      return s?.length > 0;
+     };
+     var isEmpty$3 = function (s) {
+       return !isNotEmpty(s);

+ 1 - 1
src/components/RichText.vue

@@ -44,7 +44,7 @@ import 'tinymce/plugins/paste'; // 粘贴插件
 // import 'tinymce/plugins/preview'; // 预览插件
 import 'tinymce/plugins/hr';
 import 'tinymce/plugins/autoresize'; // 自动调整大小插件
-// import 'tinymce/plugins/ax_wordlimit'; // 字数限制插件
+import 'tinymce/plugins/ax_wordlimit'; // 字数限制插件
 
 import { getRandomNumber } from '@/utils';
 import { isNodeType } from '@/utils/validate';

+ 3 - 0
src/icons/svg/add-circle.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.41663 6.4165V4.08317H7.58329V6.4165H9.91663V7.58317H7.58329V9.9165H6.41663V7.58317H4.08329V6.4165H6.41663ZM6.99996 12.8332C3.7783 12.8332 1.16663 10.2215 1.16663 6.99984C1.16663 3.77817 3.7783 1.1665 6.99996 1.1665C10.2216 1.1665 12.8333 3.77817 12.8333 6.99984C12.8333 10.2215 10.2216 12.8332 6.99996 12.8332ZM6.99996 11.6665C9.5773 11.6665 11.6666 9.57718 11.6666 6.99984C11.6666 4.42251 9.5773 2.33317 6.99996 2.33317C4.42263 2.33317 2.33329 4.42251 2.33329 6.99984C2.33329 9.57718 4.42263 11.6665 6.99996 11.6665Z" fill="#175DFF"/>
+</svg>

+ 5 - 0
src/icons/svg/check-mark.svg

@@ -0,0 +1,5 @@
+<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" clip-rule="evenodd"
+    d="M3.76258 5.71248L9.06588 0.40918L9.77299 1.11629L3.76258 7.12669L0.227051 3.59116L0.934158 2.88405L3.76258 5.71248Z"
+    fill="currentColor" />
+</svg>

+ 23 - 0
src/views/book/courseware/create/components/common/ModuleBase.vue

@@ -206,6 +206,29 @@ export default {
     &-content {
       padding: 8px;
       background-color: #fff;
+
+      .option-list {
+        .rich-wrapper {
+          flex: 1;
+          min-height: 32px;
+
+          :deep .rich-text {
+            &.mce-content-body {
+              padding-top: 4px;
+            }
+
+            &:not(.mce-edit-focus) {
+              p {
+                margin: 0;
+              }
+            }
+
+            &.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before {
+              top: 6px;
+            }
+          }
+        }
+      }
     }
   }
 }

+ 3 - 0
src/views/book/courseware/create/components/common/ModuleMixin.js

@@ -1,5 +1,7 @@
 // 组件混入
 import ModuleBase from './ModuleBase.vue';
+import RichText from '@/components/RichText.vue';
+
 import { snGenerationMethodList, viewMethodList } from '@/views/book/courseware/data/common';
 import { SaveCoursewareComponentContent, GetCoursewareComponentContent } from '@/api/book';
 
@@ -29,6 +31,7 @@ const mixin = {
   },
   components: {
     ModuleBase,
+    RichText,
   },
   provide() {
     return {

+ 2 - 0
src/views/book/courseware/create/components/common/SettingMixin.js

@@ -1,4 +1,5 @@
 import { switchSerialNumber, computedQuestionNumber } from '../../../data/common';
+import { snGenerationMethodList } from '@/views/book/courseware/data/common';
 
 import SerialNumberPosition from '@/views/book/courseware/create/components/common/SerialNumberPosition.vue';
 
@@ -7,6 +8,7 @@ const mixin = {
     return {
       switchSerialNumber,
       computedQuestionNumber,
+      snGenerationMethodList,
     };
   },
   components: { SerialNumberPosition },

+ 141 - 4
src/views/book/courseware/create/components/question/select/Select.vue

@@ -1,11 +1,32 @@
 <template>
-  <ModuleBase :type="data.type" />
+  <ModuleBase :type="data.type">
+    <template #content>
+      <ul class="option-list">
+        <li v-for="(item, i) in data.option_list" :key="item.mark" class="option-item">
+          <span class="serial-number">{{ convertNumberToLetter(i) }}.</span>
+          <div class="option-contnet">
+            <span :class="['checkbox', { active: isAnswer(item.mark) }]" @click="selectAnswer(item.mark)">
+              <SvgIcon icon-class="check-mark" width="10" height="7" />
+            </span>
+            <RichText v-model="item.content" placeholder="输入内容" :inline="true" :height="32" />
+          </div>
+          <span class="delete" @click="deleteOption">
+            <SvgIcon icon-class="delete-2" width="12" height="12" />
+          </span>
+        </li>
+      </ul>
+      <div class="add">
+        <SvgIcon icon-class="add-circle" width="14" height="14" />
+        <span class="add-button" @click="addOption">增加选项</span>
+      </div>
+    </template>
+  </ModuleBase>
 </template>
 
 <script>
 import ModuleMixin from '../../common/ModuleMixin';
 
-import { getSelectData } from '@/views/book/courseware/data/select';
+import { getSelectData, getOption } from '@/views/book/courseware/data/select';
 
 export default {
   name: 'SelectPage',
@@ -15,8 +36,124 @@ export default {
       data: getSelectData(),
     };
   },
-  methods: {},
+  methods: {
+    // 将数字转换为小写字母
+    convertNumberToLetter(number) {
+      return String.fromCharCode(97 + number);
+    },
+    /**
+     * @description 判断是否为答案
+     * @param {string} mark 选项标记
+     */
+    isAnswer(mark) {
+      return this.data.answer.answer_list.includes(mark);
+    },
+    /**
+     * @description 选择答案
+     * @param {string} mark 选项标记
+     */
+    selectAnswer(mark) {
+      const index = this.data.answer.answer_list.indexOf(mark);
+      if (index === -1) {
+        this.data.answer.answer_list.push(mark);
+      } else {
+        this.data.answer.answer_list.splice(index, 1);
+      }
+    },
+    /**
+     * @description 添加选项
+     */
+    addOption() {
+      this.data.option_list.push(getOption());
+    },
+    /**
+     * @description 删除选项
+     * @param {number} index 选项索引
+     */
+    deleteOption(index) {
+      this.data.option_list.splice(index, 1);
+    },
+  },
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.option-list {
+  display: flex;
+  flex-direction: column;
+  row-gap: 8px;
+
+  .option-item {
+    display: flex;
+    column-gap: 8px;
+    align-items: center;
+    min-height: 36px;
+
+    .serial-number {
+      width: 40px;
+      height: 36px;
+      padding: 4px 8px;
+      color: $text-color;
+      background-color: $fill-color;
+      border-radius: 2px;
+    }
+
+    .option-contnet {
+      display: flex;
+      flex: 1;
+      column-gap: 6px;
+      align-items: center;
+      padding: 0 12px;
+      overflow: hidden;
+      background-color: $fill-color;
+
+      .checkbox {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 16px;
+        height: 16px;
+        cursor: pointer;
+        background-color: #fff;
+        border: 1px solid #dcdfe6;
+        border-radius: 2px;
+
+        .svg-icon {
+          display: none;
+        }
+
+        &.active {
+          color: #fff;
+          background-color: #000;
+
+          .svg-icon {
+            display: block;
+          }
+        }
+      }
+    }
+
+    .delete {
+      margin-left: 8px;
+      cursor: pointer;
+    }
+  }
+}
+
+.add {
+  display: flex;
+  column-gap: 6px;
+  align-items: center;
+  justify-content: center;
+  margin-top: 12px;
+
+  .svg-icon {
+    cursor: pointer;
+  }
+
+  .add-button {
+    color: $main-color;
+    cursor: pointer;
+  }
+}
+</style>

+ 6 - 12
src/views/book/courseware/create/components/question/select/SelectSetting.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <el-form :model="property" :label-position="labelPosition" label-width="72px">
+    <el-form :model="property" label-width="72px">
       <el-form-item label="序号" class="serial-number">
         <el-input v-model="property.serial_number" />
         <SvgIcon icon-class="switch" size="14" @click="switchSerialNumber(property)" />
@@ -19,11 +19,11 @@
         <SerialNumberPosition :position="property.sn_position" @changeNumberPosition="changeNumberPosition" />
       </el-form-item>
       <el-divider />
-      <el-form-item label="汉字框">
+      <el-form-item label="排列">
         <el-radio
           v-for="{ value, label } in arrangeTypeList"
           :key="value"
-          v-model="property.view_method"
+          v-model="property.arrange_method"
           :label="value"
         >
           {{ label }}
@@ -36,21 +36,15 @@
 <script>
 import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
 
-import { snGenerationMethodList } from '@/views/book/courseware/data/common';
-import { arrangeTypeList } from '@/views/book/courseware/data/select';
+import { arrangeTypeList, getSelectProperty } from '@/views/book/courseware/data/select';
 
 export default {
   name: 'SelectSetting',
   mixins: [SettingMixin],
   data() {
     return {
-      property: {
-        serial_number: 1,
-        sn_type: 'number',
-        sn_position: 'top-start',
-        sn_generation_method: snGenerationMethodList[0].value,
-        arrange_method: arrangeTypeList[0].value,
-      },
+      arrangeTypeList,
+      property: getSelectProperty(),
     };
   },
   methods: {},

+ 28 - 6
src/views/book/courseware/data/select.js

@@ -3,6 +3,7 @@ import {
   serialNumberTypeList,
   serialNumberPositionList,
 } from '@/views/book/courseware/data/common';
+import { getRandomNumber } from '@/utils';
 
 // 排列方式
 export const arrangeTypeList = [
@@ -10,16 +11,37 @@ export const arrangeTypeList = [
   { value: 'vertical', label: '竖屏' },
 ];
 
+/**
+ * 获取选择题属性
+ */
+export function getSelectProperty() {
+  return {
+    serial_number: 1,
+    sn_type: serialNumberTypeList[0].value,
+    sn_position: serialNumberPositionList[0].value,
+    sn_generation_method: snGenerationMethodList[0].value,
+    arrange_method: arrangeTypeList[0].value,
+  };
+}
+
+export function getOption() {
+  return {
+    content: '',
+    mark: getRandomNumber(),
+  };
+}
+
+/**
+ * 获取选择题数据
+ */
 export function getSelectData() {
   return {
     type: 'select',
     title: '选择',
-    property: {
-      serial_number: 1, // 序号
-      sn_type: serialNumberTypeList[0].value, // 序号类型:letter字母 number数字  capital大写字母 bracket_number括号数字
-      sn_position: serialNumberPositionList[0].value, // 序号位置:top-start top top-end,left-start left left-end等
-      sn_generation_method: snGenerationMethodList[0].value, // 序号生成方式:recalculate重新计算 follow跟随
-      arrange_method: arrangeTypeList[0].value, // 查看方式:independent独立 list列表 icon图标
+    option_list: [getOption(), getOption(), getOption()],
+    answer: {
+      answer_list: [],
     },
+    property: getSelectProperty(),
   };
 }

+ 50 - 65
src/views/book/courseware/preview/components/audio/Audio.vue

@@ -1,17 +1,6 @@
 <template>
-  <div class="audio_area">
-    <div
-      v-show="data.property.sn_position.includes('top')"
-      class="top"
-      :style="getJustifyContentStyle(data.property.sn_position)"
-    >
-      {{ data.property.serial_number }}
-    </div>
-    <div
-      v-show="data.property.sn_position.includes('left')"
-      class="left"
-      :style="getJustifyContentStyle(data.property.sn_position)"
-    >
+  <div class="audio_area" :style="getAreaStyle()">
+    <div class="sn-position" :style="getJustifyContentStyle()">
       {{ data.property.serial_number }}
     </div>
     <div class="main">
@@ -54,25 +43,12 @@
         </ul>
       </div>
     </div>
-    <div
-      v-show="data.property.sn_position.includes('right')"
-      class="right"
-      :style="getJustifyContentStyle(data.property.sn_position)"
-    >
-      {{ data.property.serial_number }}
-    </div>
-    <div
-      v-show="data.property.sn_position.includes('bottom')"
-      class="bottom"
-      :style="getJustifyContentStyle(data.property.sn_position)"
-    >
-      {{ data.property.serial_number }}
-    </div>
   </div>
 </template>
 
 <script>
 import { getAudioData } from '@/views/book/courseware/data/audio';
+
 import PreviewMixin from '../common/PreviewMixin';
 import AudioPlay from '../common/AudioPlay.vue';
 
@@ -113,16 +89,44 @@ export default {
       }
     },
     /**
-     * 得到位置样式
-     * @param {string} position 位置
+     * 得到序号外部样式
      */
-    getJustifyContentStyle(position) {
+    getAreaStyle() {
+      const position = this.data.property.sn_position;
+      let grid = '';
+
+      if (position.includes('right')) {
+        grid = `"main position" / 1fr auto`;
+      } else if (position.includes('left')) {
+        grid = `"position main" / auto 1fr`;
+      } else if (position.includes('top')) {
+        grid = `"position" auto "main" 1fr`;
+      } else if (position.includes('bottom')) {
+        grid = `"main" 1fr "position" auto`;
+      }
+
+      return {
+        grid,
+      };
+    },
+    /**
+     * 得到序号位置样式
+     */
+    getJustifyContentStyle() {
+      const position = this.data.property.sn_position;
+      let placeSelf = '';
+
       if (position.includes('start')) {
-        return 'justify-content: start';
+        placeSelf = 'flex-start';
       } else if (position.includes('end')) {
-        return 'justify-content: end';
+        placeSelf = 'flex-end';
+      } else {
+        placeSelf = 'center';
       }
-      return 'justify-content: center';
+
+      return {
+        placeSelf,
+      };
     },
   },
 };
@@ -131,14 +135,14 @@ export default {
 <style lang="scss" scoped>
 .audio_area {
   display: grid;
-  grid:
-    'top top top'
-    'left main right'
-    'bottom bottom bottom';
-  grid-template-columns: 1fr 28fr 1fr;
-  width: 100%;
-
-  > div {
+  gap: 6px;
+  padding: 8px;
+
+  .sn-position {
+    grid-area: position;
+  }
+
+  > .main {
     display: flex;
     margin: 4px auto;
 
@@ -147,16 +151,6 @@ export default {
     }
   }
 
-  .top {
-    grid-area: top;
-    width: 92%;
-  }
-
-  .left {
-    flex-direction: column;
-    grid-area: left;
-  }
-
   .main {
     grid-area: main;
     width: 100%;
@@ -175,11 +169,12 @@ export default {
 
     .view_independent {
       display: flex;
-      column-gap: 3%;
-      width: 100%;
+      flex: 1;
+      column-gap: 12px;
 
       :deep .el-carousel {
-        width: 90%;
+        flex: 1;
+        max-height: 500px;
         padding: 20%;
         background-color: #d9d9d9;
 
@@ -198,21 +193,11 @@ export default {
         display: flex;
         flex-direction: column;
         row-gap: 2px;
-        width: 25%;
+        min-width: 200px;
         max-height: 500px;
         overflow: auto;
       }
     }
   }
-
-  .right {
-    flex-direction: column;
-    grid-area: right;
-  }
-
-  .bottom {
-    grid-area: bottom;
-    width: 92%;
-  }
 }
 </style>

+ 22 - 0
src/views/book/courseware/preview/components/select/SelectPreview.vue

@@ -0,0 +1,22 @@
+<template>
+  <div></div>
+</template>
+
+<script>
+import { getSelectData } from '@/views/book/courseware/data/select';
+
+import PreviewMixin from '../common/PreviewMixin';
+
+export default {
+  name: 'SelectPreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      data: getSelectData(),
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped></style>

Some files were not shown because too many files changed in this diff