修复 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 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>

View File

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

View File

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