|
@@ -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>
|