FrontendRefactoring:标签支持拖动
This commit is contained in:
parent
3288829a01
commit
7fd5da85d1
@ -158,7 +158,7 @@ INSERT INTO `oss_config`
|
||||
(tenant_id, tenant_name, config_key, access_key, secret_key, bucket_name, prefix,
|
||||
endpoint, domain, is_https, region, access_policy, status, del_flag, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES
|
||||
(1, 'admin', 'minio', 'yack-superme', 'yack-minio20260816', 'fastbee', 'report',
|
||||
(1, 'admin', 'minio', 'supermeminio', 'minio20260909', 'fastbee', 'report',
|
||||
'http://47.121.180.229:9000', '', 'N', '', '0', '0', '0', 'admin', NOW(), 'admin', NOW(), 'MinIO默认存储');
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
@ -38,7 +38,11 @@ export default {
|
||||
const $container = this.$refs.scrollContainer.$el
|
||||
const $containerWidth = $container.offsetWidth
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
const tagList = this.$parent.$refs.tag
|
||||
// $refs.tag 的下标是「首次注册顺序」,ref 只在 create/destroy 时增删;
|
||||
// 标签拖拽排序只移动 DOM,不会更新这个数组,所以直接按数组顺序取首/尾和
|
||||
// 相邻标签会取错。改为按标签在栏上的实际横坐标重排后再取。
|
||||
// [].concat 同时兼容 refs 为单个元素(v-for 只剩 1 个时不一定是数组)的情况
|
||||
const tagList = [].concat(this.$parent.$refs.tag || []).sort((a, b) => a.$el.offsetLeft - b.$el.offsetLeft)
|
||||
|
||||
let firstTag = null
|
||||
let lastTag = null
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div id="tags-view-container" class="tags-view-container">
|
||||
<div
|
||||
id="tags-view-container"
|
||||
class="tags-view-container"
|
||||
:class="{ 'is-dragging': dragMoved }"
|
||||
@click.capture="onClickCapture"
|
||||
>
|
||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
|
||||
<router-link
|
||||
v-for="(tag, index) in visibleTags"
|
||||
@ -10,6 +15,7 @@
|
||||
tag="span"
|
||||
class="tags-view-item"
|
||||
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
|
||||
@mousedown.native="onTagMouseDown(tag, index, $event)"
|
||||
@contextmenu.prevent.native="openMenu(tag,$event)"
|
||||
>
|
||||
<svg v-if="isAffix(tag)" class="tab-pin" viewBox="0 0 12 12" aria-hidden="true">
|
||||
@ -79,6 +85,8 @@ import path from 'path'
|
||||
const MAX_VISIBLE_TAGS = 14
|
||||
// 「更多」下拉面板宽度,用于把面板夹在标签栏可视范围内(与样式里的 width 保持一致)
|
||||
const MORE_PANEL_WIDTH = 200
|
||||
// 鼠标横向位移超过这个像素才算拖拽,否则仍按普通点击处理
|
||||
const DRAG_THRESHOLD = 4
|
||||
|
||||
export default {
|
||||
components: { ScrollPane },
|
||||
@ -92,7 +100,20 @@ export default {
|
||||
// 「更多」下拉是否展开
|
||||
moreVisible: false,
|
||||
// 「更多」下拉的横向位置(相对 #tags-view-container),展开时按按钮实际位置计算
|
||||
moreLeft: 0
|
||||
moreLeft: 0,
|
||||
// 拖拽排序:正在拖动时,被拖标签在 visibleTags 中的下标(-1 = 未拖动)
|
||||
dragFrom: -1,
|
||||
// 用 path 而不是下标来标记被拖动的标签:拖动过程中它会不断换位置
|
||||
dragPath: '',
|
||||
dragStartX: 0,
|
||||
dragMoved: false,
|
||||
// 拖拽结束时浏览器会紧跟着补一个 click,用它把这个 click 拦掉,避免误跳转
|
||||
suppressClick: false,
|
||||
// 被拖标签的 DOM 与拖拽开始时的槽位信息。用 offsetLeft(不含 transform)
|
||||
// 加鼠标位移就能推出标签的「视觉中心」应该在哪,重排后依然跟手
|
||||
dragEl: null,
|
||||
dragBaseLeft: 0,
|
||||
dragWidth: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -167,6 +188,8 @@ export default {
|
||||
beforeDestroy() {
|
||||
document.body.removeEventListener('click', this.closeMenu)
|
||||
document.body.removeEventListener('click', this.closeMore)
|
||||
document.removeEventListener('mousemove', this.onTagMouseMove)
|
||||
document.removeEventListener('mouseup', this.onTagMouseUp)
|
||||
},
|
||||
methods: {
|
||||
isActive(route) {
|
||||
@ -176,7 +199,8 @@ export default {
|
||||
return [
|
||||
this.isActive(tag) ? 'active' : '',
|
||||
this.isAffix(tag) ? 'is-fixed' : '',
|
||||
this.isFixedBlockEnd(index) ? 'fixed-block-end' : ''
|
||||
this.isFixedBlockEnd(index) ? 'fixed-block-end' : '',
|
||||
this.dragMoved && tag.path === this.dragPath ? 'is-drag-source' : ''
|
||||
]
|
||||
},
|
||||
isAffix(tag) {
|
||||
@ -402,6 +426,151 @@ export default {
|
||||
handleScroll() {
|
||||
this.closeMenu()
|
||||
this.closeMore()
|
||||
},
|
||||
|
||||
// ---------- 标签拖拽排序 ----------
|
||||
// 规则:普通标签可以按住左键拖动换位;固定标签锁在最左侧,既拖不动、
|
||||
// 也不接受任何标签插到它前面(落点索引一律加到固定区之后)
|
||||
onTagMouseDown(tag, index, e) {
|
||||
if (e.button !== 0 || this.isAffix(tag)) {
|
||||
return
|
||||
}
|
||||
// 点在关闭按钮上是要关闭标签,不启动拖拽
|
||||
if (e.target.closest && e.target.closest('.el-icon-close')) {
|
||||
return
|
||||
}
|
||||
// 只拦掉 mousedown 的默认行为(拖拽中选中文本),不影响随后正常派发的 click
|
||||
e.preventDefault()
|
||||
this.closeMore()
|
||||
const el = e.currentTarget
|
||||
this.dragFrom = index
|
||||
this.dragPath = tag.path
|
||||
this.dragEl = el
|
||||
this.dragBaseLeft = el.offsetLeft
|
||||
this.dragWidth = el.offsetWidth
|
||||
this.dragStartX = e.clientX
|
||||
this.dragMoved = false
|
||||
document.addEventListener('mousemove', this.onTagMouseMove)
|
||||
document.addEventListener('mouseup', this.onTagMouseUp)
|
||||
},
|
||||
onTagMouseMove(e) {
|
||||
if (this.dragFrom === -1) {
|
||||
return
|
||||
}
|
||||
// 位移没过阈值之前仍算一次普通点击,避免轻微抖动被当成拖拽
|
||||
if (!this.dragMoved) {
|
||||
if (Math.abs(e.clientX - this.dragStartX) < DRAG_THRESHOLD) {
|
||||
return
|
||||
}
|
||||
this.dragMoved = true
|
||||
}
|
||||
const to = this.resolveDropIndex(e.clientX)
|
||||
if (to >= 0 && to !== this.dragFrom) {
|
||||
this.moveTagTo(this.dragFrom, to, e.clientX)
|
||||
return
|
||||
}
|
||||
this.followPointer(e.clientX)
|
||||
},
|
||||
// 鼠标当前对应的落点槽位:从被拖标签所在的槽位出发,一路吃到鼠标为止。
|
||||
// 用 offsetLeft/offsetWidth(都不含 transform)读数,这样不会被其它标签
|
||||
// 正在进行的让位动画中间态干扰;一次算到目标位,跨多个槽位也只重排一次
|
||||
resolveDropIndex(clientX) {
|
||||
const items = this.$el.querySelectorAll('.tags-view-item')
|
||||
if (this.dragFrom < 0 || !items.length) {
|
||||
return -1
|
||||
}
|
||||
// 被拖标签的「视觉中心」= 抓住它的位置 + 鼠标位移
|
||||
const center = this.dragBaseLeft + this.dragWidth / 2 + (clientX - this.dragStartX)
|
||||
const midOf = i => items[i].offsetLeft + items[i].offsetWidth / 2
|
||||
let from = this.dragFrom
|
||||
while (from + 1 < items.length && center > midOf(from + 1)) {
|
||||
from++
|
||||
}
|
||||
// 左边界卡在固定区之后 —— 这就是「不能移到固定标签页前面」的落实点
|
||||
while (from - 1 >= this.affixCount && center < midOf(from - 1)) {
|
||||
from--
|
||||
}
|
||||
return from
|
||||
},
|
||||
// 把可见区第 from 个标签挪到第 to 个位置
|
||||
moveTagTo(from, to, clientX) {
|
||||
const visible = this.visibleTags
|
||||
// 可见下标要换算成 visitedViews 的绝对下标:被挤出到「更多」的标签仍排在
|
||||
// 可见标签前面,直接拿可见下标去 splice 会插错位置
|
||||
const fromAbs = this.visitedViews.indexOf(visible[from])
|
||||
const toAbs = this.visitedViews.indexOf(visible[to])
|
||||
if (fromAbs < 0 || toAbs < 0 || fromAbs === toAbs) {
|
||||
this.dragFrom = to
|
||||
this.followPointer(clientX)
|
||||
return
|
||||
}
|
||||
// FLIP:先量下重排前每个标签的位置,重排后让它们从旧位置滑到新位置
|
||||
const items = this.$el.querySelectorAll('.tags-view-item')
|
||||
const before = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
before.push(items[i].getBoundingClientRect().left)
|
||||
}
|
||||
this.$store.dispatch('tagsView/sortVisitedViews', { fromIndex: fromAbs, toIndex: toAbs })
|
||||
this.dragFrom = to
|
||||
this.$nextTick(() => {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const el = items[i]
|
||||
// 被拖动的这块由 followPointer 控制,不参与让位动画
|
||||
if (el === this.dragEl) {
|
||||
continue
|
||||
}
|
||||
const delta = before[i] - el.getBoundingClientRect().left
|
||||
if (!delta) {
|
||||
continue
|
||||
}
|
||||
// 先瞬移到旧位置,强制一次重排,再放开让它按 CSS 过渡滑回新位置
|
||||
el.style.transition = 'none'
|
||||
el.style.transform = 'translateX(' + delta + 'px)'
|
||||
el.offsetWidth
|
||||
el.style.transition = ''
|
||||
el.style.transform = ''
|
||||
}
|
||||
this.followPointer(clientX)
|
||||
})
|
||||
},
|
||||
// 让被拖动的标签贴着鼠标走:目标视觉位置减去当前槽位位置,就是该补偿的位移。
|
||||
// 重排会改变槽位,每次重新算这个差值,"手感"才会始终黏着指针
|
||||
followPointer(clientX) {
|
||||
const el = this.dragEl
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
const dx = clientX - this.dragStartX
|
||||
el.style.transform = 'translateX(' + (this.dragBaseLeft + dx - el.offsetLeft) + 'px)'
|
||||
},
|
||||
onTagMouseUp() {
|
||||
document.removeEventListener('mousemove', this.onTagMouseMove)
|
||||
document.removeEventListener('mouseup', this.onTagMouseUp)
|
||||
const el = this.dragEl
|
||||
if (this.dragMoved) {
|
||||
// mouseup 之后浏览器立刻补一个 click,不拦掉的话标签会立刻跳转
|
||||
this.suppressClick = true
|
||||
}
|
||||
this.dragFrom = -1
|
||||
this.dragPath = ''
|
||||
this.dragMoved = false
|
||||
this.dragEl = null
|
||||
if (el) {
|
||||
// 等 is-drag-source 摘掉(transition 恢复)再清位移,
|
||||
// 这样松手时是平滑吸附回槽位,而不是硬跳
|
||||
this.$nextTick(() => {
|
||||
el.style.transition = ''
|
||||
el.style.transform = ''
|
||||
})
|
||||
}
|
||||
},
|
||||
// 在捕获阶段掐掉拖拽结束后的那一次 click(要在 router-link 处理之前拦下)
|
||||
onClickCapture(e) {
|
||||
if (this.suppressClick) {
|
||||
this.suppressClick = false
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -436,7 +605,9 @@ export default {
|
||||
line-height: 1.4;
|
||||
max-width: 180px;
|
||||
user-select: none;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
// transform 那段是给拖拽让位动画用的:FLIP 先把元素瞬移到旧位置,
|
||||
// 靠这里恢复过渡后滑回新槽位,其它标签就会"滑开"而不是瞬移
|
||||
transition: transform 0.18s ease, background 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--top-tabs-hover, rgba(0, 0, 0, 0.06));
|
||||
@ -488,6 +659,45 @@ export default {
|
||||
height: 26px;
|
||||
background: var(--top-tabs-line, rgba(255, 255, 255, 0.12));
|
||||
}
|
||||
|
||||
// 正在被拖动的标签:从标签栏里「拿起来」成一块浮起的方块,贴着鼠标走
|
||||
&.is-drag-source {
|
||||
z-index: 6;
|
||||
// 跟手要即时,这里必须把过渡关掉,否则位移会拖泥带水
|
||||
transition: none;
|
||||
border-radius: 6px;
|
||||
background: var(--top-tabs-active-bg, #fff);
|
||||
color: var(--top-tabs-active-fg, #303133);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
|
||||
cursor: grabbing;
|
||||
|
||||
// 方块是亮底色,关闭按钮要跟着换成深色,否则在深色主题下看不见
|
||||
.el-icon-close {
|
||||
color: #909399;
|
||||
|
||||
&:hover {
|
||||
background-color: #dcdfe6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽进行中:光标变抓取态(鼠标可能已经移出标签栏,所以挂在容器上)
|
||||
&.is-dragging {
|
||||
cursor: grabbing;
|
||||
|
||||
.tags-view-item {
|
||||
cursor: grabbing;
|
||||
|
||||
// 鼠标掠过其它标签时不要有 hover 反馈,否则满屏闪
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.is-drag-source {
|
||||
background: var(--top-tabs-active-bg, #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 「更多」入口:超出上限的标签收在这里,按钮紧跟在最后一个可见标签右侧
|
||||
|
||||
@ -138,6 +138,19 @@ const mutations = {
|
||||
state.visitedViews.splice(index, 1)
|
||||
moveToAffixBlockEnd(state.visitedViews, target)
|
||||
},
|
||||
// 拖拽排序:把 visitedViews 里的 fromIndex 挪到 toIndex(toIndex 是「最终落在」的位置)
|
||||
SORT_VISITED_VIEWS: (state, { fromIndex, toIndex }) => {
|
||||
const list = state.visitedViews
|
||||
const moved = list[fromIndex]
|
||||
if (!moved) return
|
||||
// 固定区锁在最左侧:固定标签自己不能被拖动,也不接受任何标签插到它前面
|
||||
if (moved.meta && moved.meta.affix) return
|
||||
const affixCount = list.filter(v => v.meta && v.meta.affix).length
|
||||
if (fromIndex === toIndex || toIndex < affixCount || toIndex >= list.length) return
|
||||
// 先摘出再插入,「toIndex」直接就是移动后的下标(Vue 的数组响应式会照常触发)
|
||||
list.splice(fromIndex, 1)
|
||||
list.splice(toIndex, 0, moved)
|
||||
},
|
||||
DEL_RIGHT_VIEWS: (state, view) => {
|
||||
const index = state.visitedViews.findIndex(v => v.path === view.path)
|
||||
if (index === -1) {
|
||||
@ -277,6 +290,10 @@ const actions = {
|
||||
unpinView({ commit }, view) {
|
||||
commit('UNPIN_VIEW', view)
|
||||
},
|
||||
// 拖拽排序
|
||||
sortVisitedViews({ commit }, payload) {
|
||||
commit('SORT_VISITED_VIEWS', payload)
|
||||
},
|
||||
delRightTags({ commit }, view) {
|
||||
return new Promise(resolve => {
|
||||
commit('DEL_RIGHT_VIEWS', view)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user