在日常开发中,我们经常使用 TDesign 的 Dialog 组件。虽然默认的居中显示和标准样式能满足大部分需求,但在某些特定的 UI/UX 场景下(例如:类似抽屉的顶部弹窗、展示长报表、或者需要极具个性化的关闭按钮),我们需要对组件进行深度定制。
本文将分享三个实用的定制技巧:
- 修改弹窗出现的位置(顶部偏移)。
- 实现固定头部 + 内部滚动的长内容弹窗。
- 高阶技巧:监听动画状态,在弹窗外部挂载自定义 DOM(如悬浮关闭按钮)。
一、 基础定制:修改弹窗位置与尺寸
默认情况下,TDesign 的 Dialog 是垂直居中的。如果产品设计要求弹窗从顶部向下偏移一定距离显示,我们可以通过 CSS 覆盖来实现。
1. 修改位置
在 Less 文件中,我们可以针对 .t-dialog__position.t-dialog--top 进行样式覆盖。
/* 将弹窗固定在距离顶部 3vh 的位置 */
.t-dialog__position.t-dialog--top {
padding-top: 3vh;
}
或者如果你已经定义了弹窗的className, 还可以
.guide-dialog-plugin {
&.t-dialog__ctx .t-dialog__position.t-dialog--top {
padding-top: 3vh;
}
}
如果只是想居中(原版默认的是20vh不是居中)
&.t-dialog__ctx .t-dialog__position.t-dialog--top {
display: flex;
align-items: center;
justify-content: center;
padding-top: unset;
padding-bottom: unset;
}
2. 控制宽高度
对于宽度的控制,最简单的方法是在调用 DialogPlugin 或使用组件时,通过 DialogOptions 传入:
DialogPlugin({
width: '83%', // 支持百分比或 px
// ...
});
但对于高度和内部布局的精细控制,我们需要配合 CSS 类名来实现,这引出了下一部分的“长内容”场景。
二、 进阶布局:固定头部与内部滚动
场景描述:点击“查看详细报告”按钮,弹出一个包含大量文字的大型弹窗。要求弹窗高度固定(或最大高度固定),头部标题栏不动,仅内容区域滚动。
1. 定义 ClassName
首先,在调用插件时给弹窗打上唯一的标记:
DialogPlugin({
className: 'error-detail-dialog-plugin', // 关键标记
header: false, // 隐藏默认头部,使用自定义内容实现
footer: false,
body: <YourContentComponent />,
// ...
});
2. 使用 Flex 布局控制滚动
利用 Less 控制弹窗结构。核心思路是将 .t-dialog 设为 Flex 容器,内容区域 flex: 1 并开启 overflow-y: auto。
.error-detail-dialog-plugin {
.t-dialog {
height: 60rem;
max-height: 90vh; /* 避免超出屏幕 */
display: flex;
flex-direction: column;
overflow: hidden; /* 裁剪圆角 */
border-radius: 0.5rem;
background: #FFF;
.t-dialog__body {
height: 100%;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
/* 自定义头部:高度由内容撑开 */
.dialog_header {
flex-shrink: 0; /* 防止被压缩 */
padding: 1rem 2rem;
background: #F3F3F3;
// ... 其他样式
}
/* 自定义内容区:占据剩余空间并滚动 */
.dialog_content {
flex: 1;
overflow-y: auto; /* 关键:允许纵向滚动 */
overflow-x: hidden;
padding: 1.5rem 2rem;
}
}
}
}
三、 高阶技巧:监听 Portal 动画并挂载外部元素
这是一个比较 hack 的需求:设计要求在弹窗 右上角外部 悬浮一个大的圆形关闭按钮。
由于 TDesign 的 Dialog 渲染在 document.body 下的 t-portal-wrapper 中,且自带“从小变大”的 entry 动画。如果直接渲染关闭按钮,可能会跟随弹窗一起缩放,或者位置计算不准。
解决方案:使用 MutationObserver 监听 TDesign Portal 的 class 变化。只有当弹窗容器具备 .appear-done 和 .enter-done(即动画结束,处于稳定显示状态)时,再将关闭按钮挂载上去。
核心代码实现
import { useRef, useCallback } from 'react';
import { DialogPlugin } from 'tdesign-react';
function ErrorDialog() {
const dialogInstanceRef = useRef(null);
const closeButtonElementRef = useRef<HTMLDivElement | null>(null);
const observerRef = useRef<MutationObserver | null>(null);
// 1. 清理函数:销毁 Dialog 同时清理按钮和观察器
const closeDialog = useCallback(() => {
// 移除手动添加的关闭按钮
if (closeButtonElementRef.current) {
closeButtonElementRef.current.remove();
closeButtonElementRef.current = null;
}
// 断开观察器
if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;
}
// 销毁 TDesign 实例
if (dialogInstanceRef.current) {
dialogInstanceRef.current.hide();
dialogInstanceRef.current.destroy();
dialogInstanceRef.current = null;
}
}, []);
const handleShowDetail = useCallback(() => {
// 2. 创建自定义关闭按钮 DOM
const createCloseButton = () => {
const btn = document.createElement('div');
btn.className = 'custom-close-button'; // 在 Less 中定义 fixed 定位
btn.innerHTML = `<div class="close-circle">X</div>`; // 这里可以是 SVG
btn.onclick = closeDialog;
closeButtonElementRef.current = btn;
};
// 3. 检查并挂载按钮的逻辑
const checkAndAddCloseButton = () => {
// 找到当前的 Portal 包装器
const portalWrapper = document.querySelector('.t-portal-wrapper.appear-done.enter-done');
// 找到我们自定义 class 的弹窗容器
const dialogWrapper = portalWrapper?.querySelector('.error-detail-dialog-plugin');
if (dialogWrapper && !closeButtonElementRef.current) {
createCloseButton();
if (closeButtonElementRef.current) {
dialogWrapper.appendChild(closeButtonElementRef.current);
}
}
};
// 4. 使用 MutationObserver 监听动画类名变化
const observePortalWrapper = () => {
const targetNode = document.querySelector('.t-portal-wrapper');
if (!targetNode) return;
observerRef.current = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target as HTMLElement;
// 当动画类名标识为完成时
if (target.classList.contains('appear-done') && target.classList.contains('enter-done')) {
checkAndAddCloseButton();
// 只需要添加一次,添加后即可断开观察
observerRef.current?.disconnect();
}
}
});
});
observerRef.current.observe(targetNode, {
attributes: true,
attributeFilter: ['class'],
});
// Edge case: 如果代码执行时动画已经结束,直接添加
if (targetNode.classList.contains('appear-done') && targetNode.classList.contains('enter-done')) {
checkAndAddCloseButton();
observerRef.current.disconnect();
}
};
// 5. 调用插件显示弹窗
dialogInstanceRef.current = DialogPlugin({
className: 'error-detail-dialog-plugin',
// ... 其他配置
onClose: closeDialog, // 确保 ESC 或遮罩点击也能触发清理
});
// 延迟执行以确保 Portal DOM 已生成
setTimeout(() => {
observePortalWrapper();
}, 0);
}, [closeDialog]);
return <button onClick={handleShowDetail}>查看详情</button>;
}
配套的 Less 样式
关闭按钮使用了 position: fixed,虽然它是 append 到弹窗容器里的,但 fixed 会相对于视口定位(除非父级有 transform,这里要注意 TDesign 的动画层级),或者可以使用 absolute 相对于弹窗定位。
.error-detail-dialog-plugin {
// 必须保证父级没有 overflow: hidden 截断了外部按钮,
// 或者直接使用 fixed 定位在屏幕上
.custom-close-button {
position: fixed; // 悬浮在页面固定位置
top: 3vh;
right: 3vh;
z-index: 9999;
cursor: pointer;
.close-circle {
/* 你的按钮样式 */
width: 3rem;
height: 3rem;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
/* ... */
}
}
}
总结
通过组合使用 CSS 覆盖、Flex 布局以及 MutationObserver,我们突破了 UI 组件库原有的限制。这种“监听 DOM 状态后注入元素”的模式虽然略显复杂,但在处理第三方组件库的强定制动画场景时非常有效。
评论区 (0)