修复 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:
王佑琳 2026-06-05 16:03:18 +08:00
parent f0008aedde
commit 16d1496283
3 changed files with 50 additions and 16 deletions

View File

@ -47,20 +47,27 @@ const contentRef = ref(null)
const visible = ref(props.modelValue)
const position = ref({ top: 0, left: 0 })
const popoverId = ref(Math.random().toString(36).substr(2, 9))
let resizeObserver = null
if (!window.__currentOpenPopoverId__) {
window.__currentOpenPopoverId__ = null
}
const contentStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
...position.value
}))
const contentStyle = computed(() => {
const w = typeof props.width === 'number' ? `${props.width}px` : props.width
return {
...position.value,
width: w,
maxWidth: w === 'auto' ? 'none' : w,
minWidth: w === 'auto' ? '0' : w
}
})
const togglePopover = async () => {
if (visible.value) {
visible.value = false
window.__currentOpenPopoverId__ = null
stopResizeObserver()
} else {
if (window.__currentOpenPopoverId__ && window.__currentOpenPopoverId__ !== popoverId.value) {
window.dispatchEvent(new CustomEvent('close-other-popovers', { detail: { excludeId: popoverId.value } }))
@ -72,20 +79,21 @@ const togglePopover = async () => {
window.__currentOpenPopoverId__ = popoverId.value
await nextTick()
updatePosition()
startResizeObserver()
}
emit('update:modelValue', visible.value)
}
const updatePosition = () => {
if (!triggerRef.value || !contentRef.value) return
const triggerRect = triggerRef.value.getBoundingClientRect()
const contentRect = contentRef.value.getBoundingClientRect()
const gap = 25
let top = 0
let left = 0
switch (props.placement) {
case 'top':
top = triggerRect.top - contentRect.height - gap
@ -104,19 +112,35 @@ const updatePosition = () => {
left = triggerRect.right + gap
break
}
position.value = {
top: `${top}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) => {
if (!visible.value) return
const triggerEl = popoverRef.value
const contentEl = contentRef.value
if (
triggerEl &&
!triggerEl.contains(e.target) &&
@ -125,6 +149,7 @@ const handleClickOutside = (e) => {
) {
visible.value = false
window.__currentOpenPopoverId__ = null
stopResizeObserver()
emit('update:modelValue', false)
}
}
@ -132,12 +157,14 @@ const handleClickOutside = (e) => {
const handleCloseOtherPopovers = (e) => {
if (e.detail.excludeId !== popoverId.value) {
visible.value = false
stopResizeObserver()
}
}
const handleCloseOtherSelects = () => {
visible.value = false
window.__currentOpenPopoverId__ = null
stopResizeObserver()
}
watch(() => props.modelValue, async (val) => {
@ -145,6 +172,9 @@ watch(() => props.modelValue, async (val) => {
if (val) {
await nextTick()
updatePosition()
startResizeObserver()
} else {
stopResizeObserver()
}
})
@ -162,6 +192,7 @@ onBeforeUnmount(() => {
window.removeEventListener('scroll', updatePosition, true)
window.removeEventListener('close-other-popovers', handleCloseOtherPopovers)
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
stopResizeObserver()
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<Popover placement="top" :width="400">
<Popover placement="top">
<div class="proportion-container">
<div class="section">
<h3>选择比例</h3>
@ -248,6 +248,7 @@ watch(() => [props.modelValue, props.resolution], () => {
.proportion-container{
padding: 20px;
min-width: 300px;
}
.section{
@ -269,7 +270,7 @@ watch(() => [props.modelValue, props.resolution], () => {
.proportion-options{
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
gap: 8px;
margin-bottom: 16px;
background-color: #F8F9FA;
padding: 5px;
@ -325,14 +326,13 @@ watch(() => [props.modelValue, props.resolution], () => {
}
.resolution-item{
flex: 1;
padding: 10px;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
text-align: center;
transition: all 0.2s ease;
// background: #f5f5f5;
white-space: nowrap;
color: #666;
&:hover{
@ -354,6 +354,7 @@ watch(() => [props.modelValue, props.resolution], () => {
.input-group{
flex: 1;
min-width: 0;
position: relative;
label{
@ -367,6 +368,7 @@ watch(() => [props.modelValue, props.resolution], () => {
}
input{
box-sizing: border-box;
width: 100%;
height: 36px;
padding: 12px 12px 12px 30px;

View File

@ -1,5 +1,5 @@
<template>
<Popover placement="top" :width="400">
<Popover placement="top">
<div class="proportion-container">
<div class="section">
<h3>选择比例</h3>
@ -142,6 +142,7 @@ const getProportionStyle = (value) => {
.proportion-container{
padding: 20px;
min-width: 300px;
}
.section{