|
|
|
@ -40,20 +40,19 @@ |
|
|
|
v-for="tag in visitedViews" |
|
|
|
ref="tag" |
|
|
|
:key="tag.path" |
|
|
|
:data-path="tag.path" |
|
|
|
:class="[isActive(tag) ? 'active' : '', { 'is-dragging': dragTag && dragTag.path === tag.path }]" |
|
|
|
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" |
|
|
|
tag="span" |
|
|
|
class="tags-view-item" |
|
|
|
draggable="true" |
|
|
|
@dragstart.native="onTagDragStart(tag, $event)" |
|
|
|
@dragover.native.prevent="onTagDragOver(tag, $event)" |
|
|
|
@drop.native.prevent="onTagDrop(tag)" |
|
|
|
@dragend.native="onTagDragEnd" |
|
|
|
@pointerdown.native="onTagPointerDown(tag, $event)" |
|
|
|
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''" |
|
|
|
@contextmenu.prevent.native="openMenu(tag,$event)" |
|
|
|
> |
|
|
|
{{ titleFn(tag.title) }} |
|
|
|
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> |
|
|
|
<span class="tags-view-face"> |
|
|
|
{{ titleFn(tag.title) }} |
|
|
|
<span v-if="!isAffix(tag)" class="el-icon-close" @pointerdown.stop @click.prevent.stop="closeSelectedTag(tag)" /> |
|
|
|
</span> |
|
|
|
</router-link> |
|
|
|
</scroll-pane> |
|
|
|
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> |
|
|
|
@ -80,6 +79,8 @@ export default { |
|
|
|
left: 0, |
|
|
|
selectedTag: {}, |
|
|
|
dragTag: null, |
|
|
|
dropPath: '', |
|
|
|
pointerDrag: null, |
|
|
|
affixTags: [], |
|
|
|
videoLayout: 9, |
|
|
|
videoControlPanelExpanded: false, |
|
|
|
@ -134,6 +135,7 @@ export default { |
|
|
|
this.$root.$on('video-control-panel-updated', this.updateVideoControlPanel) |
|
|
|
}, |
|
|
|
beforeDestroy() { |
|
|
|
this.detachTagPointer() |
|
|
|
this.$root.$off('video-layout-updated', this.updateVideoLayout) |
|
|
|
this.$root.$off('video-control-panel-updated', this.updateVideoControlPanel) |
|
|
|
}, |
|
|
|
@ -314,31 +316,146 @@ export default { |
|
|
|
handleScroll() { |
|
|
|
this.closeMenu() |
|
|
|
}, |
|
|
|
onTagDragStart(tag, event) { |
|
|
|
this.dragTag = tag |
|
|
|
if (event.dataTransfer) { |
|
|
|
event.dataTransfer.effectAllowed = 'move' |
|
|
|
event.dataTransfer.setData('text/plain', tag.path) |
|
|
|
// 不用 HTML5 拖拽。现场大屏会给 html 加 zoom,原生拖拽要截整页快照,缩放不是 1 时会把页面卡住,松手也不换位。 |
|
|
|
onTagPointerDown(tag, event) { |
|
|
|
if (event.button !== 0) return |
|
|
|
if (event.target.closest && event.target.closest('.el-icon-close')) return |
|
|
|
if (this.isAffix(tag)) return |
|
|
|
this.detachTagPointer() |
|
|
|
const rect = event.currentTarget.getBoundingClientRect() |
|
|
|
this.pointerDrag = { |
|
|
|
tag, |
|
|
|
el: event.currentTarget, |
|
|
|
startX: event.clientX, |
|
|
|
startY: event.clientY, |
|
|
|
offsetX: event.clientX - rect.left, |
|
|
|
toPath: tag.path, |
|
|
|
active: false, |
|
|
|
pointerId: event.pointerId, |
|
|
|
userSelect: document.body.style.userSelect |
|
|
|
} |
|
|
|
}, |
|
|
|
onTagDragOver(tag, event) { |
|
|
|
if (!this.dragTag || this.dragTag.path === tag.path) return |
|
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move' |
|
|
|
}, |
|
|
|
onTagDrop(tag) { |
|
|
|
const from = this.dragTag |
|
|
|
this.dragTag = null |
|
|
|
if (!from || from.path === tag.path) return |
|
|
|
const el = event.currentTarget |
|
|
|
if (el && el.setPointerCapture) { |
|
|
|
try { el.setPointerCapture(event.pointerId) } catch (err) { /* 部分现场浏览器不支持捕获 */ } |
|
|
|
} |
|
|
|
document.addEventListener('pointermove', this.onTagPointerMove) |
|
|
|
document.addEventListener('pointerup', this.onTagPointerUp, true) |
|
|
|
document.addEventListener('pointercancel', this.onTagPointerUp, true) |
|
|
|
document.addEventListener('mouseup', this.onTagPointerUp, true) |
|
|
|
window.addEventListener('blur', this.onTagDragCancel) |
|
|
|
}, |
|
|
|
onTagPointerMove(event) { |
|
|
|
const drag = this.pointerDrag |
|
|
|
if (!drag || event.pointerId !== drag.pointerId) return |
|
|
|
const movedX = Math.abs(event.clientX - drag.startX) |
|
|
|
const movedY = Math.abs(event.clientY - drag.startY) |
|
|
|
if (!drag.active) { |
|
|
|
if (movedX < 2 && movedY < 2) return |
|
|
|
drag.active = true |
|
|
|
this.dragTag = drag.tag |
|
|
|
document.body.style.userSelect = 'none' |
|
|
|
} |
|
|
|
event.preventDefault() |
|
|
|
this.followTagPointer(drag, event.clientX) |
|
|
|
this.scrollTagsWhileDragging(event.clientX) |
|
|
|
}, |
|
|
|
onTagPointerUp(event) { |
|
|
|
const drag = this.pointerDrag |
|
|
|
if (!drag) return |
|
|
|
if (event && event.type !== 'mouseup' && event.pointerId !== undefined && event.pointerId !== drag.pointerId) return |
|
|
|
const active = drag.active |
|
|
|
const fromPath = drag.tag.path |
|
|
|
const toPath = drag.toPath |
|
|
|
document.body.style.userSelect = drag.userSelect || '' |
|
|
|
this.detachTagPointer() |
|
|
|
if (!active) return |
|
|
|
const blockClick = (e) => { |
|
|
|
e.preventDefault() |
|
|
|
e.stopPropagation() |
|
|
|
window.removeEventListener('click', blockClick, true) |
|
|
|
} |
|
|
|
window.addEventListener('click', blockClick, true) |
|
|
|
setTimeout(() => window.removeEventListener('click', blockClick, true), 0) |
|
|
|
const views = this.visitedViews |
|
|
|
const fromIndex = views.findIndex(item => item.path === from.path) |
|
|
|
const toIndex = views.findIndex(item => item.path === tag.path) |
|
|
|
if (fromIndex < 0 || toIndex < 0) return |
|
|
|
const affixCount = views.filter(item => item.meta && item.meta.affix).length |
|
|
|
if (fromIndex < affixCount || toIndex < affixCount) return |
|
|
|
this.$store.dispatch('tagsView/moveVisitedView', { from: fromIndex, to: toIndex }) |
|
|
|
}, |
|
|
|
onTagDragEnd() { |
|
|
|
const fromIndex = views.findIndex(item => item.path === fromPath) |
|
|
|
const toIndex = views.findIndex(item => item.path === toPath) |
|
|
|
if (fromIndex >= 0 && toIndex >= 0 && fromIndex !== toIndex) { |
|
|
|
const affixCount = views.filter(item => item.meta && item.meta.affix).length |
|
|
|
if (fromIndex >= affixCount && toIndex >= affixCount) { |
|
|
|
this.$store.dispatch('tagsView/moveVisitedView', { from: fromIndex, to: toIndex }) |
|
|
|
} |
|
|
|
} |
|
|
|
this.$nextTick(() => this.clearTagTransforms()) |
|
|
|
}, |
|
|
|
// 按页面上现在的标签位置拖。换过顺序后组件引用的顺序不会跟着变,不能再用它定位。 |
|
|
|
followTagPointer(drag, clientX) { |
|
|
|
const current = drag.el |
|
|
|
if (!current || !current.isConnected) return |
|
|
|
this.clearTagTransforms() |
|
|
|
const layoutLeft = current.getBoundingClientRect().left |
|
|
|
const width = current.offsetWidth |
|
|
|
const dx = clientX - drag.offsetX - layoutLeft |
|
|
|
this.setTagShift(current, dx) |
|
|
|
drag.toPath = this.resolveTagPathAt(layoutLeft + dx + width / 2, drag.tag.path) |
|
|
|
}, |
|
|
|
resolveTagPathAt(center, fromPath) { |
|
|
|
const items = this.tagElements() |
|
|
|
const affixCount = this.visitedViews.filter(item => item.meta && item.meta.affix).length |
|
|
|
let target = '' |
|
|
|
for (let i = affixCount; i < items.length; i++) { |
|
|
|
const rect = items[i].getBoundingClientRect() |
|
|
|
if (center >= rect.left && center <= rect.right) { |
|
|
|
target = items[i].dataset.path || '' |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
if (!target) { |
|
|
|
const first = items[affixCount] |
|
|
|
const last = items[items.length - 1] |
|
|
|
if (first && center < first.getBoundingClientRect().left) target = first.dataset.path || '' |
|
|
|
else if (last && center > last.getBoundingClientRect().right) target = last.dataset.path || '' |
|
|
|
} |
|
|
|
return target || fromPath |
|
|
|
}, |
|
|
|
tagElements() { |
|
|
|
const pane = this.$refs.scrollPane |
|
|
|
const root = pane && pane.$el |
|
|
|
if (!root) return [] |
|
|
|
return Array.from(root.querySelectorAll('.tags-view-item')) |
|
|
|
}, |
|
|
|
setTagShift(el, dx) { |
|
|
|
const face = el.querySelector('.tags-view-face') |
|
|
|
if (!face) return |
|
|
|
face.style.transform = dx ? `translateX(${dx}px)` : '' |
|
|
|
}, |
|
|
|
clearTagTransforms() { |
|
|
|
this.tagElements().forEach(el => this.setTagShift(el, 0)) |
|
|
|
}, |
|
|
|
onTagDragCancel() { |
|
|
|
const drag = this.pointerDrag |
|
|
|
if (!drag) return |
|
|
|
document.body.style.userSelect = drag.userSelect || '' |
|
|
|
this.detachTagPointer() |
|
|
|
}, |
|
|
|
detachTagPointer() { |
|
|
|
document.removeEventListener('pointermove', this.onTagPointerMove) |
|
|
|
document.removeEventListener('pointerup', this.onTagPointerUp, true) |
|
|
|
document.removeEventListener('pointercancel', this.onTagPointerUp, true) |
|
|
|
document.removeEventListener('mouseup', this.onTagPointerUp, true) |
|
|
|
window.removeEventListener('blur', this.onTagDragCancel) |
|
|
|
this.clearTagTransforms() |
|
|
|
this.pointerDrag = null |
|
|
|
this.dragTag = null |
|
|
|
this.dropPath = '' |
|
|
|
}, |
|
|
|
scrollTagsWhileDragging(clientX) { |
|
|
|
const pane = this.$refs.scrollPane |
|
|
|
if (!pane || !pane.$el || !pane.scrollWrapper) return |
|
|
|
const rect = pane.$el.getBoundingClientRect() |
|
|
|
const wrap = pane.scrollWrapper |
|
|
|
const edge = 28 |
|
|
|
if (clientX > rect.right - edge) wrap.scrollLeft += 16 |
|
|
|
else if (clientX < rect.left + edge) wrap.scrollLeft -= 16 |
|
|
|
}, |
|
|
|
titleFn(item, children) { |
|
|
|
let str = ""; |
|
|
|
@ -498,19 +615,29 @@ export default { |
|
|
|
display: inline-block; |
|
|
|
position: relative; |
|
|
|
cursor: grab; |
|
|
|
user-select: none; |
|
|
|
touch-action: none; |
|
|
|
height: 40px; |
|
|
|
line-height: 40px; |
|
|
|
border: 0; |
|
|
|
color: #c0c5db; |
|
|
|
background: transparent; |
|
|
|
padding: 0 26px; |
|
|
|
padding: 0; |
|
|
|
font-size: 14px; |
|
|
|
margin: 0; |
|
|
|
vertical-align: top; |
|
|
|
box-sizing: border-box; |
|
|
|
transition: color .2s, background-color .2s; |
|
|
|
|
|
|
|
// 相邻标签之间的青色短竖线(设计稿的分割线样式) |
|
|
|
.tags-view-face { |
|
|
|
position: relative; |
|
|
|
display: block; |
|
|
|
height: 40px; |
|
|
|
padding: 0 26px; |
|
|
|
line-height: 40px; |
|
|
|
} |
|
|
|
|
|
|
|
// 分隔线留在标签格子上,拖动时只移动里面的文字和关闭按钮 |
|
|
|
& + .tags-view-item::after { |
|
|
|
content: ''; |
|
|
|
position: absolute; |
|
|
|
@ -532,8 +659,9 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
&.is-dragging { |
|
|
|
opacity: 0.45; |
|
|
|
z-index: 3; |
|
|
|
cursor: grabbing; |
|
|
|
transition: none; |
|
|
|
} |
|
|
|
|
|
|
|
&.active { |
|
|
|
@ -572,21 +700,24 @@ export default { |
|
|
|
.tags-view-wrapper { |
|
|
|
.tags-view-item { |
|
|
|
.el-icon-close { |
|
|
|
margin-left: 4px; |
|
|
|
width: 16px; |
|
|
|
height: 16px; |
|
|
|
vertical-align: 2px; |
|
|
|
border-radius: 50%; |
|
|
|
position: absolute; |
|
|
|
top: 0; |
|
|
|
right: 0; |
|
|
|
width: 26px; |
|
|
|
height: 40px; |
|
|
|
margin-left: 0; |
|
|
|
line-height: 40px; |
|
|
|
border-radius: 0; |
|
|
|
text-align: center; |
|
|
|
transition: all .3s cubic-bezier(.645, .045, .355, 1); |
|
|
|
transform-origin: 100% 50%; |
|
|
|
cursor: default; |
|
|
|
transition: background-color .2s, color .2s; |
|
|
|
&:before { |
|
|
|
transform: scale(.6); |
|
|
|
display: inline-block; |
|
|
|
vertical-align: -3px; |
|
|
|
} |
|
|
|
&:hover { |
|
|
|
background-color: rgba(84, 255, 255, 0.25); |
|
|
|
background: transparent; |
|
|
|
color: #fff; |
|
|
|
} |
|
|
|
} |
|
|
|
|