修复 Popover 宽度不稳定:移除比例组件硬编码宽度,改用 min-width + ResizeObserver 自适应内容
- Popover 新增 ResizeObserver 监听内容尺寸变化,自动重定位保持居中 - popover-content 补充 maxWidth/minWidth 约束,完善 width='auto' 模式 - 所有关闭路径(点击外部、关闭其他弹窗、modelValue watch)统一清理 observer - 移除 painting/video 比例组件的 Popover 硬编码宽度,改用 min-width: 300px - 修复 painting 分辨率选项非弹性布局导致的宽度抖动(white-space: nowrap) - 修复 painting W/H 输入框盒模型和 flex 收缩问题(box-sizing + min-width: 0)
This commit is contained in:
parent
f0008aedde
commit
16d1496283
@ -47,20 +47,27 @@ const contentRef = ref(null)
|
|||||||
const visible = ref(props.modelValue)
|
const visible = ref(props.modelValue)
|
||||||
const position = ref({ top: 0, left: 0 })
|
const position = ref({ top: 0, left: 0 })
|
||||||
const popoverId = ref(Math.random().toString(36).substr(2, 9))
|
const popoverId = ref(Math.random().toString(36).substr(2, 9))
|
||||||
|
let resizeObserver = null
|
||||||
|
|
||||||
if (!window.__currentOpenPopoverId__) {
|
if (!window.__currentOpenPopoverId__) {
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentStyle = computed(() => ({
|
const contentStyle = computed(() => {
|
||||||
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
|
const w = typeof props.width === 'number' ? `${props.width}px` : props.width
|
||||||
...position.value
|
return {
|
||||||
}))
|
...position.value,
|
||||||
|
width: w,
|
||||||
|
maxWidth: w === 'auto' ? 'none' : w,
|
||||||
|
minWidth: w === 'auto' ? '0' : w
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const togglePopover = async () => {
|
const togglePopover = async () => {
|
||||||
if (visible.value) {
|
if (visible.value) {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
|
stopResizeObserver()
|
||||||
} else {
|
} else {
|
||||||
if (window.__currentOpenPopoverId__ && window.__currentOpenPopoverId__ !== popoverId.value) {
|
if (window.__currentOpenPopoverId__ && window.__currentOpenPopoverId__ !== popoverId.value) {
|
||||||
window.dispatchEvent(new CustomEvent('close-other-popovers', { detail: { excludeId: popoverId.value } }))
|
window.dispatchEvent(new CustomEvent('close-other-popovers', { detail: { excludeId: popoverId.value } }))
|
||||||
@ -72,20 +79,21 @@ const togglePopover = async () => {
|
|||||||
window.__currentOpenPopoverId__ = popoverId.value
|
window.__currentOpenPopoverId__ = popoverId.value
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updatePosition()
|
updatePosition()
|
||||||
|
startResizeObserver()
|
||||||
}
|
}
|
||||||
emit('update:modelValue', visible.value)
|
emit('update:modelValue', visible.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
if (!triggerRef.value || !contentRef.value) return
|
if (!triggerRef.value || !contentRef.value) return
|
||||||
|
|
||||||
const triggerRect = triggerRef.value.getBoundingClientRect()
|
const triggerRect = triggerRef.value.getBoundingClientRect()
|
||||||
const contentRect = contentRef.value.getBoundingClientRect()
|
const contentRect = contentRef.value.getBoundingClientRect()
|
||||||
const gap = 25
|
const gap = 25
|
||||||
|
|
||||||
let top = 0
|
let top = 0
|
||||||
let left = 0
|
let left = 0
|
||||||
|
|
||||||
switch (props.placement) {
|
switch (props.placement) {
|
||||||
case 'top':
|
case 'top':
|
||||||
top = triggerRect.top - contentRect.height - gap
|
top = triggerRect.top - contentRect.height - gap
|
||||||
@ -104,19 +112,35 @@ const updatePosition = () => {
|
|||||||
left = triggerRect.right + gap
|
left = triggerRect.right + gap
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
position.value = {
|
position.value = {
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
left: `${left}px`
|
left: `${left}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startResizeObserver = () => {
|
||||||
|
if (!contentRef.value) return
|
||||||
|
stopResizeObserver()
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
updatePosition()
|
||||||
|
})
|
||||||
|
resizeObserver.observe(contentRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopResizeObserver = () => {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClickOutside = (e) => {
|
const handleClickOutside = (e) => {
|
||||||
if (!visible.value) return
|
if (!visible.value) return
|
||||||
|
|
||||||
const triggerEl = popoverRef.value
|
const triggerEl = popoverRef.value
|
||||||
const contentEl = contentRef.value
|
const contentEl = contentRef.value
|
||||||
|
|
||||||
if (
|
if (
|
||||||
triggerEl &&
|
triggerEl &&
|
||||||
!triggerEl.contains(e.target) &&
|
!triggerEl.contains(e.target) &&
|
||||||
@ -125,6 +149,7 @@ const handleClickOutside = (e) => {
|
|||||||
) {
|
) {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
|
stopResizeObserver()
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,12 +157,14 @@ const handleClickOutside = (e) => {
|
|||||||
const handleCloseOtherPopovers = (e) => {
|
const handleCloseOtherPopovers = (e) => {
|
||||||
if (e.detail.excludeId !== popoverId.value) {
|
if (e.detail.excludeId !== popoverId.value) {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
|
stopResizeObserver()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseOtherSelects = () => {
|
const handleCloseOtherSelects = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
|
stopResizeObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, async (val) => {
|
watch(() => props.modelValue, async (val) => {
|
||||||
@ -145,6 +172,9 @@ watch(() => props.modelValue, async (val) => {
|
|||||||
if (val) {
|
if (val) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updatePosition()
|
updatePosition()
|
||||||
|
startResizeObserver()
|
||||||
|
} else {
|
||||||
|
stopResizeObserver()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -162,6 +192,7 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('scroll', updatePosition, true)
|
window.removeEventListener('scroll', updatePosition, true)
|
||||||
window.removeEventListener('close-other-popovers', handleCloseOtherPopovers)
|
window.removeEventListener('close-other-popovers', handleCloseOtherPopovers)
|
||||||
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
|
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
|
||||||
|
stopResizeObserver()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="top" :width="400">
|
<Popover placement="top">
|
||||||
<div class="proportion-container">
|
<div class="proportion-container">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>选择比例</h3>
|
<h3>选择比例</h3>
|
||||||
@ -248,6 +248,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
|
|
||||||
.proportion-container{
|
.proportion-container{
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section{
|
.section{
|
||||||
@ -269,7 +270,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
.proportion-options{
|
.proportion-options{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-between;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background-color: #F8F9FA;
|
background-color: #F8F9FA;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -325,14 +326,13 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.resolution-item{
|
.resolution-item{
|
||||||
flex: 1;
|
padding: 10px 16px;
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
// background: #f5f5f5;
|
white-space: nowrap;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|
||||||
&:hover{
|
&:hover{
|
||||||
@ -354,6 +354,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
|
|
||||||
.input-group{
|
.input-group{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
label{
|
label{
|
||||||
@ -367,6 +368,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input{
|
input{
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 12px 12px 12px 30px;
|
padding: 12px 12px 12px 30px;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="top" :width="400">
|
<Popover placement="top">
|
||||||
<div class="proportion-container">
|
<div class="proportion-container">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>选择比例</h3>
|
<h3>选择比例</h3>
|
||||||
@ -142,6 +142,7 @@ const getProportionStyle = (value) => {
|
|||||||
|
|
||||||
.proportion-container{
|
.proportion-container{
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section{
|
.section{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user