perf: drawer

This commit is contained in:
ibuler
2025-12-18 15:10:03 +08:00
parent 1850c68e6b
commit 32add61fa7
7 changed files with 282 additions and 67 deletions

View File

@@ -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;
}

View File

@@ -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()
}
}
}
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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) {

View 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
}
}