TDesign Vue Next 颜色拾取器高级技巧:撤销功能与样式穿透完全指南
在构建专业级可视化编辑器时,颜色拾取器远不止"选个颜色"那么简单。本文将以一个基于 TDesign Vue Next + Babylon.js 的 3D 编辑器项目为例,深度剖析两个高频痛点:如何实现颜色修改的撤销(Undo)功能,以及如何自定义弹出面板的样式。
一、撤销功能的实现:巧用 visible-change 事件
1.1 核心挑战:如何捕获"变化前"的值
在编辑器场景中,撤销功能是标配。但 TDesign 的 <t-color-picker> 组件的 change 事件只会返回新值,无法直接获取修改前的颜色。我们需要一个机制来"记住"用户打开拾色器那一刻的初始值。
关键洞察:用户打开拾色器面板 → 选择颜色 → 关闭面板,这个过程的边界正是 visible-change 事件的触发时机。
1.2 解决方案:事件分工与状态管理
在 BgSettings.vue 中,我们采用"预览+提交"模式:
<t-color-picker
v-model="clearColorRef"
@change="previewClearColor" <!-- 实时预览 -->
@visible-change="cmdHandleClearColor" <!-- 控制撤销命令 -->
/>
核心逻辑拆解:
// 变化前的值存储
let clearColorChangeStart = '#000'
// 1. 拾色器打开时:保存当前值作为"后悔点"
const cmdHandleClearColor = (visible: boolean) => {
if(visible){ // 面板出现
clearColorChangeStart = clearColorRef.value; // 记录起始值
} else { // 面板关闭
const finalColor = clearColorRef.value;
// 2. 先恢复初始值(撤销操作的核心)
bgController.setClearColor(clearColorChangeStart);
// 3. 再提交命令(支持后续撤销)
bgController.cmdSetClearColor(finalColor);
}
}
// 实时预览(不改变撤销栈)
const previewClearColor = (hexColor: string) => {
bgController.setClearColor(hexColor); // 直接修改,不记录命令
}
流程图:
用户点击颜色框
↓ visible-change(visible=true)
保存 clearColorChangeStart = 当前值
↓ 用户挑选颜色...
↓ change事件触发
实时预览新颜色(不记录命令)
↓ 用户确认/点击外部关闭
↓ visible-change(visible=false)
恢复为 clearColorChangeStart(撤销本次预览)
提交 cmdSetClearColor(最终值) 到命令栈
1.3 命名约定:changeStart 的语义化
观察代码可发现,clearColorChangeStart 这个变量名并非偶然。这是一种行业内约定俗成的命名模式:
-
changeStart:变化开始的值
-
changeEnd / finalValue:变化结束的值
-
oldValue / newValue:新旧值对比
这种命名让协作者能立刻理解:"哦,这是用来支持撤销的初始状态!"
1.4 为什么 focus/blur 方案会失败?
我曾尝试通过透传 Input 的 focus 和 blur 事件来替代 visible-change:
<!-- 失败的尝试 -->
<t-color-picker
@focus="handleFocus" <!-- 期望打开时触发 -->
@blur="handleBlur" <!-- 期望关闭时触发 -->
/>
失败原因:
-
事件未透传:TDesign 的
t-color-picker并未将底层 Input 的focus/blur事件暴露到组件根节点 -
Popup 机制干扰:点击拾色器面板本身不会导致 Input 失焦,因为面板是 Portal 到 body 的,不在同一 DOM 树
-
时机不对:即使能触发,
blur可能在颜色预览阶段就执行,无法区分"临时选择"和"最终确认"
结论:到目前为止visible-change 是官方为这种场景设计的唯一可靠生命周期钩子。
1.5 命令模式:让撤销可堆叠
bgController.cmdSetClearColor() 并非直接修改,而是包装成命令对象:
// BgControllerEditor.ts
public cmdSetClearColor(hexColor: string): void {
const cmd = new CmdProperty<string>(
this._setClearColor.bind(this), // 执行函数
hexColor, // 新值
this.scene.clearColor.toHexString() // 旧值(用于撤销)
);
cmdEmitter.emit("execute", cmd) // 交给命令管理器
}
这实现了:
-
执行:应用新颜色
-
撤销:自动用旧值回滚
-
重做:再次执行
二、样式穿透难题:Popup 组件的 Teleport 困局
2.1 问题现象::deep() 突然失效
当你想在 BgSettings.vue 中修改拾色器弹出面板的暗色主题时,可能会写成:
<style scoped>
:deep(.t-color-picker__panel) {
background-color: #222; /* ❌ 无效! */
}
</style>
结果:样式完全不生效。但官方文档明明说 :deep() 能穿透 scoped...
2.2 根本原因:Teleport 的"降维打击"
查阅 TDesign Popup 文档 会发现关键说明:
Popup 使用 Vue3 的
Teleport实现,默认挂载到body,因此:deep()无法作用于 Teleport 包裹的元素。
原理剖析:
组件树结构(开发者视角):
BgSettings.vue
└─ <t-color-picker>
└─ <t-popup> (逻辑上属于子组件)
实际 DOM 结构(浏览器视角):
<body>
├─ <div id="app">...</div> ← 你的组件在这里
└─ <div class="t-popup">...</div> ← 拾色器面板在这里(Teleport 插入)
scoped 样式通过 data-v-xxx 属性匹配元素,但 Teleport 将面板移动到了组件外部,丢失了 scoped 属性,导致 :deep() 无法跨越这道"次元壁"。
2.3 解决方案:全局样式精准打击
正确姿势:在根组件 EditorApp.vue 中使用不带 scoped 的 <style> 标签:
<!-- EditorApp.vue -->
<style>
/* 颜色拾取器弹出面板的输入框样式 */
.t-color-picker__format--item .input-group .input-group__item .t-input__wrap .t-input {
background-color: #222 !important;
}
/* 格式选择器下拉框 */
.t-color-picker__format-mode-select .t-select-input .t-input__wrap .t-input {
background-color: #222 !important;
}
</style>
最佳实践建议:
-
集中管理:在
EditorApp.vue或独立的global-popup-styles.css中统一覆盖 -
Specificity 控制:使用合理的选择器权重,避免
!important滥用 -
限定作用域:通过父级 class 限定,如
.dark-theme .t-color-picker__panel
/* 更安全的写法 */
.dark-theme .t-color-picker__panel {
background-color: var(--td-bg-color-container);
}
2.4 举一反三:Dialog 等组件同样适用
观察 EditorApp.vue 中的全局样式,你会发现还覆盖了对话框:
/* 可拖拽的非模态对话框样式 */
.drag-modeles-dialog .t-dialog__wrap .t-dialog__position .t-dialog--default {
display: flex;
flex-direction: column;
padding: 2px;
}
受影响组件清单:
-
t-popup及衍生:t-color-picker、t-select、t-date-picker等 -
t-dialog/t-drawer -
任何使用
attach="body"或自定义挂载点的组件
组件库设计启示:TDesign 通过 popupProps 暴露底层配置,但样式始终是全局维度的问题。
三、完整代码参考
3.1 撤销逻辑核心(BgSettings.vue)
<template>
<div class="form-item">
<label>背景色</label>
<t-color-picker
v-model="clearColorRef"
format="HEX"
:color-modes="['monochrome']"
:input-props="{ readonly: true }"
@change="previewClearColor"
@visible-change="cmdHandleClearColor"
/>
</div>
</template>
<script setup>
const clearColorRef = ref('#000')
let clearColorChangeStart = '#000'
const previewClearColor = (hexColor: string) => {
bgController.setClearColor(hexColor) // 直接预览
}
const cmdHandleClearColor = (visible: boolean) => {
if (visible) {
clearColorChangeStart = clearColorRef.value
} else {
//由于后面册小预览的时候也会引发颜色修改事件,
//这会导致clearColorRef.value发生变化,需要先保存下来
const colorFinal = clearColorRef.value
bgController.setClearColor(clearColorChangeStart) // 撤销预览
bgController.cmdSetClearColor(colorFinal) // 提交命令
}
}
</script>
3.2 全局样式覆盖(EditorApp.vue)
<style>
/* Popup 样式必须在全局作用域 */
.t-color-picker__trigger .t-input__wrap {
background-color: transparent !important;
border: none;
}
/* 所有 Teleport 组件的通用处理 */
.t-popup .t-dialog .t-drawer {
/* 你的主题变量 */
}
</style>
四、总结与最佳实践
| 场景 | 错误做法 | 正确做法 | 关键技术点 |
|---|---|---|---|
| 撤销功能 | 依赖 change 事件记录旧值 |
visible-change 保存 changeStart |
事件生命周期 + 命令模式 |
| 样式穿透 | :deep() 在 scoped 中 |
根组件全局 <style> |
Teleport DOM 分离原理 |
| 事件选择 | 使用 focus/blur |
使用 visible-change |
官方事件透传完整性 |
更多推荐

所有评论(0)