mirror of
https://github.com/jumpserver/lina.git
synced 2026-01-13 19:35:24 +00:00
perf: drawer
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<!-- DEBUG: Drawer visible={{ visible }}, component={{ component ? 'EXISTS' : 'EMPTY' }}, title={{ title }} -->
|
||||
<el-drawer
|
||||
ref="drawer"
|
||||
v-model="iVisible"
|
||||
v-el-drawer-drag-width
|
||||
:model-value="visible"
|
||||
@update:model-value="handleUpdateModelValue"
|
||||
:append-to-body="true"
|
||||
:before-close="handleClose"
|
||||
:class="['drawer', { 'drawer__no-footer': !hasFooter }]"
|
||||
@@ -31,6 +32,7 @@
|
||||
|
||||
<script>
|
||||
import { getDrawerWidth } from '@/utils/common/index'
|
||||
import { useDrawerDrag } from '@/utils/vue/useDrawerDrag'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -72,22 +74,64 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
formLabelWidth: '80px'
|
||||
formLabelWidth: '80px',
|
||||
drawerDrag: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
watch: {
|
||||
visible(val) {
|
||||
console.debug('>>> Drawer visible watch:', val, {
|
||||
component: this.component ? 'EXISTS' : 'EMPTY',
|
||||
title: this.title
|
||||
})
|
||||
if (val) {
|
||||
// 抽屉打开时,初始化拖拽功能
|
||||
this.$nextTick(() => {
|
||||
if (!this.drawerDrag) {
|
||||
this.drawerDrag = useDrawerDrag({
|
||||
storageKey: 'drawerWidth'
|
||||
})
|
||||
}
|
||||
this.drawerDrag.start()
|
||||
})
|
||||
} else {
|
||||
// 抽屉关闭时,清理拖拽功能
|
||||
if (this.drawerDrag) {
|
||||
this.drawerDrag.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.debug('>>> Drawer mounted:', {
|
||||
visible: this.visible,
|
||||
component: this.component ? 'EXISTS' : 'EMPTY',
|
||||
title: this.title
|
||||
})
|
||||
if (this.visible) {
|
||||
this.$nextTick(() => {
|
||||
if (!this.drawerDrag) {
|
||||
this.drawerDrag = useDrawerDrag({
|
||||
storageKey: 'drawerWidth'
|
||||
})
|
||||
}
|
||||
this.drawerDrag.start()
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.drawerDrag) {
|
||||
this.drawerDrag.cleanup()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleUpdateModelValue(val) {
|
||||
console.debug('>>> Drawer handleUpdateModelValue:', val, {
|
||||
component: this.component ? 'EXISTS' : 'EMPTY',
|
||||
title: this.title
|
||||
})
|
||||
this.$emit('update:visible', val)
|
||||
},
|
||||
handleClose(done) {
|
||||
this.$emit('close-drawer')
|
||||
done()
|
||||
@@ -96,7 +140,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
<style lang="scss" scoped>
|
||||
.drawer__no-footer {
|
||||
::v-deep {
|
||||
.drawer {
|
||||
@@ -242,7 +286,7 @@ export default {
|
||||
}
|
||||
|
||||
.el-drawer__header {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
margin-bottom: 0;
|
||||
padding: 15px 20px;
|
||||
font-size: 16px;
|
||||
@@ -282,7 +326,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__content, .tab-page-content {
|
||||
.drawer__content,
|
||||
.tab-page-content {
|
||||
height: 100%;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,9 @@ export default {
|
||||
this.initValue = _clonedeep(this.value)
|
||||
this.$nextTick(() => {
|
||||
// proxy
|
||||
Object.keys(this.$refs.elForm.$options.methods).forEach(item => {
|
||||
const methods = this.$refs.elForm.$options.methods || {}
|
||||
|
||||
Object.keys(methods).forEach(item => {
|
||||
if (item in this) return
|
||||
this[item] = this.$refs.elForm[item]
|
||||
})
|
||||
@@ -224,6 +226,11 @@ export default {
|
||||
return item.hidden(this.value)
|
||||
}
|
||||
return false
|
||||
},
|
||||
clearValidate() {
|
||||
if (this.$refs.elForm) {
|
||||
this.$refs.elForm.clearValidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ export default {
|
||||
},
|
||||
afterGetFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
default: value => value
|
||||
},
|
||||
// 提交前,清理form的值
|
||||
cleanFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
default: value => value
|
||||
},
|
||||
// 获取 meta
|
||||
afterGetRemoteMeta: {
|
||||
@@ -76,45 +76,47 @@ export default {
|
||||
// 创建成功的msg
|
||||
createSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return 'CreateSuccessMsg'
|
||||
}
|
||||
},
|
||||
// 保存成功,继续添加的msg
|
||||
saveSuccessContinueMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return 'SaveSuccessContinueMsg'
|
||||
}
|
||||
},
|
||||
// 更新成功的msg
|
||||
updateSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return 'UpdateSuccessMsg'
|
||||
}
|
||||
},
|
||||
// 创建成功的跳转路由
|
||||
createSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
const routeName = this.$route.name?.replace('Create', 'List')
|
||||
default: function () {
|
||||
// const routeName = this.$route.name?.replace('Create', 'List')
|
||||
const routeName = 'GroupCreate'
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
// 更新成功的跳转路由
|
||||
updateSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
const routeName = this.$route.name?.replace('Update', 'List')
|
||||
default: function () {
|
||||
// const routeName = this.$route.name?.replace('Update', 'List')
|
||||
const routeName = 'GroupUpdate'
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
objectDetailRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
const routeName = this.$route.name?.replace('Update', 'Detail')
|
||||
.replace('Create', 'Detail')
|
||||
default: function () {
|
||||
// const routeName = this.$route.name?.replace('Update', 'Detail').replace('Create', 'Detail')
|
||||
const routeName = 'GroupDetail'
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
@@ -122,12 +124,13 @@ export default {
|
||||
getNextRoute: {
|
||||
type: Function,
|
||||
default(res, method) {
|
||||
return method === 'post' ? this.createSuccessNextRoute : this.updateSuccessNextRoute
|
||||
return { name: 'GroupList' }
|
||||
// return method === 'post' ? this.createSuccessNextRoute : this.updateSuccessNextRoute
|
||||
}
|
||||
},
|
||||
cloneNameSuffix: {
|
||||
type: [String, Number],
|
||||
default: function() {
|
||||
default: function () {
|
||||
return 'Duplicate'.toLowerCase()
|
||||
}
|
||||
},
|
||||
@@ -139,7 +142,7 @@ export default {
|
||||
// 获取创建和更新的url function
|
||||
getUrl: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
default: function () {
|
||||
const objectId = this.getUpdateId()
|
||||
let url = this.url
|
||||
if (objectId) {
|
||||
@@ -180,12 +183,16 @@ export default {
|
||||
msg = msg[0].toLowerCase() + msg.slice(1)
|
||||
this.$message({
|
||||
message: h('p', null, [
|
||||
h('el-link', {
|
||||
on: {
|
||||
click: () => this.$router.push(detailRoute)
|
||||
h(
|
||||
'el-link',
|
||||
{
|
||||
on: {
|
||||
click: () => this.$router.push(detailRoute)
|
||||
},
|
||||
style: { 'vertical-align': 'top', 'margin-right': '5px' }
|
||||
},
|
||||
style: { 'vertical-align': 'top', 'margin-right': '5px' }
|
||||
}, msgLinkName),
|
||||
msgLinkName
|
||||
),
|
||||
h('span', {}, msg)
|
||||
]),
|
||||
type: 'success'
|
||||
@@ -200,9 +207,12 @@ export default {
|
||||
default(res, method, vm, addContinue) {
|
||||
const route = this.getNextRoute(res, method)
|
||||
if (!(route.params && route.params.id)) {
|
||||
route['params'] = deepmerge(route['params'] || {}, { 'id': res.id })
|
||||
route['params'] = deepmerge(route['params'] || {}, { id: res.id })
|
||||
}
|
||||
route['query'] = deepmerge(route['query'], { 'order': this.extraQueryOrder, 'updated': new Date().getTime() })
|
||||
route['query'] = deepmerge(route['query'], {
|
||||
order: this.extraQueryOrder,
|
||||
updated: new Date().getTime()
|
||||
})
|
||||
|
||||
this.$emit('submitSuccess', res)
|
||||
|
||||
@@ -344,7 +354,7 @@ export default {
|
||||
encryptFields(values) {
|
||||
// 批量提交,clean 后可能是个数组
|
||||
if (values instanceof Array) {
|
||||
return values.map((item) => this.encryptFields(item))
|
||||
return values.map(item => this.encryptFields(item))
|
||||
}
|
||||
values = { ...values }
|
||||
for (const field of this.encryptedFields) {
|
||||
@@ -372,8 +382,8 @@ export default {
|
||||
defaultOnSubmit(validValues, formName, addContinue) {
|
||||
this.isSubmitting = true
|
||||
this.performSubmit(validValues)
|
||||
.then((res) => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
|
||||
.catch((error) => this.onPerformError(error, this.method, this))
|
||||
.then(res => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
|
||||
.catch(error => this.onPerformError(error, this.method, this))
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.isSubmitting = false
|
||||
@@ -383,7 +393,7 @@ export default {
|
||||
},
|
||||
async getCloneForm(cloneFrom) {
|
||||
const [curUrl, query] = this.url.split('?')
|
||||
const url = `${curUrl}${cloneFrom}/${query ? ('?' + query) : ''}`
|
||||
const url = `${curUrl}${cloneFrom}/${query ? '?' + query : ''}`
|
||||
try {
|
||||
const object = await this.getObjectDetail(url)
|
||||
let name = ''
|
||||
@@ -438,5 +448,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -187,6 +187,12 @@ $height: 28px;
|
||||
|
||||
.org-select {
|
||||
line-height: $height;
|
||||
|
||||
::v-deep .el-select__wrapper {
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-input {
|
||||
|
||||
@@ -10,21 +10,23 @@
|
||||
<div class="nav-title">
|
||||
<span :class="switchViewOtherClasses" class="switch-view active-switch-view">
|
||||
<el-popover :open-delay="200" placement="right-start" trigger="hover">
|
||||
<span slot="reference" style="width: 100%">
|
||||
<el-tooltip
|
||||
v-show="!isCollapse"
|
||||
:content="isRouteMeta.title"
|
||||
:open-delay="1000"
|
||||
placement="bottom"
|
||||
effect="dark"
|
||||
class="view-title"
|
||||
>
|
||||
<span class="text-overflow">{{ isRouteMeta.title || '' }}</span>
|
||||
</el-tooltip>
|
||||
<span class="icon-zone">
|
||||
<svg-icon class="icon" icon-class="switch" />
|
||||
<template #reference>
|
||||
<span style="width: 100%">
|
||||
<el-tooltip
|
||||
v-show="!isCollapse"
|
||||
:content="isRouteMeta.title"
|
||||
:open-delay="1000"
|
||||
placement="bottom"
|
||||
effect="dark"
|
||||
class="view-title"
|
||||
>
|
||||
<span class="text-overflow">{{ isRouteMeta.title || '' }}</span>
|
||||
</el-tooltip>
|
||||
<span class="icon-zone">
|
||||
<svg-icon class="icon" icon-class="switch" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<ViewSwitcher mode="vertical" @view-change="handleViewChange" />
|
||||
</el-popover>
|
||||
</span>
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
export function resolveRoute(route, router) {
|
||||
const routes = router.resolve(route)
|
||||
if (!routes) {
|
||||
try {
|
||||
const routes = router.resolve(route)
|
||||
if (!routes) {
|
||||
return
|
||||
}
|
||||
// Vue Router 4: router.resolve() 直接返回路由对象,没有 .resolved 属性
|
||||
// Vue Router 3: router.resolve() 返回 { resolved: {...}, ... }
|
||||
const resolved = routes.resolved || routes
|
||||
if (!resolved || !resolved.matched) {
|
||||
return
|
||||
}
|
||||
const matched = resolved.matched.filter(
|
||||
item => item.name === route.name && item.components
|
||||
)
|
||||
if (matched.length === 0) {
|
||||
return
|
||||
}
|
||||
if (matched[0] && matched[0].components?.default) {
|
||||
return matched[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('resolveRoute error:', error, route)
|
||||
return
|
||||
}
|
||||
const matched = routes.resolved.matched.filter(
|
||||
item => item.name === route.name && item.components
|
||||
)
|
||||
if (matched.length === 0) {
|
||||
return
|
||||
}
|
||||
if (matched[0] && matched[0].components?.default) {
|
||||
return matched[0]
|
||||
}
|
||||
}
|
||||
|
||||
export function getComponentFromRoute(route, router) {
|
||||
|
||||
135
src/utils/vue/useDrawerDrag.js
Normal file
135
src/utils/vue/useDrawerDrag.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Drawer 拖拽调整宽度功能
|
||||
* 用于 Element Plus el-drawer 组件的宽度拖拽调整
|
||||
*/
|
||||
|
||||
/**
|
||||
* 初始化 drawer 拖拽功能
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {Function} options.onWidthChange - 宽度变化回调函数
|
||||
* @param {number} options.minWidthRatio - 最小宽度比例,默认 0.2
|
||||
* @param {number} options.maxWidthRatio - 最大宽度比例,默认 0.8
|
||||
* @param {string} options.storageKey - localStorage 存储键名,默认 'drawerWidth'
|
||||
* @returns {Object} 包含 cleanup 方法的对象
|
||||
*/
|
||||
export function useDrawerDrag(options = {}) {
|
||||
const {
|
||||
onWidthChange,
|
||||
minWidthRatio = 0.2,
|
||||
maxWidthRatio = 0.8,
|
||||
storageKey = 'drawerWidth'
|
||||
} = options
|
||||
|
||||
let dragHandle = null
|
||||
let observer = null
|
||||
let drawerEle = null
|
||||
|
||||
const findDrawer = () => {
|
||||
const drawers = document.querySelectorAll('.el-drawer')
|
||||
return drawers.length > 0 ? drawers[drawers.length - 1] : null
|
||||
}
|
||||
|
||||
const initDrag = element => {
|
||||
if (!element || element.querySelector('.el-drawer-drag-handle')) return
|
||||
|
||||
drawerEle = element
|
||||
|
||||
// 创建拖拽手柄
|
||||
dragHandle = Object.assign(document.createElement('div'), {
|
||||
className: 'el-drawer-drag-handle',
|
||||
style: 'height:100%;width:5px;cursor:ew-resize;position:absolute;left:0;z-index:1000;'
|
||||
})
|
||||
|
||||
// 确保 drawer 有相对定位
|
||||
if (getComputedStyle(element).position === 'static') {
|
||||
element.style.position = 'relative'
|
||||
}
|
||||
|
||||
element.appendChild(dragHandle)
|
||||
|
||||
// 拖拽逻辑
|
||||
dragHandle.onmousedown = e => {
|
||||
e.preventDefault()
|
||||
const startX = e.pageX
|
||||
const startWidth = element.offsetWidth
|
||||
const minWidth = window.innerWidth * minWidthRatio
|
||||
const maxWidth = window.innerWidth * maxWidthRatio
|
||||
|
||||
const move = e => {
|
||||
const deltaX = startX - e.pageX
|
||||
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX))
|
||||
element.style.width = `${newWidth}px`
|
||||
|
||||
if (storageKey) {
|
||||
localStorage.setItem(storageKey, newWidth)
|
||||
}
|
||||
|
||||
if (onWidthChange) {
|
||||
onWidthChange(newWidth)
|
||||
}
|
||||
}
|
||||
|
||||
const up = () => {
|
||||
document.removeEventListener('mousemove', move)
|
||||
document.removeEventListener('mouseup', up)
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
|
||||
document.body.style.userSelect = 'none'
|
||||
document.addEventListener('mousemove', move)
|
||||
document.addEventListener('mouseup', up)
|
||||
}
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
cleanup()
|
||||
|
||||
// 立即尝试
|
||||
const element = findDrawer()
|
||||
if (element) {
|
||||
initDrag(element)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没找到,使用 MutationObserver 监听
|
||||
observer = new MutationObserver(() => {
|
||||
const element = findDrawer()
|
||||
if (element) {
|
||||
initDrag(element)
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
|
||||
// 超时保护
|
||||
setTimeout(() => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (dragHandle?.parentNode) {
|
||||
dragHandle.remove()
|
||||
dragHandle = null
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
drawerEle = null
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user