Index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <script setup>
  2. import subMenuBox from "./components/subMenuBox.vue"
  3. import menuBox from "./components/menubox.vue"
  4. import {apiGetHelpDocClassify,apiGetHelpDocDetail} from "@/api/helpApi.js"
  5. import {ref,nextTick,onMounted,onUnmounted } from 'vue'
  6. import {useRoute} from 'vue-router'
  7. const route = useRoute()
  8. const documentData=ref([])
  9. const currentNodeKey=ref('')
  10. const defaultActiveId=ref(0)
  11. const isRightFold=ref(false)
  12. const helpDocument=ref({})
  13. const Content=ref('')
  14. let isFirstLoad=true
  15. const hasAnchorData=ref(false)
  16. let videoList=[]
  17. const businessCode = route.query.bus_code || ''
  18. const treeProp={
  19. label:'AnchorName',
  20. children: 'Child'
  21. }
  22. const getDocumentData=()=>{
  23. apiGetHelpDocClassify({bus_code:businessCode}).then(res=>{
  24. if(res.code == 200){
  25. documentData.value=res.data || []
  26. getFirstId(documentData.value[0])
  27. getDocument(defaultActiveId.value)
  28. }
  29. })
  30. }
  31. // 获取第一个分类
  32. const getFirstId=(item)=>{
  33. if(!item){
  34. hasAnchorData.value=false
  35. isRightFold.value=true
  36. return
  37. }
  38. if(item.Children && item.Children.length>0){
  39. getFirstId(item.Children[0])
  40. }else{
  41. defaultActiveId.value = item.ClassifyId
  42. }
  43. }
  44. const menuChange=(data,node)=>{
  45. currentNodeKey.value = data.AnchorId
  46. }
  47. const getDocument=(id)=>{
  48. apiGetHelpDocDetail({bus_code:businessCode,classify_id:id}).then(res=>{
  49. if(res.code == 200){
  50. helpDocument.value=res.data || {}
  51. Content.value = helpDocument.value.Content + createBottomHref(helpDocument.value.Recommend)
  52. if(helpDocument.value?.Anchor?.length>0){
  53. isRightFold.value=false
  54. hasAnchorData.value=true
  55. }else{
  56. isRightFold.value=true
  57. hasAnchorData.value=false
  58. }
  59. setTimeout(()=>{
  60. scrollTopList.value=[]
  61. getScrollTopList(helpDocument.value.Anchor || [])
  62. videoList=document.getElementsByTagName('video') || []
  63. if(videoList && videoList.length>0){
  64. for (let i = 0; i < videoList.length; i++) {
  65. const element = videoList[i];
  66. element.addEventListener('play',setVideosStatus(i))
  67. }
  68. }
  69. },300)
  70. if(isFirstLoad){
  71. isFirstLoad=false
  72. }else{
  73. window.scrollTo(0, document.getElementById('operation-document-body').offsetTop)
  74. }
  75. }else if(res.code == 4001){
  76. helpDocument.value={}
  77. isRightFold.value=true
  78. hasAnchorData.value=false
  79. }
  80. })
  81. }
  82. const setVideosStatus=(i)=>{
  83. return ()=>{
  84. // 暂停其他播放的视频,只能播放一个
  85. for (let j = 0; j < videoList.length; j++) {
  86. if(j==i) continue;
  87. if(!videoList[j].paused){
  88. videoList[j].pause()
  89. }
  90. }
  91. }
  92. }
  93. // 生成底部的两个链接
  94. const createBottomHref=(RecommendData)=>{
  95. if(!(RecommendData && RecommendData.length>0)) return ''
  96. let hrefStringBuiler='<ul style="margin-top:40px">'
  97. RecommendData.map(item =>{
  98. if(item.Name){
  99. hrefStringBuiler+=`<li><a href="${item.Url}" target="_blank" style="text-decoration: underline;">${item.Name}</a></li>`
  100. }
  101. })
  102. return hrefStringBuiler+"</ul>"
  103. }
  104. getDocumentData()
  105. const headerHeight = ref(0)
  106. const scrollTopList=ref([])
  107. const stop=ref(false)
  108. onMounted(() => {
  109. document.addEventListener('scroll',scrollChange)
  110. headerHeight.value=document.getElementById("operation-document-body").offsetTop
  111. })
  112. onUnmounted(() => {
  113. document.removeEventListener('scroll',scrollChange)
  114. if(videoList && videoList.length>0){
  115. for (let i = 0; i < videoList.length; i++) {
  116. const element = videoList[i];
  117. element.removeEventListener('play',setVideosStatus(i))
  118. }
  119. }
  120. })
  121. const getScrollTopList=(list)=>{
  122. list.map(item =>{
  123. if(item.AnchorName){
  124. scrollTopList.value.push({key:item.AnchorId,clientRectTop:document.getElementById(item.Anchor).offsetTop+headerHeight.value})
  125. }
  126. if(item.Child && item.Child.length>0) getScrollTopList(item.Child || [])
  127. })
  128. }
  129. const navigate = (e,id)=>{
  130. // 阻止滚动监听,防止右侧节点的定位 被滚动监听干扰
  131. stop.value=true
  132. e.preventDefault();
  133. document.querySelector(id).scrollIntoView(true)
  134. nextTick(()=>{
  135. // 放开滚动监听
  136. stop.value=false
  137. })
  138. }
  139. const scrollChange=()=>{
  140. if(stop.value) return
  141. let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
  142. let hashit=false
  143. for (let i = scrollTopList.value.length ; i > 0; i--) {
  144. const element = scrollTopList.value[i-1];
  145. if(scrollTop - element.clientRectTop>-5){
  146. hashit=true
  147. currentNodeKey.value = element.key
  148. break;
  149. }
  150. }
  151. if(!hashit){
  152. currentNodeKey.value=''
  153. }
  154. }
  155. </script>
  156. <template>
  157. <div class="operation-document-container" id="operation-document-container">
  158. <div class="operation-document-neck">
  159. <div class="banner-image">
  160. <img src="@/assets/img//help/data.png">
  161. <div class="banner-text">
  162. <div class="text">数据源</div>
  163. <div class="text">数据录入、更新</div>
  164. </div>
  165. </div>
  166. <img src="@/assets/img/icon/line-arrow-blue.png" class="banner-line">
  167. <div class="banner-image">
  168. <img src="@/assets/img/help/database.png">
  169. <div class="banner-text">
  170. <div class="text">ETA指标库</div>
  171. <div class="text">添加指标</div>
  172. </div>
  173. </div>
  174. <img src="@/assets/img/icon/line-arrow-blue.png" class="banner-line">
  175. <div class="banner-image">
  176. <img src="@/assets/img/help/chart.png">
  177. <div class="banner-text">
  178. <div class="text">ETA图库</div>
  179. <div class="text">指标作图</div>
  180. </div>
  181. </div>
  182. <img src="@/assets/img/icon/line-arrow-blue.png" class="banner-line">
  183. <div class="banner-image">
  184. <img src="@/assets/img/help/report.png">
  185. <div class="banner-text">
  186. <div class="text">研报</div>
  187. <div class="text">编辑研报/插入图表</div>
  188. </div>
  189. </div>
  190. </div>
  191. <div class="operation-document-body" id="operation-document-body">
  192. <div class="document-body-left">
  193. <el-scrollbar style="height: calc(100vh - 48px)">
  194. <el-menu class="docuemnt-menu" text-color="#333333" :default-active="defaultActiveId+''">
  195. <template v-for="menuItem in documentData" :key="menuItem.ClassifyId">
  196. <subMenuBox @getDocument="getDocument" v-if="menuItem.Children && menuItem.Children.length>0"
  197. :item="menuItem"></subMenuBox>
  198. <menuBox @getDocument="getDocument" :item="menuItem" v-else></menuBox>
  199. </template>
  200. </el-menu>
  201. </el-scrollbar>
  202. </div>
  203. <div class="document-body-center" id="document-body-center" :style="{'border-right':isRightFold?'none':'solid 1px #DCDFE6'}">
  204. <template v-if="helpDocument.Title">
  205. <div class="body-center-title">
  206. <div class="body-center-title-text">{{ helpDocument.Title }}</div>
  207. <div class="body-center-title-signature">最后更新时间:{{ helpDocument.Author }} {{ helpDocument.ModifyTime }}</div>
  208. </div>
  209. <div class="rich-text-box fr-view" id="rich-text-box" v-html="Content">
  210. </div>
  211. </template>
  212. <template v-else>
  213. <div class="nodata" style="text-align: center;">
  214. <img src="~@/assets/img/nodata.png" style="width: 300px;"/>
  215. <p>暂无信息</p>
  216. </div>
  217. </template>
  218. </div>
  219. <div class="document-body-right" :style="{'max-width': isRightFold?'0':'300px'}">
  220. <div class="body-right-box">
  221. <el-tree :data="helpDocument.Anchor" node-key="AnchorId" @current-change="menuChange" class="right-anchor-tree"
  222. ref="rightTreeRef" :props="treeProp" :current-node-key="currentNodeKey" icon="none" empty-text="暂无数据"
  223. default-expand-all :expand-on-click-node="false" >
  224. <template #default="{ node, data }">
  225. <a @click="(e)=>navigate(e,'#'+data.Anchor)" class="custom-tree-node" v-html="data.AnchorName"
  226. :class="currentNodeKey==data.AnchorId?'active-node':''" :style="!node.isLeaf?'margin-top:12px':''" ></a>
  227. </template>
  228. </el-tree>
  229. </div>
  230. </div>
  231. <div class="fold-right-icon" @click="isRightFold=!isRightFold" v-if="hasAnchorData">
  232. <img src="@/assets/img/icon/fold.png" v-show="!isRightFold" />
  233. <img src="@/assets/img/icon/unfold.png" v-show="isRightFold" />
  234. </div>
  235. </div>
  236. </div>
  237. </template>
  238. <style lang="scss" scoped>
  239. .operation-document-container{
  240. min-width: 1000px;
  241. width: 100%;
  242. .operation-document-neck{
  243. padding: 60px 20px;
  244. height: 140px;
  245. display: flex;
  246. align-items: center;
  247. justify-content: center;
  248. .banner-image{
  249. height: 140px;
  250. width: 250px;
  251. min-width: 200px;
  252. position: relative;
  253. .banner-text{
  254. position: absolute;
  255. top: 24px;
  256. left: 24px;
  257. .text{
  258. color: white;
  259. font-weight: 400;
  260. line-height: 28px;
  261. font-size: 20px;
  262. &:last-child{
  263. margin-top: 20px;
  264. line-height: 22px;
  265. font-size: 16px;
  266. }
  267. }
  268. }
  269. img{
  270. height:100%;
  271. width:100%;
  272. }
  273. }
  274. .banner-line{
  275. width: 40px;
  276. margin: 0 10px;
  277. }
  278. }
  279. .operation-document-body{
  280. width: 100%;
  281. // min-height:100vh;
  282. display: flex;
  283. position: relative;
  284. .document-body-left{
  285. max-width: 300px;
  286. flex:1;
  287. max-height: 100vh;
  288. position: sticky;
  289. top: 0;
  290. padding: 24px 24px 24px 12px;
  291. box-sizing: border-box;
  292. align-self: flex-start;
  293. .docuemnt-menu{
  294. border-right: none;
  295. }
  296. }
  297. .document-body-center{
  298. flex: 3;
  299. min-height: 100%;
  300. padding: 20px;
  301. border-left: solid 1px #DCDFE6;
  302. // border-right: solid 1px #DCDFE6;
  303. box-sizing: border-box;
  304. min-height:100vh;
  305. .body-center-title{
  306. border-bottom: solid 1px #C0C4CC;
  307. margin-bottom: 30px;
  308. .body-center-title-text{
  309. font-size: 34px;
  310. line-height: 48px;
  311. text-align: center;
  312. }
  313. .body-center-title-signature{
  314. font-size: 14px;
  315. color: #666666;
  316. padding: 10px 0 10px 10px;
  317. }
  318. }
  319. }
  320. .document-body-right{
  321. flex: 1;
  322. max-width: 300px;
  323. max-height: 100vh;
  324. position: sticky;
  325. top: 0;
  326. overflow: hidden;
  327. transition: all 0.1s ease;
  328. box-sizing: border-box;
  329. align-self: flex-start;
  330. .body-right-box{
  331. padding: 12px 24px 24px;
  332. box-sizing: border-box;
  333. .custom-tree-node{
  334. padding: 8px 12px;
  335. font-size: 14px;
  336. line-height: 20px;
  337. color: #333333;
  338. overflow: hidden;
  339. text-overflow: ellipsis;
  340. white-space: nowrap;
  341. width: 100%;
  342. }
  343. }
  344. }
  345. .fold-right-icon{
  346. width: 20px;
  347. height: 20px;
  348. position: sticky;
  349. right: 10px;
  350. top: 0;
  351. cursor: pointer;
  352. }
  353. }
  354. }
  355. </style>
  356. <style lang="scss">
  357. // froala-editor 预览时的样式,如需使用在展示富文本的节点上加上 fr-view 的类
  358. @import '/public/froala_style.min.css';
  359. // 因为富文本编辑的地方在hz_crm_web 项目,加入后台的样式保持两边看起来一致
  360. @import '/public/reset.min.css';
  361. p[data-f-id="pbf"] {
  362. display: none;
  363. }
  364. .el-scrollbar__wrap {
  365. overflow-x: hidden;
  366. }
  367. .el-sub-menu .el-menu-item{
  368. padding-left:12px !important;
  369. }
  370. a{
  371. text-decoration: none;
  372. }
  373. .el-tree-node:focus > .el-tree-node__content {
  374. background-color: transparent !important;
  375. }
  376. .el-tree-node__content{
  377. height: unset!important;
  378. }
  379. .el-tree-node__content:hover {
  380. background-color: transparent;
  381. .custom-tree-node{
  382. color: #366EF4!important;
  383. }
  384. }
  385. .active-node{
  386. color: #366EF4!important;
  387. }
  388. .el-tree-node__children{
  389. .active-node{
  390. color: #666666!important;
  391. text-decoration:underline;
  392. }
  393. .custom-tree-node{
  394. color: #666666!important;
  395. padding: 0 8px!important;
  396. }
  397. }
  398. .el-tree-node__children{
  399. .el-tree-node__content:hover{
  400. background-color: transparent;
  401. text-decoration: underline;
  402. .custom-tree-node{
  403. color: #666666!important;
  404. }
  405. }
  406. }
  407. .el-tree-node__expand-icon{
  408. display: none;
  409. }
  410. </style>