fix: 优化动态列表和弹框UI体验

This commit is contained in:
imeepos
2025-07-18 18:45:13 +08:00
parent da4aeaccb9
commit 4da48c3281
16 changed files with 1601 additions and 346 deletions

View File

@@ -0,0 +1,147 @@
# Modal 组件迁移指南
## 🎯 问题背景
项目中多个组件使用了 `fixed inset-0` 的遮罩实现,这在某些情况下会遇到问题:
1. **父级容器有 transform 属性**时,会创建新的层叠上下文,导致 fixed 定位相对于该容器而不是视口
2. **iOS Safari 的地址栏变化**会影响视口高度
3. **复杂布局容器**可能会限制 fixed 元素的定位
4. **滚动条处理不一致**,用户体验不统一
## 🔧 解决方案
使用统一的 `Modal` 组件,它包含以下优化:
-**智能 fixed 定位检测**:自动检测并修复定位问题
-**完善的滚动条处理**:保存/恢复滚动位置,防止背景滚动
-**iOS Safari 优化**:防止橡皮筋效果
-**统一的样式系统**:一致的动画、阴影、圆角
-**可访问性支持**ESC 键、焦点管理、屏幕阅读器
## 📋 需要迁移的组件
### 1. CreateDynamicModal ✅ 已完成
- **原实现**: `fixed inset-0 bg-black/50`
- **新实现**: 使用 Modal 组件
- **状态**: 已迁移完成
### 2. ProjectTemplateBindingForm
- **位置**: `apps/desktop/src/components/ProjectTemplateBindingForm.tsx:146`
- **原实现**: `fixed inset-0 bg-black bg-opacity-50`
- **状态**: 待迁移
### 3. ResetUsageDialog
- **位置**: `apps/desktop/src/components/ResetUsageDialog.tsx:94`
- **原实现**: `fixed inset-0 bg-black bg-opacity-50`
- **状态**: 待迁移
### 4. MaterialImportDialog
- **位置**: `apps/desktop/src/components/MaterialImportDialog.tsx:245`
- **原实现**: `fixed inset-0 bg-black bg-opacity-50`
- **状态**: 待迁移
### 5. MaterialEditDialog
- **位置**: `apps/desktop/src/components/MaterialEditDialog.tsx:101`
- **原实现**: `fixed inset-0 bg-black bg-opacity-60`
- **状态**: 待迁移
### 6. MaterialSegmentDetailModal
- **位置**: `apps/desktop/src/components/MaterialSegmentDetailModal.tsx:84`
- **原实现**: `fixed inset-0 bg-black bg-opacity-50`
- **状态**: 待迁移
### 7. DeleteConfirmDialog
- **位置**: `apps/desktop/src/components/DeleteConfirmDialog.tsx:38-42`
- **原实现**: 复杂的 fixed 实现
- **状态**: 待迁移
## 🚀 迁移步骤
### 步骤 1: 导入 Modal 组件
```tsx
import { Modal } from './Modal';
```
### 步骤 2: 替换遮罩层结构
**原代码**:
```tsx
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4">
{/* 内容 */}
</div>
</div>
```
**新代码**:
```tsx
<Modal
isOpen={true}
onClose={onClose}
title="标题"
size="lg"
variant="default"
>
{/* 内容 */}
</Modal>
```
### 步骤 3: 配置 Modal 属性
- `size`: 'sm' | 'md' | 'lg' | 'xl' | 'full'
- `variant`: 'default' | 'danger' | 'success' | 'warning' | 'info'
- `closeOnBackdropClick`: 是否点击遮罩关闭
- `closeOnEscape`: 是否 ESC 键关闭
### 步骤 4: 移除原有的头部和关闭按钮
Modal 组件会自动处理头部和关闭按钮。
### 步骤 5: 测试验证
- 在不同父级容器中测试
- 验证滚动条处理
- 检查键盘导航
- 测试移动端表现
## 📝 迁移模板
```tsx
// 迁移前
const OldModal = ({ onClose, children }) => (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4">
<div className="flex items-center justify-between p-6 border-b">
<h2></h2>
<button onClick={onClose}>×</button>
</div>
<div className="p-6">
{children}
</div>
</div>
</div>
);
// 迁移后
const NewModal = ({ onClose, children }) => (
<Modal
isOpen={true}
onClose={onClose}
title="标题"
size="lg"
>
<div className="p-6">
{children}
</div>
</Modal>
);
```
## ✅ 迁移检查清单
- [ ] 导入 Modal 组件
- [ ] 替换遮罩层结构
- [ ] 配置 Modal 属性
- [ ] 移除原有头部实现
- [ ] 更新样式类名
- [ ] 测试功能完整性
- [ ] 验证响应式表现
- [ ] 检查可访问性
- [ ] 更新相关文档

View File

@@ -0,0 +1,190 @@
/**
* Modal Portal 工具函数
* 专门处理复杂容器结构中的 Modal 渲染问题
*/
/**
* 创建 Modal 容器并确保正确的层级
*/
export const createModalContainer = (id: string = 'modal-root'): HTMLElement => {
// 检查是否已存在容器
let container = document.getElementById(id);
if (!container) {
container = document.createElement('div');
container.id = id;
// 设置容器样式,确保不受任何父级影响
container.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 999999;
isolation: isolate;
`;
// 直接添加到 body避免复杂容器结构的影响
document.body.appendChild(container);
}
return container;
};
/**
* 清理 Modal 容器
*/
export const cleanupModalContainer = (id: string = 'modal-root'): void => {
const container = document.getElementById(id);
if (container && container.children.length === 0) {
document.body.removeChild(container);
}
};
/**
* 检测当前环境是否有复杂的容器结构
*/
export const detectComplexContainerStructure = (element?: HTMLElement): boolean => {
const targetElement = element || document.querySelector('[class*="h-screen"]') as HTMLElement;
if (!targetElement) return false;
const indicators = [
'h-screen',
'flex-col',
'overflow-y-auto',
'overflow-auto',
'overflow-scroll'
];
// 检查是否包含复杂容器结构的指示器
const hasComplexStructure = indicators.some(indicator =>
document.querySelector(`[class*="${indicator}"]`)
);
if (hasComplexStructure) {
console.log('Complex container structure detected in DOM');
}
return hasComplexStructure;
};
/**
* 应用复杂容器结构的修复
*/
export const applyComplexContainerFix = (modalElement: HTMLElement): void => {
if (!modalElement) return;
// 添加修复类
modalElement.classList.add('complex-container-fix');
modalElement.setAttribute('data-complex-container', 'true');
// 强制样式修复
const fixStyles = {
position: 'fixed',
top: '0',
left: '0',
right: '0',
bottom: '0',
width: '100vw',
height: '100vh',
zIndex: '999999',
transform: 'translateZ(0)',
isolation: 'isolate',
contain: 'none',
clipPath: 'none',
clip: 'auto'
};
Object.assign(modalElement.style, fixStyles);
// 修复背景遮罩
const backdrop = modalElement.querySelector('.modal-backdrop-fixed') as HTMLElement;
if (backdrop) {
const backdropStyles = {
position: 'fixed',
top: '0',
left: '0',
right: '0',
bottom: '0',
width: '100vw',
height: '100vh',
zIndex: '-1'
};
Object.assign(backdrop.style, backdropStyles);
}
console.log('Applied complex container fix to modal');
};
/**
* 移除复杂容器结构的修复
*/
export const removeComplexContainerFix = (modalElement: HTMLElement): void => {
if (!modalElement) return;
modalElement.classList.remove('complex-container-fix');
modalElement.removeAttribute('data-complex-container');
// 重置样式(让 CSS 类接管)
const stylesToReset = [
'position', 'top', 'left', 'right', 'bottom',
'width', 'height', 'zIndex', 'transform',
'isolation', 'contain', 'clipPath', 'clip'
];
stylesToReset.forEach(prop => {
modalElement.style.removeProperty(prop);
});
const backdrop = modalElement.querySelector('.modal-backdrop-fixed') as HTMLElement;
if (backdrop) {
stylesToReset.forEach(prop => {
backdrop.style.removeProperty(prop);
});
}
};
/**
* 获取安全的 Modal 渲染容器
* 确保 Modal 始终渲染在正确的位置
*/
export const getSafeModalContainer = (): HTMLElement => {
// 优先使用专门的 modal 容器
let container = document.getElementById('modal-root');
if (!container) {
container = createModalContainer('modal-root');
}
// 确保容器在 body 的最后,避免被其他元素覆盖
if (container.parentElement === document.body) {
document.body.appendChild(container);
}
return container;
};
/**
* 监听窗口大小变化,确保 Modal 始终正确覆盖
*/
export const setupModalResizeHandler = (modalElement: HTMLElement): (() => void) => {
const handleResize = () => {
if (modalElement.hasAttribute('data-complex-container')) {
// 重新应用修复
applyComplexContainerFix(modalElement);
}
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
// 返回清理函数
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
};
};