Переглянути джерело

富文本组件编辑标题模式

zq 1 місяць тому
батько
коміт
6a2fb82793

+ 30 - 9
src/components/CommonPreview.vue

@@ -430,6 +430,7 @@ export default {
       default: false,
     },
   },
+  inject: ['processHtmlString'],
   data() {
     const sidebarIconList = [
       { icon: 'search', title: '搜索', handle: 'getSearch', param: { type: '5' } },
@@ -646,7 +647,7 @@ export default {
         ({ courseware_info }) => {
           this.courseware_info = { ...this.courseware_info, ...courseware_info };
           this.getLangList();
-        },
+        }
       );
     },
     /**
@@ -656,6 +657,7 @@ export default {
     getCoursewareComponentContent_View(id) {
       ContentGetCoursewareContent_View({ id }).then(
         ({ content, component_list, content_group_row_list, title_list }) => {
+          if (title_list) this.title_list = title_list || [];
           if (content) {
             const _content = JSON.parse(content);
             this.data = _content;
@@ -666,7 +668,7 @@ export default {
           } else {
             this.data = { row_list: [] };
           }
-
+          let processHtmlString = typeof this.processHtmlString === 'function' ? this.processHtmlString : null;
           if (component_list) this.component_list = component_list;
           this.component_list.forEach((x) => {
             if (x.component_type === 'audio') {
@@ -684,20 +686,39 @@ export default {
               }
 
               x.content = JSON.stringify(_c);
+            } else if (x.component_type === 'richtext') {
+              if (!processHtmlString) return;
+              let _c = JSON.parse(x.content);
+              let p = _c.property || {};
+              let lev = Number(p.title_style_level);
+              if (p.is_title !== 'true' || lev < 1 || !_c.content) return;
+
+              let style = this.title_list.find((y) => y.level == lev) || {};
+              if (style && style.style) {
+                style = JSON.parse(style.style);
+                let c_text = _c.content;
+
+                const parser = new DOMParser();
+                const doc = parser.parseFromString(c_text, 'text/html');
+                const body = doc.body;
+                const pElements = body.querySelectorAll('p');
+                processHtmlString(pElements, style);
+                _c.content = body.innerHTML;
+
+                x.content = JSON.stringify(_c);
+              }
             }
           });
 
           if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list) || [];
-          if (title_list) this.title_list = title_list || [];
-        },
+        }
       );
     },
-
     getLangList() {
       GetLanguageTypeList({ book_id: this.courseware_info.book_id, is_contain_zh: 'true' }).then(
         ({ language_type_list }) => {
           this.langList = language_type_list;
-        },
+        }
       );
     },
 
@@ -810,7 +831,7 @@ export default {
       this.$refs.courserware.handleResult(
         this.$refs.previewMain.scrollTop,
         this.$refs.previewMain.scrollLeft,
-        this.select_node,
+        this.select_node
       );
     },
     /**
@@ -1133,7 +1154,7 @@ export default {
           x.coursewareId === note.coursewareId &&
           x.blockId === note.blockId &&
           x.startIndex === note.startIndex &&
-          x.endIndex === note.endIndex,
+          x.endIndex === note.endIndex
       );
       if (old) {
         this.oldRichData = old;
@@ -1227,7 +1248,7 @@ export default {
           x.coursewareId === collect.coursewareId &&
           x.blockId === collect.blockId &&
           x.startIndex === collect.startIndex &&
-          x.endIndex === collect.endIndex,
+          x.endIndex === collect.endIndex
       );
       if (old) {
         this.$message({

+ 77 - 1
src/components/RichText.vue

@@ -129,6 +129,10 @@ export default {
       type: Number,
       default: null,
     },
+    isTitle: {
+      type: Boolean,
+      default: false,
+    },
   },
   data() {
     return {
@@ -141,6 +145,8 @@ export default {
         left: 0,
       },
       id: getRandomNumber(),
+      editorIsInited: false,
+      editorBeforeInitConfig: {},
       init: {
         content_style: `
           mjx-container, mjx-container * {
@@ -237,6 +243,7 @@ export default {
                 remove_similar: true,
               });
             }
+            this.editorIsInited = true;
           });
 
           // 自定义行高下拉(因为没有内置 lineheight 插件)
@@ -484,6 +491,7 @@ export default {
       },
     };
   },
+  inject: ['processHtmlString'],
   watch: {
     isViewNote: {
       handler(newVal) {
@@ -541,7 +549,31 @@ export default {
         }
       },
     },
+    isTitle: {
+      handler(newVal) {
+        this.displayToolbar(newVal);
+      },
+    },
+    // 未初始化完成或者数据未加载完成的时候,执行会有问题
+    editorIsInited: {
+      handler(newVal) {
+        if (newVal) {
+          let isTitle = this.editorBeforeInitConfig['isTitle'];
+          if (isTitle === true || isTitle === false) {
+            this.displayToolbar(isTitle);
+            this.editorBeforeInitConfig['isTitle'] = null;
+          }
+
+          let style = this.editorBeforeInitConfig['style'];
+          if (style) {
+            this.setRichTitleFormat(style);
+            this.editorBeforeInitConfig['style'] = null;
+          }
+        }
+      },
+    },
   },
+  computed: {},
   created() {
     if (this.pageFrom !== 'audit') {
       window.addEventListener('click', this.hideToolbarDrawer);
@@ -562,6 +594,29 @@ export default {
     }
   },
   methods: {
+    displayToolbar(isTitle, isInit) {
+      if (!this.editorIsInited) {
+        this.editorBeforeInitConfig['isTitle'] = isTitle;
+        return;
+      }
+
+      let editor = tinymce.get(this.id);
+      if (!editor) return;
+      const header = editor.editorContainer?.querySelector('.tox-editor-header');
+      if (header) {
+        header.style.display = !isTitle ? '' : 'none';
+        if (isTitle && !isInit) {
+          const body = editor.getBody();
+          if (!body) return;
+
+          const pElements = body.querySelectorAll('p');
+          if (this.processHtmlString && typeof this.processHtmlString === 'function') {
+            this.processHtmlString(pElements, {}, true);
+          }
+        }
+      }
+    },
+
     smartPreserveLineBreaks(editor, content) {
       let body = editor.getBody();
       let originalParagraphs = Array.from(body.getElementsByTagName('p'));
@@ -709,6 +764,27 @@ export default {
       }
       editor.selection.collapse(false);
     },
+    setRichTitleFormat(config) {
+      if (!this.editorIsInited) {
+        this.editorBeforeInitConfig['style'] = config;
+        return;
+      }
+
+      let editor = tinymce.get(this.id);
+      if (!editor) return;
+
+      // 获取编辑器内容区域
+      const body = editor.getBody();
+      if (!body) return;
+
+      const pElements = body.querySelectorAll('p');
+      if (typeof this.processHtmlString === 'function') {
+        this.processHtmlString(pElements, config);
+      }
+      editor.fire('change');
+      editor.nodeChanged();
+    },
+
     /**
      * 图片上传自定义逻辑函数
      * @param {object} blobInfo 文件数据
@@ -1154,7 +1230,7 @@ export default {
         {
           acceptNode: (node) => (node.textContent.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT),
         },
-        false,
+        false
       );
       return walker.nextNode();
     },

+ 21 - 0
src/layouts/default/index.vue

@@ -18,6 +18,27 @@ export default {
     LayoutHeader,
     LayoutBreadcrumb,
   },
+  provide() {
+    return {
+      processHtmlString: (pElements, styleConfig, isClear) => {
+        pElements.forEach((pElement) => {
+          if (isClear) {
+            pElement.removeAttribute('style');
+          } else {
+            const style = pElement.style;
+
+            if (styleConfig.font) style.fontFamily = styleConfig.font;
+            if (styleConfig.font_size) style.fontSize = styleConfig.font_size;
+            if (styleConfig.text_color) style.color = styleConfig.text_color;
+
+            style.fontWeight = styleConfig.bold === 'true' ? 'bold' : 'normal';
+            style.fontStyle = styleConfig.italic === 'true' ? 'italic' : 'normal';
+            if (styleConfig.indent || styleConfig.indent == 0) style.marginLeft = styleConfig.indent + 'px';
+          }
+        });
+      },
+    };
+  },
   data() {
     return {
       isShowBreadcrumb: false,

+ 32 - 5
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -6,7 +6,7 @@
           ref="richText"
           v-model="data.content"
           :is-view-note="true"
-          placeholder="输入内容"
+          :is-title="isEnable(data.property.is_title)"
           :is-view-pinyin="isEnable(data.property.view_pinyin)"
           :font-size="data?.unified_attrib?.font_size"
           :font-family="data?.unified_attrib?.font"
@@ -14,6 +14,7 @@
           @selectContentSetMemo="selectContentSetMemo"
           @createParsedTextInfoPinyin="createParsedTextInfoPinyin"
           @compareAnnotationAndSave="compareAnnotationAndSave"
+          placeholder="输入内容"
         />
         <el-button class="btn" @click="openMultilingual">多语言</el-button>
         <MultilingualFill
@@ -63,6 +64,7 @@ export default {
   name: 'RichTextPage',
   components: { PinyinText, ExplanatoryNoteDialog },
   mixins: [ModuleMixin],
+  inject: ['getBookUnifiedTitleList'],
   data() {
     return {
       isEnable,
@@ -71,6 +73,7 @@ export default {
       richId: '',
       isViewExplanatoryNoteDialog: false,
       oldRichData: {},
+      titleStyleList: [],
     };
   },
   watch: {
@@ -86,6 +89,31 @@ export default {
       },
       deep: true,
     },
+    'data.property.is_title': {
+      handler(newLevel) {
+        this.$nextTick(() => {
+          if (!isEnable(this.data.property.is_title)) {
+            this.data.property.title_style_level = '';
+          }
+        });
+      },
+      immediate: true,
+    },
+    'data.property.title_style_level': {
+      handler(newLevel) {
+        this.$nextTick(() => {
+          if (Number(newLevel) < 1) return;
+          if (this.titleStyleList.length == 0 && typeof this.getBookUnifiedTitleList === 'function') {
+            this.titleStyleList = this.getBookUnifiedTitleList() || [];
+          }
+          if (this.titleStyleList.length > 0) {
+            var style = this.titleStyleList.find((x) => x.level === newLevel) || {};
+            this.$refs.richText.setRichTitleFormat(style);
+          }
+        });
+      },
+      immediate: true,
+    },
     'data.content': {
       handler: 'handlerMindMap',
       deep: true,
@@ -124,8 +152,7 @@ export default {
         return;
       }
       data.text = text.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
-      data.is_first_sentence_first_hz_pinyin_first_char_upper_case =
-        this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case;
+      data.is_first_sentence_first_hz_pinyin_first_char_upper_case = this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case;
       PinyinBuild_OldFormat(data).then(({ parsed_text }) => {
         if (parsed_text) {
           const mergedData = parsed_text.paragraph_list.map((outerArr, i) =>
@@ -149,8 +176,8 @@ export default {
                   Object.assign(tmp.activeTextStyle, styles);
                 }
                 return tmp;
-              }),
-            ),
+              })
+            )
           );
           this.data.paragraph_list = mergedData; // 取出合并后的数组
         }

+ 15 - 0
src/views/book/courseware/create/components/base/rich_text/RichTextSetting.vue

@@ -5,6 +5,16 @@
 
       <BackgroundSet :property="property" />
 
+      <el-form-item label="是否标题">
+        <el-switch v-model="property.is_title" active-value="true" inactive-value="false" />
+      </el-form-item>
+
+      <el-form-item label="标题属性" v-if="property.is_title === 'true'">
+        <el-select v-model="property.title_style_level" placeholder="请选择标题级别" style="width: 100px">
+          <el-option v-for="item in titleList" :key="item.level" :label="item.level_title" :value="item.level" />
+        </el-select>
+      </el-form-item>
+
       <el-form-item label="拼音">
         <el-switch v-model="property.view_pinyin" active-value="true" inactive-value="false" />
       </el-form-item>
@@ -61,8 +71,13 @@ export default {
       selectContent: '',
       visible: false,
       content: '',
+      titleList: [],
     };
   },
+  inject: ['getBookUnifiedTitleList'],
+  created() {
+    if (typeof this.getBookUnifiedTitleList === 'function') this.titleList = this.getBookUnifiedTitleList() || [];
+  },
   methods: {},
 };
 </script>

+ 16 - 0
src/views/book/courseware/create/index.vue

@@ -45,6 +45,7 @@
 
 <script>
 import { bookTypeOption, componentSettingList } from '../data/bookType';
+import { GetTitleStyle } from '@/api/book';
 
 import CreateCanvas from './components/CreateCanvas.vue';
 import SelectBackground from './components/SelectBackground.vue';
@@ -62,6 +63,7 @@ export default {
       getCurSettingId: () => this.curSettingId,
       courseware_id: this.courseware_id,
       project_id: this.$route.query.project_id,
+      getBookUnifiedTitleList: () => this.title_style_list,
     };
   },
   data() {
@@ -82,8 +84,12 @@ export default {
       isChange: false, // 是否有改动
       showBackTop: false,
       copyData: null,
+      title_style_list: [],
     };
   },
+  created() {
+    this.getBookUnifiedTitle();
+  },
   mounted() {
     this.$nextTick(() => {
       const middle = this.$refs.createMiddle;
@@ -222,6 +228,16 @@ export default {
     insertTemplateData_Create({ row_list, content_group_row_list }) {
       this.$refs.createCanvas.insertTemplateData_CreateCanvas({ row_list, content_group_row_list });
     },
+    async getBookUnifiedTitle() {
+      this.title_style_list = [];
+      await GetTitleStyle({ book_id: this.$route.query.project_id }).then(({ title_list }) => {
+        if (title_list && title_list.length > 0) {
+          title_list.map((x) => {
+            if (x.style) this.title_style_list.push(JSON.parse(x.style));
+          });
+        }
+      });
+    },
   },
 };
 </script>

+ 2 - 0
src/views/book/courseware/data/richText.js

@@ -463,6 +463,8 @@ export function getRichTextProperty() {
     pinyin_position: pinyinPositionList[0].value, // top bottom
     pinyin_overall_position: pinyinOverallPositionList[0].value, // left center right
     is_first_sentence_first_hz_pinyin_first_char_upper_case: 'true', // 句首大写
+    title_style_level: '', // 标题样式级别
+    is_title: 'false', // 是否标题
     ...commonComponentProperty,
   };
 }

+ 41 - 3
src/views/personal_workbench/edit_task/edit/UseTemplate.vue

@@ -88,6 +88,7 @@ export default {
       required: true,
     },
   },
+  inject: ['processHtmlString'],
   data() {
     return {
       loading: false,
@@ -164,6 +165,8 @@ export default {
     getCoursewareComponentContent_View(id) {
       ContentGetCoursewareContent_View({ id }).then(
         ({ content, component_list, content_group_row_list, title_list }) => {
+          if (title_list) this.title_list = title_list || [];
+
           if (content) {
             const _content = JSON.parse(content);
             this.coursewareData = _content;
@@ -176,6 +179,7 @@ export default {
           }
 
           if (component_list) this.component_list = component_list;
+          let processHtmlString = typeof this.processHtmlString === 'function' ? this.processHtmlString : null;
           this.component_list.forEach((x) => {
             if (x.component_type === 'audio') {
               let _c = JSON.parse(x.content);
@@ -192,15 +196,49 @@ export default {
               }
 
               x.content = JSON.stringify(_c);
+            } else if (x.component_type === 'richtext') {
+              if (!processHtmlString) return;
+              let _c = JSON.parse(x.content);
+              let p = _c.property || {};
+              let lev = Number(p.title_style_level);
+              if (p.is_title !== 'true' || lev < 1 || !_c.content) return;
+
+              let style = this.title_list.find((y) => y.level == lev) || {};
+              if (style && style.style) {
+                style = JSON.parse(style.style);
+                let c_text = _c.content;
+
+                const parser = new DOMParser();
+                const doc = parser.parseFromString(c_text, 'text/html');
+                const body = doc.body;
+                const pElements = body.querySelectorAll('p');
+                processHtmlString(pElements, style);
+                _c.content = body.innerHTML;
+
+                x.content = JSON.stringify(_c);
+              }
             }
           });
 
           if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list) || [];
-
-          if (title_list) this.title_list = title_list || [];
-        },
+        }
       );
     },
+    resetStylesMulti(html, config) {
+      // 提取所有文本(保留换行)
+      const text = html
+        .replace(/<br\s*\/?>/gi, '\n') // 保留换行
+        .replace(/<[^>]+>/g, '') // 移除标签
+        .replace(/&nbsp;/g, ' ') // 处理空格
+        .trim();
+
+      // 构建新HTML
+      return `<p style="font-family: ${config.font}; font-size: ${config.font_size}; color: ${
+        config.text_color
+      }; font-weight: ${config.bold === 'true' ? 'bold' : 'normal'}; font-style: ${
+        config.italic === 'true' ? 'italic' : 'normal'
+      }; margin-left: ${config.indent}px;">${text}</p>`;
+    },
     /**
      * 得到课件内容(编辑内容)
      * @param {string} id - 课件ID

+ 7 - 21
src/views/personal_workbench/project/components/BookUnifiedTitle.vue

@@ -55,13 +55,13 @@
                   <el-input-number
                     v-model="item.indent"
                     :min="0"
-                    :max="10"
-                    :step="1"
+                    :max="500"
+                    :step="5"
                     controls-position="right"
                     placeholder="0"
                     style="width: 80px"
                   />
-                  <span style="margin-left: 8px; font-size: 12px; color: #909399">字符</span>
+                  <span style="margin-left: 8px; font-size: 12px; color: #909399">PX</span>
                 </div>
               </el-form-item>
             </div>
@@ -69,7 +69,7 @@
         </el-form>
       </div>
     </div>
-
+<div style="margin-top:5px;color:#f53f3f">说明:修改设置后,编辑界面需刷新!</div>
     <div slot="footer" class="dialog-footer">
       <el-button @click="dialogClose">取 消</el-button>
       <el-button type="primary" @click="saveBookUnifiedTitle">确 定</el-button>
@@ -78,7 +78,7 @@
 </template>
 
 <script>
-import { GetTitleStyle, ApplyBookUnifiedAttrib, SaveTitleStyle } from '@/api/book';
+import { GetTitleStyle, SaveTitleStyle } from '@/api/book';
 import { unified_title } from '@/common/data';
 
 export default {
@@ -125,10 +125,10 @@ export default {
     },
   },
   created() {
-    this.initTitleSettings();
+    this.initDefaultTitleSettings();
   },
   methods: {
-    initTitleSettings() {
+    initDefaultTitleSettings() {
       // 各级别的默认字号
       const defaultFontSizes = ['16pt', '14pt', '12pt', '10pt', '8pt'];
 
@@ -175,20 +175,6 @@ export default {
 
       if (!isSuccess) this.titleSettings = this.defaultTitleSettings;
     },
-    applyBookUnifiedAttr() {
-      let loading = this.$loading({ fullscreen: true, text: '正在应用,请稍后...' });
-      ApplyBookUnifiedAttrib({
-        book_id: this.bookId,
-        content: JSON.stringify(this.unified_title),
-      })
-        .then(() => {
-          this.$message.success('应用成功');
-          this.dialogClose();
-        })
-        .finally(() => {
-          loading.close();
-        });
-    },
     saveBookUnifiedTitle() {
       let title_list = [];
       this.titleSettings.forEach((config, index) => {