Browse Source

帮助中心配置

yujinwen 3 months ago
parent
commit
3bdcc75be1

+ 4 - 0
index.html

@@ -5,9 +5,13 @@
     <link rel="icon" type="image/x-icon" href="/fa.ico" id="icon"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>ETA社区</title>
+    <link href='/froala_editor.pkgd.min.css' rel='stylesheet' type='text/css' />
   </head>
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.js"></script>
+    <script src="/jquery-3.6.0.min.js"></script>
+    <script type='text/javascript' src='/froala_editor.pkgd.min.js'></script>
+    <script type='text/javascript' src='/froala_editor_zh_cn.js'></script>
   </body>
 </html>

File diff suppressed because it is too large
+ 6 - 0
public/froala_editor.pkgd.min.css


File diff suppressed because it is too large
+ 6 - 0
public/froala_editor.pkgd.min.js


+ 304 - 0
public/froala_editor_zh_cn.js

@@ -0,0 +1,304 @@
+/*!
+ * froala_editor v4.0.19 (https://www.froala.com/wysiwyg-editor)
+ * License https://froala.com/wysiwyg-editor/terms/
+ * Copyright 2014-2023 Froala Labs
+ */
+
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('froala-editor')) :
+  typeof define === 'function' && define.amd ? define(['froala-editor'], factory) :
+  (factory(global.FroalaEditor));
+}(this, (function (FE) { 'use strict';
+
+  FE = FE && FE.hasOwnProperty('default') ? FE['default'] : FE;
+
+  /**
+  * Simplified Chinese spoken in China.
+  */
+  FE.LANGUAGE['zh_cn'] = {
+    translation: {
+      // Place holder
+      'Type something': '输入内容',
+      // Basic formatting
+      'Bold': '粗体',
+      'Italic': '斜体',
+      'Underline': '下划线',
+      'Strikethrough': '删除线',
+      // Main buttons
+      'Insert': '插入',
+      'Delete': '删除',
+      'Cancel': '取消',
+      'OK': '确定',
+      'Back': '后退',
+      'Remove': '删除',
+      'More': '更多',
+      'Update': '更新',
+      'Style': '样式',
+      // Font
+      'Font Family': '字体',
+      'Font Size': '字号',
+      // Colors
+      'Colors': '颜色',
+      'Background': '背景',
+      'Text': '字体',
+      'HEX Color': '十六进制颜色',
+      // Paragraphs
+      'Paragraph Format': '段落格式',
+      'Normal': '正文',
+      'Code': '代码',
+      'Heading 1': '标题1',
+      'Heading 2': '标题2',
+      'Heading 3': '标题3',
+      'Heading 4': '标题4',
+      // Style
+      'Paragraph Style': '段落样式',
+      'Inline Style': '内联样式',
+      // Alignment
+      'Align': '对齐方式',
+      'Align Left': '左对齐',
+      'Align Center': '居中',
+      'Align Right': '右对齐',
+      'Align Justify': '两端对齐',
+      'None': '无',
+      // Lists
+      'Ordered List': '编号',
+      'Unordered List': '项目符号',
+      // Indent
+      'Decrease Indent': '减少缩进量',
+      'Increase Indent': '增加缩进量',
+      // Links
+      'Insert Link': '插入超链接',
+      'Open in new tab': '在新标签页中打开',
+      'Open Link': '打开超链接',
+      'Edit Link': '编辑超链接',
+      'Unlink': '删除超链接',
+      'Choose Link': '选择超链接',
+      // Images
+      'Insert Image': '插入图片',
+      'Upload Image': '上传图片',
+      'By URL': '通过 URL',
+      'Browse': '浏览',
+      'Drop image': '拖入图片',
+      'or click': '或点击',
+      'Manage Images': '管理图片',
+      'Loading': '加载中',
+      'Deleting': '删除中',
+      'Tags': '标签',
+      'Are you sure? Image will be deleted.': '图片将会被删除,是否确认?',
+      'Replace': '替换',
+      'Uploading': '上传中',
+      'Loading image': '图片加载中',
+      'Display': '显示',
+      'Inline': '嵌入型',
+      'Break Text': '上下型环绕',
+      'Alternative Text': '替换文字',
+      'Change Size': '改变大小',
+      'Width': '宽度',
+      'Height': '高度',
+      'Something went wrong. Please try again.': '发生错误,请重试。',
+      'Image Caption': '图片标题',
+      'Advanced Edit': '高级编辑',
+      // Video
+      'Insert Video': '插入视频',
+      'Embedded Code': '嵌入代码',
+      'Paste in a video URL': '粘贴视频网址',
+      'Drop video': '拖入视频',
+      'Your browser does not support HTML5 video.': '您的浏览器不支持 HTML5 视频。',
+      'Upload Video': '上传视频',
+      // Tables
+      'Insert Table': '插入表格',
+      'Table Header': '表头',
+      'Remove Table': '删除表格',
+      'Table Style': '表格样式',
+      'Horizontal Align': '水平对齐方式',
+      'Row': '行',
+      'Insert row above': '在上方插入',
+      'Insert row below': '在下方插入',
+      'Delete row': '删除行',
+      'Column': '列',
+      'Insert column before': '在左侧插入',
+      'Insert column after': '在右侧插入',
+      'Delete column': '删除列',
+      'Cell': '单元格',
+      'Merge cells': '合并单元格',
+      'Horizontal split': '水平分割',
+      'Vertical split': '垂直分割',
+      'Cell Background': '单元格背景',
+      'Vertical Align': '垂直对齐方式',
+      'Top': '靠上',
+      'Middle': '居中',
+      'Bottom': '靠下',
+      'Align Top': '靠上对齐',
+      'Align Middle': '居中对齐',
+      'Align Bottom': '靠下对齐',
+      'Cell Style': '单元格样式',
+      // Files
+      'Upload File': '上传文件',
+      'Drop file': '拖入文件',
+      // Emoticons
+      'Emoticons': '表情符号',
+      'Grinning face': '露齿笑脸',
+      'Grinning face with smiling eyes': '露齿笑到眯起眼',
+      'Face with tears of joy': '笑哭',
+      'Smiling face with open mouth': '张嘴微笑',
+      'Smiling face with open mouth and smiling eyes': '眯眼张嘴微笑',
+      'Smiling face with open mouth and cold sweat': '带冷汗的张嘴微笑',
+      'Smiling face with open mouth and tightly-closed eyes': '紧闭双眼张嘴微笑',
+      'Smiling face with halo': '带光环微笑',
+      'Smiling face with horns': '带牛角的微笑',
+      'Winking face': '眨眼',
+      'Smiling face with smiling eyes': '眯眼微笑',
+      'Face savoring delicious food': '馋',
+      'Relieved face': '如释重负',
+      'Smiling face with heart-shaped eyes': '桃心眼微笑',
+      'Smiling face with sunglasses': '戴太阳镜微笑',
+      'Smirking face': '得意地笑',
+      'Neutral face': '中性脸',
+      'Expressionless face': '面无表情',
+      'Unamused face': '不高兴',
+      'Face with cold sweat': '冷汗',
+      'Pensive face': '沉思',
+      'Confused face': '迷惑',
+      'Confounded face': '困惑',
+      'Kissing face': '嘴巴嘟嘟',
+      'Face throwing a kiss': '飞吻',
+      'Kissing face with smiling eyes': '眯眼接吻',
+      'Kissing face with closed eyes': '闭眼接吻',
+      'Face with stuck out tongue': '吐舌',
+      'Face with stuck out tongue and winking eye': '眨眼吐舌',
+      'Face with stuck out tongue and tightly-closed eyes': '眯眼吐舌',
+      'Disappointed face': '失望',
+      'Worried face': '担心',
+      'Angry face': '生气',
+      'Pouting face': '撅嘴',
+      'Crying face': '大哭',
+      'Persevering face': '坚强',
+      'Face with look of triumph': '扬眉吐气',
+      'Disappointed but relieved face': '失望',
+      'Frowning face with open mouth': '皱眉',
+      'Anguished face': '痛苦',
+      'Fearful face': '害怕',
+      'Weary face': '疲惫',
+      'Sleepy face': '困了',
+      'Tired face': '累了',
+      'Grimacing face': '扭曲脸',
+      'Loudly crying face': '大哭',
+      'Face with open mouth': '张开嘴',
+      'Hushed face': '安静',
+      'Face with open mouth and cold sweat': '冷汗',
+      'Face screaming in fear': '害怕尖叫',
+      'Astonished face': '惊讶',
+      'Flushed face': '脸红',
+      'Sleeping face': '熟睡',
+      'Dizzy face': '眩晕',
+      'Face without mouth': '没有嘴的脸',
+      'Face with medical mask': '口罩脸',
+      // Line breaker
+      'Break': '换行',
+      // Math
+      'Subscript': '下标',
+      'Superscript': '上标',
+      // Full screen
+      'Fullscreen': '全屏',
+      // Horizontal line
+      'Insert Horizontal Line': '插入水平线',
+      // Clear formatting
+      'Clear Formatting': '清除格式',
+      // Save
+      'Save': '保存',
+      // Undo, redo
+      'Undo': '撤消',
+      'Redo': '恢复',
+      // Select all
+      'Select All': '全选',
+      // Code view
+      'Code View': '代码视图',
+      // Quote
+      'Quote': '引用',
+      'Increase': '增加引用级别',
+      'Decrease': '减少引用级别',
+      // Quick Insert
+      'Quick Insert': '快速插入',
+      // Spcial Characters
+      'Special Characters': '特殊字符',
+      'Latin': '拉丁字母',
+      'Greek': '希腊字母',
+      'Cyrillic': '西里尔字母',
+      'Punctuation': '标点',
+      'Currency': '货币',
+      'Arrows': '箭头',
+      'Math': '数学',
+      'Misc': '杂项',
+      // Print.
+      'Print': '打印',
+      // Spell Checker.
+      'Spell Checker': '拼写检查器',
+      // Help
+      'Help': '帮助',
+      'Shortcuts': '快捷键',
+      'Inline Editor': '内联编辑器',
+      'Show the editor': '显示编辑器',
+      'Common actions': '常用操作',
+      'Copy': '复制',
+      'Cut': '剪切',
+      'Paste': '粘贴',
+      'Basic Formatting': '基本格式',
+      'Increase quote level': '增加引用级别',
+      'Decrease quote level': '减少引用级别',
+      'Image / Video': '图像/视频',
+      'Resize larger': '放大',
+      'Resize smaller': '缩小',
+      'Table': '表格',
+      'Select table cell': '选择单元格',
+      'Extend selection one cell': '增加选中的单元格',
+      'Extend selection one row': '增加选中的行',
+      'Navigation': '导航',
+      'Focus popup / toolbar': '焦点弹出/工具栏',
+      'Return focus to previous position': '将焦点返回到上一个位置',
+      // Embed.ly
+      'Embed URL': '嵌入网址',
+      'Paste in a URL to embed': '粘贴要嵌入的网址',
+      // Word Paste.
+      'The pasted content is coming from a Microsoft Word document. Do you want to keep the format or clean it up?': '粘贴的内容来自微软 Word 文档。你想保留还是清除格式?',
+      'Keep': '保留',
+      'Clean': '清除',
+      'Word Paste Detected': '检测到粘贴自 Word 的内容',
+      // Character Counter
+      'Characters': '字数统计',
+      // More Buttons
+      'More Text': ' 更多文字',
+      'More Paragraph': '更多段落',
+      'More Rich': '更多丰富',
+      'More Misc': '更多杂项',
+      'Rounded': '圆角',
+      'Bordered': '边框',
+      'Shadow': '阴影',
+      'Download PDF': '下载PDF',
+      'Text Color': '字体颜色',
+      'Background Color': '背景颜色',
+      'Inline Class': '内联类',
+      'Highlighted': '高亮',
+      'Transparent': '透明',
+      'Big Red': '大号红',
+      'Small Blue': '小号蓝',
+      'Default': '默认',
+      'Lower Alpha': 'a,b,c...',
+      'Lower Greek': 'α,β,γ...',
+      'Lower Roman': 'i,ii,iii...',
+      'Upper Alpha': 'A,B,C...',
+      'Upper Roman': 'Ⅰ,Ⅱ,Ⅲ...',
+      'Circle': '○ 空心圆',
+      'Disc': '● 实心圆',
+      'Square': '■ 实心方块',
+      'Gray': '灰色',
+      'Spaced': '字母间隙',
+      'Uppercase': '大写',
+      'Line Height': '行高',
+      'Single': '1',
+      'Double': '2'
+    },
+    direction: 'ltr'
+  };
+
+})));
+//# sourceMappingURL=zh_cn.js.map

File diff suppressed because it is too large
+ 1 - 0
public/jquery-3.6.0.min.js


+ 45 - 0
src/api/system/helpCenter.js

@@ -0,0 +1,45 @@
+import {get,post} from '@/api/index'
+// 帮助中心
+export default{
+  // 分类列表
+  classifyList:params=>{
+    return get('/help_doc/classify/list',params)
+  },
+  // 新增分类
+  classifyAdd:params=>{
+    return post('/help_doc/classify/add',params)
+  },
+  // 编辑分类
+  classifyEdit:params=>{
+    return post('/help_doc/classify/edit',params)
+  },
+  // 移动分类
+  classifyMove:params=>{
+    return post('/help_doc/classify/move',params)
+  },
+  // 删除分类
+  classifyDelete:params=>{
+    return get('/help_doc/classify/delete',params)
+  },
+
+  // 获取文章列表
+  getDocmentList:params=>{
+    return get('/help_doc/list',params)
+  },
+  // 删除文章
+  deleteDocment:params=>{
+    return post('/help_doc/delete',params)
+  },
+  // 发布\取消发布文章
+  publishDocment:params=>{
+    return post('/help_doc/publish',params)
+  },
+  // 新增文章
+  addDocment:params=>{
+    return post('/help_doc/add',params)
+  },
+  // 文章详情
+  docmentInfo:params=>{
+    return get('/help_doc/detail',params)
+  }
+}

+ 3 - 1
src/api/system/index.js

@@ -1,8 +1,10 @@
 import apiSystemSet from './set'
 import apiSystemCommon from './common'
+import apiSystemHelpCenter from './helpCenter'
 
 export {
     apiSystemSet,
-    apiSystemCommon
+    apiSystemCommon,
+    apiSystemHelpCenter
 }
 

+ 132 - 0
src/hooks/useFroalaEditor.js

@@ -0,0 +1,132 @@
+import { ref,nextTick } from "vue";
+
+export function useInitFroalaEditor() {
+	let frolaEditorContentChange=ref(false)//富文本内容是否改变
+	let lastFocusPosition=ref(null)//最后焦点位置
+	let imgUploadFlag=ref(true)//图片是否上传完成
+
+	let options = {
+		toolbarButtons: [
+			"insertImage",
+			"insertVideo",
+			"embedly",
+			"insertFile",
+			"textColor",
+			"bold",
+			"italic",
+			"underline",
+			"strikeThrough",
+			"subscript",
+			"superscript",
+			"fontFamily",
+			"fontSize",
+			"color",
+			"inlineClass",
+			"inlineStyle",
+			"paragraphStyle",
+			"lineHeight",
+			"paragraphFormat",
+			"align",
+			"formatOL",
+			"formatUL",
+			"outdent",
+			"indent",
+			"quote",
+			"insertTable",
+			"emoticons",
+			"fontAwesome",
+			"specialCharacters",
+			"insertHR",
+			"selectAll",
+			"clearFormatting",
+			"html",
+			"undo",
+			"redo"
+		],
+		height: 500,
+		fontSize: ["6","8","10","12", "13", "14", "15", "16", "18", "20", "24", "28", "32", "36", "40"],
+		
+		fontSizeDefaultSelection: "16",
+		theme: "dark", //主题
+		placeholderText: "请输入内容",
+		language: "zh_cn", //国际化
+		imageUploadURL: import.meta.env.VITE_APP_API_URL + '/report/uploadImg', //上传url
+		videoUploadURL: import.meta.env.VITE_APP_API_URL + '/report/uploadImg', //上传url
+		fileUploadURL: import.meta.env.VITE_APP_API_URL + '/report/uploadImg', //上传url 更多上传介绍 请访问https://www.froala.com/wysiwyg-editor/docs/options
+		imageDefaultWidth: false,
+		quickInsertEnabled: false,
+		// quickInsertButtons: ['image', 'ul', 'ol'], //快速插入项
+		toolbarVisibleWithoutSelection: true, //是否开启 不选中模式
+		toolbarSticky: false, //操作栏是否自动吸顶
+		saveInterval: 0,
+		charCounterCount: false,
+		events:{
+			'xhr.beforeSend': function (xhr) {
+				console.log(xhr);
+				xhr.setRequestHeader('Authorization', sessionStorage.getItem('token')||''); // 替换为你的 Token
+			},
+			// 内容变化事件
+			contentChanged:function (){
+				frolaEditorContentChange.value=true
+				getSelection().rangeCount&&(lastFocusPosition.value=getSelection().getRangeAt(0))
+			},
+			keyup:function(e,editor){
+				nextTick(()=>{
+					getSelection().rangeCount&&(lastFocusPosition.value=getSelection().getRangeAt(0))
+				})
+			},
+			click:function(e,editor){
+				nextTick(()=>{
+					getSelection().rangeCount&&(lastFocusPosition.value=getSelection().getRangeAt(0))
+				})
+			},
+			'image.beforePasteUpload':function(){
+				imgUploadFlag.value=false
+			},
+			'image.beforeUpload':function(){
+				imgUploadFlag.value=false
+			},
+			'image.inserted':function(){
+				imgUploadFlag.value=true
+			},
+			'image.error':function(){
+				imgUploadFlag.value=true
+			},
+		}
+	};
+
+	/**
+	 * 初始化编辑器
+	 * @param  el domid
+	 * @param  opts 配置项
+	 * 由于要获取到富文本实例 要在new FroalaEditor(el,opt,callback)的callback中才能获取到
+	 * 方案一返回一个promise
+	 * 方案二直接返回富文本实例 无法在调用initFroalaEditor方法后立即执行实例上的一些方法或者读取属性
+	 * 一般写个settimeout 可以解决
+	 */
+	const initFroalaEditor = (el,opts) => {
+		// 方案一
+		// let ins=null
+		// return new Promise((resolve,reject)=>{
+		// 	options.height=opts?.height??500
+		// 	console.log(options);
+		// 	ins=new FroalaEditor(el, options,()=>{
+		// 		// console.log(ins);
+		// 		resolve(ins)
+		// 	})
+		// })
+
+		// 方案二
+		options={...options,...opts}
+		// options.height=options.height<350?350:options.height
+		console.log(options);
+		return new FroalaEditor(el, options);
+	};
+
+	return {
+		lastFocusPosition,
+		frolaEditorContentChange,
+		imgUploadFlag,
+		initFroalaEditor,
+	}
+}

+ 34 - 0
src/router/modules/system.js

@@ -36,5 +36,39 @@ export default[
         },
       },
     ]
+  },
+  {
+    path:'/system',
+    name:'System',
+    component:LayoutIndex,
+    meta:{
+      title:'系统设置'
+    },
+    children:[
+      {
+        path:'helpCenter',
+        name:'SystemHelpCenter',
+        component:()=>import('@/views/system/helpCenter/Index.vue'),
+        meta:{
+          title:'帮助中心配置'
+        },
+      },
+      {
+        path:'helpCenter/addDoc',
+        name:'SystemHelpCenterAddDoc',
+        component:()=>import('@/views/system/helpCenter/AddDocment.vue'),
+        meta:{
+          title:'添加文章'
+        },
+      },
+      {
+        path:'helpCenter/detail',
+        name:'SystemHelpCenterDetail',
+        component:()=>import('@/views/system/helpCenter/DocmentDetail.vue'),
+        meta:{
+          title:'查看文章'
+        },
+      },
+    ]
   }
 ]

+ 7 - 1
src/styles/common.scss

@@ -72,4 +72,10 @@ ul li{
 	-webkit-line-clamp: 3;
 	line-break: anywhere;
 	-webkit-box-orient: vertical;
-}
+}
+
+// froala样式
+.fr-second-toolbar {
+    display: none !important;
+}
+a[href="https://froala.com/wysiwyg-editor"], a[href="https://www.froala.com/wysiwyg-editor?k=u"]{ border:1px solid #eaeaea; background:#fff !important; color:#ccc !important; }

+ 341 - 0
src/views/system/helpCenter/AddDocment.vue

@@ -0,0 +1,341 @@
+<script setup>
+import { apiSystemHelpCenter } from '@/api/system'
+import { useInitFroalaEditor } from '@/hooks/useFroalaEditor'
+import { useTemplateRef } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+
+const { lastFocusPosition, initFroalaEditor, frolaEditorContentChange } = useInitFroalaEditor()
+const router=useRouter()
+const route=useRoute()
+
+let reportContentEditorIns = null//报告内容编辑器实例
+
+// 锚点数据
+let anchorData = []
+// 搜索标题标签h1,h2
+function searchTitleTag(searchPosition, firstLevel) {
+  let htmlContent=reportContentEditorIns.html.get(true)
+  console.log(htmlContent);
+
+  let frontH1Posiiton, nextH1Posiiton, H2Posiiton = 0
+  let frontH1RightPosiiton, H2RightPosiiton = 0
+  let backH1Posiiton, backH2Posiiton = 0
+  // 本次搜索第一个h1的位置
+  frontH1Posiiton = htmlContent.indexOf('<h1', searchPosition)
+  // 右闭合标签
+  frontH1RightPosiiton = htmlContent.indexOf('>', frontH1Posiiton)
+
+  if (frontH1Posiiton == -1) return
+
+  let anchorText = `id="doc_anchor_${firstLevel}"`
+  // console.log(frontH1Posiiton,firstLevel,'firstLevel');
+  htmlContent = htmlContent.substring(0, frontH1Posiiton + 3)
+    + " " + anchorText + htmlContent.substring(frontH1RightPosiiton);
+  // 再次获取右闭合标签
+  frontH1RightPosiiton = htmlContent.indexOf('>', frontH1Posiiton)
+  // 对应的</h1>的位置 原本用的</h1>,后来发现> 和 </h1>之间会掺其他标签
+  backH1Posiiton = htmlContent.indexOf('<', frontH1RightPosiiton)
+  // 获取标题
+  let AnchorTitle = htmlContent.substring(frontH1RightPosiiton + 1, backH1Posiiton)
+
+  anchorData.push({
+    AnchorId: `${firstLevel}`,
+    Anchor: `doc_anchor_${firstLevel}`,
+    AnchorName: AnchorTitle,
+    Child: []
+  })
+  // 本次搜索下一个h1的位置
+  nextH1Posiiton = htmlContent.indexOf('<h1', backH1Posiiton) == -1 ?
+    htmlContent.length : htmlContent.indexOf('<h1', backH1Posiiton)
+  // 从第一个h1的位置开始查找h2标签
+  H2Posiiton = htmlContent.indexOf('<h2', backH1Posiiton)
+
+  let secondLevel = 1
+  while (!(H2Posiiton == -1 || H2Posiiton > nextH1Posiiton)) {
+    // 右闭合标签
+    H2RightPosiiton = htmlContent.indexOf('>', H2Posiiton)
+
+    // 找到了,并且位置小于下一个h1的位置 
+    let anchorTextH2 = `id="doc_anchor_${firstLevel}_${secondLevel}"`
+    // console.log(H2Posiiton,secondLevel,'secondLevel');
+    htmlContent = htmlContent.substring(0, H2Posiiton + 3)
+      + " " + anchorTextH2 + htmlContent.substring(H2RightPosiiton);
+    // 再次获取右闭合标签
+    H2RightPosiiton = htmlContent.indexOf('>', H2Posiiton)
+    // 对应的</h2>的位置  原本用的</h2>,后来发现> 和 </h2>之间会掺其他标签
+    backH2Posiiton = htmlContent.indexOf('<', H2RightPosiiton)
+    // 获取标题
+    let AnchorTitleLevelTwo = htmlContent.substring(H2RightPosiiton + 1, backH2Posiiton)
+    anchorData[firstLevel - 1].Child.push(
+      {
+        AnchorId: `${firstLevel}_${secondLevel}`,
+        Anchor: `doc_anchor_${firstLevel}_${secondLevel}`,
+        AnchorName: AnchorTitleLevelTwo,
+        Child: []
+      })
+    // nextH1Posiiton 和 secondLevel 随之增加
+    // nextH1Posiiton +=anchorTextH2.length+1
+    // 更新nextH1Posiiton位置
+    nextH1Posiiton = htmlContent.indexOf('<h1', backH1Posiiton) == -1 ?
+      htmlContent.length : htmlContent.indexOf('<h1', backH1Posiiton)
+    secondLevel++
+    H2Posiiton = htmlContent.indexOf('<h2', backH2Posiiton)
+  }
+  // 结束一轮 <h1></h1>标签的寻找
+  if (htmlContent.indexOf('<h1', backH1Posiiton + 4) != -1) {
+    // 如果有下一个h1的标签,说明寻找还没结束,继续寻找
+    firstLevel++
+    searchTitleTag(nextH1Posiiton, firstLevel)
+  }
+}
+// 生成锚点
+function generateAnchor() {
+  anchorData = []
+  // 搜索富文本中的h1和h2标签 当做一级和二级的锚点
+  searchTitleTag(0, 1)
+}
+
+const classifyOpts = ref([])
+async function getClassify() {
+  const res = await apiSystemHelpCenter.classifyList()
+  if (res.Ret !== 200) return
+  classifyOpts.value = res.Data.AllNodes || []
+}
+getClassify()
+
+const btnLoading = ref(false)
+const modifyTime=ref('')
+const FORM_RULES = {
+  title: [{ required: true, message: '文章标题不能为空' }],
+  classifyId: [{ required: true, message: '文章所属分类不能为空' }],
+  author: [{ required: true, message: '文章作者不能为空' }],
+};
+const formIns = useTemplateRef('formIns')
+const formData = reactive({
+  title: '',
+  classifyId: '',
+  author: '',
+  Status: 1,
+  AnchorData: [],
+  RecommendData: [{ Name: "", Url: "" }, { Name: "", Url: "" }]
+})
+
+let autoSaveTimer=null//自动保存定时器
+async function handleSaveDocument(type, isAuto) {
+  if (btnLoading.value) return
+  const validRes = await formIns.value.validate()
+  if (validRes !== true) return
+  const htmlContent = reportContentEditorIns.html.get(true)
+  if (!htmlContent) {
+    MessagePlugin.warning('文章内容不能为空')
+    return
+  }
+  if (!isAuto) {
+    btnLoading.value = true
+  }
+  if (type === '发布') {
+    formData.Status = 2
+  }
+  generateAnchor()
+  console.log(anchorData);
+  formData.AnchorData=anchorData
+  const res=await apiSystemHelpCenter.addDocment({
+    IsChange:frolaEditorContentChange.value,
+    Title:formData.title,
+    ClassifyId:formData.classifyId,
+    Author:formData.author,
+    Status:formData.Status,
+    Content:htmlContent,
+    AnchorData:formData.AnchorData,
+    RecommendData:formData.RecommendData,
+    Id:route.query.DocId?Number(route.query.DocId):0
+  })
+  if(res.Ret!==200) return
+  frolaEditorContentChange.value=false
+  if(!isAuto){
+    MessagePlugin.success('操作成功')
+  }
+  if(type==='发布'){
+    setTimeout(() => {
+      router.back()
+    }, 1000);
+  }else{
+    btnLoading.value=false
+    modifyTime.value=res.Data.ModifyTime
+    if(!route.query.DocId){
+      //新增
+      setTimeout(()=>{
+        router.replace("/system/helpCenter/addDoc?DocId="+res.Data.HelpDocId)
+        if(!autoSaveTimer){
+          autoSaveTimer=setInterval(()=>{
+            handleSaveDocument('保存',true)
+          },6000)
+        }
+      },1000)
+    }
+  }
+}
+
+//预览
+function handlePreviewDoc(){
+  if(btnLoading.value) return
+  const htmlContent = reportContentEditorIns.html.get(true)
+  if (!htmlContent) {
+    MessagePlugin.warning('文章内容不能为空')
+    return
+  }
+  sessionStorage.setItem("documentDocContent",htmlContent)
+  sessionStorage.setItem("Recommend",JSON.stringify(formData.RecommendData))
+  let { href } = router.resolve({ path: "/system/helpCenter/detail" });
+  window.open(href, "_blank");
+}
+
+
+// 获取文章详情
+async function handleGetDocmentInfo(){
+  const res=await apiSystemHelpCenter.docmentInfo({
+    DocId:Number(route.query.DocId)
+  })
+  if(res.Ret!==200) return
+  formData.title=res.Data.Title
+  formData.classifyId=res.Data.ClassifyId
+  formData.author=res.Data.Author
+  formData.Status=res.Data.Status
+  formData.RecommendData=res.Data.Recommend || [{Name:"",Url:""},{Name:"",Url:""}]
+  reportContentEditorIns.html.set(res.Data.Content)
+  modifyTime.value=res.Data.ModifyTime
+  if(!autoSaveTimer){
+    autoSaveTimer=setInterval(()=>{
+      handleSaveDocument('保存',true)
+    },6000)
+  }
+}
+
+onMounted(() => {
+  const el = document.getElementById('editor')
+  reportContentEditorIns = initFroalaEditor('#editor', { height: el.offsetHeight - 80 })
+  if(route.query.DocId){
+    handleGetDocmentInfo()
+  }
+})
+
+
+</script>
+
+<template>
+  <div class="flex add-docment-page">
+    <div class="bg-white left-content-wrap" id="editor"></div>
+    <div class="bg-white right-wrap">
+      <div class="flex top-btn-box">
+        <t-button theme="primary" :loading="btnLoading" @click="handlePreviewDoc">预览</t-button>
+        <t-button
+          theme="primary"
+          :loading="btnLoading"
+          @click="handleSaveDocument('保存')"
+          >保存</t-button
+        >
+        <t-button
+          theme="primary"
+          :loading="btnLoading"
+          @click="handleSaveDocument('发布')"
+          >发布</t-button
+        >
+      </div>
+      <t-form
+        ref="formIns"
+        :rules="FORM_RULES"
+        :data="formData"
+        :colon="false"
+        labelAlign="top"
+      >
+        <t-form-item label="文章标题" name="title">
+          <t-input
+            v-model="formData.title"
+            placeholder="请输入文章标题"
+          ></t-input>
+        </t-form-item>
+        <t-form-item label="所属分类" name="classifyId">
+          <t-cascader
+            v-model="formData.classifyId"
+            :options="classifyOpts"
+            :keys="{
+              value: 'ClassifyId',
+              label: 'ClassifyName',
+              children: 'Children',
+            }"
+            clearable
+            placeholder="所属分类"
+          />
+        </t-form-item>
+        <t-form-item label="文章作者" name="author">
+          <t-input
+            v-model="formData.author"
+            placeholder="请输入文章作者"
+          ></t-input>
+        </t-form-item>
+        <t-form-item label="相关推荐" name="RecommendData">
+          <div style="width: 100%">
+            <div
+              v-for="(item, index) in formData.RecommendData"
+              :key="index"
+              class="form-item-recommendedLink"
+            >
+              <t-input
+                v-model="item.Name"
+                placeholder="请输入链接名称"
+                style="width: 190px"
+              ></t-input>
+              <div class="recommendedLink-line"></div>
+              <t-input
+                v-model="item.Url"
+                placeholder="请输入链接"
+                style="width: 190px"
+              ></t-input>
+            </div>
+          </div>
+        </t-form-item>
+      </t-form>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.add-docment-page {
+  gap: 0 20px;
+  .left-content-wrap {
+    flex: 1;
+    height: calc(100vh - 130px);
+  }
+  .right-wrap {
+    width: 440px;
+    flex-shrink: 0;
+    border: 1px solid var(--border-color);
+    .top-btn-box {
+      padding: 20px;
+      gap: 20px;
+      box-shadow: 0 5px 10px #ececec;
+      .t-button {
+        flex: 1;
+      }
+    }
+    .t-form {
+      padding: 20px;
+      .form-item-recommendedLink {
+        display: flex;
+        align-items: center;
+        justify-content: flex-start;
+        width: 100%;
+        margin-bottom: 20px;
+        &:last-child {
+          margin-bottom: 0;
+        }
+        .recommendedLink-line {
+          flex: 1;
+          height: 1px;
+          background-color: #dcdfe6;
+        }
+      }
+    }
+  }
+}
+</style>

+ 49 - 0
src/views/system/helpCenter/DocmentDetail.vue

@@ -0,0 +1,49 @@
+<script setup>
+import { useRoute } from "vue-router";
+import {apiSystemHelpCenter} from '@/api/system'
+
+function createBottomHref(RecommendData){
+  let hrefStringBuiler='<ul style="margin-top:40px">'
+  RecommendData.map(item =>{
+    if(item.Name){
+      hrefStringBuiler+=`<li><a href="${item.Url}" target="_blank" style="text-decoration: underline;">${item.Name}</a></li>`
+    }
+  })
+  return hrefStringBuiler+"</ul>"
+}
+
+const route=useRoute()
+
+const content=ref('')
+async function getDocmentDetail(){
+  if(route.query.DocId){
+    const res=await apiSystemHelpCenter.docmentInfo({
+      DocId:Number(route.query.DocId)
+    })
+    if(res.Ret!==200) return
+    content.value=res.Data.Content + createBottomHref(res.Data.Recommend || [{Name:"",Url:""},{Name:"",Url:""}])
+  }else{
+    const Recommend=sessionStorage.getItem("Recommend")?JSON.parse(sessionStorage.getItem("Recommend")):''
+    content.value = sessionStorage.getItem("documentDocContent") +createBottomHref(Recommend || [{Name:"",Url:""},{Name:"",Url:""}])
+  }
+}
+
+getDocmentDetail()
+
+</script>
+
+<template>
+  <div class="assistance-detail-container">
+    <div class="assistance-detail-box fr-view" v-html="content"></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.assistance-detail-container{
+  background-color: white;
+  min-height: calc(100vh - 180px);
+  padding: 20px;
+  box-sizing: border-box;
+  border: solid 1px #ECECEC;
+}
+</style>

+ 154 - 0
src/views/system/helpCenter/Index.vue

@@ -0,0 +1,154 @@
+<script setup>
+import { SearchIcon } from 'tdesign-icons-vue-next';
+import {apiSystemHelpCenter} from '@/api/system'
+
+const keyword=ref('')
+
+// 分类
+const classifyValue=ref('')
+const classifyOpts=ref([])
+async function getClassify(){
+  const res=await apiSystemHelpCenter.classifyList()
+  if(res.Ret!==200) return
+  classifyOpts.value=res.Data.AllNodes||[]
+}
+getClassify()
+
+// 获取列表
+const tableData = ref([])
+const columns = [
+  { align: 'center', colKey: 'Title', title: '文章标题' },
+  { align: 'center', colKey: 'ClassifyName', title: '所属分类' },
+  { align: 'center', colKey: 'Author', title: '文章作者' },
+  { align: 'center', colKey: 'Status', title: '发布状态' },
+  { align: 'center', colKey: 'CreateTime', title: '创建时间' },
+  { align: 'center', colKey: 'ModifyTime', title: '更新时间'},
+  {
+    align: 'center',
+    colKey: 'opt',
+    title: '操作',
+  },
+]
+const tablePagination = ref({
+  current: 1,
+  pageSize: 20,
+  total: 0,
+  showPageSize:false
+})
+async function getList(){
+  const res=await apiSystemHelpCenter.getDocmentList({
+    PageSize:tablePagination.pageSize,
+    CurrentIndex:tablePagination.current,
+    ClassifyIds:classifyValue.value?classifyValue.value.join(','):'',
+    KeyWord:keyword.value,
+  })
+  if(res.Ret!==200) return
+  tableData.value=res.Data.List||[]
+  tablePagination.value.total=res.Data.Paging.Totals
+}
+getList()
+function handlePageChange(e){
+  tablePagination.value.current=e.current
+  getList()
+}
+function handleRefreshList(){
+  tablePagination.value.current=1
+  tableData.value=[]
+  getList()
+}
+
+// 删除文章
+async function handleDelDoc(row){
+  await $confirmDialog({
+    body:'删除后不可恢复,是否确认删除?',
+  })
+  const res=await apiSystemHelpCenter.deleteDocment({
+    DocId:row.Id
+  })
+  if(res.Ret!==200) return
+  MessagePlugin.success('删除成功')
+  handleRefreshList()
+}
+
+//发布状态变更
+async function handleDocPublishChange(row){
+  await $confirmDialog({
+    body:`是否确认${row.Status==2?'取消发布':'发布'}?`
+  })
+  const res=await apiSystemHelpCenter.publishDocment({
+    DocId:row.Id,
+    Status:3-row.Status
+  })
+  if(res.Ret!==200) return
+  MessagePlugin.success(`${row.Status==2?'取消发布':'发布'}成功`)
+  handleRefreshList()
+}
+
+</script>
+
+<template>
+  <div class="bg-white help-center-page">
+    <div class="flex top-wrap">
+      <t-button theme="primary" @click="$router.push('/system/helpCenter/addDoc')">添加文章</t-button>
+      <t-button theme="primary">分类管理</t-button>
+      <t-cascader 
+        v-model="classifyValue" 
+        :options="classifyOpts"
+        :keys="{
+          value:'ClassifyId',
+          label:'ClassifyName',
+          children:'Children'
+        }" 
+        :showAllLevels="false"
+        multiple 
+        clearable
+        :minCollapsedNum="1"
+        placeholder="所属分类"
+        @change="handleRefreshList"
+        style="width:200px;margin-left:auto"
+      />
+      <t-input
+        style="width: 310px;"
+        placeholder="文章标题"
+        v-model="keyword"
+        clearable
+        @change="handleRefreshList"
+      >
+        <template #prefixIcon><SearchIcon /></template>
+      </t-input>
+    </div>
+    <t-table
+      rowKey="UserId"
+      :data="tableData"
+      :columns="columns"
+      bordered
+      :pagination="tablePagination"
+      show-header
+      resizable
+      @page-change="handlePageChange"
+    >
+      <template #Title="{row}">
+        <t-link theme="primary" @click="$router.push({path:'/system/helpCenter/detail',query:{DocId:row.Id}})">{{row.Title}}</t-link>
+      </template>
+      <template #Status="{row}">
+        <t-button variant="text" :theme="row.Status==2?'success':'danger'">{{row.Status==2?"已发布":"未发布"}}</t-button>
+      </template>
+      <template #opt="{ row }">
+        <t-button size="small" variant="text" theme="primary" @click="$router.push({path:'/system/helpCenter/addDoc',query:{DocId:row.Id}})" v-if="row.Status==1">编辑</t-button>
+        <t-button size="small" variant="text" theme="primary" @click="handleDocPublishChange(row)" v-if="row.Status==1">发布</t-button>
+        <t-button size="small" variant="text" theme="primary" @click="handleDocPublishChange(row)" v-if="row.Status==2">取消发布</t-button>
+        <t-button size="small" variant="text" theme="danger" @click="handleDelDoc(row)">删除</t-button>
+      </template>
+    </t-table>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.help-center-page{
+  padding: 20px;
+  .top-wrap{
+    gap: 20px;
+    margin-bottom: 20px;
+  }
+}
+</style>

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