assistanceDocAdd.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <script setup>
  2. import {assistanceDocInterence} from "@/api/api.js"
  3. import {createBottomHref} from "./utils/common"
  4. import { ref, reactive, watch, onUnmounted } from 'vue'
  5. import { useRoute, useRouter } from 'vue-router'
  6. import { ElMessage } from 'element-plus'
  7. const route = useRoute()
  8. const router = useRouter()
  9. const froalaConfig = {
  10. toolbarButtons: [
  11. "insertImage",
  12. "insertVideo",
  13. "embedly",
  14. "insertFile",
  15. "textColor",
  16. "bold",
  17. "italic",
  18. "underline",
  19. "strikeThrough",
  20. "subscript",
  21. "superscript",
  22. "fontFamily",
  23. "fontSize",
  24. "color",
  25. "inlineClass",
  26. "inlineStyle",
  27. "paragraphStyle",
  28. "lineHeight",
  29. "paragraphFormat",
  30. "align",
  31. "formatOL",
  32. "formatUL",
  33. "outdent",
  34. "indent",
  35. "quote",
  36. "insertTable",
  37. "emoticons",
  38. "fontAwesome",
  39. "specialCharacters",
  40. "insertHR",
  41. "selectAll",
  42. "clearFormatting",
  43. "html",
  44. "undo",
  45. "redo"
  46. ],
  47. height:"calc(100vh - 230px)",
  48. fontSize: ["12", "14", "16", "18", "20", "24", "28", "32", "36", "40"],
  49. fontSizeDefaultSelection: "16",
  50. theme: "dark", //主题
  51. placeholderText: "请输入内容",
  52. language: "zh_cn", //国际化
  53. imageUploadURL: import.meta.env.VITE_APP_API_URL + "/report/uploadImg", //上传url
  54. videoUploadURL: import.meta.env.VITE_APP_API_URL + "/report/uploadImg", //上传url
  55. fileUploadURL: import.meta.env.VITE_APP_API_URL + "/report/uploadImg", //上传url 更多上传介绍 请访问https://www.froala.com/wysiwyg-editor/docs/options
  56. imageEditButtons:['imageAlign', 'imageCaption', 'imageRemove', '-', 'imageDisplay', 'imageSize'],
  57. quickInsertButtons: ["image","video","hr"], //快速插入项
  58. quickInsertEnabled:false, // 是否启用快速插入功能
  59. toolbarVisibleWithoutSelection: false, //是否开启 不选中模式
  60. toolbarSticky: false, //操作栏是否自动吸顶
  61. saveInterval: 0,
  62. }
  63. let classifyList = ref([])
  64. let addDocForm = reactive({
  65. Title:"",
  66. ClassifyId:"",
  67. Author:"",
  68. Status:1,
  69. Content:'',
  70. AnchorData:[],
  71. RecommendData:[{Name:"",Url:""},{Name:"",Url:""}],
  72. })
  73. const addDocRules = reactive({
  74. Title:{required:true,message:'文章标题不能为空',trigger:'blur'},
  75. ClassifyId:{required:true,message:'文章所属分类不能为空',trigger:'change'},
  76. Author:{required:true,message:'文章作者不能为空',trigger:'blur'}
  77. })
  78. let anchorData = ref([])
  79. let isSubmiting = ref(false)
  80. let autoSaveTimer = ref(null)
  81. let modifyTime = ref('')
  82. let contentChange = ref(false)
  83. watch(addDocForm,()=>{
  84. contentChange.value=true
  85. },{deep:true})
  86. function getClassifyData(){
  87. assistanceDocInterence.getAssistanceClassifyList().then(res=>{
  88. if(res.Ret == 200){
  89. classifyList.value = res.Data?.AllNodes||[]
  90. }
  91. })
  92. }
  93. function init(){
  94. getClassifyData()
  95. if(route.query.DocId){
  96. assistanceDocInterence.getAssistanceDoc({DocId:route.query.DocId}).then(res=>{
  97. if(res.Ret == 200){
  98. Object.assign(addDocForm,{
  99. Id:res.Data.Id,
  100. Title:res.Data.Title,
  101. ClassifyId:res.Data.ClassifyId,
  102. Author:res.Data.Author,
  103. Status:res.Data.Status,
  104. Content:res.Data.Content,
  105. RecommendData:res.Data.Recommend || [{Name:"",Url:""},{Name:"",Url:""}]
  106. })
  107. modifyTime.value=res.Data.ModifyTime
  108. if((!autoSaveTimer.value) && addDocForm.Id){
  109. autoSaveTimer.value=setInterval(()=>{
  110. saveDocument('保存',true)
  111. },6000)
  112. }
  113. }
  114. })
  115. }else{
  116. addDocForm.Content=""
  117. }
  118. }
  119. init()
  120. onUnmounted(()=>{
  121. clearInterval(autoSaveTimer.value)
  122. autoSaveTimer.value=null
  123. })
  124. // 生成锚点
  125. function generateAnchor(){
  126. anchorData.value=[]
  127. searchTitleTag(0,1)
  128. }
  129. // 搜索标题标签h1,h2
  130. function searchTitleTag(searchPosition,firstLevel){
  131. let frontH1Posiiton,nextH1Posiiton,H2Posiiton=0
  132. let frontH1RightPosiiton,H2RightPosiiton=0
  133. let backH1Posiiton,backH2Posiiton=0
  134. // 本次搜索第一个h1的位置
  135. frontH1Posiiton = addDocForm.Content.indexOf('<h1',searchPosition)
  136. // 右闭合标签
  137. frontH1RightPosiiton = addDocForm.Content.indexOf('>',frontH1Posiiton)
  138. if(frontH1Posiiton == -1) return
  139. let anchorText=`id="doc_anchor_${firstLevel}"`
  140. // console.log(frontH1Posiiton,firstLevel,'firstLevel');
  141. addDocForm.Content = addDocForm.Content.substring(0, frontH1Posiiton+3)
  142. +" "+anchorText + addDocForm.Content.substring(frontH1RightPosiiton);
  143. // 再次获取右闭合标签
  144. frontH1RightPosiiton = addDocForm.Content.indexOf('>',frontH1Posiiton)
  145. // 对应的</h1>的位置 原本用的</h1>,后来发现> 和 </h1>之间会掺其他标签
  146. backH1Posiiton = addDocForm.Content.indexOf('<',frontH1RightPosiiton)
  147. // 获取标题
  148. let AnchorTitle = addDocForm.Content.substring(frontH1RightPosiiton+1,backH1Posiiton)
  149. anchorData.push({
  150. AnchorId:`${firstLevel}`,
  151. Anchor:`doc_anchor_${firstLevel}`,
  152. AnchorName:AnchorTitle,
  153. Child:[]})
  154. // 本次搜索下一个h1的位置
  155. nextH1Posiiton = addDocForm.Content.indexOf('<h1',backH1Posiiton)==-1?
  156. addDocForm.Content.length:addDocForm.Content.indexOf('<h1',backH1Posiiton)
  157. // 从第一个h1的位置开始查找h2标签
  158. H2Posiiton = addDocForm.Content.indexOf('<h2',backH1Posiiton)
  159. let secondLevel=1
  160. while (!(H2Posiiton==-1 || H2Posiiton>nextH1Posiiton)) {
  161. // 右闭合标签
  162. H2RightPosiiton = addDocForm.Content.indexOf('>',H2Posiiton)
  163. // 找到了,并且位置小于下一个h1的位置
  164. let anchorTextH2=`id="doc_anchor_${firstLevel}_${secondLevel}"`
  165. // console.log(H2Posiiton,secondLevel,'secondLevel');
  166. addDocForm.Content = addDocForm.Content.substring(0, H2Posiiton+3)
  167. +" "+anchorTextH2 + addDocForm.Content.substring(H2RightPosiiton);
  168. // 再次获取右闭合标签
  169. H2RightPosiiton = addDocForm.Content.indexOf('>',H2Posiiton)
  170. // 对应的</h2>的位置 原本用的</h2>,后来发现> 和 </h2>之间会掺其他标签
  171. backH2Posiiton = addDocForm.Content.indexOf('<',H2RightPosiiton)
  172. // 获取标题
  173. let AnchorTitleLevelTwo = addDocForm.Content.substring(H2RightPosiiton+1,backH2Posiiton)
  174. anchorData[firstLevel-1].Child.push(
  175. {AnchorId:`${firstLevel}_${secondLevel}`,
  176. Anchor:`doc_anchor_${firstLevel}_${secondLevel}`,
  177. AnchorName:AnchorTitleLevelTwo,
  178. Child:[]
  179. })
  180. // nextH1Posiiton 和 secondLevel 随之增加
  181. // nextH1Posiiton +=anchorTextH2.length+1
  182. // 更新nextH1Posiiton位置
  183. nextH1Posiiton = addDocForm.Content.indexOf('<h1',backH1Posiiton)==-1?
  184. addDocForm.Content.length:addDocForm.Content.indexOf('<h1',backH1Posiiton)
  185. secondLevel++
  186. H2Posiiton = addDocForm.Content.indexOf('<h2',backH2Posiiton)
  187. }
  188. // 结束一轮 <h1></h1>标签的寻找
  189. if(addDocForm.Content.indexOf('<h1',backH1Posiiton+4)!=-1){
  190. // 如果有下一个h1的标签,说明寻找还没结束,继续寻找
  191. firstLevel++
  192. searchTitleTag(nextH1Posiiton,firstLevel)
  193. }
  194. }
  195. function previewDocument(){
  196. if(isSubmiting.value) return
  197. if(!addDocForm.Content){
  198. ElMessage.error("文章内容不能为空")
  199. return
  200. }
  201. let bottomLink = createBottomHref(addDocForm.RecommendData)
  202. sessionStorage.setItem("documentDoc",addDocForm.Content+bottomLink)
  203. let { href } = router.resolve({ path: "/assistanceDocDetail" });
  204. window.open(href, "_blank");
  205. }
  206. let addDocFormRef = ref(null)
  207. function saveDocument(type,isAuto){
  208. if(isSubmiting.value) return
  209. addDocFormRef.value?.validate(valid=>{
  210. if(valid){
  211. if(!addDocForm.Content){
  212. ElMessage.error("文章内容不能为空")
  213. return
  214. }
  215. if(!isAuto) isSubmiting.value=true
  216. if(type=="发布") addDocForm.Status=2
  217. generateAnchor()
  218. addDocForm.AnchorData = anchorData.value
  219. //保存
  220. assistanceDocInterence.addAssistanceDoc({...addDocForm,IsChange:contentChange.value}).then(res=>{
  221. if(res.Ret == 200){
  222. contentChange.value=false
  223. !isAuto && ElMessage({
  224. type:'success',
  225. message:'操作成功',
  226. duration:1000
  227. })
  228. if(type=="发布"){
  229. setTimeout(()=>{
  230. router.back()
  231. },1000)
  232. }else{
  233. isSubmiting.value=false
  234. modifyTime.value=res.Data.ModifyTime
  235. if(!addDocForm.Id){
  236. //新增
  237. setTimeout(()=>{
  238. router.replace("/assistanceDocAdd?DocId="+res.Data.HelpDocId)
  239. addDocForm.Id = res.Data.HelpDocId
  240. if((!autoSaveTimer) && addDocForm.Id){
  241. autoSaveTimer=setInterval(()=>{
  242. saveDocument('保存',true)
  243. },6000)
  244. }
  245. },1000)
  246. }
  247. }
  248. }
  249. }).catch(()=>{
  250. isSubmiting.value=false
  251. })
  252. }
  253. })
  254. }
  255. </script>
  256. <template>
  257. <div class="assistance-edit-container">
  258. <div class="edit-container-rich-text">
  259. <froala
  260. id="froala-editor"
  261. ref="froalaEditor"
  262. :tag="'textarea'"
  263. :config="froalaConfig"
  264. v-model:value="addDocForm.Content"
  265. ></froala>
  266. </div>
  267. <div class="right-area">
  268. <div class="save-time" v-if="modifyTime">
  269. 最近保存时间:{{ modifyTime }}
  270. </div>
  271. <div class="edit-container-document-options">
  272. <div class="document-options-button-box">
  273. <el-button type="primary" class="document-options-button" @click="previewDocument" v-loading="isSubmiting">预览</el-button>
  274. <el-button type="primary" class="document-options-button" @click="saveDocument('保存')" v-loading="isSubmiting">保存</el-button>
  275. <el-button type="primary" class="document-options-button" @click="saveDocument('发布')" v-loading="isSubmiting">发布</el-button>
  276. </div>
  277. <div class="document-options-form">
  278. <el-form :model="addDocForm" ref="addDocFormRef" :rules="addDocRules" label-position="top">
  279. <el-form-item label="文章标题" prop="Title">
  280. <el-input v-model="addDocForm.Title" placeholder="请输入文章标题"></el-input>
  281. </el-form-item>
  282. <el-form-item label="所属分类" prop="ClassifyId">
  283. <el-cascader style="width: 100%;"
  284. v-model="addDocForm.ClassifyId" :options="classifyList"
  285. :props="{value:'ClassifyId',label:'ClassifyName',children:'Children',emitPath:false,disabled:'Disabled'}" placeholder="所属分类"/>
  286. </el-form-item>
  287. <el-form-item label="文章作者" prop="Author">
  288. <el-input v-model="addDocForm.Author" placeholder="请输入文章作者"></el-input>
  289. </el-form-item>
  290. <el-form-item label="相关推荐">
  291. <div v-for="(item,index) in addDocForm.RecommendData" :key="index" class="form-item-recommendedLink">
  292. <el-input v-model="item.Name" placeholder="请输入链接名称" style="width: 190px;"></el-input>
  293. <div class="recommendedLink-line"></div>
  294. <el-input v-model="item.Url" placeholder="请输入链接" style="width: 190px;"></el-input>
  295. </div>
  296. </el-form-item>
  297. </el-form>
  298. </div>
  299. </div>
  300. </div>
  301. </div>
  302. </template>
  303. <style lang="scss" scoped>
  304. .assistance-edit-container{
  305. display: flex;
  306. justify-content: flex-start;
  307. .edit-container-rich-text{
  308. flex-grow: 1;
  309. min-height: calc(100vh - 110px);
  310. background-color: white;
  311. border:solid 1px #ECECEC;
  312. box-sizing: border-box;
  313. }
  314. .right-area{
  315. margin-left: 20px;
  316. min-height: calc(100vh - 112px);
  317. .save-time{
  318. font-size: 16px;
  319. line-height: 22px;
  320. color: #000000;
  321. font-weight: 400;
  322. background-color: unset;
  323. margin-bottom: 10px;
  324. }
  325. .edit-container-document-options{
  326. min-height: calc(100% - 32px);
  327. background-color: white;
  328. border:solid 1px #ECECEC;
  329. box-sizing: border-box;
  330. width: 440px;
  331. min-width: 440px;
  332. .document-options-button-box{
  333. width: 100%;
  334. height: 80px;
  335. box-sizing: border-box;
  336. padding: 20px;
  337. box-shadow: 0px 5px 10px #ECECEC;
  338. border-bottom: solid 1px #ECECEC;
  339. .document-options-button{
  340. height: 40px;
  341. width: 120px;
  342. }
  343. }
  344. .document-options-form{
  345. padding: 30px 20px;
  346. .form-item-recommendedLink{
  347. display: flex;
  348. align-items: center;
  349. justify-content: flex-start;
  350. width: 100%;
  351. margin-bottom: 20px;
  352. &:last-child{
  353. margin-bottom: 0;
  354. }
  355. .recommendedLink-line{
  356. flex: 1;
  357. height: 1px;
  358. background-color:#DCDFE6 ;
  359. }
  360. }
  361. }
  362. }
  363. }
  364. }
  365. </style>
  366. <style lang="scss">
  367. .assistance-edit-container{
  368. .fr-toolbar,.fr-box.fr-basic .fr-wrapper{
  369. border: none;
  370. }
  371. }
  372. .fr-popup.fr-active{
  373. z-index: 100000!important;
  374. opacity: 1!important;
  375. }
  376. </style>