背景介绍
我们项目提供了在线的项目文档,便于用户在使用过程中有什么问题可以通过直接查文档解决,也在一定程度上减少了公司运营及客服成本。
在实际使用过程中,用户反馈造作文档的使用不是很方便,基本上要打开新Tab页签,不能方便的一遍对照文档,一边操作。
解决方案
为解决以上问题,在参考阿里云及腾讯云相关产品的文档模式后,引入了随动帮助文档机制。
该机制主要实现模式如下:
1、在相关功能指定位置添加文档入口
2、单击文档名称后,在页面左下角显示文档弹出层(一下简称「文档查看器」),主要是系统的大部分功能使用右侧展开的抽屉组件 (Drawer)进行显示。
3、打开的文档查看器,可垂直方向最大,可上下左右四边及四个角落通过拖拽调整大小
4、可拖拽待页面任意位置
5、文档查看器不影响页面中其他组件的交互
该功能是系统每个功能模块都有可能会用到的,所以将起提取为一个公共组件。组件只需传入功能标识,然后调用内容管理系统的接口获取数据,如果有数据则显示文档入口,由于一个功能极有可能会对应有多篇帮助文档,用户从文档入口单击将展开文档列表,用户单击需要查看的文档,则在左下角显示文档。
实现过程
示例中均使用模拟的数据,实际数据在生产环境调用内容管理系统的接口获取。
实现文档查看器组件
HelpModal.vue
文档查看器的拖拽及大小调整均通过指令控制
<template>
<el-dialog
ref="dlg"
v-dragAndResize :modal="false"
top="auto"
:visible.sync="isShow" custom-class="help-modal-wrap"
width="400px"
:close-on-click-modal="false" append-to-body
>
<div slot="title" class="dialog-title">
<h3>功能指南</h3>
<div class="panel-actions">
<em
class="panel-action console-svg svg-vertical-expand margin-right-xs"
:class="{
'svg-vertical-expand': !verticalMaxed,
'svg-vertical-collapse': verticalMaxed
}"
:title="verticalMaxed ? '还原' :'垂直最大化'" @click="setDlgMaxHeight"
/>
</div>
</div>
<div class="dlg-body-inner">
<div class="content-wrap">
<h2 class="article-title">
如何使用一键转区
</h2>
<div class="article-subtitle">
<span class="article-time">更新时间:2022-08-09 10:10</span>
</div>
<div class="article-content">
<p style="line-height:normal">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px">
可使用【一键转区】功能,将超算账号的数据转移至指定的超算上进行使用,如超算中心A3分区的sc0001账号(即为源目标)转移至超算分区A6的sc0001(即为目的目标),具体操作如下:</span>
</p>
<p style="line-height:normal">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px">
1. 单击【一键转区】中的【申请一键转区】</span>
</p>
<p>
<span style="font-size:14px"><img
src="https://kbs.paratera.com/uploads/1/image/public/202208/20220809100845_a3269g5dky.png" title=""
alt="image.png"
></span>
</p>
<p>
<span style="font-size:14px"><img
src="https://kbs.paratera.com/uploads/1/image/public/202208/20220809100855_atmxepobl8.png" title=""
alt="image.png"
></span>
</p>
<p><span style="font-size:14px"> </span></p>
<p style="line-height:normal">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:14px">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 2.
在【源目标】处选择可用的超算分区及超算账号,例如A3超算分区的sc0001;</span></span>
</p>
<p style="line-height:normal">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px">
在【目的目标】处选择匹配的超算分区,不用选择转区后的超算账号,例如A6超算分区;</span>
</p>
<p style="line-height:normal">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px">
在【源目标账号环境信息】处填软件使用时的环境要求。</span>
</p>
<p style="line-height:normal">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px">
3. 单击【确定】,会有提示框告知源目标超算账号保存3个月,3个月后超算账号回收的提醒。</span>
</p>
<p style="line-height:normal">
<span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px">
4. 在【一键转区】的页面查看工单进展。</span>
</p>
</div>
</div>
<div class="outer-link-wrap">
<el-button type="primary" class="btn-open-in-window" @click="openDocLink">
新窗口查看文档
</el-button>
</div>
</div>
</el-dialog>
</template>
<script>
import { setStyle, on, off, addClass } from 'element-ui/lib/utils/dom'
export default {
name: 'HelpModal',
directives: {
dragAndResize: {
bind(el, binding, vnode, oldVnode) {
addClass(el, 'dlg-wrap-help')
// 弹框可拉伸最小宽高
const minWidth = 300
const minHeight = 300
// 获取弹框头部(这部分可双击全屏)
const dialogHeaderEl = el.querySelector('.el-dialog__header')
// 弹窗
const dragDom = el.querySelector('.el-dialog')
dragDom.className = dragDom.className + ' dialog-can-resize'
// // 给弹窗加上overflow auto;不然缩小时框内的标签可能超出dialog;
// dragDom.style.overflow = 'auto'
// 清除选择头部文字效果
dialogHeaderEl.onselectstart = new Function('return false')
// 头部加上可拖动cursor
dialogHeaderEl.style.cursor = 'move'
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null)
// 初始设置在页面左下角
const initialHeight = window.innerHeight * 2 / 3
setStyle(dragDom, 'height', initialHeight + 'px')
setStyle(dragDom, 'top', window.innerHeight * 1 / 3 - 10 + 'px')
const moveDown = (e) => {
e.preventDefault()
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
// 获取到的值带px 正则匹配替换
let styL, styT
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/%/g, '') / 100)
styT = +document.body.clientHeight * (+sty.top.replace(/%/g, '') / 100)
} else {
styL = +sty.left.replace(/px/g, '')
styT = +sty.top.replace(/px/g, '')
}
document.onmousemove = function(e) {
e.preventDefault()
// 通过事件委托,计算移动的距离
const l = e.clientX - disX
const t = e.clientY - disY
// 移动当前元素
dragDom.style.left = `${l + styL}px`
dragDom.style.top = `${t + styT}px`
// 将此时的位置传出去
// binding.value({x:e.pageX,y:e.pageY})
}
document.onmouseup = function(e) {
document.onmousemove = null
document.onmouseup = null
}
}
on(dialogHeaderEl, 'mousedown', moveDown)
// 添加位置调整锚点
const buildResizeElements = (dragDom) => {
const directions = ['top', 'right', 'bottom', 'left', 'top-right', 'top-left', 'bottom-right', 'bottom-left']
for (const direction of directions) {
const resizeEl = document.createElement('div')
resizeEl.className = `resize-handle resize-${direction}`
resizeEl.setAttribute('resize', direction)
dragDom.appendChild(resizeEl)
}
}
buildResizeElements(dragDom)
let moveOpts = null
on(document, 'mousedown', (event) => {
if (event.which !== 1 && event.type === 'mousedown') {
return
}
const { target } = event
if (!target.className.includes('resize-handle')) {
return
}
const resizeDirection = target.getAttribute('resize')
moveOpts = {
resizeDirection: resizeDirection,
clientX: event.clientX,
clientY: event.clientY,
startWidth: dragDom.offsetWidth,
startHeight: dragDom.offsetHeight,
offsetLeft: dragDom.offsetLeft,
offsetTop: dragDom.offsetTop
}
const screenWidth = window.innerWidth
const screenHeight = window.innerHeight
const mouseMoveHandler = (event) => {
event.preventDefault()
const style = dragDom.style
const resizeDirection = moveOpts.resizeDirection
const { startWidth, startHeight } = moveOpts
let left = moveOpts.offsetLeft
let top = moveOpts.offsetTop
let width = moveOpts.startWidth
let height = moveOpts.startHeight
let clientX = event.clientX
let clientY = event.clientY
let x = (clientX = screenWidth <= clientX ? screenWidth : clientX) <= 0 ? 0 : clientX
let y = (clientY = screenHeight <= clientY ? screenHeight : clientY) <= 0 ? 0 : clientY
x = event.clientX - moveOpts.clientX
y = event.clientY - moveOpts.clientY
switch (resizeDirection) {
case 'top':
top = y + top
height = -y + height
break
case 'right':
width = x + width
break
case 'bottom':
height = y + height
break
case 'left':
left = x + left
width = -x + width
break
case 'top-left':
left = x + left
top = y + top
width = -x + width
height = -y + height
break
case 'top-right':
top = y + top
width = x + width
height = -y + height
break
case 'bottom-right':
width = x + startWidth
height = y + startHeight
break
case 'bottom-left':
left = x + left
width = -x + startWidth
height = y + startHeight
}
left = left <= 0 ? 0 : left
top = top <= 0 ? 0 : top
style.left = left + 'px'
style.top = top + 'px'
style.width = Math.max(minWidth, width) + 'px'
style.height = Math.max(minHeight, height) + 'px'
}
const mouseUpHandler = (event) => {
console.error('mouseUp', event)
off(document, 'mousemove', mouseMoveHandler)
off(document, 'mouseup', mouseUpHandler)
}
on(document, 'mousemove', mouseMoveHandler)
on(document, 'mouseup', mouseUpHandler)
})
}
}
},
props: {
show: {
type: Boolean,
required: true,
default: false
},
docId: {
type: Number,
required: true
}
},
data() {
return {
originStyle: null,
verticalMaxed: false
}
},
computed: {
isShow: {
get() {
return this.show
},
set(val) {
this.$emit('update:show', val)
}
}
},
created() {
this.init()
},
methods: {
async init() {
},
setDlgMaxHeight() {
const $dlg = this.$refs['dlg']
console.log('dlg', $dlg)
const dlgEl = $dlg.$el.childNodes[0]
const dlgStyle = dlgEl.style
if (this.verticalMaxed) {
setStyle(dlgEl, 'top', this.originStyle.top)
setStyle(dlgEl, 'height', this.originStyle.height)
} else {
this.originStyle = {
top: dlgStyle.top,
height: dlgStyle.height
}
setStyle(dlgEl, 'top', 0)
setStyle(dlgEl, 'height', window.innerHeight + 'px')
}
this.verticalMaxed = !this.verticalMaxed
},
openDocLink() {
window.open('https://kbs.paratera.com/info/1272')
}
}
}
</script>
<style lang="scss">
@import "./modal-resize.scss";
.help-modal-wrap {
border-radius: 0;
pointer-events: all;
margin: 0;
left: 10px;
top: calc(100vh - 100% - 10px);
// transition: all ease 0.2s;
.el-dialog__header {
padding: 10px;
background-image: url(./header-bg-2.jpg);
background-repeat: no-repeat;
background-position: 100% 0%;
background-size:cover;
background-color: $theme-light-1;
color:#fff;
position: relative;
em.console-svg {
font-size: 16px;
cursor: pointer
}
.el-dialog__headerbtn {
top: 15px;
right: 15px;
}
.el-dialog__close {
color:#fff;
}
}
.el-dialog__body {
flex: auto;
overflow: auto;
height: 100%;
padding: 0;
}
.dlg-body-inner {
box-sizing: border-box;
height: 100%;
// position: relative;
// overflow: auto;
// min-height: 352px;
// max-height: calc(100vh - 60px);
}
}
</style>
<style lang="scss" scoped>
@import "../../dashboard/modals/ArticleContent.scss";
:deep(.dlg-wrap-help) {
pointer-events: none;
}
.dialog-title {
h3 {
font-size: 16px;
font-weight:normal;
}
}
.panel-actions {
position: absolute;
right: 30px;
top: 10px;
}
.panel-action {
width: 24px;
height:24px;
line-height: 24px;
text-align: center;
display:inline-block;
margin: 0 3px;
cursor: pointer;
&:hover {
background-color:rgba(0,0,0,0.15)
}
}
.content-wrap {
height: calc(100% - 95px);
overflow: auto;
padding: 10px;
}
article {
padding-top: 20px;
}
.article-title {
text-align: left;
color: #333333;
padding-top: 10px;
font-size: 22px;
font-weight: normal;
}
.article-subtitle {
position: relative;
width: 100%;
padding-top: 10px;
text-align: left;
color: #999;
font-size: 14px;
}
.article-content {
padding: 10px;
}
.outer-link-wrap {
padding: 10px;
position: static;
background-color: #fff;
width: 100%;
bottom: 0;
}
.btn-open-in-window {
display: block;
width: 100%;
box-sizing: border-box;
margin: 0 auto;
}
</style>
<style>
.dlg-wrap-help {
pointer-events: none;
z-index: 9000!important;
}
</style>
modal-resize.scss
.dialog-can-resize .resize-handle {
position: absolute;
z-index: 100;
display: block
}
.dialog-can-resize .resize-top {
cursor: n-resize;
top: -3px;
left: 0px;
height: 7px;
width: 100%
}
.dialog-can-resize .resize-bottom {
cursor: s-resize;
bottom: -3px;
left: 0px;
height: 7px;
width: 100%
}
.dialog-can-resize .resize-right {
cursor: e-resize;
right: -3px;
top: 0px;
width: 7px;
height: 100%
}
.dialog-can-resize .resize-left {
cursor: w-resize;
left: -3px;
top: 0px;
width: 7px;
height: 100%
}
.dialog-can-resize .resize-bottom-right {
cursor: se-resize;
width: 20px;
height: 20px;
right: -8px;
bottom: -8px;
background: url('./resize_corner.png') no-repeat;
opacity: .4;
filter: alpha(opacity=40)
}
.dialog-can-resize .resize-bottom-left {
cursor: sw-resize;
width: 16px;
height: 16px;
left: -8px;
bottom: -8px
}
.dialog-can-resize .resize-top-left {
cursor: nw-resize;
width: 16px;
height: 16px;
left: -8px;
top: -8px
}
.dialog-can-resize .resize-top-right {
cursor: ne-resize;
width: 16px;
height: 16px;
right: -8px;
top: -8px
}
.dialog-can-resize .aui-min,.dialog-can-resize .aui-max {
display: block
}
.dialog-can-resize .resize-top-right:before,.dialog-can-resize .resize-bottom-right:before,.dialog-can-resize .resize-bottom-left:before,.dialog-can-resize .resize-top-left:before {
position: absolute;
content: "";
-ms-transition: all .2s;
-webkit-transition: all .2s;
-moz-transition: all .2s;
-o-transition: all .2s;
transition: all .2s;
width: 15px;
height: 15px;
opacity: 0;
pointer-events: none;
border: 1px solid #1890ff;
border-radius: 0
}
.dialog-can-resize .resize-top-right:hover:before,.dialog-can-resize .resize-bottom-right:hover:before,.dialog-can-resize .resize-bottom-left:hover:before,.dialog-can-resize .resize-top-left:hover:before,.dialog-can-resize .resize-top-right:active:before,.dialog-can-resize .resize-bottom-right:active:before,.dialog-can-resize .resize-bottom-left:active:before,.dialog-can-resize .resize-top-left:active:before {
opacity: 1
}
.dialog-can-resize .resize-top-right:before {
border-left: none;
border-bottom: none;
border-radius: 0 4px 0 0;
left: -8px;
top: 8px
}
.dialog-can-resize .resize-bottom-right:before {
border-left: none;
border-top: none;
border-radius: 0 0 4px 0;
left: -4px;
top: -4px
}
.dialog-can-resize .resize-bottom-left:before {
border-right: none;
border-top: none;
border-radius: 0 0 0 4px;
left: 8px;
top: -8px
}
.dialog-can-resize .resize-top-left:before {
border-right: none;
border-bottom: none;
border-radius: 4px 0 0 0;
left: 8px;
top: 8px
}
实现文档入口组件
<template>
<div v-if="docList.length > 0" class="fn-doc-wrap">
<el-popover
position="bottom"
trigger="hover"
>
<el-button slot="reference" type="text" class="padding-xs">
<em class="console-svg svg-fn-help" />
</el-button>
<ul class="doc-list">
<li v-for="docItem in docList" :key="docItem.id" @click="showHelpModal(docItem.id)">
<p v-text="docItem.title" /><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-tool-arrow" data-v-68bcb17a=""><path d="M13.7031 11.5259L13.7031 4.77588L6.95314 4.77589M4.30093 14.177L13.3949 5.08301" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</li>
</ul>
</el-popover>
</div>
</template>
<script>
export default {
name: 'ParaFnDoc',
desc: '随动帮助文档入口组件,使用过程中只需要传入关键词即可',
props: {
keyword: {
type: String,
required: true
}
},
data() {
return {
docList: []
}
},
created() {
this.init()
},
methods: {
init() {
// TODO 根据关键词获取文档列表
this.docList = [
{
id: 1,
title: '一键转区功能说明'
},
{
id: 2,
title: '为什么一键转区功能不可用?'
},
{
id: 3,
title: '为什么一键转区提交后没有及时生效?'
}
]
},
showHelpModal(docId) {
window.ParaCommon.showHelpModal(docId)
}
}
}
</script>
<style lang="scss" scoped>
.fn-doc-wrap {
display: inline-block;
vertical-align: middle;
}
.doc-list li {
line-height: 22px;
margin-bottom: 5px;
padding: 7px 10px;
list-style:none;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #f7f8fa;
cursor: pointer;
.icon-tool-arrow {
stroke: #1e80ff;
}
}
p {
flex: 1
}
.link-arrow {
color:blue
}
}
</style>
评论 (0)